From 0716bd7511dba2b068aa6b31800772b6b387b9ef Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Mon, 10 Mar 2025 13:17:54 -0400 Subject: [PATCH 01/23] Add new BSON types to public API (#330) --- common/api-review/firestore-lite.api.md | 79 ++++ common/api-review/firestore.api.md | 79 ++++ packages/firestore/lite/index.ts | 23 +- packages/firestore/src/api.ts | 23 +- .../firestore/src/api/field_value_impl.ts | 9 +- packages/firestore/src/core/target.ts | 20 +- .../src/index/firestore_index_value_writer.ts | 1 + .../src/lite-api/bson_binary_data.ts | 59 +++ .../firestore/src/lite-api/bson_object_Id.ts | 35 ++ .../src/lite-api/bson_timestamp_value.ts | 35 ++ .../src/lite-api/field_value_impl.ts | 89 ++++ .../firestore/src/lite-api/int32_value.ts | 35 ++ packages/firestore/src/lite-api/max_key.ts | 36 ++ packages/firestore/src/lite-api/min_key.ts | 36 ++ .../firestore/src/lite-api/regex_value.ts | 35 ++ .../src/lite-api/user_data_reader.ts | 149 +++++- .../src/lite-api/user_data_writer.ts | 90 +++- packages/firestore/src/model/object_value.ts | 5 +- packages/firestore/src/model/type_order.ts | 27 +- packages/firestore/src/model/values.ts | 332 ++++++++++--- .../test/integration/api/database.test.ts | 440 +++++++++++++++++- .../test/integration/api/type.test.ts | 395 +++++++++++++++- .../firestore/test/lite/integration.test.ts | 48 ++ .../test/unit/model/document.test.ts | 37 ++ .../test/unit/model/object_value.test.ts | 153 +++++- .../firestore/test/unit/model/target.test.ts | 12 +- .../firestore/test/unit/model/values.test.ts | 158 ++++++- .../test/unit/remote/serializer.helper.ts | 62 ++- 28 files changed, 2386 insertions(+), 116 deletions(-) create mode 100644 packages/firestore/src/lite-api/bson_binary_data.ts create mode 100644 packages/firestore/src/lite-api/bson_object_Id.ts create mode 100644 packages/firestore/src/lite-api/bson_timestamp_value.ts create mode 100644 packages/firestore/src/lite-api/int32_value.ts create mode 100644 packages/firestore/src/lite-api/max_key.ts create mode 100644 packages/firestore/src/lite-api/min_key.ts create mode 100644 packages/firestore/src/lite-api/regex_value.ts diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index 4a9ef4c0171..04faa9c47c6 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -65,6 +65,41 @@ export function arrayUnion(...elements: unknown[]): FieldValue; // @public export function average(field: string | FieldPath): AggregateField; +// @public +export class BsonBinaryData { + constructor(subtype: number, data: Uint8Array); + readonly data: Uint8Array; + isEqual(other: BsonBinaryData): boolean; + readonly subtype: number; +} + +// @public +export function bsonBinaryData(subtype: number, data: Uint8Array): BsonBinaryData; + +// @public +export class BsonObjectId { + constructor(value: string); + isEqual(other: BsonObjectId): boolean; + // (undocumented) + readonly value: string; +} + +// @public +export function bsonObjectId(value: string): BsonObjectId; + +// @public +export function bsonTimestamp(seconds: number, increment: number): BsonTimestampValue; + +// @public +export class BsonTimestampValue { + constructor(seconds: number, increment: number); + // (undocumented) + readonly increment: number; + isEqual(other: BsonTimestampValue): boolean; + // (undocumented) + readonly seconds: number; +} + // @public export class Bytes { static fromBase64String(base64: string): Bytes; @@ -249,6 +284,17 @@ export function initializeFirestore(app: FirebaseApp, settings: Settings): Fires // @beta export function initializeFirestore(app: FirebaseApp, settings: Settings, databaseId?: string): Firestore; +// @public +export function int32(value: number): Int32Value; + +// @public +export class Int32Value { + constructor(value: number); + isEqual(other: Int32Value): boolean; + // (undocumented) + readonly value: number; +} + // @public export function limit(limit: number): QueryLimitConstraint; @@ -257,6 +303,26 @@ export function limitToLast(limit: number): QueryLimitConstraint; export { LogLevel } +// @public +export class MaxKey { + // (undocumented) + static instance(): MaxKey; + readonly type = "MaxKey"; +} + +// @public +export function maxKey(): MaxKey; + +// @public +export class MinKey { + // (undocumented) + static instance(): MinKey; + readonly type = "MinKey"; +} + +// @public +export function minKey(): MinKey; + // @public export type NestedUpdateFields> = UnionToIntersection<{ [K in keyof T & string]: ChildUpdateFields; @@ -360,6 +426,19 @@ export class QueryStartAtConstraint extends QueryConstraint { // @public export function refEqual(left: DocumentReference | CollectionReference, right: DocumentReference | CollectionReference): boolean; +// @public +export function regex(pattern: string, options: string): RegexValue; + +// @public +export class RegexValue { + constructor(pattern: string, options: string); + isEqual(other: RegexValue): boolean; + // (undocumented) + readonly options: string; + // (undocumented) + readonly pattern: string; +} + // @public export function runTransaction(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise, options?: TransactionOptions): Promise; diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 34b56b97f21..5d3c2286859 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -65,6 +65,41 @@ export function arrayUnion(...elements: unknown[]): FieldValue; // @public export function average(field: string | FieldPath): AggregateField; +// @public +export class BsonBinaryData { + constructor(subtype: number, data: Uint8Array); + readonly data: Uint8Array; + isEqual(other: BsonBinaryData): boolean; + readonly subtype: number; +} + +// @public +export function bsonBinaryData(subtype: number, data: Uint8Array): BsonBinaryData; + +// @public +export class BsonObjectId { + constructor(value: string); + isEqual(other: BsonObjectId): boolean; + // (undocumented) + readonly value: string; +} + +// @public +export function bsonObjectId(value: string): BsonObjectId; + +// @public +export function bsonTimestamp(seconds: number, increment: number): BsonTimestampValue; + +// @public +export class BsonTimestampValue { + constructor(seconds: number, increment: number); + // (undocumented) + readonly increment: number; + isEqual(other: BsonTimestampValue): boolean; + // (undocumented) + readonly seconds: number; +} + // @public export class Bytes { static fromBase64String(base64: string): Bytes; @@ -344,6 +379,17 @@ export interface IndexField { // @public export function initializeFirestore(app: FirebaseApp, settings: FirestoreSettings, databaseId?: string): Firestore; +// @public +export function int32(value: number): Int32Value; + +// @public +export class Int32Value { + constructor(value: number); + isEqual(other: Int32Value): boolean; + // (undocumented) + readonly value: number; +} + // @public export function limit(limit: number): QueryLimitConstraint; @@ -374,6 +420,16 @@ export interface LoadBundleTaskProgress { export { LogLevel } +// @public +export class MaxKey { + // (undocumented) + static instance(): MaxKey; + readonly type = "MaxKey"; +} + +// @public +export function maxKey(): MaxKey; + // @public export interface MemoryCacheSettings { garbageCollector?: MemoryGarbageCollector; @@ -411,6 +467,16 @@ export function memoryLruGarbageCollector(settings?: { cacheSizeBytes?: number; }): MemoryLruGarbageCollector; +// @public +export class MinKey { + // (undocumented) + static instance(): MinKey; + readonly type = "MinKey"; +} + +// @public +export function minKey(): MinKey; + // @public export function namedQuery(firestore: Firestore, name: string): Promise; @@ -620,6 +686,19 @@ export class QueryStartAtConstraint extends QueryConstraint { // @public export function refEqual(left: DocumentReference | CollectionReference, right: DocumentReference | CollectionReference): boolean; +// @public +export function regex(pattern: string, options: string): RegexValue; + +// @public +export class RegexValue { + constructor(pattern: string, options: string); + isEqual(other: RegexValue): boolean; + // (undocumented) + readonly options: string; + // (undocumented) + readonly pattern: string; +} + // @public export function runTransaction(firestore: Firestore, updateFunction: (transaction: Transaction) => Promise, options?: TransactionOptions): Promise; diff --git a/packages/firestore/lite/index.ts b/packages/firestore/lite/index.ts index b751f0a8254..6d1d6c01998 100644 --- a/packages/firestore/lite/index.ts +++ b/packages/firestore/lite/index.ts @@ -128,7 +128,14 @@ export { arrayUnion, serverTimestamp, deleteField, - vector + vector, + int32, + regex, + bsonBinaryData, + bsonObjectId, + bsonTimestamp, + minKey, + maxKey } from '../src/lite-api/field_value_impl'; export { @@ -141,6 +148,20 @@ export { export { VectorValue } from '../src/lite-api/vector_value'; +export { Int32Value } from '../src/lite-api/int32_value'; + +export { RegexValue } from '../src/lite-api/regex_value'; + +export { BsonBinaryData } from '../src/lite-api/bson_binary_data'; + +export { BsonObjectId } from '../src/lite-api/bson_object_Id'; + +export { BsonTimestampValue } from '../src/lite-api/bson_timestamp_value'; + +export { MinKey } from '../src/lite-api/min_key'; + +export { MaxKey } from '../src/lite-api/max_key'; + export { WriteBatch, writeBatch } from '../src/lite-api/write_batch'; export { TransactionOptions } from '../src/lite-api/transaction_options'; diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index ea969c6b94c..46fb1b3bba3 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -173,11 +173,32 @@ export { deleteField, increment, serverTimestamp, - vector + vector, + int32, + regex, + bsonBinaryData, + bsonObjectId, + bsonTimestamp, + minKey, + maxKey } from './api/field_value_impl'; export { VectorValue } from './lite-api/vector_value'; +export { Int32Value } from './lite-api/int32_value'; + +export { RegexValue } from './lite-api/regex_value'; + +export { BsonBinaryData } from './lite-api/bson_binary_data'; + +export { BsonObjectId } from './lite-api/bson_object_Id'; + +export { BsonTimestampValue } from './lite-api/bson_timestamp_value'; + +export { MinKey } from './lite-api/min_key'; + +export { MaxKey } from './lite-api/max_key'; + export { LogLevelString as LogLevel, setLogLevel } from './util/log'; export { Bytes } from './api/bytes'; diff --git a/packages/firestore/src/api/field_value_impl.ts b/packages/firestore/src/api/field_value_impl.ts index 1b1283a3543..6e65d273259 100644 --- a/packages/firestore/src/api/field_value_impl.ts +++ b/packages/firestore/src/api/field_value_impl.ts @@ -21,5 +21,12 @@ export { arrayUnion, serverTimestamp, deleteField, - vector + vector, + int32, + regex, + bsonBinaryData, + bsonObjectId, + bsonTimestamp, + minKey, + maxKey } from '../lite-api/field_value_impl'; diff --git a/packages/firestore/src/core/target.ts b/packages/firestore/src/core/target.ts index 4b12857fc2a..664a2ef9a08 100644 --- a/packages/firestore/src/core/target.ts +++ b/packages/firestore/src/core/target.ts @@ -25,8 +25,8 @@ import { import { FieldPath, ResourcePath } from '../model/path'; import { canonicalId, - MAX_VALUE, - MIN_VALUE, + INTERNAL_MAX_VALUE, + INTERNAL_MIN_VALUE, lowerBoundCompare, upperBoundCompare, valuesGetLowerBound, @@ -302,7 +302,7 @@ export function targetGetNotInValues( /** * Returns a lower bound of field values that can be used as a starting point to - * scan the index defined by `fieldIndex`. Returns `MIN_VALUE` if no lower bound + * scan the index defined by `fieldIndex`. Returns `INTERNAL_MIN_VALUE` if no lower bound * exists. */ export function targetGetLowerBound( @@ -328,7 +328,7 @@ export function targetGetLowerBound( /** * Returns an upper bound of field values that can be used as an ending point - * when scanning the index defined by `fieldIndex`. Returns `MAX_VALUE` if no + * when scanning the index defined by `fieldIndex`. Returns `INTERNAL_MAX_VALUE` if no * upper bound exists. */ export function targetGetUpperBound( @@ -362,13 +362,13 @@ function targetGetAscendingBound( fieldPath: FieldPath, bound: Bound | null ): { value: ProtoValue; inclusive: boolean } { - let value: ProtoValue = MIN_VALUE; + let value: ProtoValue = INTERNAL_MIN_VALUE; let inclusive = true; // Process all filters to find a value for the current field segment for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) { - let filterValue: ProtoValue = MIN_VALUE; + let filterValue: ProtoValue = INTERNAL_MIN_VALUE; let filterInclusive = true; switch (fieldFilter.op) { @@ -387,7 +387,7 @@ function targetGetAscendingBound( break; case Operator.NOT_EQUAL: case Operator.NOT_IN: - filterValue = MIN_VALUE; + filterValue = INTERNAL_MIN_VALUE; break; default: // Remaining filters cannot be used as lower bounds. @@ -437,12 +437,12 @@ function targetGetDescendingBound( fieldPath: FieldPath, bound: Bound | null ): { value: ProtoValue; inclusive: boolean } { - let value: ProtoValue = MAX_VALUE; + let value: ProtoValue = INTERNAL_MAX_VALUE; let inclusive = true; // Process all filters to find a value for the current field segment for (const fieldFilter of targetGetFieldFiltersForPath(target, fieldPath)) { - let filterValue: ProtoValue = MAX_VALUE; + let filterValue: ProtoValue = INTERNAL_MAX_VALUE; let filterInclusive = true; switch (fieldFilter.op) { @@ -462,7 +462,7 @@ function targetGetDescendingBound( break; case Operator.NOT_EQUAL: case Operator.NOT_IN: - filterValue = MAX_VALUE; + filterValue = INTERNAL_MAX_VALUE; break; default: // Remaining filters cannot be used as upper bounds. diff --git a/packages/firestore/src/index/firestore_index_value_writer.ts b/packages/firestore/src/index/firestore_index_value_writer.ts index dfdb3836578..f831862a0de 100644 --- a/packages/firestore/src/index/firestore_index_value_writer.ts +++ b/packages/firestore/src/index/firestore_index_value_writer.ts @@ -124,6 +124,7 @@ export class FirestoreIndexValueWriter { encoder.writeNumber(geoPoint.latitude || 0); encoder.writeNumber(geoPoint.longitude || 0); } else if ('mapValue' in indexValue) { + // TODO(Mila/BSON): add bson types for indexing if (isMaxValue(indexValue)) { this.writeValueTypeLabel(encoder, Number.MAX_SAFE_INTEGER); } else if (isVectorValue(indexValue)) { diff --git a/packages/firestore/src/lite-api/bson_binary_data.ts b/packages/firestore/src/lite-api/bson_binary_data.ts new file mode 100644 index 00000000000..233dd790aec --- /dev/null +++ b/packages/firestore/src/lite-api/bson_binary_data.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2025 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. + */ + +import { ByteString } from '../util/byte_string'; +import { Code, FirestoreError } from '../util/error'; + +/** + * Represents a BSON Binary Data type in Firestore documents. + * + * @class BsonBinaryData + */ +export class BsonBinaryData { + /** The subtype for the data */ + readonly subtype: number; + + /** The binary data as a byte array */ + readonly data: Uint8Array; + + constructor(subtype: number, data: Uint8Array) { + if (subtype < 0 || subtype > 255) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'The subtype for BsonBinaryData must be a value in the inclusive [0, 255] range.' + ); + } + this.subtype = subtype; + // Make a copy of the data. + this.data = Uint8Array.from(data); + } + + /** + * Returns true if this `BsonBinaryData` is equal to the provided one. + * + * @param other - The `BsonBinaryData` to compare against. + * @return 'true' if this `BsonBinaryData` is equal to the provided one. + */ + isEqual(other: BsonBinaryData): boolean { + return ( + this.subtype === other.subtype && + ByteString.fromUint8Array(this.data).isEqual( + ByteString.fromUint8Array(other.data) + ) + ); + } +} diff --git a/packages/firestore/src/lite-api/bson_object_Id.ts b/packages/firestore/src/lite-api/bson_object_Id.ts new file mode 100644 index 00000000000..71ee13d8860 --- /dev/null +++ b/packages/firestore/src/lite-api/bson_object_Id.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2025 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. + */ + +/** + * Represents a BSON ObjectId type in Firestore documents. + * + * @class BsonObjectId + */ +export class BsonObjectId { + constructor(readonly value: string) {} + + /** + * Returns true if this `BsonObjectId` is equal to the provided one. + * + * @param other - The `BsonObjectId` to compare against. + * @return 'true' if this `BsonObjectId` is equal to the provided one. + */ + isEqual(other: BsonObjectId): boolean { + return this.value === other.value; + } +} diff --git a/packages/firestore/src/lite-api/bson_timestamp_value.ts b/packages/firestore/src/lite-api/bson_timestamp_value.ts new file mode 100644 index 00000000000..60b48157906 --- /dev/null +++ b/packages/firestore/src/lite-api/bson_timestamp_value.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2025 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. + */ + +/** + * Represents a BSON Timestamp type in Firestore documents. + * + * @class BsonTimestampValue + */ +export class BsonTimestampValue { + constructor(readonly seconds: number, readonly increment: number) {} + + /** + * Returns true if this `BsonTimestampValue` is equal to the provided one. + * + * @param other - The `BsonTimestampValue` to compare against. + * @return 'true' if this `BsonTimestampValue` is equal to the provided one. + */ + isEqual(other: BsonTimestampValue): boolean { + return this.seconds === other.seconds && this.increment === other.increment; + } +} diff --git a/packages/firestore/src/lite-api/field_value_impl.ts b/packages/firestore/src/lite-api/field_value_impl.ts index 2c910bdace5..2cc1e3522b0 100644 --- a/packages/firestore/src/lite-api/field_value_impl.ts +++ b/packages/firestore/src/lite-api/field_value_impl.ts @@ -15,7 +15,14 @@ * limitations under the License. */ +import { BsonBinaryData } from './bson_binary_data'; +import { BsonObjectId } from './bson_object_Id'; +import { BsonTimestampValue } from './bson_timestamp_value'; import { FieldValue } from './field_value'; +import { Int32Value } from './int32_value'; +import { MaxKey } from './max_key'; +import { MinKey } from './min_key'; +import { RegexValue } from './regex_value'; import { ArrayRemoveFieldValueImpl, ArrayUnionFieldValueImpl, @@ -109,3 +116,85 @@ export function increment(n: number): FieldValue { export function vector(values?: number[]): VectorValue { return new VectorValue(values); } + +/** + * Creates a new `Int32Value` constructed with the given number. + * + * @param value - The 32-bit number to be used for constructing the Int32Value + * + * @returns A new `Int32Value` constructed with the given number. + */ +export function int32(value: number): Int32Value { + return new Int32Value(value); +} + +/** + * Creates a new `RegexValue` constructed with the given pattern and options. + * + * @param subtype - The subtype of the BSON binary data. + * @param data - The data to use for the BSON binary data. + * + * @returns A new `RegexValue` constructed with the given pattern and options. + */ +export function regex(pattern: string, options: string): RegexValue { + return new RegexValue(pattern, options); +} + +/** + * Creates a new `BsonBinaryData` constructed with the given subtype and data. + * + * @param subtype - Create a `BsonBinaryData` instance with the given subtype. + * @param data - Create a `BsonBinaryData` instance with a copy of this array of numbers. + * + * @returns A new `BsonBinaryData` constructed with the given subtype and data. + */ +export function bsonBinaryData( + subtype: number, + data: Uint8Array +): BsonBinaryData { + return new BsonBinaryData(subtype, data); +} + +/** + * Creates a new `BsonObjectId` constructed with the given string. + * + * @param value - The 24-character hex string representing the ObjectId. + * + * @returns A new `BsonObjectId` constructed with the given string. + */ +export function bsonObjectId(value: string): BsonObjectId { + return new BsonObjectId(value); +} + +/** + * Creates a new `BsonTimestampValue` constructed with the given seconds and increment. + * + * @param seconds - The underlying unsigned 32-bit integer for seconds. + * @param seconds - The underlying unsigned 32-bit integer for increment. + * + * @returns A new `BsonTimestampValue` constructed with the given seconds and increment. + */ +export function bsonTimestamp( + seconds: number, + increment: number +): BsonTimestampValue { + return new BsonTimestampValue(seconds, increment); +} + +/** + * Creates or returns a `MinKey` instance. + * + * @returns A `MinKey` instance. + */ +export function minKey(): MinKey { + return MinKey.instance(); +} + +/** + * Creates or returns a `MaxKey` instance. + * + * @returns A `MaxKey` instance. + */ +export function maxKey(): MaxKey { + return MaxKey.instance(); +} diff --git a/packages/firestore/src/lite-api/int32_value.ts b/packages/firestore/src/lite-api/int32_value.ts new file mode 100644 index 00000000000..cfa0003c0f6 --- /dev/null +++ b/packages/firestore/src/lite-api/int32_value.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2025 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. + */ + +/** + * Represents a 32-bit integer type in Firestore documents. + * + * @class Int32Value + */ +export class Int32Value { + constructor(readonly value: number) {} + + /** + * Returns true if this `Int32Value` is equal to the provided one. + * + * @param other - The `Int32Value` to compare against. + * @return 'true' if this `Int32Value` is equal to the provided one. + */ + isEqual(other: Int32Value): boolean { + return this.value === other.value; + } +} diff --git a/packages/firestore/src/lite-api/max_key.ts b/packages/firestore/src/lite-api/max_key.ts new file mode 100644 index 00000000000..3f37986315e --- /dev/null +++ b/packages/firestore/src/lite-api/max_key.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2025 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. + */ + +/** + * Represent a "Max Key" type in Firestore documents. + * + * @class MaxKey + */ +export class MaxKey { + private static MAX_KEY_VALUE_INSTANCE: MaxKey | null = null; + /** A type string to uniquely identify instances of this class. */ + readonly type = 'MaxKey'; + + private constructor() {} + + static instance(): MaxKey { + if (!MaxKey.MAX_KEY_VALUE_INSTANCE) { + MaxKey.MAX_KEY_VALUE_INSTANCE = new MaxKey(); + } + return MaxKey.MAX_KEY_VALUE_INSTANCE; + } +} diff --git a/packages/firestore/src/lite-api/min_key.ts b/packages/firestore/src/lite-api/min_key.ts new file mode 100644 index 00000000000..a901b9611a5 --- /dev/null +++ b/packages/firestore/src/lite-api/min_key.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2025 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. + */ + +/** + * Represent a "Min Key" type in Firestore documents. + * + * @class MinKey + */ +export class MinKey { + private static MIN_KEY_VALUE_INSTANCE: MinKey | null = null; + /** A type string to uniquely identify instances of this class. */ + readonly type = 'MinKey'; + + private constructor() {} + + static instance(): MinKey { + if (!MinKey.MIN_KEY_VALUE_INSTANCE) { + MinKey.MIN_KEY_VALUE_INSTANCE = new MinKey(); + } + return MinKey.MIN_KEY_VALUE_INSTANCE; + } +} diff --git a/packages/firestore/src/lite-api/regex_value.ts b/packages/firestore/src/lite-api/regex_value.ts new file mode 100644 index 00000000000..b4d4f70962b --- /dev/null +++ b/packages/firestore/src/lite-api/regex_value.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2025 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. + */ + +/** + * Represents a regular expression type in Firestore documents. + * + * @class RegexValue + */ +export class RegexValue { + constructor(readonly pattern: string, readonly options: string) {} + + /** + * Returns true if this `RegexValue` is equal to the provided one. + * + * @param other - The `RegexValue` to compare against. + * @return 'true' if this `RegexValue` is equal to the provided one. + */ + isEqual(other: RegexValue): boolean { + return this.pattern === other.pattern && this.options === other.options; + } +} diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index ebd4b49085f..3d0ce031599 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -44,14 +44,25 @@ import { import { TYPE_KEY, VECTOR_MAP_VECTORS_KEY, - VECTOR_VALUE_SENTINEL + RESERVED_VECTOR_KEY, + RESERVED_REGEX_KEY, + RESERVED_REGEX_PATTERN_KEY, + RESERVED_REGEX_OPTIONS_KEY, + RESERVED_BSON_OBJECT_ID_KEY, + RESERVED_INT32_KEY, + RESERVED_BSON_TIMESTAMP_KEY, + RESERVED_BSON_TIMESTAMP_SECONDS_KEY, + RESERVED_BSON_TIMESTAMP_INCREMENT_KEY, + RESERVED_BSON_BINARY_KEY, + RESERVED_MIN_KEY, + RESERVED_MAX_KEY } from '../model/values'; import { newSerializer } from '../platform/serializer'; import { MapValue as ProtoMapValue, Value as ProtoValue } from '../protos/firestore_proto_api'; -import { toDouble, toNumber } from '../remote/number_serializer'; +import { toDouble, toInteger, toNumber } from '../remote/number_serializer'; import { JsonProtoSerializer, toBytes, @@ -59,20 +70,28 @@ import { toTimestamp } from '../remote/serializer'; import { debugAssert, fail } from '../util/assert'; +import { ByteString } from '../util/byte_string'; import { Code, FirestoreError } from '../util/error'; import { isPlainObject, valueDescription } from '../util/input_validation'; import { Dict, forEach, isEmpty } from '../util/obj'; +import { BsonBinaryData } from './bson_binary_data'; +import { BsonObjectId } from './bson_object_Id'; +import { BsonTimestampValue } from './bson_timestamp_value'; import { Bytes } from './bytes'; import { Firestore } from './database'; import { FieldPath } from './field_path'; import { FieldValue } from './field_value'; import { GeoPoint } from './geo_point'; +import { Int32Value } from './int32_value'; +import { MaxKey } from './max_key'; +import { MinKey } from './min_key'; import { DocumentReference, PartialWithFieldValue, WithFieldValue } from './reference'; +import { RegexValue } from './regex_value'; import { Timestamp } from './timestamp'; import { VectorValue } from './vector_value'; @@ -909,6 +928,20 @@ function parseScalarValue( }; } else if (value instanceof VectorValue) { return parseVectorValue(value, context); + } else if (value instanceof RegexValue) { + return parseRegexValue(value); + } else if (value instanceof BsonObjectId) { + return parseBsonObjectId(value); + } else if (value instanceof Int32Value) { + return parseInt32Value(value); + } else if (value instanceof BsonTimestampValue) { + return parseBsonTimestamp(value); + } else if (value instanceof BsonBinaryData) { + return parseBsonBinaryData(context.serializer, value); + } else if (value instanceof MinKey) { + return parseMinKey(); + } else if (value instanceof MaxKey) { + return parseMaxKey(); } else { throw context.createError( `Unsupported field value: ${valueDescription(value)}` @@ -926,7 +959,7 @@ export function parseVectorValue( const mapValue: ProtoMapValue = { fields: { [TYPE_KEY]: { - stringValue: VECTOR_VALUE_SENTINEL + stringValue: RESERVED_VECTOR_KEY }, [VECTOR_MAP_VECTORS_KEY]: { arrayValue: { @@ -947,6 +980,107 @@ export function parseVectorValue( return { mapValue }; } +export function parseRegexValue(value: RegexValue): ProtoValue { + const mapValue: ProtoMapValue = { + fields: { + [RESERVED_REGEX_KEY]: { + mapValue: { + fields: { + [RESERVED_REGEX_PATTERN_KEY]: { + stringValue: value.pattern + }, + [RESERVED_REGEX_OPTIONS_KEY]: { + stringValue: value.options + } + } + } + } + } + }; + + return { mapValue }; +} + +export function parseMinKey(): ProtoValue { + const mapValue: ProtoMapValue = { + fields: { + [RESERVED_MIN_KEY]: { + nullValue: 'NULL_VALUE' + } + } + }; + return { mapValue }; +} + +export function parseMaxKey(): ProtoValue { + const mapValue: ProtoMapValue = { + fields: { + [RESERVED_MAX_KEY]: { + nullValue: 'NULL_VALUE' + } + } + }; + return { mapValue }; +} + +export function parseBsonObjectId(value: BsonObjectId): ProtoValue { + const mapValue: ProtoMapValue = { + fields: { + [RESERVED_BSON_OBJECT_ID_KEY]: { + stringValue: value.value + } + } + }; + return { mapValue }; +} + +export function parseInt32Value(value: Int32Value): ProtoValue { + const mapValue: ProtoMapValue = { + fields: { + [RESERVED_INT32_KEY]: toInteger(value.value) + } + }; + return { mapValue }; +} + +export function parseBsonTimestamp(value: BsonTimestampValue): ProtoValue { + const mapValue: ProtoMapValue = { + fields: { + [RESERVED_BSON_TIMESTAMP_KEY]: { + mapValue: { + fields: { + [RESERVED_BSON_TIMESTAMP_SECONDS_KEY]: toInteger(value.seconds), + [RESERVED_BSON_TIMESTAMP_INCREMENT_KEY]: toInteger(value.increment) + } + } + } + } + }; + return { mapValue }; +} + +export function parseBsonBinaryData( + serializer: JsonProtoSerializer, + value: BsonBinaryData +): ProtoValue { + const subtypeAndData = new Uint8Array(value.data.length + 1); + // This converts the subtype from `number` to a byte. + subtypeAndData[0] = value.subtype; + // Concatenate the rest of the data starting at index 1. + subtypeAndData.set(value.data, /* offset */ 1); + + const mapValue: ProtoMapValue = { + fields: { + [RESERVED_BSON_BINARY_KEY]: { + bytesValue: toBytes( + serializer, + ByteString.fromUint8Array(subtypeAndData) + ) + } + } + }; + return { mapValue }; +} /** * Checks whether an object looks like a JSON object that should be converted * into a struct. Normal class/prototype instances are considered to look like @@ -965,7 +1099,14 @@ function looksLikeJsonObject(input: unknown): boolean { !(input instanceof Bytes) && !(input instanceof DocumentReference) && !(input instanceof FieldValue) && - !(input instanceof VectorValue) + !(input instanceof VectorValue) && + !(input instanceof MinKey) && + !(input instanceof MaxKey) && + !(input instanceof Int32Value) && + !(input instanceof RegexValue) && + !(input instanceof BsonObjectId) && + !(input instanceof BsonTimestampValue) && + !(input instanceof BsonBinaryData) ); } diff --git a/packages/firestore/src/lite-api/user_data_writer.ts b/packages/firestore/src/lite-api/user_data_writer.ts index e903991cb58..0de02b822b2 100644 --- a/packages/firestore/src/lite-api/user_data_writer.ts +++ b/packages/firestore/src/lite-api/user_data_writer.ts @@ -30,7 +30,19 @@ import { getPreviousValue } from '../model/server_timestamps'; import { TypeOrder } from '../model/type_order'; -import { VECTOR_MAP_VECTORS_KEY, typeOrder } from '../model/values'; +import { + RESERVED_BSON_BINARY_KEY, + RESERVED_INT32_KEY, + RESERVED_BSON_OBJECT_ID_KEY, + RESERVED_REGEX_KEY, + RESERVED_REGEX_OPTIONS_KEY, + RESERVED_REGEX_PATTERN_KEY, + RESERVED_BSON_TIMESTAMP_INCREMENT_KEY, + RESERVED_BSON_TIMESTAMP_KEY, + RESERVED_BSON_TIMESTAMP_SECONDS_KEY, + typeOrder, + VECTOR_MAP_VECTORS_KEY +} from '../model/values'; import { ApiClientObjectMap, ArrayValue as ProtoArrayValue, @@ -46,7 +58,13 @@ import { ByteString } from '../util/byte_string'; import { logError } from '../util/log'; import { forEach } from '../util/obj'; +import { BsonBinaryData } from './bson_binary_data'; +import { BsonObjectId } from './bson_object_Id'; +import { BsonTimestampValue } from './bson_timestamp_value'; +import { maxKey, minKey } from './field_value_impl'; import { GeoPoint } from './geo_point'; +import { Int32Value } from './int32_value'; +import { RegexValue } from './regex_value'; import { Timestamp } from './timestamp'; import { VectorValue } from './vector_value'; @@ -65,10 +83,16 @@ export abstract class AbstractUserDataWriter { ): unknown { switch (typeOrder(value)) { case TypeOrder.NullValue: + if ('mapValue' in value) { + return minKey(); + } return null; case TypeOrder.BooleanValue: return value.booleanValue!; case TypeOrder.NumberValue: + if ('mapValue' in value) { + return this.convertToInt32Value(value.mapValue!); + } return normalizeNumber(value.integerValue || value.doubleValue); case TypeOrder.TimestampValue: return this.convertTimestamp(value.timestampValue!); @@ -88,6 +112,16 @@ export abstract class AbstractUserDataWriter { return this.convertObject(value.mapValue!, serverTimestampBehavior); case TypeOrder.VectorValue: return this.convertVectorValue(value.mapValue!); + case TypeOrder.RegexValue: + return this.convertToRegexValue(value.mapValue!); + case TypeOrder.BsonObjectIdValue: + return this.convertToBsonObjectIdValue(value.mapValue!); + case TypeOrder.BsonBinaryValue: + return this.convertToBsonBinaryValue(value.mapValue!); + case TypeOrder.BsonTimestampValue: + return this.convertToBsonTimestampValue(value.mapValue!); + case TypeOrder.MaxKeyValue: + return maxKey(); default: throw fail('Invalid value type: ' + JSON.stringify(value)); } @@ -127,6 +161,60 @@ export abstract class AbstractUserDataWriter { return new VectorValue(values); } + private convertToBsonObjectIdValue(mapValue: ProtoMapValue): BsonObjectId { + const oid = + mapValue!.fields?.[RESERVED_BSON_OBJECT_ID_KEY]?.stringValue ?? ''; + return new BsonObjectId(oid); + } + + private convertToBsonBinaryValue(mapValue: ProtoMapValue): BsonBinaryData { + const fields = mapValue!.fields?.[RESERVED_BSON_BINARY_KEY]; + const subtypeAndData = fields?.bytesValue; + if (!subtypeAndData) { + throw new Error('Received incorrect bytesValue for BsonBinaryData'); + } + + const bytes = normalizeByteString(subtypeAndData).toUint8Array(); + if (bytes.length === 0) { + throw new Error('Received empty bytesValue for BsonBinaryData'); + } + const subtype = bytes.at(0); + const data = bytes.slice(1); + return new BsonBinaryData(Number(subtype), data); + } + + private convertToBsonTimestampValue( + mapValue: ProtoMapValue + ): BsonTimestampValue { + const fields = mapValue!.fields?.[RESERVED_BSON_TIMESTAMP_KEY]; + const seconds = Number( + fields?.mapValue?.fields?.[RESERVED_BSON_TIMESTAMP_SECONDS_KEY] + ?.integerValue + ); + const increment = Number( + fields?.mapValue?.fields?.[RESERVED_BSON_TIMESTAMP_INCREMENT_KEY] + ?.integerValue + ); + return new BsonTimestampValue(seconds, increment); + } + + private convertToRegexValue(mapValue: ProtoMapValue): RegexValue { + const pattern = + mapValue!.fields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[ + RESERVED_REGEX_PATTERN_KEY + ]?.stringValue ?? ''; + const options = + mapValue!.fields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[ + RESERVED_REGEX_OPTIONS_KEY + ]?.stringValue ?? ''; + return new RegexValue(pattern, options); + } + + private convertToInt32Value(mapValue: ProtoMapValue): Int32Value { + const value = Number(mapValue!.fields?.[RESERVED_INT32_KEY]?.integerValue); + return new Int32Value(value); + } + private convertGeoPoint(value: ProtoLatLng): GeoPoint { return new GeoPoint( normalizeNumber(value.latitude), diff --git a/packages/firestore/src/model/object_value.ts b/packages/firestore/src/model/object_value.ts index d5cb273eb9d..35d8733e40a 100644 --- a/packages/firestore/src/model/object_value.ts +++ b/packages/firestore/src/model/object_value.ts @@ -25,7 +25,7 @@ import { forEach } from '../util/obj'; import { FieldMask } from './field_mask'; import { FieldPath } from './path'; import { isServerTimestamp } from './server_timestamps'; -import { deepClone, isMapValue, valueEquals } from './values'; +import { deepClone, isBsonType, isMapValue, valueEquals } from './values'; export interface JsonObject { [name: string]: T; @@ -188,7 +188,8 @@ export function extractFieldMask(value: ProtoMapValue): FieldMask { const fields: FieldPath[] = []; forEach(value!.fields, (key, value) => { const currentPath = new FieldPath([key]); - if (isMapValue(value)) { + // BSON types do not need to extract reserved keys, ie,__regex__. + if (isMapValue(value) && !isBsonType(value)) { const nestedMask = extractFieldMask(value.mapValue!); const nestedFields = nestedMask.fields; if (nestedFields.length === 0) { diff --git a/packages/firestore/src/model/type_order.ts b/packages/firestore/src/model/type_order.ts index 749b8e8036d..7ac6ed11e47 100644 --- a/packages/firestore/src/model/type_order.ts +++ b/packages/firestore/src/model/type_order.ts @@ -24,18 +24,27 @@ */ export const enum TypeOrder { // This order is based on the backend's ordering, but modified to support - // server timestamps and `MAX_VALUE`. + // server timestamps and `MAX_VALUE` inside the SDK. + // NULL and MIN_KEY sort the same. NullValue = 0, + MinKeyValue = 0, BooleanValue = 1, NumberValue = 2, TimestampValue = 3, - ServerTimestampValue = 4, - StringValue = 5, - BlobValue = 6, - RefValue = 7, - GeoPointValue = 8, - ArrayValue = 9, - VectorValue = 10, - ObjectValue = 11, + // TODO(Mila/BSON): which should come first considering indexes? + BsonTimestampValue = 4, + ServerTimestampValue = 5, + StringValue = 6, + BlobValue = 7, + BsonBinaryValue = 8, + RefValue = 9, + BsonObjectIdValue = 10, + GeoPointValue = 11, + RegexValue = 12, + ArrayValue = 13, + VectorValue = 14, + ObjectValue = 15, + // TODO(Mila/BSON):should MaxKeyValue and MaxValue combined? how would this affect indexes? + MaxKeyValue = 16, MaxValue = 9007199254740991 // Number.MAX_SAFE_INTEGER } diff --git a/packages/firestore/src/model/values.ts b/packages/firestore/src/model/values.ts index 1977767515e..d6344409ed9 100644 --- a/packages/firestore/src/model/values.ts +++ b/packages/firestore/src/model/values.ts @@ -35,28 +35,54 @@ import { normalizeNumber, normalizeTimestamp } from './normalize'; -import { - getLocalWriteTime, - getPreviousValue, - isServerTimestamp -} from './server_timestamps'; +import { getLocalWriteTime, getPreviousValue } from './server_timestamps'; import { TypeOrder } from './type_order'; export const TYPE_KEY = '__type__'; -const MAX_VALUE_TYPE = '__max__'; -export const MAX_VALUE: Value = { + +export const RESERVED_VECTOR_KEY = '__vector__'; +export const VECTOR_MAP_VECTORS_KEY = 'value'; + +const RESERVED_SERVER_TIMESTAMP_KEY = 'server_timestamp'; + +export const RESERVED_MIN_KEY = '__min__'; +export const RESERVED_MAX_KEY = '__max__'; + +export const RESERVED_REGEX_KEY = '__regex__'; +export const RESERVED_REGEX_PATTERN_KEY = 'pattern'; +export const RESERVED_REGEX_OPTIONS_KEY = 'options'; + +export const RESERVED_BSON_OBJECT_ID_KEY = '__oid__'; + +export const RESERVED_INT32_KEY = '__int__'; + +export const RESERVED_BSON_TIMESTAMP_KEY = '__request_timestamp__'; +export const RESERVED_BSON_TIMESTAMP_SECONDS_KEY = 'seconds'; +export const RESERVED_BSON_TIMESTAMP_INCREMENT_KEY = 'increment'; + +export const RESERVED_BSON_BINARY_KEY = '__binary__'; + +export const INTERNAL_MIN_VALUE: Value = { + nullValue: 'NULL_VALUE' +}; + +export const INTERNAL_MAX_VALUE: Value = { mapValue: { fields: { - '__type__': { stringValue: MAX_VALUE_TYPE } + '__type__': { stringValue: RESERVED_MAX_KEY } } } }; -export const VECTOR_VALUE_SENTINEL = '__vector__'; -export const VECTOR_MAP_VECTORS_KEY = 'value'; - -export const MIN_VALUE: Value = { - nullValue: 'NULL_VALUE' +export const MIN_VECTOR_VALUE = { + mapValue: { + fields: { + [TYPE_KEY]: { stringValue: RESERVED_VECTOR_KEY }, + [VECTOR_MAP_VECTORS_KEY]: { + arrayValue: {} + } + } + } }; /** Extracts the backend's type order for the provided value. */ @@ -80,14 +106,31 @@ export function typeOrder(value: Value): TypeOrder { } else if ('arrayValue' in value) { return TypeOrder.ArrayValue; } else if ('mapValue' in value) { - if (isServerTimestamp(value)) { - return TypeOrder.ServerTimestampValue; - } else if (isMaxValue(value)) { - return TypeOrder.MaxValue; - } else if (isVectorValue(value)) { - return TypeOrder.VectorValue; + const valueType = detectSpecialMapType(value); + switch (valueType) { + case 'serverTimestampValue': + return TypeOrder.ServerTimestampValue; + case 'maxValue': + return TypeOrder.MaxValue; + case 'vectorValue': + return TypeOrder.VectorValue; + case 'regexValue': + return TypeOrder.RegexValue; + case 'bsonObjectIdValue': + return TypeOrder.BsonObjectIdValue; + case 'int32Value': + return TypeOrder.NumberValue; + case 'bsonTimestampValue': + return TypeOrder.BsonTimestampValue; + case 'bsonBinaryValue': + return TypeOrder.BsonBinaryValue; + case 'minKeyValue': + return TypeOrder.MinKeyValue; + case 'maxKeyValue': + return TypeOrder.MaxKeyValue; + default: + return TypeOrder.ObjectValue; } - return TypeOrder.ObjectValue; } else { return fail('Invalid value type: ' + JSON.stringify(value)); } @@ -107,6 +150,11 @@ export function valueEquals(left: Value, right: Value): boolean { switch (leftType) { case TypeOrder.NullValue: + case TypeOrder.MaxValue: + // MaxKeys are all equal. + case TypeOrder.MaxKeyValue: + // MinKeys are all equal. + case TypeOrder.MinKeyValue: return true; case TypeOrder.BooleanValue: return left.booleanValue === right.booleanValue; @@ -133,8 +181,14 @@ export function valueEquals(left: Value, right: Value): boolean { case TypeOrder.VectorValue: case TypeOrder.ObjectValue: return objectEquals(left, right); - case TypeOrder.MaxValue: - return true; + case TypeOrder.BsonBinaryValue: + return compareBsonBinaryData(left, right) === 0; + case TypeOrder.BsonTimestampValue: + return compareBsonTimestamps(left, right) === 0; + case TypeOrder.RegexValue: + return compareRegex(left, right) === 0; + case TypeOrder.BsonObjectIdValue: + return compareBsonObjectIds(left, right) === 0; default: return fail('Unexpected value type: ' + JSON.stringify(left)); } @@ -174,10 +228,12 @@ function blobEquals(left: Value, right: Value): boolean { } export function numberEquals(left: Value, right: Value): boolean { - if ('integerValue' in left && 'integerValue' in right) { - return ( - normalizeNumber(left.integerValue) === normalizeNumber(right.integerValue) - ); + if ( + ('integerValue' in left && 'integerValue' in right) || + (detectSpecialMapType(left) === 'int32Value' && + detectSpecialMapType(right) === 'int32Value') + ) { + return extractNumber(left) === extractNumber(right); } else if ('doubleValue' in left && 'doubleValue' in right) { const n1 = normalizeNumber(left.doubleValue!); const n2 = normalizeNumber(right.doubleValue!); @@ -237,6 +293,8 @@ export function valueCompare(left: Value, right: Value): number { switch (leftType) { case TypeOrder.NullValue: + case TypeOrder.MinKeyValue: + case TypeOrder.MaxKeyValue: case TypeOrder.MaxValue: return 0; case TypeOrder.BooleanValue: @@ -264,14 +322,33 @@ export function valueCompare(left: Value, right: Value): number { return compareVectors(left.mapValue!, right.mapValue!); case TypeOrder.ObjectValue: return compareMaps(left.mapValue!, right.mapValue!); + case TypeOrder.BsonTimestampValue: + return compareBsonTimestamps(left, right); + case TypeOrder.BsonBinaryValue: + return compareBsonBinaryData(left, right); + case TypeOrder.RegexValue: + return compareRegex(left, right); + case TypeOrder.BsonObjectIdValue: + return compareBsonObjectIds(left, right); + default: throw fail('Invalid value type: ' + leftType); } } +export function extractNumber(value: Value): number { + let numberValue; + if (detectSpecialMapType(value) === 'int32Value') { + numberValue = value.mapValue!.fields![RESERVED_INT32_KEY].integerValue!; + } else { + numberValue = value.integerValue || value.doubleValue; + } + return normalizeNumber(numberValue); +} + function compareNumbers(left: Value, right: Value): number { - const leftNumber = normalizeNumber(left.integerValue || left.doubleValue); - const rightNumber = normalizeNumber(right.integerValue || right.doubleValue); + const leftNumber = extractNumber(left); + const rightNumber = extractNumber(right); if (leftNumber < rightNumber) { return -1; @@ -379,11 +456,14 @@ function compareVectors(left: MapValue, right: MapValue): number { } function compareMaps(left: MapValue, right: MapValue): number { - if (left === MAX_VALUE.mapValue && right === MAX_VALUE.mapValue) { + if ( + left === INTERNAL_MAX_VALUE.mapValue && + right === INTERNAL_MAX_VALUE.mapValue + ) { return 0; - } else if (left === MAX_VALUE.mapValue) { + } else if (left === INTERNAL_MAX_VALUE.mapValue) { return 1; - } else if (right === MAX_VALUE.mapValue) { + } else if (right === INTERNAL_MAX_VALUE.mapValue) { return -1; } @@ -413,6 +493,80 @@ function compareMaps(left: MapValue, right: MapValue): number { return primitiveComparator(leftKeys.length, rightKeys.length); } +function compareBsonTimestamps(left: Value, right: Value): number { + const leftSecondField = + left.mapValue!.fields?.[RESERVED_BSON_TIMESTAMP_KEY].mapValue?.fields?.[ + RESERVED_BSON_TIMESTAMP_SECONDS_KEY + ]; + const rightSecondField = + right.mapValue!.fields?.[RESERVED_BSON_TIMESTAMP_KEY].mapValue?.fields?.[ + RESERVED_BSON_TIMESTAMP_SECONDS_KEY + ]; + + const leftIncrementField = + left.mapValue!.fields?.[RESERVED_BSON_TIMESTAMP_KEY].mapValue?.fields?.[ + RESERVED_BSON_TIMESTAMP_INCREMENT_KEY + ]; + const rightIncrementField = + right.mapValue!.fields?.[RESERVED_BSON_TIMESTAMP_KEY].mapValue?.fields?.[ + RESERVED_BSON_TIMESTAMP_INCREMENT_KEY + ]; + + const secondsDiff = compareNumbers(leftSecondField!, rightSecondField!); + return secondsDiff !== 0 + ? secondsDiff + : compareNumbers(leftIncrementField!, rightIncrementField!); +} + +function compareBsonBinaryData(left: Value, right: Value): number { + const leftBytes = + left.mapValue!.fields?.[RESERVED_BSON_BINARY_KEY]?.bytesValue; + const rightBytes = + right.mapValue!.fields?.[RESERVED_BSON_BINARY_KEY]?.bytesValue; + if (!rightBytes || !leftBytes) { + throw new Error('Received incorrect bytesValue for BsonBinaryData'); + } + return compareBlobs(leftBytes, rightBytes); +} + +function compareRegex(left: Value, right: Value): number { + const leftFields = left.mapValue!.fields; + const leftPattern = + leftFields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[ + RESERVED_REGEX_PATTERN_KEY + ]?.stringValue ?? ''; + const leftOptions = + leftFields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[ + RESERVED_REGEX_OPTIONS_KEY + ]?.stringValue ?? ''; + + const rightFields = right.mapValue!.fields; + const rightPattern = + rightFields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[ + RESERVED_REGEX_PATTERN_KEY + ]?.stringValue ?? ''; + const rightOptions = + rightFields?.[RESERVED_REGEX_KEY]?.mapValue?.fields?.[ + RESERVED_REGEX_OPTIONS_KEY + ]?.stringValue ?? ''; + + // First order by patterns, and then options. + const patternDiff = primitiveComparator(leftPattern, rightPattern); + return patternDiff !== 0 + ? patternDiff + : primitiveComparator(leftOptions, rightOptions); +} + +function compareBsonObjectIds(left: Value, right: Value): number { + const leftOid = + left.mapValue!.fields?.[RESERVED_BSON_OBJECT_ID_KEY]?.stringValue ?? ''; + const rightOid = + right.mapValue!.fields?.[RESERVED_BSON_OBJECT_ID_KEY]?.stringValue ?? ''; + + // TODO(Mila/BSON): use compareUtf8Strings once the bug fix is merged. + return primitiveComparator(leftOid, rightOid); +} + /** * Generates the canonical ID for the provided field value (as used in Target * serialization). @@ -443,6 +597,10 @@ function canonifyValue(value: Value): string { } else if ('arrayValue' in value) { return canonifyArray(value.arrayValue!); } else if ('mapValue' in value) { + // BsonBinaryValue contains an array of bytes, and needs to extract `subtype` and `data` from it before canonifying. + if (detectSpecialMapType(value) === 'bsonBinaryValue') { + return canonifyBsonBinaryData(value.mapValue!); + } return canonifyMap(value.mapValue!); } else { return fail('Invalid value type: ' + JSON.stringify(value)); @@ -466,6 +624,19 @@ function canonifyReference(referenceValue: string): string { return DocumentKey.fromName(referenceValue).toString(); } +function canonifyBsonBinaryData(mapValue: MapValue): string { + const fields = mapValue!.fields?.[RESERVED_BSON_BINARY_KEY]; + const subtypeAndData = fields?.bytesValue; + if (!subtypeAndData) { + throw new Error('Received incorrect bytesValue for BsonBinaryData'); + } + // Normalize the bytesValue to Uint8Array before extracting subtype and data. + const bytes = normalizeByteString(subtypeAndData).toUint8Array(); + return `{__binary__:{subType:${bytes.at(0)},data:${canonifyByteString( + bytes.slice(1) + )}}}`; +} + function canonifyMap(mapValue: MapValue): string { // Iteration order in JavaScript is not guaranteed. To ensure that we generate // matching canonical IDs for identical maps, we need to sort the keys. @@ -508,10 +679,16 @@ function canonifyArray(arrayValue: ArrayValue): string { export function estimateByteSize(value: Value): number { switch (typeOrder(value)) { case TypeOrder.NullValue: + // MinKeyValue and NullValue has same TypeOrder number, but MinKeyValue is encoded as MapValue + // and its size should be estimated differently. + if ('mapValue' in value) { + return estimateMapByteSize(value.mapValue!); + } return 4; case TypeOrder.BooleanValue: return 4; case TypeOrder.NumberValue: + // TODO(Mila/BSON): return 16 if the value is 128 decimal value return 8; case TypeOrder.TimestampValue: // Timestamps are made up of two distinct numbers (seconds + nanoseconds) @@ -535,6 +712,11 @@ export function estimateByteSize(value: Value): number { return estimateArrayByteSize(value.arrayValue!); case TypeOrder.VectorValue: case TypeOrder.ObjectValue: + case TypeOrder.RegexValue: + case TypeOrder.BsonObjectIdValue: + case TypeOrder.BsonBinaryValue: + case TypeOrder.BsonTimestampValue: + case TypeOrder.MaxKeyValue: return estimateMapByteSize(value.mapValue!); default: throw fail('Invalid value type: ' + JSON.stringify(value)); @@ -619,10 +801,67 @@ export function isMapValue( return !!value && 'mapValue' in value; } -/** Returns true if `value` is a VetorValue. */ +/** Returns true if `value` is a VectorValue. */ export function isVectorValue(value: ProtoValue | null): boolean { - const type = (value?.mapValue?.fields || {})[TYPE_KEY]?.stringValue; - return type === VECTOR_VALUE_SENTINEL; + return !!value && detectSpecialMapType(value) === 'vectorValue'; +} + +/** Returns true if the `Value` represents the canonical {@link #INTERNAL_MAX_VALUE} . */ +export function isMaxValue(value: Value): boolean { + return detectSpecialMapType(value) === 'maxValue'; +} + +function detectSpecialMapType(value: Value): string { + if (!value || !value.mapValue || !value.mapValue.fields) { + return ''; // Not a special map type + } + + const fields = value.mapValue.fields; + + // Check for type-based mappings + const type = fields[TYPE_KEY]?.stringValue; + if (type) { + const typeMap: Record = { + [RESERVED_VECTOR_KEY]: 'vectorValue', + [RESERVED_MAX_KEY]: 'maxValue', + [RESERVED_SERVER_TIMESTAMP_KEY]: 'serverTimestampValue' + }; + if (typeMap[type]) { + return typeMap[type]; + } + } + + // Check for BSON-related mappings + const bsonMap: Record = { + [RESERVED_REGEX_KEY]: 'regexValue', + [RESERVED_BSON_OBJECT_ID_KEY]: 'bsonObjectIdValue', + [RESERVED_INT32_KEY]: 'int32Value', + [RESERVED_BSON_TIMESTAMP_KEY]: 'bsonTimestampValue', + [RESERVED_BSON_BINARY_KEY]: 'bsonBinaryValue', + [RESERVED_MIN_KEY]: 'minKeyValue', + [RESERVED_MAX_KEY]: 'maxKeyValue' + }; + + for (const key in bsonMap) { + if (fields[key]) { + return bsonMap[key]; + } + } + + return ''; +} + +export function isBsonType(value: Value): boolean { + const bsonTypes = new Set([ + 'regexValue', + 'bsonObjectIdValue', + 'int32Value', + 'bsonTimestampValue', + 'bsonBinaryValue', + 'minKeyValue', + 'maxKeyValue' + ]); + return bsonTypes.has(detectSpecialMapType(value)); } /** Creates a deep copy of `source`. */ @@ -652,29 +891,10 @@ export function deepClone(source: Value): Value { } } -/** Returns true if the Value represents the canonical {@link #MAX_VALUE} . */ -export function isMaxValue(value: Value): boolean { - return ( - (((value.mapValue || {}).fields || {})['__type__'] || {}).stringValue === - MAX_VALUE_TYPE - ); -} - -export const MIN_VECTOR_VALUE = { - mapValue: { - fields: { - [TYPE_KEY]: { stringValue: VECTOR_VALUE_SENTINEL }, - [VECTOR_MAP_VECTORS_KEY]: { - arrayValue: {} - } - } - } -}; - /** Returns the lowest value for the given value type (inclusive). */ export function valuesGetLowerBound(value: Value): Value { if ('nullValue' in value) { - return MIN_VALUE; + return INTERNAL_MIN_VALUE; } else if ('booleanValue' in value) { return { booleanValue: false }; } else if ('integerValue' in value || 'doubleValue' in value) { @@ -692,6 +912,7 @@ export function valuesGetLowerBound(value: Value): Value { } else if ('arrayValue' in value) { return { arrayValue: {} }; } else if ('mapValue' in value) { + // TODO(Mila/BSON): add lower bound for bson types for indexing if (isVectorValue(value)) { return MIN_VECTOR_VALUE; } @@ -722,10 +943,11 @@ export function valuesGetUpperBound(value: Value): Value { } else if ('arrayValue' in value) { return MIN_VECTOR_VALUE; } else if ('mapValue' in value) { + // TODO(Mila/BSON): add upper bound for bson types for indexing if (isVectorValue(value)) { return { mapValue: {} }; } - return MAX_VALUE; + return INTERNAL_MAX_VALUE; } else { return fail('Invalid value type: ' + JSON.stringify(value)); } diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 1cda49d9229..2856862f6de 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -20,6 +20,7 @@ import { Deferred } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import { AutoId } from '../../../src/util/misc'; import { EventsAccumulator } from '../util/events_accumulator'; import { addDoc, @@ -67,7 +68,15 @@ import { FirestoreError, QuerySnapshot, vector, - getDocsFromServer + getDocsFromServer, + bsonBinaryData, + bsonObjectId, + bsonTimestamp, + int32, + maxKey, + minKey, + regex, + or } from '../util/firebase_export'; import { apiDescribe, @@ -2424,4 +2433,433 @@ apiDescribe('Database', persistence => { }); }); }); + + describe('BSON types', () => { + // TODO(Mila/BSON): simplify the test setup once prod support BSON + const NIGHTLY_PROJECT_ID = 'firestore-sdk-nightly'; + const settings = { + ...DEFAULT_SETTINGS, + host: 'test-firestore.sandbox.googleapis.com' + }; + + it('can write and read BSON types', async () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + + const docRef = await addDoc(coll, { + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + objectId: bsonObjectId('507f191e810c19729de860ea'), + int32: int32(1), + min: minKey(), + max: maxKey(), + regex: regex('^foo', 'i') + }); + + await setDoc( + docRef, + { + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: bsonTimestamp(1, 2), + int32: int32(2) + }, + { merge: true } + ); + + const snapshot = await getDoc(docRef); + expect( + snapshot + .get('objectId') + .isEqual(bsonObjectId('507f191e810c19729de860ea')) + ).to.be.true; + expect(snapshot.get('int32').isEqual(int32(2))).to.be.true; + expect(snapshot.get('min') === minKey()).to.be.true; + expect(snapshot.get('max') === maxKey()).to.be.true; + expect( + snapshot + .get('binary') + .isEqual(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ).to.be.true; + expect(snapshot.get('timestamp').isEqual(bsonTimestamp(1, 2))).to.be + .true; + expect(snapshot.get('regex').isEqual(regex('^foo', 'i'))).to.be.true; + } + ); + }); + + it('can filter and order objectIds', async () => { + const testDocs = { + a: { key: bsonObjectId('507f191e810c19729de860ea') }, + b: { key: bsonObjectId('507f191e810c19729de860eb') }, + c: { key: bsonObjectId('507f191e810c19729de860ec') } + }; + + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + await addDoc(coll, testDocs['a']); + await addDoc(coll, testDocs['b']); + await addDoc(coll, testDocs['c']); + + let orderedQuery = query( + coll, + where('key', '>', bsonObjectId('507f191e810c19729de860ea')), + orderBy('key', 'desc') + ); + + let snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['c'], + testDocs['b'] + ]); + + orderedQuery = query( + coll, + where('key', 'in', [ + bsonObjectId('507f191e810c19729de860ea'), + bsonObjectId('507f191e810c19729de860eb') + ]), + orderBy('key', 'desc') + ); + + snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['b'], + testDocs['a'] + ]); + } + ); + }); + + it('can filter and order Int32 values', async () => { + const testDocs = { + a: { key: int32(-1) }, + b: { key: int32(1) }, + c: { key: int32(2) } + }; + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + await addDoc(coll, testDocs['a']); + await addDoc(coll, testDocs['b']); + await addDoc(coll, testDocs['c']); + + let orderedQuery = query( + coll, + where('key', '>=', int32(1)), + orderBy('key', 'desc') + ); + + let snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['c'], + testDocs['b'] + ]); + + orderedQuery = query( + coll, + where('key', 'not-in', [int32(1)]), + orderBy('key', 'desc') + ); + + snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['c'], + testDocs['a'] + ]); + } + ); + }); + + it('can filter and order Timestamp values', async () => { + const testDocs = { + a: { key: bsonTimestamp(1, 1) }, + b: { key: bsonTimestamp(1, 2) }, + c: { key: bsonTimestamp(2, 1) } + }; + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + await addDoc(coll, testDocs['a']); + await addDoc(coll, testDocs['b']); + await addDoc(coll, testDocs['c']); + + let orderedQuery = query( + coll, + where('key', '>', bsonTimestamp(1, 1)), + orderBy('key', 'desc') + ); + + let snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['c'], + testDocs['b'] + ]); + + orderedQuery = query( + coll, + where('key', '!=', bsonTimestamp(1, 1)), + orderBy('key', 'desc') + ); + + snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['c'], + testDocs['b'] + ]); + } + ); + }); + + it('can filter and order Binary values', async () => { + const testDocs = { + a: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, + b: { key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) }, + c: { key: bsonBinaryData(2, new Uint8Array([1, 2, 3])) } + }; + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + await addDoc(coll, testDocs['a']); + await addDoc(coll, testDocs['b']); + await addDoc(coll, testDocs['c']); + + let orderedQuery = query( + coll, + where('key', '>', bsonBinaryData(1, new Uint8Array([1, 2, 3]))), + orderBy('key', 'desc') + ); + + let snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['c'], + testDocs['b'] + ]); + + orderedQuery = query( + coll, + where('key', '>=', bsonBinaryData(1, new Uint8Array([1, 2, 3]))), + where('key', '<', bsonBinaryData(2, new Uint8Array([1, 2, 3]))), + orderBy('key', 'desc') + ); + + snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['b'], + testDocs['a'] + ]); + } + ); + }); + + it('can filter and order Regex values', async () => { + const testDocs = { + a: { key: regex('^bar', 'i') }, + b: { key: regex('^bar', 'x') }, + c: { key: regex('^baz', 'i') } + }; + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + await addDoc(coll, testDocs['a']); + await addDoc(coll, testDocs['b']); + await addDoc(coll, testDocs['c']); + + const orderedQuery = query( + coll, + or( + where('key', '>', regex('^bar', 'x')), + where('key', '!=', regex('^bar', 'x')) + ), + orderBy('key', 'desc') + ); + + const snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['c'], + testDocs['a'] + ]); + } + ); + }); + + it('can filter and order minKey values', async () => { + const testDocs = { + a: { key: minKey() }, + b: { key: minKey() }, + c: { key: maxKey() } + }; + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + await addDoc(coll, testDocs['a']); + await addDoc(coll, testDocs['b']); + await addDoc(coll, testDocs['c']); + + const orderedQuery = query( + coll, + where('key', '==', minKey()), + orderBy('key', 'desc') // minKeys are equal, would sort by documentId as secondary order + ); + const snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['b'], + testDocs['a'] + ]); + } + ); + }); + + it('can filter and order maxKey values', async () => { + const testDocs = { + a: { key: minKey() }, + b: { key: maxKey() }, + c: { key: maxKey() } + }; + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + await addDoc(coll, testDocs['a']); + await addDoc(coll, testDocs['b']); + await addDoc(coll, testDocs['c']); + + const orderedQuery = query( + coll, + where('key', '==', maxKey()), + orderBy('key', 'desc') // maxKeys are equal, would sort by documentId as secondary order + ); + const snapshot = await getDocs(orderedQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['c'], + testDocs['b'] + ]); + } + ); + }); + + it('can listen to documents with bson types', async () => { + const testDocs = { + a: { key: maxKey() }, + b: { key: minKey() }, + c: { key: bsonTimestamp(1, 2) }, + d: { key: bsonObjectId('507f191e810c19729de860ea') }, + e: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, + f: { key: regex('^foo', 'i') } + }; + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + await addDoc(coll, testDocs['a']); + await addDoc(coll, testDocs['b']); + await addDoc(coll, testDocs['c']); + await addDoc(coll, testDocs['d']); + await addDoc(coll, testDocs['e']); + await addDoc(coll, testDocs['f']); + + const orderedQuery = query(coll, orderBy('key', 'asc')); + + const storeEvent = new EventsAccumulator(); + const unsubscribe = onSnapshot(orderedQuery, storeEvent.storeEvent); + + let listenSnapshot = await storeEvent.awaitEvent(); + expect(toDataArray(listenSnapshot)).to.deep.equal([ + testDocs['b'], + testDocs['c'], + testDocs['e'], + testDocs['d'], + testDocs['f'], + testDocs['a'] + ]); + + const newData = { key: int32(2) }; + await setDoc(doc(coll, 'g'), newData); + listenSnapshot = await storeEvent.awaitEvent(); + expect(toDataArray(listenSnapshot)).to.deep.equal([ + testDocs['b'], + newData, + testDocs['c'], + testDocs['e'], + testDocs['d'], + testDocs['f'], + testDocs['a'] + ]); + + unsubscribe(); + } + ); + }); + + // TODO(Mila/BSON): Skip the runTransaction tests against nightly when running on browsers. remove when it is supported by prod + // eslint-disable-next-line no-restricted-properties + it.skip('can run transactions on documents with bson types', async () => { + const testDocs = { + a: { key: bsonTimestamp(1, 2) }, + b: { key: regex('^foo', 'i') }, + c: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) } + }; + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + const docA = await addDoc(coll, testDocs['a']); + const docB = await addDoc(coll, { key: 'place holder' }); + const docC = await addDoc(coll, testDocs['c']); + + await runTransaction(dbs[0], async transaction => { + const docSnapshot = await transaction.get(docA); + expect(docSnapshot.data()).to.deep.equal(testDocs['a']); + transaction.set(docB, testDocs['b']); + transaction.delete(docC); + }); + + const orderedQuery = query(coll, orderBy('key', 'asc')); + const snapshot = await getDocs(orderedQuery); + + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['a'], + testDocs['b'] + ]); + } + ); + }); + }); }); diff --git a/packages/firestore/test/integration/api/type.test.ts b/packages/firestore/test/integration/api/type.test.ts index 0fd9c19ccad..a6218f6a1ad 100644 --- a/packages/firestore/test/integration/api/type.test.ts +++ b/packages/firestore/test/integration/api/type.test.ts @@ -17,25 +17,46 @@ import { expect } from 'chai'; +import { AutoId } from '../../../src/util/misc'; import { addEqualityMatcher } from '../../util/equality_matcher'; import { EventsAccumulator } from '../util/events_accumulator'; import { + bsonBinaryData, + bsonObjectId, + bsonTimestamp, Bytes, collection, doc, + DocumentData, + DocumentReference, DocumentSnapshot, Firestore, + FirestoreError, GeoPoint, getDoc, getDocs, + int32, + maxKey, + minKey, onSnapshot, + orderBy, + query, QuerySnapshot, + refEqual, + regex, runTransaction, setDoc, Timestamp, - updateDoc + updateDoc, + vector } from '../util/firebase_export'; -import { apiDescribe, withTestDb, withTestDoc } from '../util/helpers'; +import { + apiDescribe, + withTestDb, + withTestDbsSettings, + withTestDoc +} from '../util/helpers'; +import { DEFAULT_SETTINGS } from '../util/settings'; apiDescribe('Firestore', persistence => { addEqualityMatcher(); @@ -82,6 +103,43 @@ apiDescribe('Firestore', persistence => { return docSnapshot; } + // TODO(Mila/BSON): Transactions against nightly is having issue, remove this after prod supports BSON + async function expectRoundtripWithoutTransaction( + db: Firestore, + data: {}, + validateSnapshots = true, + expectedData?: {} + ): Promise { + expectedData = expectedData ?? data; + + const collRef = collection(db, doc(collection(db, 'a')).id); + const docRef = doc(collRef); + + await setDoc(docRef, data); + let docSnapshot = await getDoc(docRef); + expect(docSnapshot.data()).to.deep.equal(expectedData); + + await updateDoc(docRef, data); + docSnapshot = await getDoc(docRef); + expect(docSnapshot.data()).to.deep.equal(expectedData); + + if (validateSnapshots) { + let querySnapshot = await getDocs(collRef); + docSnapshot = querySnapshot.docs[0]; + expect(docSnapshot.data()).to.deep.equal(expectedData); + + const eventsAccumulator = new EventsAccumulator(); + const unlisten = onSnapshot(collRef, eventsAccumulator.storeEvent); + querySnapshot = await eventsAccumulator.awaitEvent(); + docSnapshot = querySnapshot.docs[0]; + expect(docSnapshot.data()).to.deep.equal(expectedData); + + unlisten(); + } + + return docSnapshot; + } + it('can read and write null fields', () => { return withTestDb(persistence, async db => { await expectRoundtrip(db, { a: 1, b: null }); @@ -177,4 +235,337 @@ apiDescribe('Firestore', persistence => { await expectRoundtrip(db, { a: 42, refs: [doc] }); }); }); + + it('can read and write vector fields', () => { + return withTestDoc(persistence, async (doc, db) => { + await expectRoundtrip(db, { vector: vector([1, 2, 3]) }); + }); + }); + + // TODO(Mila/BSON): simplify the test setup once prod support BSON + const NIGHTLY_PROJECT_ID = 'firestore-sdk-nightly'; + const settings = { + ...DEFAULT_SETTINGS, + host: 'test-firestore.sandbox.googleapis.com' + }; + + it('can read and write minKey fields', () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + await expectRoundtripWithoutTransaction(dbs[0], { min: minKey() }); + } + ); + }); + + it('can read and write maxKey fields', () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + await expectRoundtripWithoutTransaction(dbs[0], { max: maxKey() }); + } + ); + }); + + it('can read and write regex fields', () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + await expectRoundtripWithoutTransaction(dbs[0], { + regex: regex('^foo', 'i') + }); + } + ); + }); + + it('can read and write int32 fields', () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + await expectRoundtripWithoutTransaction(dbs[0], { int32: int32(1) }); + } + ); + }); + + it('can read and write bsonTimestamp fields', () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + await expectRoundtripWithoutTransaction(dbs[0], { + bsonTimestamp: bsonTimestamp(1, 2) + }); + } + ); + }); + + it('can read and write bsonObjectId fields', () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + await expectRoundtripWithoutTransaction(dbs[0], { + objectId: bsonObjectId('507f191e810c19729de860ea') + }); + } + ); + }); + + it('can read and write bsonBinaryData fields', () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + await expectRoundtripWithoutTransaction(dbs[0], { + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + }); + } + ); + }); + + it('can read and write bson fields in an array', () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + await expectRoundtripWithoutTransaction(dbs[0], { + array: [ + bsonBinaryData(1, new Uint8Array([1, 2, 3])), + bsonObjectId('507f191e810c19729de860ea'), + int32(1), + minKey(), + maxKey(), + regex('^foo', 'i') + ] + }); + } + ); + }); + + it('can read and write bson fields in an object', () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + await expectRoundtripWithoutTransaction(dbs[0], { + object: { + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + objectId: bsonObjectId('507f191e810c19729de860ea'), + int32: int32(1), + min: minKey(), + max: maxKey(), + regex: regex('^foo', 'i') + } + }); + } + ); + }); + + it('invalid 32-bit integer gets rejected', async () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const docRef = doc(dbs[0], 'test-collection/test-doc'); + let errorMessage; + try { + await setDoc(docRef, { key: int32(2147483648) }); + } catch (err) { + errorMessage = (err as FirestoreError)?.message; + } + expect(errorMessage).to.contains( + "The field '__int__' value (2,147,483,648) is too large to be converted to a 32-bit integer." + ); + + try { + await setDoc(docRef, { key: int32(-2147483650) }); + } catch (err) { + errorMessage = (err as FirestoreError)?.message; + } + expect(errorMessage).to.contains( + "The field '__int__' value (-2,147,483,650) is too large to be converted to a 32-bit integer." + ); + } + ); + }); + + it('invalid BSON timestamp gets rejected', async () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const docRef = doc(dbs[0], 'test-collection/test-doc'); + let errorMessage; + try { + // BSON timestamp larger than 32-bit integer gets rejected + await setDoc(docRef, { key: bsonTimestamp(4294967296, 2) }); + } catch (err) { + errorMessage = (err as FirestoreError)?.message; + } + expect(errorMessage).to.contains( + "The field 'seconds' value (4,294,967,296) does not represent an unsigned 32-bit integer." + ); + + try { + // negative BSON timestamp gets rejected + await setDoc(docRef, { key: bsonTimestamp(-1, 2) }); + } catch (err) { + errorMessage = (err as FirestoreError)?.message; + } + expect(errorMessage).to.contains( + "The field 'seconds' value (-1) does not represent an unsigned 32-bit integer." + ); + } + ); + }); + + it('invalid regex value gets rejected', async () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const docRef = doc(dbs[0], 'test-collection/test-doc'); + let errorMessage; + try { + await setDoc(docRef, { key: regex('foo', 'a') }); + } catch (err) { + errorMessage = (err as FirestoreError)?.message; + } + expect(errorMessage).to.contains( + "Invalid regex option 'a'. Supported options are 'i', 'm', 's', 'u', and 'x'." + ); + } + ); + }); + + it('invalid bsonObjectId value gets rejected', async () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const docRef = doc(dbs[0], 'test-collection/test-doc'); + + let errorMessage; + try { + // bsonObjectId with length not equal to 24 gets rejected + await setDoc(docRef, { key: bsonObjectId('foo') }); + } catch (err) { + errorMessage = (err as FirestoreError)?.message; + } + expect(errorMessage).to.contains( + 'Object ID hex string has incorrect length.' + ); + } + ); + }); + + it('invalid bsonBinaryData value gets rejected', async () => { + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const docRef = doc(dbs[0], 'test-collection/test-doc'); + let errorMessage; + try { + await setDoc(docRef, { + key: bsonBinaryData(1234, new Uint8Array([1, 2, 3])) + }); + } catch (err) { + errorMessage = (err as FirestoreError)?.message; + } + expect(errorMessage).to.contains( + 'The subtype for BsonBinaryData must be a value in the inclusive [0, 255] range.' + ); + } + ); + }); + + it('can order values of different TypeOrder together', async () => { + const testDocs: { [key: string]: DocumentData } = { + nullValue: { key: null }, + minValue: { key: minKey() }, + booleanValue: { key: true }, + nanValue: { key: NaN }, + int32Value: { key: int32(1) }, + doubleValue: { key: 2.0 }, + integerValue: { key: 3 }, + timestampValue: { key: new Timestamp(100, 123456000) }, + bsonTimestampValue: { key: bsonTimestamp(1, 2) }, + stringValue: { key: 'string' }, + bytesValue: { key: Bytes.fromUint8Array(new Uint8Array([0, 1, 255])) }, + bsonBinaryValue: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, + // referenceValue: {key: ref('coll/doc')}, + referenceValue: { key: 'placeholder' }, + objectIdValue: { key: bsonObjectId('507f191e810c19729de860ea') }, + geoPointValue: { key: new GeoPoint(0, 0) }, + regexValue: { key: regex('^foo', 'i') }, + arrayValue: { key: [1, 2] }, + vectorValue: { key: vector([1, 2]) }, + objectValue: { key: { a: 1 } }, + maxValue: { key: maxKey() } + }; + + return withTestDbsSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + 1, + async dbs => { + const coll = collection(dbs[0], AutoId.newId()); + for (const key of Object.keys(testDocs)) { + await setDoc(doc(coll, key), testDocs[key]); + } + + // TODO(Mila/BSON): replace after prod supports bson + const docRef = doc(coll, 'doc'); + await setDoc(doc(coll, 'referenceValue'), { key: docRef }); + + const orderedQuery = query(coll, orderBy('key')); + const snapshot = await getDocs(orderedQuery); + for (let i = 0; i < snapshot.docs.length; i++) { + const actualDoc = snapshot.docs[i].data().key; + const expectedDoc = + testDocs[snapshot.docs[i].id as keyof typeof testDocs].key; + if (actualDoc instanceof DocumentReference) { + // deep.equal doesn't work with DocumentReference + expect(refEqual(actualDoc, docRef)).to.be.true; + } else { + expect(actualDoc).to.deep.equal(expectedDoc); + } + } + } + ); + }); }); diff --git a/packages/firestore/test/lite/integration.test.ts b/packages/firestore/test/lite/integration.test.ts index 780db5f4f9c..9b647587503 100644 --- a/packages/firestore/test/lite/integration.test.ts +++ b/packages/firestore/test/lite/integration.test.ts @@ -40,8 +40,15 @@ import { FieldValue } from '../../src/lite-api/field_value'; import { arrayRemove, arrayUnion, + bsonBinaryData, + bsonObjectId, + bsonTimestamp, deleteField, increment, + int32, + maxKey, + minKey, + regex, serverTimestamp, vector } from '../../src/lite-api/field_value_impl'; @@ -2960,3 +2967,44 @@ describe('Vectors', () => { }); }); }); + +// eslint-disable-next-line no-restricted-properties +describe.skip('BSON types', () => { + // TODO(Mila/BSON): enable this test once prod supports bson + it('can be read and written using the lite SDK', async () => { + return withTestCollection(async coll => { + const ref = await addDoc(coll, { + objectId: bsonObjectId('507f191e810c19729de860ea'), + int32: int32(1), + min: minKey(), + max: maxKey(), + regex: regex('^foo', 'i') + }); + + await setDoc( + ref, + { + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: bsonTimestamp(1, 2), + int32: int32(2) + }, + { merge: true } + ); + + const snap1 = await getDoc(ref); + expect( + snap1.get('objectId').isEqual(bsonObjectId('507f191e810c19729de860ea')) + ).to.be.true; + expect(snap1.get('int32').isEqual(int32(2))).to.be.true; + expect(snap1.get('min') === minKey()).to.be.true; + expect(snap1.get('max') === maxKey()).to.be.true; + expect( + snap1 + .get('binary') + .isEqual(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ).to.be.true; + expect(snap1.get('timestamp').isEqual(bsonTimestamp(1, 2))).to.be.true; + expect(snap1.get('regex').isEqual(regex('^foo', 'i'))).to.be.true; + }); + }); +}); diff --git a/packages/firestore/test/unit/model/document.test.ts b/packages/firestore/test/unit/model/document.test.ts index cfb93d15e6f..2c2387cca63 100644 --- a/packages/firestore/test/unit/model/document.test.ts +++ b/packages/firestore/test/unit/model/document.test.ts @@ -17,6 +17,15 @@ import { expect } from 'chai'; +import { + bsonBinaryData, + bsonObjectId, + bsonTimestamp, + int32, + maxKey, + minKey, + regex +} from '../../../src/lite-api/field_value_impl'; import { doc, expectEqual, @@ -44,6 +53,34 @@ describe('Document', () => { expect(document.hasLocalMutations).to.equal(false); }); + it('can be constructed with bson types', () => { + const data = { + objectId: bsonObjectId('foo'), + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: bsonTimestamp(1, 2), + min: minKey(), + max: maxKey(), + regex: regex('a', 'b'), + int32: int32(1) + }; + const document = doc('rooms/Eros', 1, data); + + const value = document.data; + expect(value.value).to.deep.equal( + wrap({ + objectId: bsonObjectId('foo'), + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: bsonTimestamp(1, 2), + min: minKey(), + max: maxKey(), + regex: regex('a', 'b'), + int32: int32(1) + }) + ); + expect(value).not.to.equal(data); + expect(document.hasLocalMutations).to.equal(false); + }); + it('returns fields correctly', () => { const data = { desc: 'Discuss all the project related stuff', diff --git a/packages/firestore/test/unit/model/object_value.test.ts b/packages/firestore/test/unit/model/object_value.test.ts index 9e96056d957..13cfa02131b 100644 --- a/packages/firestore/test/unit/model/object_value.test.ts +++ b/packages/firestore/test/unit/model/object_value.test.ts @@ -17,7 +17,16 @@ import { expect } from 'chai'; -import { vector } from '../../../src/lite-api/field_value_impl'; +import { + vector, + bsonObjectId, + bsonBinaryData, + bsonTimestamp, + int32, + regex, + minKey, + maxKey +} from '../../../src/lite-api/field_value_impl'; import { extractFieldMask, ObjectValue } from '../../../src/model/object_value'; import { TypeOrder } from '../../../src/model/type_order'; import { typeOrder } from '../../../src/model/values'; @@ -27,7 +36,16 @@ describe('ObjectValue', () => { it('can extract fields', () => { const objValue = wrapObject({ foo: { a: 1, b: true, c: 'string' }, - embedding: vector([1]) + embedding: vector([1]), + bson: { + objectId: bsonObjectId('foo'), + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: bsonTimestamp(1, 2), + min: minKey(), + max: maxKey(), + regex: regex('a', 'b'), + int32: int32(1) + } }); expect(typeOrder(objValue.field(field('foo'))!)).to.equal( @@ -45,6 +63,27 @@ describe('ObjectValue', () => { expect(typeOrder(objValue.field(field('embedding'))!)).to.equal( TypeOrder.VectorValue ); + expect(typeOrder(objValue.field(field('bson.objectId'))!)).to.equal( + TypeOrder.BsonObjectIdValue + ); + expect(typeOrder(objValue.field(field('bson.binary'))!)).to.equal( + TypeOrder.BsonBinaryValue + ); + expect(typeOrder(objValue.field(field('bson.timestamp'))!)).to.equal( + TypeOrder.BsonTimestampValue + ); + expect(typeOrder(objValue.field(field('bson.min'))!)).to.equal( + TypeOrder.MinKeyValue + ); + expect(typeOrder(objValue.field(field('bson.max'))!)).to.equal( + TypeOrder.MaxKeyValue + ); + expect(typeOrder(objValue.field(field('bson.regex'))!)).to.equal( + TypeOrder.RegexValue + ); + expect(typeOrder(objValue.field(field('bson.int32'))!)).to.equal( + TypeOrder.NumberValue + ); expect(objValue.field(field('foo.a.b'))).to.be.null; expect(objValue.field(field('bar'))).to.be.null; @@ -60,13 +99,42 @@ describe('ObjectValue', () => { expect(objValue.field(field('foo.a'))).to.deep.equal(wrap(1)); expect(objValue.field(field('foo.b'))).to.deep.equal(wrap(true)); expect(objValue.field(field('foo.c'))).to.deep.equal(wrap('string')); + + expect(objValue.field(field('bson'))!).to.deep.equal( + wrap({ + objectId: bsonObjectId('foo'), + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: bsonTimestamp(1, 2), + min: minKey(), + max: maxKey(), + regex: regex('a', 'b'), + int32: int32(1) + }) + ); + expect(objValue.field(field('bson.objectId'))!).to.deep.equal( + wrap(bsonObjectId('foo')) + ); + expect(objValue.field(field('bson.binary'))!).to.deep.equal( + wrap(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ); + expect(objValue.field(field('bson.timestamp'))!).to.deep.equal( + wrap(bsonTimestamp(1, 2)) + ); + expect(objValue.field(field('bson.min'))!).to.deep.equal(wrap(minKey())); + expect(objValue.field(field('bson.max'))!).to.deep.equal(wrap(maxKey())); + expect(objValue.field(field('bson.regex'))!).to.deep.equal( + wrap(regex('a', 'b')) + ); + expect(objValue.field(field('bson.int32'))!).to.deep.equal(wrap(int32(1))); }); it('can overwrite existing fields', () => { const objValue = wrapObject({ foo: 'foo-value' }); objValue.set(field('foo'), wrap('new-foo-value')); - assertObjectEquals(objValue, { foo: 'new-foo-value' }); + assertObjectEquals(objValue, { + foo: 'new-foo-value' + }); }); it('can add new fields', () => { @@ -163,11 +231,77 @@ describe('ObjectValue', () => { assertObjectEquals(objValue, {}); }); + it('can handle bson types in ObjectValue', () => { + const objValue = ObjectValue.empty(); + // Add new fields + objValue.set(field('objectId'), wrap(bsonObjectId('foo-value'))); + objValue.set( + field('binary'), + wrap(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ); + objValue.set(field('timestamp'), wrap(bsonTimestamp(1, 2))); + objValue.set(field('regex'), wrap(regex('a', 'b'))); + objValue.set(field('int32'), wrap(int32(1))); + objValue.set(field('min'), wrap(minKey())); + objValue.set(field('max'), wrap(maxKey())); + + assertObjectEquals(objValue, { + objectId: bsonObjectId('foo-value'), + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: bsonTimestamp(1, 2), + regex: regex('a', 'b'), + int32: int32(1), + min: minKey(), + max: maxKey() + }); + + // Overwrite existing fields + objValue.set(field('objectId'), wrap(bsonObjectId('new-foo-value'))); + + // Create nested objects + objValue.set( + field('foo.binary'), + wrap(bsonBinaryData(2, new Uint8Array([1, 2, 3]))) + ); + objValue.set(field('foo.timestamp'), wrap(bsonTimestamp(1, 2))); + + // Delete fields + objValue.delete(field('binary')); + + // overwrite nested objects + objValue.set(field('foo.timestamp'), wrap(bsonTimestamp(2, 1))); + + // Overwrite primitive values to create objects + objValue.set(field('min'), wrap(null)); + + assertObjectEquals(objValue, { + objectId: bsonObjectId('new-foo-value'), + timestamp: bsonTimestamp(1, 2), + regex: regex('a', 'b'), + int32: int32(1), + min: null, + max: maxKey(), + foo: { + binary: bsonBinaryData(2, new Uint8Array([1, 2, 3])), + timestamp: bsonTimestamp(2, 1) + } + }); + }); + it('provides field mask', () => { const objValue = wrapObject({ a: 'b', map: { a: 1, b: true, c: 'string', nested: { d: 'e' } }, - emptymap: {} + emptymap: {}, + bar: { + objectId: bsonObjectId('foo'), + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: bsonTimestamp(1, 2), + min: minKey(), + max: maxKey(), + regex: regex('a', 'b'), + int32: int32(1) + } }); const expectedMask = mask( 'a', @@ -175,7 +309,14 @@ describe('ObjectValue', () => { 'map.b', 'map.c', 'map.nested.d', - 'emptymap' + 'emptymap', + 'bar.objectId', + 'bar.binary', + 'bar.timestamp', + 'bar.min', + 'bar.max', + 'bar.regex', + 'bar.int32' ); const actualMask = extractFieldMask(objValue.value.mapValue); expect(actualMask.isEqual(expectedMask)).to.be.true; @@ -185,6 +326,6 @@ describe('ObjectValue', () => { objValue: ObjectValue, data: { [k: string]: unknown } ): void { - expect(objValue.isEqual(wrapObject(data))); + expect(objValue.isEqual(wrapObject(data))).to.be.true; } }); diff --git a/packages/firestore/test/unit/model/target.test.ts b/packages/firestore/test/unit/model/target.test.ts index bbeea5dec83..1fa2e58b298 100644 --- a/packages/firestore/test/unit/model/target.test.ts +++ b/packages/firestore/test/unit/model/target.test.ts @@ -31,8 +31,8 @@ import { import { IndexKind } from '../../../src/model/field_index'; import { canonicalId, - MAX_VALUE, - MIN_VALUE, + INTERNAL_MAX_VALUE, + INTERNAL_MIN_VALUE, valueEquals } from '../../../src/model/values'; import { @@ -207,11 +207,11 @@ describe('Target Bounds', () => { const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] }); const lowerBound = targetGetLowerBound(target, index); - expect(lowerBound?.position[0]).to.equal(MIN_VALUE); + expect(lowerBound?.position[0]).to.equal(INTERNAL_MIN_VALUE); expect(lowerBound?.inclusive).to.be.true; const upperBound = targetGetUpperBound(target, index); - expect(upperBound?.position[0]).to.equal(MAX_VALUE); + expect(upperBound?.position[0]).to.equal(INTERNAL_MAX_VALUE); expect(upperBound?.inclusive).to.be.true; }); @@ -241,7 +241,7 @@ describe('Target Bounds', () => { verifyBound(lowerBound, true, 'bar'); const upperBound = targetGetUpperBound(target, index); - expect(upperBound?.position[0]).to.equal(MAX_VALUE); + expect(upperBound?.position[0]).to.equal(INTERNAL_MAX_VALUE); expect(upperBound?.inclusive).to.be.true; }); @@ -337,7 +337,7 @@ describe('Target Bounds', () => { const index = fieldIndex('c', { fields: [['foo', IndexKind.ASCENDING]] }); const lowerBound = targetGetLowerBound(target, index); - expect(lowerBound?.position[0]).to.equal(MIN_VALUE); + expect(lowerBound?.position[0]).to.equal(INTERNAL_MIN_VALUE); expect(lowerBound?.inclusive).to.be.true; const upperBound = targetGetUpperBound(target, index); diff --git a/packages/firestore/test/unit/model/values.test.ts b/packages/firestore/test/unit/model/values.test.ts index 722d2db6fa5..bf46386c800 100644 --- a/packages/firestore/test/unit/model/values.test.ts +++ b/packages/firestore/test/unit/model/values.test.ts @@ -19,7 +19,23 @@ import { expect } from 'chai'; import { GeoPoint, Timestamp } from '../../../src'; import { DatabaseId } from '../../../src/core/database_info'; -import { vector } from '../../../src/lite-api/field_value_impl'; +import { BsonBinaryData } from '../../../src/lite-api/bson_binary_data'; +import { BsonObjectId } from '../../../src/lite-api/bson_object_Id'; +import { BsonTimestampValue } from '../../../src/lite-api/bson_timestamp_value'; +import { + vector, + regex, + bsonTimestamp, + int32, + bsonBinaryData, + bsonObjectId, + minKey, + maxKey +} from '../../../src/lite-api/field_value_impl'; +import { Int32Value } from '../../../src/lite-api/int32_value'; +import { MaxKey } from '../../../src/lite-api/max_key'; +import { MinKey } from '../../../src/lite-api/min_key'; +import { RegexValue } from '../../../src/lite-api/regex_value'; import { serverTimestamp } from '../../../src/model/server_timestamps'; import { canonicalId, @@ -31,7 +47,7 @@ import { valuesGetLowerBound, valuesGetUpperBound, TYPE_KEY, - VECTOR_VALUE_SENTINEL, + RESERVED_VECTOR_KEY, VECTOR_MAP_VECTORS_KEY } from '../../../src/model/values'; import * as api from '../../../src/protos/firestore_proto_api'; @@ -55,7 +71,14 @@ describe('Values', () => { const values: api.Value[][] = [ [wrap(true), wrap(true)], [wrap(false), wrap(false)], - [wrap(null), wrap(null)], + // MinKeys are all equal, and sort the same as null. + [ + wrap(null), + wrap(null), + wrap(minKey()), + wrap(minKey()), + wrap(MinKey.instance()) + ], [wrap(0 / 0), wrap(Number.NaN), wrap(NaN)], // -0.0 and 0.0 order the same but are not considered equal. [wrap(-0.0)], @@ -92,7 +115,21 @@ describe('Values', () => { [wrap({ bar: 1, foo: 1 })], [wrap({ foo: 1 })], [wrap(vector([]))], - [wrap(vector([1, 2.3, -4.0]))] + [wrap(vector([1, 2.3, -4.0]))], + [wrap(regex('^foo', 'i')), wrap(new RegexValue('^foo', 'i'))], + [wrap(bsonTimestamp(57, 4)), wrap(new BsonTimestampValue(57, 4))], + [ + wrap(bsonBinaryData(128, Uint8Array.from([7, 8, 9]))), + wrap(new BsonBinaryData(128, Uint8Array.from([7, 8, 9]))), + wrap(bsonBinaryData(128, Buffer.from([7, 8, 9]))), + wrap(new BsonBinaryData(128, Buffer.from([7, 8, 9]))) + ], + [ + wrap(bsonObjectId('123456789012')), + wrap(new BsonObjectId('123456789012')) + ], + [wrap(int32(255)), wrap(new Int32Value(255))], + [wrap(maxKey()), wrap(maxKey()), wrap(MaxKey.instance())] ]; expectEqualitySets(values, (v1, v2) => valueEquals(v1, v2)); }); @@ -129,7 +166,7 @@ describe('Values', () => { it('orders types correctly', () => { const groups = [ // null first - [wrap(null)], + [wrap(null), wrap(minKey())], // booleans [wrap(false)], @@ -141,15 +178,24 @@ describe('Values', () => { [wrap(-Number.MAX_VALUE)], [wrap(Number.MIN_SAFE_INTEGER - 1)], [wrap(Number.MIN_SAFE_INTEGER)], + // 64-bit and 32-bit integers order together numerically. + [{ integerValue: -2147483648 }, wrap(int32(-2147483648))], [wrap(-1.1)], - // Integers and Doubles order the same. - [{ integerValue: -1 }, { doubleValue: -1 }], + // Integers, Int32Values and Doubles order the same. + [{ integerValue: -1 }, { doubleValue: -1 }, wrap(int32(-1))], [wrap(-Number.MIN_VALUE)], // zeros all compare the same. - [{ integerValue: 0 }, { doubleValue: 0 }, { doubleValue: -0 }], + [ + { integerValue: 0 }, + { doubleValue: 0 }, + { doubleValue: -0 }, + wrap(int32(0)) + ], [wrap(Number.MIN_VALUE)], - [{ integerValue: 1 }, { doubleValue: 1 }], + [{ integerValue: 1 }, { doubleValue: 1.0 }, wrap(int32(1))], [wrap(1.1)], + [wrap(int32(2))], + [wrap(int32(2147483647))], [wrap(Number.MAX_SAFE_INTEGER)], [wrap(Number.MAX_SAFE_INTEGER + 1)], [wrap(Infinity)], @@ -164,6 +210,11 @@ describe('Values', () => { { timestampValue: '2020-04-05T14:30:01.000000000Z' } ], + // request timestamp + [wrap(bsonTimestamp(123, 4))], + [wrap(bsonTimestamp(123, 5))], + [wrap(bsonTimestamp(124, 0))], + // server timestamps come after all concrete timestamps. [serverTimestamp(Timestamp.fromDate(date1), null)], [serverTimestamp(Timestamp.fromDate(date2), null)], @@ -187,6 +238,13 @@ describe('Values', () => { [wrap(blob(0, 1, 2, 4, 3))], [wrap(blob(255))], + [ + wrap(bsonBinaryData(5, Buffer.from([1, 2, 3]))), + wrap(bsonBinaryData(5, new Uint8Array([1, 2, 3]))) + ], + [wrap(bsonBinaryData(7, Buffer.from([1])))], + [wrap(bsonBinaryData(7, new Uint8Array([2])))], + // reference values [refValue(dbId('p1', 'd1'), key('c1/doc1'))], [refValue(dbId('p1', 'd1'), key('c1/doc2'))], @@ -195,6 +253,13 @@ describe('Values', () => { [refValue(dbId('p1', 'd2'), key('c1/doc1'))], [refValue(dbId('p2', 'd1'), key('c1/doc1'))], + // ObjectId + [wrap(bsonObjectId('foo')), wrap(bsonObjectId('foo'))], + // TODO(Mila/BSON): uncomment after string sort bug is fixed + // [wrap(bsonObjectId('Ḟoo'))], // with latin capital letter f with dot above + // [wrap(bsonObjectId('foo\u0301'))], // with combining acute accent + [wrap(bsonObjectId('xyz'))], + // geo points [wrap(new GeoPoint(-90, -180))], [wrap(new GeoPoint(-90, 0))], @@ -209,6 +274,12 @@ describe('Values', () => { [wrap(new GeoPoint(90, 0))], [wrap(new GeoPoint(90, 180))], + // regular expressions + [wrap(regex('a', 'bar1'))], + [wrap(regex('foo', 'bar1'))], + [wrap(regex('foo', 'bar2'))], + [wrap(regex('go', 'bar1'))], + // arrays [wrap([])], [wrap(['bar'])], @@ -227,7 +298,10 @@ describe('Values', () => { [wrap({ bar: 0, foo: 1 })], [wrap({ foo: 1 })], [wrap({ foo: 2 })], - [wrap({ foo: '0' })] + [wrap({ foo: '0' })], + + // MaxKey + [wrap(maxKey())] ]; expectCorrectComparisonGroups( @@ -331,7 +405,31 @@ describe('Values', () => { { expectedByteSize: 49, elements: [wrap(vector([1, 2])), wrap(vector([-100, 20000098.123445]))] - } + }, + { + expectedByteSize: 27, + elements: [wrap(regex('a', 'b')), wrap(regex('c', 'd'))] + }, + { + expectedByteSize: 13, + elements: [wrap(bsonObjectId('foo')), wrap(bsonObjectId('bar'))] + }, + { + expectedByteSize: 53, + elements: [wrap(bsonTimestamp(1, 2)), wrap(bsonTimestamp(3, 4))] + }, + { + expectedByteSize: 8, + elements: [wrap(int32(1)), wrap(int32(2147483647))] + }, + { + expectedByteSize: 16, + elements: [ + wrap(bsonBinaryData(1, new Uint8Array([127, 128]))), + wrap(bsonBinaryData(128, new Uint8Array([1, 2]))) + ] + }, + { expectedByteSize: 11, elements: [wrap(minKey()), wrap(maxKey())] } ]; for (const group of equalityGroups) { @@ -361,7 +459,13 @@ describe('Values', () => { [wrap({ a: 'a', b: 'b' }), wrap({ a: 'a', bc: 'b' })], [wrap({ a: 'a', b: 'b' }), wrap({ a: 'a', b: 'b', c: 'c' })], [wrap({ a: 'a', b: 'b' }), wrap({ a: 'a', b: 'b', c: 'c' })], - [wrap(vector([2, 3])), wrap(vector([1, 2, 3]))] + [wrap(vector([2, 3])), wrap(vector([1, 2, 3]))], + [wrap(regex('a', 'b')), wrap(regex('cc', 'dd'))], + [wrap(bsonObjectId('foo')), wrap(bsonObjectId('foobar'))], + [ + wrap(bsonBinaryData(128, new Uint8Array([127, 128]))), + wrap(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ] ]; for (const group of relativeGroups) { @@ -376,9 +480,14 @@ describe('Values', () => { }); it('computes lower bound', () => { + // TODO(Mila/BSON):add cases for bson types const groups = [ - // null first - [valuesGetLowerBound({ nullValue: 'NULL_VALUE' }), wrap(null)], + // null and minKey first + [ + valuesGetLowerBound({ nullValue: 'NULL_VALUE' }), + wrap(null), + wrap(minKey()) + ], // booleans [valuesGetLowerBound({ booleanValue: true }), wrap(false)], @@ -420,7 +529,7 @@ describe('Values', () => { valuesGetLowerBound({ mapValue: { fields: { - [TYPE_KEY]: { stringValue: VECTOR_VALUE_SENTINEL }, + [TYPE_KEY]: { stringValue: RESERVED_VECTOR_KEY }, [VECTOR_MAP_VECTORS_KEY]: { arrayValue: { values: [{ doubleValue: 1 }] @@ -433,7 +542,10 @@ describe('Values', () => { ], // objects - [valuesGetLowerBound({ mapValue: {} }), wrap({})] + [valuesGetLowerBound({ mapValue: {} }), wrap({})], + + // MaxKey + [wrap(maxKey())] ]; expectCorrectComparisonGroups( @@ -445,6 +557,7 @@ describe('Values', () => { }); it('computes upper bound', () => { + // TODO(Mila/BSON):add cases for bson types const groups = [ // null first [wrap(null)], @@ -526,6 +639,19 @@ describe('Values', () => { expect( canonicalId(wrap({ 'a': ['b', { 'c': new GeoPoint(30, 60) }] })) ).to.equal('{a:[b,{c:geo(30,60)}]}'); + expect(canonicalId(wrap(regex('a', 'b')))).to.equal( + '{__regex__:{options:b,pattern:a}}' + ); + expect(canonicalId(wrap(bsonObjectId('foo')))).to.equal('{__oid__:foo}'); + expect(canonicalId(wrap(bsonTimestamp(1, 2)))).to.equal( + '{__request_timestamp__:{increment:2,seconds:1}}' + ); + expect(canonicalId(wrap(int32(1)))).to.equal('{__int__:1}'); + expect( + canonicalId(wrap(bsonBinaryData(1, new Uint8Array([1, 2, 3])))) + ).to.equal('{__binary__:{subType:1,data:AQID}}'); + expect(canonicalId(wrap(minKey()))).to.equal('{__min__:null}'); + expect(canonicalId(wrap(maxKey()))).to.equal('{__max__:null}'); }); it('canonical IDs ignore sort order', () => { diff --git a/packages/firestore/test/unit/remote/serializer.helper.ts b/packages/firestore/test/unit/remote/serializer.helper.ts index d523c8fab83..9c116549928 100644 --- a/packages/firestore/test/unit/remote/serializer.helper.ts +++ b/packages/firestore/test/unit/remote/serializer.helper.ts @@ -52,7 +52,16 @@ import { } from '../../../src/core/query'; import { SnapshotVersion } from '../../../src/core/snapshot_version'; import { Target, targetEquals, TargetImpl } from '../../../src/core/target'; -import { vector } from '../../../src/lite-api/field_value_impl'; +import { + bsonBinaryData, + bsonObjectId, + bsonTimestamp, + int32, + maxKey, + minKey, + regex, + vector +} from '../../../src/lite-api/field_value_impl'; import { parseQueryValue } from '../../../src/lite-api/user_data_reader'; import { TargetData, TargetPurpose } from '../../../src/local/target_data'; import { FieldMask } from '../../../src/model/field_mask'; @@ -565,6 +574,57 @@ export function serializerTest( jsonValue: expectedJson.mapValue }); }); + + it('converts BSON types in mapValue', () => { + const examples = [ + bsonObjectId('foo'), + bsonTimestamp(1, 2), + minKey(), + maxKey(), + regex('a', 'b'), + int32(1) + ]; + + for (const example of examples) { + expect(userDataWriter.convertValue(wrap(example))).to.deep.equal( + example + ); + + verifyFieldValueRoundTrip({ + value: example, + valueType: 'mapValue', + jsonValue: wrap(example).mapValue + }); + } + + // BsonBinaryData will be serialized differently Proto3Json VS. regular Protobuf format + const bsonBinary = bsonBinaryData(1, new Uint8Array([1, 2, 3])); + const expectedJson: api.Value = { + mapValue: { + fields: { + '__binary__': { + 'bytesValue': 'AQECAw==' + } + } + } + }; + + const expectedProtoJson: api.Value = { + mapValue: { + fields: { + '__binary__': { + 'bytesValue': new Uint8Array([1, 1, 2, 3]) + } + } + } + }; + verifyFieldValueRoundTrip({ + value: bsonBinary, + valueType: 'mapValue', + jsonValue: expectedJson.mapValue, + protoJsValue: expectedProtoJson.mapValue + }); + }); }); describe('toKey', () => { From 3c743b25c77fa076f6bc4db254e5e02036773de3 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Tue, 11 Mar 2025 13:38:22 -0400 Subject: [PATCH 02/23] Implement indexing for bson types (#331) --- packages/firestore/src/core/target.ts | 6 +- .../src/index/firestore_index_value_writer.ts | 102 ++- packages/firestore/src/lite-api/query.ts | 27 + .../src/lite-api/user_data_writer.ts | 5 +- packages/firestore/src/model/type_order.ts | 39 +- packages/firestore/src/model/values.ts | 231 +++++-- .../test/integration/api/database.test.ts | 424 +++++++++--- .../test/integration/api/query.test.ts | 14 - .../test/integration/api/type.test.ts | 55 +- .../test/integration/api/validation.test.ts | 14 + .../test/integration/util/helpers.ts | 74 ++- .../firestore_index_value_writer.test.ts | 366 ++++++++++- .../test/unit/local/index_manager.test.ts | 607 +++++++++++++++++- .../firestore/test/unit/model/values.test.ts | 117 +++- 14 files changed, 1823 insertions(+), 258 deletions(-) diff --git a/packages/firestore/src/core/target.ts b/packages/firestore/src/core/target.ts index 664a2ef9a08..cc2732e8f8a 100644 --- a/packages/firestore/src/core/target.ts +++ b/packages/firestore/src/core/target.ts @@ -28,6 +28,8 @@ import { INTERNAL_MAX_VALUE, INTERNAL_MIN_VALUE, lowerBoundCompare, + MAX_KEY_VALUE, + MIN_KEY_VALUE, upperBoundCompare, valuesGetLowerBound, valuesGetUpperBound @@ -387,7 +389,7 @@ function targetGetAscendingBound( break; case Operator.NOT_EQUAL: case Operator.NOT_IN: - filterValue = INTERNAL_MIN_VALUE; + filterValue = MIN_KEY_VALUE; break; default: // Remaining filters cannot be used as lower bounds. @@ -462,7 +464,7 @@ function targetGetDescendingBound( break; case Operator.NOT_EQUAL: case Operator.NOT_IN: - filterValue = INTERNAL_MAX_VALUE; + filterValue = MAX_KEY_VALUE; break; default: // Remaining filters cannot be used as upper bounds. diff --git a/packages/firestore/src/index/firestore_index_value_writer.ts b/packages/firestore/src/index/firestore_index_value_writer.ts index f831862a0de..d02a07313fe 100644 --- a/packages/firestore/src/index/firestore_index_value_writer.ts +++ b/packages/firestore/src/index/firestore_index_value_writer.ts @@ -22,9 +22,16 @@ import { normalizeTimestamp } from '../model/normalize'; import { - isVectorValue, VECTOR_MAP_VECTORS_KEY, - isMaxValue + detectSpecialMapType, + RESERVED_BSON_TIMESTAMP_KEY, + RESERVED_REGEX_KEY, + RESERVED_BSON_OBJECT_ID_KEY, + RESERVED_BSON_BINARY_KEY, + SpecialMapValueType, + RESERVED_REGEX_PATTERN_KEY, + RESERVED_REGEX_OPTIONS_KEY, + RESERVED_INT32_KEY } from '../model/values'; import { ArrayValue, MapValue, Value } from '../protos/firestore_proto_api'; import { fail } from '../util/assert'; @@ -32,22 +39,28 @@ import { isNegativeZero } from '../util/types'; import { DirectionalIndexByteEncoder } from './directional_index_byte_encoder'; -// Note: This code is copied from the backend. Code that is not used by -// Firestore was removed. +// Note: This file is copied from the backend. Code that is not used by +// Firestore was removed. Code that has different behavior was modified. const INDEX_TYPE_NULL = 5; +const INDEX_TYPE_MIN_KEY = 7; const INDEX_TYPE_BOOLEAN = 10; const INDEX_TYPE_NAN = 13; const INDEX_TYPE_NUMBER = 15; const INDEX_TYPE_TIMESTAMP = 20; +const INDEX_TYPE_BSON_TIMESTAMP = 22; const INDEX_TYPE_STRING = 25; const INDEX_TYPE_BLOB = 30; +const INDEX_TYPE_BSON_BINARY = 31; const INDEX_TYPE_REFERENCE = 37; +const INDEX_TYPE_BSON_OBJECT_ID = 43; const INDEX_TYPE_GEOPOINT = 45; +const INDEX_TYPE_REGEX = 47; const INDEX_TYPE_ARRAY = 50; const INDEX_TYPE_VECTOR = 53; const INDEX_TYPE_MAP = 55; const INDEX_TYPE_REFERENCE_SEGMENT = 60; +const INDEX_TYPE_MAX_VALUE = 999; // A terminator that indicates that a truncatable value was not truncated. // This must be smaller than all other type labels. @@ -124,11 +137,30 @@ export class FirestoreIndexValueWriter { encoder.writeNumber(geoPoint.latitude || 0); encoder.writeNumber(geoPoint.longitude || 0); } else if ('mapValue' in indexValue) { - // TODO(Mila/BSON): add bson types for indexing - if (isMaxValue(indexValue)) { + const type = detectSpecialMapType(indexValue); + if (type === SpecialMapValueType.INTERNAL_MAX) { this.writeValueTypeLabel(encoder, Number.MAX_SAFE_INTEGER); - } else if (isVectorValue(indexValue)) { + } else if (type === SpecialMapValueType.VECTOR) { this.writeIndexVector(indexValue.mapValue!, encoder); + } else if (type === SpecialMapValueType.MAX_KEY) { + this.writeValueTypeLabel(encoder, INDEX_TYPE_MAX_VALUE); + } else if (type === SpecialMapValueType.MIN_KEY) { + this.writeValueTypeLabel(encoder, INDEX_TYPE_MIN_KEY); + } else if (type === SpecialMapValueType.BSON_BINARY) { + this.writeIndexBsonBinaryData(indexValue.mapValue!, encoder); + } else if (type === SpecialMapValueType.REGEX) { + this.writeIndexRegex(indexValue.mapValue!, encoder); + } else if (type === SpecialMapValueType.BSON_TIMESTAMP) { + this.writeIndexBsonTimestamp(indexValue.mapValue!, encoder); + } else if (type === SpecialMapValueType.BSON_OBJECT_ID) { + this.writeIndexBsonObjectId(indexValue.mapValue!, encoder); + } else if (type === SpecialMapValueType.INT32) { + this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER); + encoder.writeNumber( + normalizeNumber( + indexValue.mapValue!.fields![RESERVED_INT32_KEY]!.integerValue! + ) + ); } else { this.writeIndexMap(indexValue.mapValue!, encoder); this.writeTruncationMarker(encoder); @@ -202,7 +234,10 @@ export class FirestoreIndexValueWriter { encoder: DirectionalIndexByteEncoder ): void { this.writeValueTypeLabel(encoder, INDEX_TYPE_REFERENCE); - const path = DocumentKey.fromName(referenceValue).path; + const segments: string[] = referenceValue + .split('/') + .filter(segment => segment.length > 0); + const path = DocumentKey.fromSegments(segments.slice(5)).path; path.forEach(segment => { this.writeValueTypeLabel(encoder, INDEX_TYPE_REFERENCE_SEGMENT); this.writeUnlabeledIndexString(segment, encoder); @@ -222,4 +257,55 @@ export class FirestoreIndexValueWriter { // references, arrays and maps). encoder.writeNumber(NOT_TRUNCATED); } + + private writeIndexBsonTimestamp( + mapValue: MapValue, + encoder: DirectionalIndexByteEncoder + ): void { + this.writeValueTypeLabel(encoder, INDEX_TYPE_BSON_TIMESTAMP); + const fields = mapValue.fields || {}; + if (fields) { + // The JS SDK encodes BSON timestamps differently than the backend. + // This is due to the limitation of `number` in JS which handles up to 53-bit precision. + this.writeIndexMap( + fields[RESERVED_BSON_TIMESTAMP_KEY].mapValue!, + encoder + ); + } + } + + private writeIndexBsonObjectId( + mapValue: MapValue, + encoder: DirectionalIndexByteEncoder + ): void { + this.writeValueTypeLabel(encoder, INDEX_TYPE_BSON_OBJECT_ID); + const fields = mapValue.fields || {}; + const oid = fields[RESERVED_BSON_OBJECT_ID_KEY]?.stringValue || ''; + encoder.writeBytes(normalizeByteString(oid)); + } + + private writeIndexBsonBinaryData( + mapValue: MapValue, + encoder: DirectionalIndexByteEncoder + ): void { + this.writeValueTypeLabel(encoder, INDEX_TYPE_BSON_BINARY); + const fields = mapValue.fields || {}; + const binary = fields[RESERVED_BSON_BINARY_KEY]?.bytesValue || ''; + encoder.writeBytes(normalizeByteString(binary)); + this.writeTruncationMarker(encoder); + } + + private writeIndexRegex( + mapValue: MapValue, + encoder: DirectionalIndexByteEncoder + ): void { + this.writeValueTypeLabel(encoder, INDEX_TYPE_REGEX); + const fields = mapValue.fields || {}; + const regex = fields[RESERVED_REGEX_KEY]?.mapValue?.fields || {}; + if (regex) { + encoder.writeString(regex[RESERVED_REGEX_PATTERN_KEY]?.stringValue || ''); + encoder.writeString(regex[RESERVED_REGEX_OPTIONS_KEY]?.stringValue || ''); + } + this.writeTruncationMarker(encoder); + } } diff --git a/packages/firestore/src/lite-api/query.ts b/packages/firestore/src/lite-api/query.ts index f0a357b828c..67245f96d07 100644 --- a/packages/firestore/src/lite-api/query.ts +++ b/packages/firestore/src/lite-api/query.ts @@ -811,6 +811,8 @@ export function newQueryFilter( value: unknown ): FieldFilter { let fieldValue: ProtoValue; + validateQueryOperator(value, op); + if (fieldPath.isKeyField()) { if (op === Operator.ARRAY_CONTAINS || op === Operator.ARRAY_CONTAINS_ANY) { throw new FirestoreError( @@ -1064,6 +1066,31 @@ function validateDisjunctiveFilterElements( } } +/** + * Validates the input string as a field comparison operator. + */ +export function validateQueryOperator( + value: unknown, + operator: Operator +): void { + if ( + typeof value === 'number' && + isNaN(value) && + operator !== '==' && + operator !== '!=' + ) { + throw new Error( + "Invalid query. You can only perform '==' and '!=' comparisons on NaN." + ); + } + + if (value === null && operator !== '==' && operator !== '!=') { + throw new Error( + "Invalid query. You can only perform '==' and '!=' comparisons on Null." + ); + } +} + /** * Given an operator, returns the set of operators that cannot be used with it. * diff --git a/packages/firestore/src/lite-api/user_data_writer.ts b/packages/firestore/src/lite-api/user_data_writer.ts index 0de02b822b2..012b04874c3 100644 --- a/packages/firestore/src/lite-api/user_data_writer.ts +++ b/packages/firestore/src/lite-api/user_data_writer.ts @@ -83,9 +83,6 @@ export abstract class AbstractUserDataWriter { ): unknown { switch (typeOrder(value)) { case TypeOrder.NullValue: - if ('mapValue' in value) { - return minKey(); - } return null; case TypeOrder.BooleanValue: return value.booleanValue!; @@ -122,6 +119,8 @@ export abstract class AbstractUserDataWriter { return this.convertToBsonTimestampValue(value.mapValue!); case TypeOrder.MaxKeyValue: return maxKey(); + case TypeOrder.MinKeyValue: + return minKey(); default: throw fail('Invalid value type: ' + JSON.stringify(value)); } diff --git a/packages/firestore/src/model/type_order.ts b/packages/firestore/src/model/type_order.ts index 7ac6ed11e47..a13e16f4211 100644 --- a/packages/firestore/src/model/type_order.ts +++ b/packages/firestore/src/model/type_order.ts @@ -27,24 +27,25 @@ export const enum TypeOrder { // server timestamps and `MAX_VALUE` inside the SDK. // NULL and MIN_KEY sort the same. NullValue = 0, - MinKeyValue = 0, - BooleanValue = 1, - NumberValue = 2, - TimestampValue = 3, - // TODO(Mila/BSON): which should come first considering indexes? - BsonTimestampValue = 4, - ServerTimestampValue = 5, - StringValue = 6, - BlobValue = 7, - BsonBinaryValue = 8, - RefValue = 9, - BsonObjectIdValue = 10, - GeoPointValue = 11, - RegexValue = 12, - ArrayValue = 13, - VectorValue = 14, - ObjectValue = 15, - // TODO(Mila/BSON):should MaxKeyValue and MaxValue combined? how would this affect indexes? - MaxKeyValue = 16, + MinKeyValue = 1, + BooleanValue = 2, + // Note: all numbers (32-bit int, 64-bit int, 64-bit double, 128-bit decimal, + // etc.) are sorted together numerically. The `numberEquals` function + // distinguishes between different number types and compares them accordingly. + NumberValue = 3, + TimestampValue = 4, + BsonTimestampValue = 5, + ServerTimestampValue = 6, + StringValue = 7, + BlobValue = 8, + BsonBinaryValue = 9, + RefValue = 10, + BsonObjectIdValue = 11, + GeoPointValue = 12, + RegexValue = 13, + ArrayValue = 14, + VectorValue = 15, + ObjectValue = 16, + MaxKeyValue = 17, MaxValue = 9007199254740991 // Number.MAX_SAFE_INTEGER } diff --git a/packages/firestore/src/model/values.ts b/packages/firestore/src/model/values.ts index d6344409ed9..02404130a77 100644 --- a/packages/firestore/src/model/values.ts +++ b/packages/firestore/src/model/values.ts @@ -21,7 +21,6 @@ import { LatLng, MapValue, Timestamp, - Value as ProtoValue, Value } from '../protos/firestore_proto_api'; import { fail } from '../util/assert'; @@ -74,7 +73,7 @@ export const INTERNAL_MAX_VALUE: Value = { } }; -export const MIN_VECTOR_VALUE = { +export const MIN_VECTOR_VALUE: Value = { mapValue: { fields: { [TYPE_KEY]: { stringValue: RESERVED_VECTOR_KEY }, @@ -85,6 +84,96 @@ export const MIN_VECTOR_VALUE = { } }; +export const MIN_KEY_VALUE: Value = { + mapValue: { + fields: { + [RESERVED_MIN_KEY]: { + nullValue: 'NULL_VALUE' + } + } + } +}; + +export const MAX_KEY_VALUE: Value = { + mapValue: { + fields: { + [RESERVED_MAX_KEY]: { + nullValue: 'NULL_VALUE' + } + } + } +}; + +export const MIN_BSON_OBJECT_ID_VALUE: Value = { + mapValue: { + fields: { + [RESERVED_BSON_OBJECT_ID_KEY]: { + stringValue: '' + } + } + } +}; + +export const MIN_BSON_TIMESTAMP_VALUE: Value = { + mapValue: { + fields: { + [RESERVED_BSON_TIMESTAMP_KEY]: { + mapValue: { + fields: { + // Both seconds and increment are 32 bit unsigned integers + [RESERVED_BSON_TIMESTAMP_SECONDS_KEY]: { + integerValue: 0 + }, + [RESERVED_BSON_TIMESTAMP_INCREMENT_KEY]: { + integerValue: 0 + } + } + } + } + } + } +}; + +export const MIN_REGEX_VALUE: Value = { + mapValue: { + fields: { + [RESERVED_REGEX_KEY]: { + mapValue: { + fields: { + [RESERVED_REGEX_PATTERN_KEY]: { stringValue: '' }, + [RESERVED_REGEX_OPTIONS_KEY]: { stringValue: '' } + } + } + } + } + } +}; + +export const MIN_BSON_BINARY_VALUE: Value = { + mapValue: { + fields: { + [RESERVED_BSON_BINARY_KEY]: { + // bsonBinaryValue should have at least one byte as subtype + bytesValue: Uint8Array.from([0]) + } + } + } +}; + +export enum SpecialMapValueType { + REGEX = 'regexValue', + BSON_OBJECT_ID = 'bsonObjectIdValue', + INT32 = 'int32Value', + BSON_TIMESTAMP = 'bsonTimestampValue', + BSON_BINARY = 'bsonBinaryValue', + MIN_KEY = 'minKeyValue', + MAX_KEY = 'maxKeyValue', + INTERNAL_MAX = 'maxValue', + VECTOR = 'vectorValue', + SERVER_TIMESTAMP = 'serverTimestampValue', + REGULAR_MAP = 'regularMapValue' +} + /** Extracts the backend's type order for the provided value. */ export function typeOrder(value: Value): TypeOrder { if ('nullValue' in value) { @@ -108,25 +197,25 @@ export function typeOrder(value: Value): TypeOrder { } else if ('mapValue' in value) { const valueType = detectSpecialMapType(value); switch (valueType) { - case 'serverTimestampValue': + case SpecialMapValueType.SERVER_TIMESTAMP: return TypeOrder.ServerTimestampValue; - case 'maxValue': + case SpecialMapValueType.INTERNAL_MAX: return TypeOrder.MaxValue; - case 'vectorValue': + case SpecialMapValueType.VECTOR: return TypeOrder.VectorValue; - case 'regexValue': + case SpecialMapValueType.REGEX: return TypeOrder.RegexValue; - case 'bsonObjectIdValue': + case SpecialMapValueType.BSON_OBJECT_ID: return TypeOrder.BsonObjectIdValue; - case 'int32Value': + case SpecialMapValueType.INT32: return TypeOrder.NumberValue; - case 'bsonTimestampValue': + case SpecialMapValueType.BSON_TIMESTAMP: return TypeOrder.BsonTimestampValue; - case 'bsonBinaryValue': + case SpecialMapValueType.BSON_BINARY: return TypeOrder.BsonBinaryValue; - case 'minKeyValue': + case SpecialMapValueType.MIN_KEY: return TypeOrder.MinKeyValue; - case 'maxKeyValue': + case SpecialMapValueType.MAX_KEY: return TypeOrder.MaxKeyValue; default: return TypeOrder.ObjectValue; @@ -230,8 +319,8 @@ function blobEquals(left: Value, right: Value): boolean { export function numberEquals(left: Value, right: Value): boolean { if ( ('integerValue' in left && 'integerValue' in right) || - (detectSpecialMapType(left) === 'int32Value' && - detectSpecialMapType(right) === 'int32Value') + (detectSpecialMapType(left) === SpecialMapValueType.INT32 && + detectSpecialMapType(right) === SpecialMapValueType.INT32) ) { return extractNumber(left) === extractNumber(right); } else if ('doubleValue' in left && 'doubleValue' in right) { @@ -338,7 +427,7 @@ export function valueCompare(left: Value, right: Value): number { export function extractNumber(value: Value): number { let numberValue; - if (detectSpecialMapType(value) === 'int32Value') { + if (detectSpecialMapType(value) === SpecialMapValueType.INT32) { numberValue = value.mapValue!.fields![RESERVED_INT32_KEY].integerValue!; } else { numberValue = value.integerValue || value.doubleValue; @@ -598,7 +687,7 @@ function canonifyValue(value: Value): string { return canonifyArray(value.arrayValue!); } else if ('mapValue' in value) { // BsonBinaryValue contains an array of bytes, and needs to extract `subtype` and `data` from it before canonifying. - if (detectSpecialMapType(value) === 'bsonBinaryValue') { + if (detectSpecialMapType(value) === SpecialMapValueType.BSON_BINARY) { return canonifyBsonBinaryData(value.mapValue!); } return canonifyMap(value.mapValue!); @@ -679,11 +768,6 @@ function canonifyArray(arrayValue: ArrayValue): string { export function estimateByteSize(value: Value): number { switch (typeOrder(value)) { case TypeOrder.NullValue: - // MinKeyValue and NullValue has same TypeOrder number, but MinKeyValue is encoded as MapValue - // and its size should be estimated differently. - if ('mapValue' in value) { - return estimateMapByteSize(value.mapValue!); - } return 4; case TypeOrder.BooleanValue: return 4; @@ -716,6 +800,7 @@ export function estimateByteSize(value: Value): number { case TypeOrder.BsonObjectIdValue: case TypeOrder.BsonBinaryValue: case TypeOrder.BsonTimestampValue: + case TypeOrder.MinKeyValue: case TypeOrder.MaxKeyValue: return estimateMapByteSize(value.mapValue!); default: @@ -801,19 +886,9 @@ export function isMapValue( return !!value && 'mapValue' in value; } -/** Returns true if `value` is a VectorValue. */ -export function isVectorValue(value: ProtoValue | null): boolean { - return !!value && detectSpecialMapType(value) === 'vectorValue'; -} - -/** Returns true if the `Value` represents the canonical {@link #INTERNAL_MAX_VALUE} . */ -export function isMaxValue(value: Value): boolean { - return detectSpecialMapType(value) === 'maxValue'; -} - -function detectSpecialMapType(value: Value): string { +export function detectSpecialMapType(value: Value): SpecialMapValueType { if (!value || !value.mapValue || !value.mapValue.fields) { - return ''; // Not a special map type + return SpecialMapValueType.REGULAR_MAP; // Not a special map type } const fields = value.mapValue.fields; @@ -821,10 +896,10 @@ function detectSpecialMapType(value: Value): string { // Check for type-based mappings const type = fields[TYPE_KEY]?.stringValue; if (type) { - const typeMap: Record = { - [RESERVED_VECTOR_KEY]: 'vectorValue', - [RESERVED_MAX_KEY]: 'maxValue', - [RESERVED_SERVER_TIMESTAMP_KEY]: 'serverTimestampValue' + const typeMap: Record = { + [RESERVED_VECTOR_KEY]: SpecialMapValueType.VECTOR, + [RESERVED_MAX_KEY]: SpecialMapValueType.INTERNAL_MAX, + [RESERVED_SERVER_TIMESTAMP_KEY]: SpecialMapValueType.SERVER_TIMESTAMP }; if (typeMap[type]) { return typeMap[type]; @@ -832,14 +907,14 @@ function detectSpecialMapType(value: Value): string { } // Check for BSON-related mappings - const bsonMap: Record = { - [RESERVED_REGEX_KEY]: 'regexValue', - [RESERVED_BSON_OBJECT_ID_KEY]: 'bsonObjectIdValue', - [RESERVED_INT32_KEY]: 'int32Value', - [RESERVED_BSON_TIMESTAMP_KEY]: 'bsonTimestampValue', - [RESERVED_BSON_BINARY_KEY]: 'bsonBinaryValue', - [RESERVED_MIN_KEY]: 'minKeyValue', - [RESERVED_MAX_KEY]: 'maxKeyValue' + const bsonMap: Record = { + [RESERVED_REGEX_KEY]: SpecialMapValueType.REGEX, + [RESERVED_BSON_OBJECT_ID_KEY]: SpecialMapValueType.BSON_OBJECT_ID, + [RESERVED_INT32_KEY]: SpecialMapValueType.INT32, + [RESERVED_BSON_TIMESTAMP_KEY]: SpecialMapValueType.BSON_TIMESTAMP, + [RESERVED_BSON_BINARY_KEY]: SpecialMapValueType.BSON_BINARY, + [RESERVED_MIN_KEY]: SpecialMapValueType.MIN_KEY, + [RESERVED_MAX_KEY]: SpecialMapValueType.MAX_KEY }; for (const key in bsonMap) { @@ -848,18 +923,18 @@ function detectSpecialMapType(value: Value): string { } } - return ''; + return SpecialMapValueType.REGULAR_MAP; } export function isBsonType(value: Value): boolean { const bsonTypes = new Set([ - 'regexValue', - 'bsonObjectIdValue', - 'int32Value', - 'bsonTimestampValue', - 'bsonBinaryValue', - 'minKeyValue', - 'maxKeyValue' + SpecialMapValueType.REGEX, + SpecialMapValueType.BSON_OBJECT_ID, + SpecialMapValueType.INT32, + SpecialMapValueType.BSON_TIMESTAMP, + SpecialMapValueType.BSON_BINARY, + SpecialMapValueType.MIN_KEY, + SpecialMapValueType.MAX_KEY ]); return bsonTypes.has(detectSpecialMapType(value)); } @@ -912,9 +987,24 @@ export function valuesGetLowerBound(value: Value): Value { } else if ('arrayValue' in value) { return { arrayValue: {} }; } else if ('mapValue' in value) { - // TODO(Mila/BSON): add lower bound for bson types for indexing - if (isVectorValue(value)) { + const type = detectSpecialMapType(value); + if (type === SpecialMapValueType.VECTOR) { return MIN_VECTOR_VALUE; + } else if (type === SpecialMapValueType.BSON_OBJECT_ID) { + return MIN_BSON_OBJECT_ID_VALUE; + } else if (type === SpecialMapValueType.BSON_TIMESTAMP) { + return MIN_BSON_TIMESTAMP_VALUE; + } else if (type === SpecialMapValueType.BSON_BINARY) { + return MIN_BSON_BINARY_VALUE; + } else if (type === SpecialMapValueType.REGEX) { + return MIN_REGEX_VALUE; + } else if (type === SpecialMapValueType.INT32) { + // int32Value is treated the same as integerValue and doubleValue + return { doubleValue: NaN }; + } else if (type === SpecialMapValueType.MIN_KEY) { + return MIN_KEY_VALUE; + } else if (type === SpecialMapValueType.MAX_KEY) { + return MAX_KEY_VALUE; } return { mapValue: {} }; } else { @@ -925,29 +1015,44 @@ export function valuesGetLowerBound(value: Value): Value { /** Returns the largest value for the given value type (exclusive). */ export function valuesGetUpperBound(value: Value): Value { if ('nullValue' in value) { - return { booleanValue: false }; + return MIN_KEY_VALUE; } else if ('booleanValue' in value) { return { doubleValue: NaN }; } else if ('integerValue' in value || 'doubleValue' in value) { return { timestampValue: { seconds: Number.MIN_SAFE_INTEGER } }; } else if ('timestampValue' in value) { - return { stringValue: '' }; + return MIN_BSON_TIMESTAMP_VALUE; } else if ('stringValue' in value) { return { bytesValue: '' }; } else if ('bytesValue' in value) { - return refValue(DatabaseId.empty(), DocumentKey.empty()); + return MIN_BSON_BINARY_VALUE; } else if ('referenceValue' in value) { - return { geoPointValue: { latitude: -90, longitude: -180 } }; + return MIN_BSON_OBJECT_ID_VALUE; } else if ('geoPointValue' in value) { - return { arrayValue: {} }; + return MIN_REGEX_VALUE; } else if ('arrayValue' in value) { return MIN_VECTOR_VALUE; } else if ('mapValue' in value) { - // TODO(Mila/BSON): add upper bound for bson types for indexing - if (isVectorValue(value)) { + const type = detectSpecialMapType(value); + if (type === SpecialMapValueType.VECTOR) { return { mapValue: {} }; + } else if (type === SpecialMapValueType.BSON_OBJECT_ID) { + return { geoPointValue: { latitude: -90, longitude: -180 } }; + } else if (type === SpecialMapValueType.BSON_TIMESTAMP) { + return { stringValue: '' }; + } else if (type === SpecialMapValueType.BSON_BINARY) { + return refValue(DatabaseId.empty(), DocumentKey.empty()); + } else if (type === SpecialMapValueType.REGEX) { + return { arrayValue: {} }; + } else if (type === SpecialMapValueType.INT32) { + // int32Value is treated the same as integerValue and doubleValue + return { timestampValue: { seconds: Number.MIN_SAFE_INTEGER } }; + } else if (type === SpecialMapValueType.MIN_KEY) { + return { booleanValue: false }; + } else if (type === SpecialMapValueType.MAX_KEY) { + return INTERNAL_MAX_VALUE; } - return INTERNAL_MAX_VALUE; + return MAX_KEY_VALUE; } else { return fail('Invalid value type: ' + JSON.stringify(value)); } diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 2856862f6de..d4091768e7c 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -62,7 +62,6 @@ import { WithFieldValue, Timestamp, FieldPath, - newTestFirestore, SnapshotOptions, newTestApp, FirestoreError, @@ -76,7 +75,10 @@ import { maxKey, minKey, regex, - or + or, + newTestFirestore, + GeoPoint, + Bytes } from '../util/firebase_export'; import { apiDescribe, @@ -89,7 +91,9 @@ import { withNamedTestDbsOrSkipUnlessUsingEmulator, toDataArray, checkOnlineAndOfflineResultsMatch, - toIds + toIds, + withTestProjectIdAndCollectionSettings, + checkCacheRoundTrip } from '../util/helpers'; import { DEFAULT_SETTINGS, DEFAULT_PROJECT_ID } from '../util/settings'; @@ -2443,14 +2447,12 @@ apiDescribe('Database', persistence => { }; it('can write and read BSON types', async () => { - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); - + {}, + async coll => { const docRef = await addDoc(coll, { binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), objectId: bsonObjectId('507f191e810c19729de860ea'), @@ -2491,6 +2493,49 @@ apiDescribe('Database', persistence => { ); }); + it('can write and read BSON types offline', async () => { + return withTestProjectIdAndCollectionSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + {}, + async (coll, db) => { + await disableNetwork(db); + const docRef = doc(coll, 'testDoc'); + + // Adding docs to cache, do not wait for promise to resolve. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + setDoc(docRef, { + binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), + objectId: bsonObjectId('507f191e810c19729de860ea'), + int32: int32(1), + regex: regex('^foo', 'i'), + timestamp: bsonTimestamp(1, 2), + min: minKey(), + max: maxKey() + }); + + const snapshot = await getDocFromCache(docRef); + expect( + snapshot + .get('binary') + .isEqual(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ).to.be.true; + expect( + snapshot + .get('objectId') + .isEqual(bsonObjectId('507f191e810c19729de860ea')) + ).to.be.true; + expect(snapshot.get('int32').isEqual(int32(1))).to.be.true; + expect(snapshot.get('regex').isEqual(regex('^foo', 'i'))).to.be.true; + expect(snapshot.get('timestamp').isEqual(bsonTimestamp(1, 2))).to.be + .true; + expect(snapshot.get('min') === minKey()).to.be.true; + expect(snapshot.get('max') === maxKey()).to.be.true; + } + ); + }); + it('can filter and order objectIds', async () => { const testDocs = { a: { key: bsonObjectId('507f191e810c19729de860ea') }, @@ -2498,17 +2543,12 @@ apiDescribe('Database', persistence => { c: { key: bsonObjectId('507f191e810c19729de860ec') } }; - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); - await addDoc(coll, testDocs['a']); - await addDoc(coll, testDocs['b']); - await addDoc(coll, testDocs['c']); - + testDocs, + async (coll, db) => { let orderedQuery = query( coll, where('key', '>', bsonObjectId('507f191e810c19729de860ea')), @@ -2520,6 +2560,7 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['b'] ]); + await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); orderedQuery = query( coll, @@ -2535,6 +2576,7 @@ apiDescribe('Database', persistence => { testDocs['b'], testDocs['a'] ]); + await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); } ); }); @@ -2545,17 +2587,12 @@ apiDescribe('Database', persistence => { b: { key: int32(1) }, c: { key: int32(2) } }; - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); - await addDoc(coll, testDocs['a']); - await addDoc(coll, testDocs['b']); - await addDoc(coll, testDocs['c']); - + testDocs, + async (coll, db) => { let orderedQuery = query( coll, where('key', '>=', int32(1)), @@ -2567,6 +2604,7 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['b'] ]); + await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); orderedQuery = query( coll, @@ -2579,6 +2617,7 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['a'] ]); + await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); } ); }); @@ -2589,17 +2628,12 @@ apiDescribe('Database', persistence => { b: { key: bsonTimestamp(1, 2) }, c: { key: bsonTimestamp(2, 1) } }; - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); - await addDoc(coll, testDocs['a']); - await addDoc(coll, testDocs['b']); - await addDoc(coll, testDocs['c']); - + testDocs, + async (coll, db) => { let orderedQuery = query( coll, where('key', '>', bsonTimestamp(1, 1)), @@ -2611,6 +2645,7 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['b'] ]); + await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); orderedQuery = query( coll, @@ -2623,6 +2658,7 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['b'] ]); + await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); } ); }); @@ -2633,17 +2669,12 @@ apiDescribe('Database', persistence => { b: { key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) }, c: { key: bsonBinaryData(2, new Uint8Array([1, 2, 3])) } }; - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); - await addDoc(coll, testDocs['a']); - await addDoc(coll, testDocs['b']); - await addDoc(coll, testDocs['c']); - + testDocs, + async (coll, db) => { let orderedQuery = query( coll, where('key', '>', bsonBinaryData(1, new Uint8Array([1, 2, 3]))), @@ -2655,6 +2686,7 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['b'] ]); + await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); orderedQuery = query( coll, @@ -2668,6 +2700,7 @@ apiDescribe('Database', persistence => { testDocs['b'], testDocs['a'] ]); + await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); } ); }); @@ -2678,17 +2711,12 @@ apiDescribe('Database', persistence => { b: { key: regex('^bar', 'x') }, c: { key: regex('^baz', 'i') } }; - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); - await addDoc(coll, testDocs['a']); - await addDoc(coll, testDocs['b']); - await addDoc(coll, testDocs['c']); - + testDocs, + async (coll, db) => { const orderedQuery = query( coll, or( @@ -2703,6 +2731,7 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['a'] ]); + await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); } ); }); @@ -2711,29 +2740,62 @@ apiDescribe('Database', persistence => { const testDocs = { a: { key: minKey() }, b: { key: minKey() }, - c: { key: maxKey() } + c: { key: null }, + d: { key: 1 }, + e: { key: maxKey() } }; - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); - await addDoc(coll, testDocs['a']); - await addDoc(coll, testDocs['b']); - await addDoc(coll, testDocs['c']); + testDocs, + async (coll, db) => { + let filteredQuery = query(coll, where('key', '==', minKey())); + let snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['a'], + testDocs['b'] + ]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); - const orderedQuery = query( - coll, - where('key', '==', minKey()), - orderBy('key', 'desc') // minKeys are equal, would sort by documentId as secondary order - ); - const snapshot = await getDocs(orderedQuery); + filteredQuery = query(coll, where('key', '!=', minKey())); + snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ - testDocs['b'], - testDocs['a'] + testDocs['d'], + testDocs['e'] ]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '>=', minKey())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['a'], + testDocs['b'] + ]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '<=', minKey())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['a'], + testDocs['b'] + ]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '>', minKey())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '<', minKey())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '<', 1)); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); } ); }); @@ -2741,30 +2803,98 @@ apiDescribe('Database', persistence => { it('can filter and order maxKey values', async () => { const testDocs = { a: { key: minKey() }, - b: { key: maxKey() }, - c: { key: maxKey() } + b: { key: 1 }, + c: { key: maxKey() }, + d: { key: maxKey() }, + e: { key: null } }; - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); - await addDoc(coll, testDocs['a']); - await addDoc(coll, testDocs['b']); - await addDoc(coll, testDocs['c']); - - const orderedQuery = query( - coll, - where('key', '==', maxKey()), - orderBy('key', 'desc') // maxKeys are equal, would sort by documentId as secondary order - ); - const snapshot = await getDocs(orderedQuery); + testDocs, + async (coll, db) => { + let filteredQuery = query(coll, where('key', '==', maxKey())); + let snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ testDocs['c'], + testDocs['d'] + ]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '!=', maxKey())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['a'], testDocs['b'] ]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '>=', maxKey())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['c'], + testDocs['d'] + ]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '<=', maxKey())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['c'], + testDocs['d'] + ]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '>', maxKey())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '<', maxKey())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '>', 1)); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + } + ); + }); + + it('can handle null with bson values', async () => { + const testDocs = { + a: { key: minKey() }, + b: { key: null }, + c: { key: null }, + d: { key: 1 }, + e: { key: maxKey() } + }; + + return withTestProjectIdAndCollectionSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + testDocs, + async (coll, db) => { + let filteredQuery = query(coll, where('key', '==', null)); + let snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['b'], + testDocs['c'] + ]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + + filteredQuery = query(coll, where('key', '!=', null)); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['a'], + testDocs['d'], + testDocs['e'] + ]); + await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); } ); }); @@ -2778,20 +2908,12 @@ apiDescribe('Database', persistence => { e: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, f: { key: regex('^foo', 'i') } }; - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); - await addDoc(coll, testDocs['a']); - await addDoc(coll, testDocs['b']); - await addDoc(coll, testDocs['c']); - await addDoc(coll, testDocs['d']); - await addDoc(coll, testDocs['e']); - await addDoc(coll, testDocs['f']); - + testDocs, + async (coll, db) => { const orderedQuery = query(coll, orderBy('key', 'asc')); const storeEvent = new EventsAccumulator(); @@ -2861,5 +2983,125 @@ apiDescribe('Database', persistence => { } ); }); + + // eslint-disable-next-line no-restricted-properties + (persistence.gc === 'lru' ? describe : describe.skip)('From Cache', () => { + it('SDK orders different value types together the same way online and offline', async () => { + const testDocs: { [key: string]: DocumentData } = { + a: { key: null }, + b: { key: minKey() }, + c: { key: true }, + d: { key: NaN }, + e: { key: int32(1) }, + f: { key: 2.0 }, + g: { key: 3 }, + h: { key: new Timestamp(100, 123456000) }, + i: { key: bsonTimestamp(1, 2) }, + j: { key: 'string' }, + k: { key: Bytes.fromUint8Array(new Uint8Array([0, 1, 255])) }, + l: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, + n: { key: bsonObjectId('507f191e810c19729de860ea') }, + o: { key: new GeoPoint(0, 0) }, + p: { key: regex('^foo', 'i') }, + q: { key: [1, 2] }, + r: { key: vector([1, 2]) }, + s: { key: { a: 1 } }, + t: { key: maxKey() } + }; + + return withTestProjectIdAndCollectionSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + testDocs, + async coll => { + // TODO(Mila/BSON): remove after prod supports bson, and use `ref` helper function instead + const docRef = doc(coll, 'doc'); + await setDoc(doc(coll, 'm'), { key: docRef }); + + const orderedQuery = query(coll, orderBy('key', 'desc')); + await checkOnlineAndOfflineResultsMatch( + orderedQuery, + 't', + 's', + 'r', + 'q', + 'p', + 'o', + 'n', + 'm', + 'l', + 'k', + 'j', + 'i', + 'h', + 'g', + 'f', + 'e', + 'd', + 'c', + 'b', + 'a' + ); + } + ); + }); + + it('SDK orders bson types the same way online and offline', async () => { + const testDocs: { [key: string]: DocumentData } = { + a: { key: maxKey() }, // maxKeys are all equal + b: { key: maxKey() }, + c: { key: int32(1) }, + d: { key: int32(-1) }, + e: { key: int32(0) }, + f: { key: bsonTimestamp(1, 1) }, + g: { key: bsonTimestamp(2, 1) }, + h: { key: bsonTimestamp(1, 2) }, + i: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, + j: { key: bsonBinaryData(1, new Uint8Array([1, 1, 4])) }, + k: { key: bsonBinaryData(2, new Uint8Array([1, 0, 0])) }, + l: { key: bsonObjectId('507f191e810c19729de860eb') }, + m: { key: bsonObjectId('507f191e810c19729de860ea') }, + n: { key: bsonObjectId('407f191e810c19729de860ea') }, + o: { key: regex('^foo', 'i') }, + p: { key: regex('^foo', 'm') }, + q: { key: regex('^bar', 'i') }, + r: { key: minKey() }, // minKeys are all equal + s: { key: minKey() } + }; + + return withTestProjectIdAndCollectionSettings( + persistence, + NIGHTLY_PROJECT_ID, + settings, + testDocs, + async coll => { + const orderedQuery = query(coll, orderBy('key')); + await checkOnlineAndOfflineResultsMatch( + orderedQuery, + 'r', + 's', + 'd', + 'e', + 'c', + 'f', + 'h', + 'g', + 'j', + 'i', + 'k', + 'n', + 'm', + 'l', + 'q', + 'o', + 'p', + 'a', + 'b' + ); + } + ); + }); + }); }); }); diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index 01fd0e47e35..91090d04fa9 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -925,20 +925,6 @@ apiDescribe('Queries', persistence => { { array: ['a', 42, 'c'] }, { array: [42], array2: ['bingo'] } ]); - - // NOTE: The backend doesn't currently support null, NaN, objects, or - // arrays, so there isn't much of anything else interesting to test. - // With null. - const snapshot3 = await getDocs( - query(coll, where('zip', 'array-contains', null)) - ); - expect(toDataArray(snapshot3)).to.deep.equal([]); - - // With NaN. - const snapshot4 = await getDocs( - query(coll, where('zip', 'array-contains', Number.NaN)) - ); - expect(toDataArray(snapshot4)).to.deep.equal([]); }); }); diff --git a/packages/firestore/test/integration/api/type.test.ts b/packages/firestore/test/integration/api/type.test.ts index a6218f6a1ad..156eba426f8 100644 --- a/packages/firestore/test/integration/api/type.test.ts +++ b/packages/firestore/test/integration/api/type.test.ts @@ -17,7 +17,6 @@ import { expect } from 'chai'; -import { AutoId } from '../../../src/util/misc'; import { addEqualityMatcher } from '../../util/equality_matcher'; import { EventsAccumulator } from '../util/events_accumulator'; import { @@ -52,6 +51,7 @@ import { } from '../util/firebase_export'; import { apiDescribe, + withTestProjectIdAndCollectionSettings, withTestDb, withTestDbsSettings, withTestDoc @@ -384,13 +384,13 @@ apiDescribe('Firestore', persistence => { }); it('invalid 32-bit integer gets rejected', async () => { - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const docRef = doc(dbs[0], 'test-collection/test-doc'); + {}, + async coll => { + const docRef = doc(coll, 'test-doc'); let errorMessage; try { await setDoc(docRef, { key: int32(2147483648) }); @@ -414,13 +414,13 @@ apiDescribe('Firestore', persistence => { }); it('invalid BSON timestamp gets rejected', async () => { - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const docRef = doc(dbs[0], 'test-collection/test-doc'); + {}, + async coll => { + const docRef = doc(coll, 'test-doc'); let errorMessage; try { // BSON timestamp larger than 32-bit integer gets rejected @@ -446,13 +446,13 @@ apiDescribe('Firestore', persistence => { }); it('invalid regex value gets rejected', async () => { - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const docRef = doc(dbs[0], 'test-collection/test-doc'); + {}, + async coll => { + const docRef = doc(coll, 'test-doc'); let errorMessage; try { await setDoc(docRef, { key: regex('foo', 'a') }); @@ -467,13 +467,13 @@ apiDescribe('Firestore', persistence => { }); it('invalid bsonObjectId value gets rejected', async () => { - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const docRef = doc(dbs[0], 'test-collection/test-doc'); + {}, + async coll => { + const docRef = doc(coll, 'test-doc'); let errorMessage; try { @@ -490,13 +490,13 @@ apiDescribe('Firestore', persistence => { }); it('invalid bsonBinaryData value gets rejected', async () => { - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const docRef = doc(dbs[0], 'test-collection/test-doc'); + {}, + async coll => { + const docRef = doc(coll, 'test-doc'); let errorMessage; try { await setDoc(docRef, { @@ -537,18 +537,13 @@ apiDescribe('Firestore', persistence => { maxValue: { key: maxKey() } }; - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); - for (const key of Object.keys(testDocs)) { - await setDoc(doc(coll, key), testDocs[key]); - } - - // TODO(Mila/BSON): replace after prod supports bson + testDocs, + async coll => { + // TODO(Mila/BSON): remove after prod supports bson const docRef = doc(coll, 'doc'); await setDoc(doc(coll, 'referenceValue'), { key: docRef }); diff --git a/packages/firestore/test/integration/api/validation.test.ts b/packages/firestore/test/integration/api/validation.test.ts index 9c74634affa..72978f71fe3 100644 --- a/packages/firestore/test/integration/api/validation.test.ts +++ b/packages/firestore/test/integration/api/validation.test.ts @@ -856,6 +856,20 @@ apiDescribe('Validation:', persistence => { ).to.throw("Invalid query. You cannot use more than one '!=' filter."); }); + validationIt(persistence, 'rejects invalid NaN filter', db => { + const coll = collection(db, 'test'); + expect(() => query(coll, where('foo', '>', NaN))).to.throw( + "Invalid query. You can only perform '==' and '!=' comparisons on NaN." + ); + }); + + validationIt(persistence, 'rejects invalid Null filter', db => { + const coll = collection(db, 'test'); + expect(() => query(coll, where('foo', '>', null))).to.throw( + "Invalid query. You can only perform '==' and '!=' comparisons on Null." + ); + }); + validationIt(persistence, 'with != and not-in filters fail', db => { expect(() => query( diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index 465bc8edd61..2c789ec9151 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -44,7 +44,9 @@ import { Query, getDocsFromServer, getDocsFromCache, - _AutoId + _AutoId, + disableNetwork, + enableNetwork } from './firebase_export'; import { ALT_PROJECT_ID, @@ -444,10 +446,27 @@ export function withTestCollectionSettings( settings: PrivateSettings, docs: { [key: string]: DocumentData }, fn: (collection: CollectionReference, db: Firestore) => Promise +): Promise { + return withTestProjectIdAndCollectionSettings( + persistence, + DEFAULT_PROJECT_ID, + settings, + docs, + fn + ); +} + +export function withTestProjectIdAndCollectionSettings( + persistence: PersistenceMode | typeof PERSISTENCE_MODE_UNSPECIFIED, + projectId: string, + settings: PrivateSettings, + docs: { [key: string]: DocumentData }, + fn: (collection: CollectionReference, db: Firestore) => Promise ): Promise { const collectionId = _AutoId.newId(); - return batchCommitDocsToCollection( + return batchCommitDocsToCollectionWithSettings( persistence, + projectId, settings, docs, collectionId, @@ -462,10 +481,28 @@ export function batchCommitDocsToCollection( collectionId: string, fn: (collection: CollectionReference, db: Firestore) => Promise ): Promise { - return withTestDbsSettings( + return batchCommitDocsToCollectionWithSettings( persistence, DEFAULT_PROJECT_ID, settings, + docs, + collectionId, + fn + ); +} + +export function batchCommitDocsToCollectionWithSettings( + persistence: PersistenceMode | typeof PERSISTENCE_MODE_UNSPECIFIED, + projectId: string, + settings: PrivateSettings, + docs: { [key: string]: DocumentData }, + collectionId: string, + fn: (collection: CollectionReference, db: Firestore) => Promise +): Promise { + return withTestDbsSettings( + persistence, + projectId, + settings, 2, ([testDb, setupDb]) => { const testCollection = collection(testDb, collectionId); @@ -557,3 +594,34 @@ export async function checkOnlineAndOfflineResultsMatch( const docsFromCache = await getDocsFromCache(query); expect(toIds(docsFromServer)).to.deep.equal(toIds(docsFromCache)); } + +/** + * Checks that documents fetched from the server and stored in the cache can be + * successfully retrieved from the cache and matches the expected documents. + * + * This function performs the following steps: + * 1. Fetch documents from the server for provided query and populate the cache. + * 2. Disables the network connection to simulate offline mode. + * 3. Retrieves the documents from the cache using the same query. + * 4. Compares the cached documents with the expected documents. + * + * @param query The query to check. + * @param db The Firestore database instance. + * @param expectedDocs Optional ordered list of document data that are expected to be retrieved from the cache. + */ +export async function checkCacheRoundTrip( + query: Query, + db: Firestore, + expectedDocs: DocumentData[] +): Promise { + await getDocsFromServer(query); + + await disableNetwork(db); + const docsFromCache = await getDocsFromCache(query); + + if (expectedDocs.length !== 0) { + expect(expectedDocs).to.deep.equal(toDataArray(docsFromCache)); + } + + await enableNetwork(db); +} diff --git a/packages/firestore/test/unit/index/firestore_index_value_writer.test.ts b/packages/firestore/test/unit/index/firestore_index_value_writer.test.ts index 8daa97eb77d..c646726feeb 100644 --- a/packages/firestore/test/unit/index/firestore_index_value_writer.test.ts +++ b/packages/firestore/test/unit/index/firestore_index_value_writer.test.ts @@ -16,13 +16,35 @@ */ import { expect } from 'chai'; +import { + bsonBinaryData, + bsonObjectId, + bsonTimestamp, + int32, + regex +} from '../../../lite'; import { FirestoreIndexValueWriter } from '../../../src/index/firestore_index_value_writer'; import { IndexByteEncoder } from '../../../src/index/index_byte_encoder'; import { Timestamp } from '../../../src/lite-api/timestamp'; +import { + parseBsonBinaryData, + parseInt32Value, + parseMaxKey, + parseMinKey, + parseBsonObjectId, + parseRegexValue, + parseBsonTimestamp +} from '../../../src/lite-api/user_data_reader'; import { IndexKind } from '../../../src/model/field_index'; import type { Value } from '../../../src/protos/firestore_proto_api'; -import { toTimestamp } from '../../../src/remote/serializer'; -import { JSON_SERIALIZER } from '../local/persistence_test_helpers'; +import { + JsonProtoSerializer, + toTimestamp +} from '../../../src/remote/serializer'; +import { + JSON_SERIALIZER, + TEST_DATABASE_ID +} from '../local/persistence_test_helpers'; import { compare } from './ordered_code_writer.test'; @@ -247,4 +269,344 @@ describe('Firestore Index Value Writer', () => { ).to.equal(1); }); }); + + describe('can gracefully handle BSON types', () => { + it('can compare BSON ObjectIds', () => { + const value1 = { + mapValue: { + fields: { + '__oid__': { stringValue: '507f191e810c19729de860ea' } + } + } + }; + const value2 = { + mapValue: { + fields: { + '__oid__': { stringValue: '507f191e810c19729de860eb' } + } + } + }; + const value3 = parseBsonObjectId( + bsonObjectId('507f191e810c19729de860ea') + ); + + expect( + compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value1, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value1, value1, IndexKind.ASCENDING) + ).to.equal(0); + + expect( + compareIndexEncodedValues(value3, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value3, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value3, value1, IndexKind.ASCENDING) + ).to.equal(0); + }); + + it('can compare BSON Timestamps', () => { + const value1 = { + mapValue: { + fields: { + '__request_timestamp__': { + mapValue: { + fields: { + seconds: { integerValue: 1 }, + increment: { integerValue: 2 } + } + } + } + } + } + }; + const value2 = { + mapValue: { + fields: { + '__request_timestamp__': { + mapValue: { + fields: { + seconds: { integerValue: 1 }, + increment: { integerValue: 3 } + } + } + } + } + } + }; + const value3 = parseBsonTimestamp(bsonTimestamp(1, 2)); + const value4 = parseBsonTimestamp(bsonTimestamp(2, 1)); + + expect( + compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value1, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value1, value1, IndexKind.ASCENDING) + ).to.equal(0); + + expect( + compareIndexEncodedValues(value3, value1, IndexKind.ASCENDING) + ).to.equal(0); + expect( + compareIndexEncodedValues(value3, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value3, IndexKind.ASCENDING) + ).to.equal(1); + + expect( + compareIndexEncodedValues(value4, value1, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value4, value2, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value4, value3, IndexKind.ASCENDING) + ).to.equal(1); + }); + + it('can compare BSON Binary', () => { + const value1 = { + mapValue: { + fields: { + '__binary__': { + bytesValue: 'AQECAw==' // 1, 1, 2, 3 + } + } + } + }; + const value2 = { + mapValue: { + fields: { + '__binary__': { + bytesValue: 'AQECBA==' // 1, 1, 2, 4 + } + } + } + }; + + const serializer = new JsonProtoSerializer( + TEST_DATABASE_ID, + /* useProto3Json= */ false + ); + const value3 = parseBsonBinaryData( + serializer, + bsonBinaryData(1, new Uint8Array([1, 2, 3])) + ); + + const jsonSerializer = new JsonProtoSerializer( + TEST_DATABASE_ID, + /* useProto3Json= */ true + ); + + const value4 = parseBsonBinaryData( + jsonSerializer, + bsonBinaryData(1, new Uint8Array([1, 2, 3])) + ); + + expect( + compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value1, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value1, value1, IndexKind.ASCENDING) + ).to.equal(0); + + expect( + compareIndexEncodedValues(value3, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value3, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value3, value1, IndexKind.ASCENDING) + ).to.equal(0); + + expect( + compareIndexEncodedValues(value4, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value4, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value4, value1, IndexKind.ASCENDING) + ).to.equal(0); + }); + + it('can compare BSON Regex', () => { + const value1 = { + mapValue: { + fields: { + '__regex__': { + mapValue: { + fields: { + 'pattern': { stringValue: '^foo' }, + 'options': { stringValue: 'i' } + } + } + } + } + } + }; + const value2 = { + mapValue: { + fields: { + '__regex__': { + mapValue: { + fields: { + 'pattern': { stringValue: '^foo' }, + 'options': { stringValue: 'm' } + } + } + } + } + } + }; + const value3 = parseRegexValue(regex('^foo', 'i')); + const value4 = parseRegexValue(regex('^zoo', 'i')); + + expect( + compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value1, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value1, value1, IndexKind.ASCENDING) + ).to.equal(0); + + expect( + compareIndexEncodedValues(value3, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value3, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value3, value1, IndexKind.ASCENDING) + ).to.equal(0); + + expect( + compareIndexEncodedValues(value4, value1, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value4, value2, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value4, value3, IndexKind.ASCENDING) + ).to.equal(1); + }); + + it('can compare BSON Int32', () => { + const value1 = { + mapValue: { + fields: { + '__int__': { integerValue: 1 } + } + } + }; + const value2 = { + mapValue: { + fields: { + '__int__': { integerValue: 2 } + } + } + }; + const value3 = parseInt32Value(int32(1)); + + expect( + compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value1, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value1, value1, IndexKind.ASCENDING) + ).to.equal(0); + + expect( + compareIndexEncodedValues(value3, value2, IndexKind.ASCENDING) + ).to.equal(-1); + expect( + compareIndexEncodedValues(value2, value3, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value3, value1, IndexKind.ASCENDING) + ).to.equal(0); + }); + + it('can compare BSON MinKey', () => { + const value1 = { + mapValue: { + fields: { + '__min__': { + nullValue: 'NULL_VALUE' as const + } + } + } + }; + const value2 = { + mapValue: { + fields: { + '__min__': { + nullValue: 'NULL_VALUE' as const + } + } + } + }; + const value3 = parseMinKey(); + + expect( + compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING) + ).to.equal(0); + expect( + compareIndexEncodedValues(value1, value3, IndexKind.DESCENDING) + ).to.equal(0); + expect( + compareIndexEncodedValues(value1, value1, IndexKind.ASCENDING) + ).to.equal(0); + }); + + it('can compare BSON MaxKey', () => { + const value1 = { + mapValue: { + fields: { + '__max__': { + nullValue: 'NULL_VALUE' as const + } + } + } + }; + const value2 = { + mapValue: { + fields: { + '__max__': { + nullValue: 'NULL_VALUE' as const + } + } + } + }; + const value3 = parseMaxKey(); + + expect( + compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING) + ).to.equal(0); + expect( + compareIndexEncodedValues(value1, value3, IndexKind.DESCENDING) + ).to.equal(0); + expect( + compareIndexEncodedValues(value1, value1, IndexKind.ASCENDING) + ).to.equal(0); + }); + }); }); diff --git a/packages/firestore/test/unit/local/index_manager.test.ts b/packages/firestore/test/unit/local/index_manager.test.ts index 2521be99bf5..51bef76b31e 100644 --- a/packages/firestore/test/unit/local/index_manager.test.ts +++ b/packages/firestore/test/unit/local/index_manager.test.ts @@ -17,6 +17,7 @@ import { expect } from 'chai'; +import { Bytes, GeoPoint } from '../../../src/'; import { User } from '../../../src/auth/user'; import { FieldFilter } from '../../../src/core/filter'; import { @@ -30,7 +31,16 @@ import { queryWithLimit, queryWithStartAt } from '../../../src/core/query'; -import { vector } from '../../../src/lite-api/field_value_impl'; +import { + bsonBinaryData, + bsonObjectId, + bsonTimestamp, + int32, + maxKey, + minKey, + regex, + vector +} from '../../../src/lite-api/field_value_impl'; import { Timestamp } from '../../../src/lite-api/timestamp'; import { displayNameForIndexType, @@ -71,6 +81,7 @@ import { orFilter, path, query, + ref, version, wrap } from '../../util/helpers'; @@ -327,6 +338,14 @@ describe('IndexedDbIndexManager', async () => { await addDoc('coll/doc2', {}); }); + it('adds string', async () => { + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['exists', IndexKind.ASCENDING]] }) + ); + await addDoc('coll/doc1', { 'exists': 'a' }); + await addDoc('coll/doc2', { 'exists': 'b' }); + }); + it('applies orderBy', async () => { await indexManager.addFieldIndex( fieldIndex('coll', { fields: [['count', IndexKind.ASCENDING]] }) @@ -1856,6 +1875,592 @@ describe('IndexedDbIndexManager', async () => { await validateIsNoneIndex(query2); }); + describe('BSON type indexing', () => { + it('can index BSON ObjectId fields', async () => { + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) + ); + + await addDoc('coll/doc1', { + key: bsonObjectId('507f191e810c19729de860ea') + }); + await addDoc('coll/doc2', { + key: bsonObjectId('507f191e810c19729de860eb') + }); + await addDoc('coll/doc3', { + key: bsonObjectId('507f191e810c19729de860ec') + }); + + const fieldIndexes = await indexManager.getFieldIndexes('coll'); + expect(fieldIndexes).to.have.length(1); + + let q = queryWithAddedOrderBy(query('coll'), orderBy('key')); + await verifyResults(q, 'coll/doc1', 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '==', bsonObjectId('507f191e810c19729de860ea')) + ); + await verifyResults(q, 'coll/doc1'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '!=', bsonObjectId('507f191e810c19729de860ea')) + ); + await verifyResults(q, 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>=', bsonObjectId('507f191e810c19729de860eb')) + ); + await verifyResults(q, 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<=', bsonObjectId('507f191e810c19729de860eb')) + ); + await verifyResults(q, 'coll/doc1', 'coll/doc2'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', bsonObjectId('507f191e810c19729de860eb')) + ); + await verifyResults(q, 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', bsonObjectId('507f191e810c19729de860eb')) + ); + await verifyResults(q, 'coll/doc1'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', bsonObjectId('507f191e810c19729de860ec')) + ); + await verifyResults(q); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', bsonObjectId('507f191e810c19729de860ea')) + ); + await verifyResults(q); + }); + + it('can index BSON Binary Data fields', async () => { + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) + ); + await addDoc('coll/doc1', { + key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + }); + await addDoc('coll/doc2', { + key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) + }); + await addDoc('coll/doc3', { + key: bsonBinaryData(1, new Uint8Array([2, 1, 2])) + }); + + const fieldIndexes = await indexManager.getFieldIndexes('coll'); + expect(fieldIndexes).to.have.length(1); + + let q = queryWithAddedOrderBy(query('coll'), orderBy('key')); + await verifyResults(q, 'coll/doc1', 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '==', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ); + await verifyResults(q, 'coll/doc1'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '!=', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ); + await verifyResults(q, 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>=', bsonBinaryData(1, new Uint8Array([1, 2, 4]))) + ); + await verifyResults(q, 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<=', bsonBinaryData(1, new Uint8Array([1, 2, 4]))) + ); + await verifyResults(q, 'coll/doc1', 'coll/doc2'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', bsonBinaryData(1, new Uint8Array([1, 2, 4]))) + ); + await verifyResults(q, 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', bsonBinaryData(1, new Uint8Array([1, 2, 4]))) + ); + await verifyResults(q, 'coll/doc1'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', bsonBinaryData(1, new Uint8Array([2, 1, 2]))) + ); + await verifyResults(q); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ); + await verifyResults(q); + }); + + it('can index BSON Timestamp fields', async () => { + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) + ); + await addDoc('coll/doc1', { + key: bsonTimestamp(1, 1) + }); + await addDoc('coll/doc2', { + key: bsonTimestamp(1, 2) + }); + await addDoc('coll/doc3', { + key: bsonTimestamp(2, 1) + }); + + const fieldIndexes = await indexManager.getFieldIndexes('coll'); + expect(fieldIndexes).to.have.length(1); + + let q = queryWithAddedOrderBy(query('coll'), orderBy('key')); + await verifyResults(q, 'coll/doc1', 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '==', bsonTimestamp(1, 1)) + ); + await verifyResults(q, 'coll/doc1'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '!=', bsonTimestamp(1, 1)) + ); + await verifyResults(q, 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>=', bsonTimestamp(1, 2)) + ); + await verifyResults(q, 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<=', bsonTimestamp(1, 2)) + ); + await verifyResults(q, 'coll/doc1', 'coll/doc2'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', bsonTimestamp(1, 2)) + ); + await verifyResults(q, 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', bsonTimestamp(1, 2)) + ); + await verifyResults(q, 'coll/doc1'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', bsonTimestamp(2, 1)) + ); + await verifyResults(q); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', bsonTimestamp(1, 1)) + ); + await verifyResults(q); + }); + + it('can index Int32 fields', async () => { + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) + ); + await addDoc('coll/doc1', { + key: int32(1) + }); + await addDoc('coll/doc2', { + key: int32(2) + }); + await addDoc('coll/doc3', { + key: int32(3) + }); + const fieldIndexes = await indexManager.getFieldIndexes('coll'); + expect(fieldIndexes).to.have.length(1); + + let q = queryWithAddedOrderBy(query('coll'), orderBy('key')); + await verifyResults(q, 'coll/doc1', 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter(query('coll'), filter('key', '==', int32(1))); + await verifyResults(q, 'coll/doc1'); + + q = queryWithAddedFilter(query('coll'), filter('key', '!=', int32(1))); + await verifyResults(q, 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter(query('coll'), filter('key', '>=', int32(2))); + await verifyResults(q, 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter(query('coll'), filter('key', '<=', int32(2))); + await verifyResults(q, 'coll/doc1', 'coll/doc2'); + + q = queryWithAddedFilter(query('coll'), filter('key', '>', int32(2))); + await verifyResults(q, 'coll/doc3'); + + q = queryWithAddedFilter(query('coll'), filter('key', '<', int32(2))); + await verifyResults(q, 'coll/doc1'); + + q = queryWithAddedFilter(query('coll'), filter('key', '>', int32(3))); + await verifyResults(q); + + q = queryWithAddedFilter(query('coll'), filter('key', '<', int32(1))); + await verifyResults(q); + }); + + it('can index regex fields', async () => { + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) + ); + await addDoc('coll/doc1', { + key: regex('a', 'i') + }); + await addDoc('coll/doc2', { + key: regex('a', 'm') + }); + await addDoc('coll/doc3', { + key: regex('b', 'i') + }); + const fieldIndexes = await indexManager.getFieldIndexes('coll'); + expect(fieldIndexes).to.have.length(1); + let q = queryWithAddedOrderBy(query('coll'), orderBy('key')); + await verifyResults(q, 'coll/doc1', 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '==', regex('a', 'i')) + ); + await verifyResults(q, 'coll/doc1'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '!=', regex('a', 'i')) + ); + await verifyResults(q, 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>=', regex('a', 'm')) + ); + await verifyResults(q, 'coll/doc2', 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<=', regex('a', 'm')) + ); + await verifyResults(q, 'coll/doc1', 'coll/doc2'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', regex('a', 'm')) + ); + await verifyResults(q, 'coll/doc3'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', regex('a', 'm')) + ); + await verifyResults(q, 'coll/doc1'); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', regex('b', 'i')) + ); + await verifyResults(q); + + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', regex('a', 'i')) + ); + await verifyResults(q); + }); + + it('can index minKey fields', async () => { + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) + ); + await addDoc('coll/doc1', { + key: minKey() + }); + await addDoc('coll/doc2', { + key: minKey() + }); + await addDoc('coll/doc3', { + key: null + }); + await addDoc('coll/doc4', { + key: 1 + }); + await addDoc('coll/doc5', { + key: maxKey() + }); + + const fieldIndexes = await indexManager.getFieldIndexes('coll'); + expect(fieldIndexes).to.have.length(1); + + let q = queryWithAddedOrderBy(query('coll'), orderBy('key')); + await verifyResults( + q, + 'coll/doc3', + 'coll/doc1', + 'coll/doc2', + 'coll/doc4', + 'coll/doc5' + ); + + q = queryWithAddedFilter(query('coll'), filter('key', '==', minKey())); + await verifyResults(q, 'coll/doc1', 'coll/doc2'); + + q = queryWithAddedFilter(query('coll'), filter('key', '!=', minKey())); + await verifyResults(q, 'coll/doc4', 'coll/doc5'); + + q = queryWithAddedFilter(query('coll'), filter('key', '>=', minKey())); + await verifyResults(q, 'coll/doc1', 'coll/doc2'); + + q = queryWithAddedFilter(query('coll'), filter('key', '<=', minKey())); + await verifyResults(q, 'coll/doc1', 'coll/doc2'); + + q = queryWithAddedFilter(query('coll'), filter('key', '>', minKey())); + await verifyResults(q); + + q = queryWithAddedFilter(query('coll'), filter('key', '<', minKey())); + await verifyResults(q); + }); + + it('can index maxKey fields', async () => { + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) + ); + await addDoc('coll/doc1', { + key: minKey() + }); + await addDoc('coll/doc2', { + key: 1 + }); + await addDoc('coll/doc3', { + key: maxKey() + }); + await addDoc('coll/doc4', { + key: maxKey() + }); + await addDoc('coll/doc5', { + key: null + }); + + const fieldIndexes = await indexManager.getFieldIndexes('coll'); + expect(fieldIndexes).to.have.length(1); + + let q = queryWithAddedOrderBy(query('coll'), orderBy('key')); + await verifyResults( + q, + 'coll/doc5', + 'coll/doc1', + 'coll/doc2', + 'coll/doc3', + 'coll/doc4' + ); + + q = queryWithAddedFilter(query('coll'), filter('key', '==', maxKey())); + await verifyResults(q, 'coll/doc3', 'coll/doc4'); + + q = queryWithAddedFilter(query('coll'), filter('key', '!=', maxKey())); + await verifyResults(q, 'coll/doc1', 'coll/doc2'); + + q = queryWithAddedFilter(query('coll'), filter('key', '>=', maxKey())); + await verifyResults(q, 'coll/doc3', 'coll/doc4'); + + q = queryWithAddedFilter(query('coll'), filter('key', '<=', maxKey())); + await verifyResults(q, 'coll/doc3', 'coll/doc4'); + + q = queryWithAddedFilter(query('coll'), filter('key', '>', maxKey())); + await verifyResults(q); + + q = queryWithAddedFilter(query('coll'), filter('key', '<', maxKey())); + await verifyResults(q); + }); + + it('can index fields of BSON types together', async () => { + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['key', IndexKind.DESCENDING]] }) + ); + await addDoc('coll/doc1', { + key: minKey() + }); + + await addDoc('coll/doc2', { + key: int32(2) + }); + await addDoc('coll/doc3', { + key: int32(1) + }); + + await addDoc('coll/doc4', { + key: bsonTimestamp(1, 2) + }); + await addDoc('coll/doc5', { + key: bsonTimestamp(1, 1) + }); + + await addDoc('coll/doc6', { + key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) + }); + await addDoc('coll/doc7', { + key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + }); + await addDoc('coll/doc8', { + key: bsonObjectId('507f191e810c19729de860eb') + }); + await addDoc('coll/doc9', { + key: bsonObjectId('507f191e810c19729de860ea') + }); + + await addDoc('coll/doc10', { + key: regex('a', 'm') + }); + await addDoc('coll/doc11', { + key: regex('a', 'i') + }); + + await addDoc('coll/doc12', { + key: maxKey() + }); + + const fieldIndexes = await indexManager.getFieldIndexes('coll'); + expect(fieldIndexes).to.have.length(1); + + const q = queryWithAddedOrderBy(query('coll'), orderBy('key', 'desc')); + await verifyResults( + q, + 'coll/doc12', + 'coll/doc10', + 'coll/doc11', + 'coll/doc8', + 'coll/doc9', + 'coll/doc6', + 'coll/doc7', + 'coll/doc4', + 'coll/doc5', + 'coll/doc2', + 'coll/doc3', + 'coll/doc1' + ); + }); + }); + + it('can index fields of all types together', async () => { + await indexManager.addFieldIndex( + fieldIndex('coll', { fields: [['key', IndexKind.DESCENDING]] }) + ); + await addDoc('coll/a', { + key: null + }); + await addDoc('coll/b', { + key: minKey() + }); + await addDoc('coll/c', { + key: true + }); + await addDoc('coll/d', { + key: NaN + }); + await addDoc('coll/e', { + key: int32(1) + }); + await addDoc('coll/f', { + key: 2.0 + }); + await addDoc('coll/g', { + key: 3 + }); + await addDoc('coll/h', { + key: new Timestamp(100, 123456000) + }); + await addDoc('coll/i', { + key: bsonTimestamp(1, 2) + }); + await addDoc('coll/j', { + key: 'string' + }); + await addDoc('coll/k', { + key: Bytes.fromUint8Array(new Uint8Array([0, 1, 255])) as Bytes + }); + await addDoc('coll/l', { + key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + }); + await addDoc('coll/m', { + key: ref('coll/doc') + }); + await addDoc('coll/n', { + key: bsonObjectId('507f191e810c19729de860ea') + }); + await addDoc('coll/o', { + key: new GeoPoint(0, 1) + }); + await addDoc('coll/p', { + key: regex('^foo', 'i') + }); + await addDoc('coll/q', { + key: [1, 2] + }); + await addDoc('coll/r', { + key: vector([1, 2]) + }); + await addDoc('coll/s', { + key: { a: 1 } + }); + await addDoc('coll/t', { + key: maxKey() + }); + + const fieldIndexes = await indexManager.getFieldIndexes('coll'); + expect(fieldIndexes).to.have.length(1); + + const q = queryWithAddedOrderBy(query('coll'), orderBy('key', 'desc')); + await verifyResults( + q, + 'coll/t', + 'coll/s', + 'coll/r', + 'coll/q', + 'coll/p', + 'coll/o', + 'coll/n', + 'coll/m', + 'coll/l', + 'coll/k', + 'coll/j', + 'coll/i', + 'coll/h', + 'coll/g', + 'coll/f', + 'coll/e', + 'coll/d', + 'coll/c', + 'coll/b', + 'coll/a' + ); + }); + async function validateIsPartialIndex(query: Query): Promise { await validateIndexType(query, IndexType.PARTIAL); } diff --git a/packages/firestore/test/unit/model/values.test.ts b/packages/firestore/test/unit/model/values.test.ts index bf46386c800..7a2558d2461 100644 --- a/packages/firestore/test/unit/model/values.test.ts +++ b/packages/firestore/test/unit/model/values.test.ts @@ -48,7 +48,14 @@ import { valuesGetUpperBound, TYPE_KEY, RESERVED_VECTOR_KEY, - VECTOR_MAP_VECTORS_KEY + VECTOR_MAP_VECTORS_KEY, + MIN_BSON_TIMESTAMP_VALUE, + MIN_VECTOR_VALUE, + RESERVED_INT32_KEY, + MIN_BSON_BINARY_VALUE, + MIN_KEY_VALUE, + MIN_REGEX_VALUE, + MIN_BSON_OBJECT_ID_VALUE } from '../../../src/model/values'; import * as api from '../../../src/protos/firestore_proto_api'; import { primitiveComparator } from '../../../src/util/misc'; @@ -71,14 +78,8 @@ describe('Values', () => { const values: api.Value[][] = [ [wrap(true), wrap(true)], [wrap(false), wrap(false)], - // MinKeys are all equal, and sort the same as null. - [ - wrap(null), - wrap(null), - wrap(minKey()), - wrap(minKey()), - wrap(MinKey.instance()) - ], + [wrap(null), wrap(null)], + [wrap(minKey()), wrap(minKey()), wrap(MinKey.instance())], [wrap(0 / 0), wrap(Number.NaN), wrap(NaN)], // -0.0 and 0.0 order the same but are not considered equal. [wrap(-0.0)], @@ -166,7 +167,10 @@ describe('Values', () => { it('orders types correctly', () => { const groups = [ // null first - [wrap(null), wrap(minKey())], + [wrap(null)], + + // MinKey is after null + [wrap(minKey())], // booleans [wrap(false)], @@ -480,21 +484,25 @@ describe('Values', () => { }); it('computes lower bound', () => { - // TODO(Mila/BSON):add cases for bson types const groups = [ - // null and minKey first - [ - valuesGetLowerBound({ nullValue: 'NULL_VALUE' }), - wrap(null), - wrap(minKey()) - ], + // lower bound of null is null + [valuesGetLowerBound({ nullValue: 'NULL_VALUE' }), wrap(null)], + + // lower bound of MinKey is MinKey + [valuesGetLowerBound(MIN_KEY_VALUE), wrap(minKey())], // booleans [valuesGetLowerBound({ booleanValue: true }), wrap(false)], [wrap(true)], // numbers - [valuesGetLowerBound({ doubleValue: 0 }), wrap(NaN)], + [ + valuesGetLowerBound({ doubleValue: 0 }), + valuesGetLowerBound({ + mapValue: { fields: { [RESERVED_INT32_KEY]: { integerValue: 0 } } } + }), + wrap(NaN) + ], [wrap(Number.NEGATIVE_INFINITY)], [wrap(Number.MIN_VALUE)], @@ -502,10 +510,31 @@ describe('Values', () => { [valuesGetLowerBound({ timestampValue: {} })], [wrap(date1)], + // bson timestamps + [ + valuesGetLowerBound(wrap(bsonTimestamp(4294967295, 4294967295))), + MIN_BSON_TIMESTAMP_VALUE, + wrap(bsonTimestamp(0, 0)) + ], + [wrap(bsonTimestamp(1, 1))], + // strings - [valuesGetLowerBound({ stringValue: '' }), wrap('')], + [valuesGetLowerBound({ stringValue: 'Z' }), wrap('')], [wrap('\u0000')], + // blobs + [valuesGetLowerBound({ bytesValue: 'Z' }), wrap(blob())], + [wrap(blob(0))], + + // bson binary data + [ + valuesGetLowerBound( + wrap(bsonBinaryData(128, new Uint8Array([128, 128]))) + ), + MIN_BSON_BINARY_VALUE + ], + [wrap(bsonBinaryData(0, new Uint8Array([0])))], + // resource names [ valuesGetLowerBound({ referenceValue: '' }), @@ -513,6 +542,14 @@ describe('Values', () => { ], [refValue(DatabaseId.empty(), key('a/a'))], + // bson object ids + [ + valuesGetLowerBound(wrap(bsonObjectId('ZZZ'))), + wrap(bsonObjectId('')), + MIN_BSON_OBJECT_ID_VALUE + ], + [wrap(bsonObjectId('a'))], + // geo points [ valuesGetLowerBound({ geoPointValue: {} }), @@ -520,6 +557,14 @@ describe('Values', () => { ], [wrap(new GeoPoint(-90, 0))], + // regular expressions + [ + valuesGetLowerBound(wrap(regex('ZZZ', 'i'))), + wrap(regex('', '')), + MIN_REGEX_VALUE + ], + [wrap(regex('a', 'i'))], + // arrays [valuesGetLowerBound({ arrayValue: {} }), wrap([])], [wrap([false])], @@ -557,17 +602,22 @@ describe('Values', () => { }); it('computes upper bound', () => { - // TODO(Mila/BSON):add cases for bson types const groups = [ // null first [wrap(null)], - [valuesGetUpperBound({ nullValue: 'NULL_VALUE' })], + + // upper value of null is MinKey + [valuesGetUpperBound({ nullValue: 'NULL_VALUE' }), wrap(minKey())], + + // upper value of MinKey is boolean `false` + [valuesGetUpperBound(MIN_KEY_VALUE), wrap(false)], // booleans [wrap(true)], [valuesGetUpperBound({ booleanValue: false })], // numbers + [wrap(int32(2147483647))], //largest int32 value [wrap(Number.MAX_SAFE_INTEGER)], [wrap(Number.POSITIVE_INFINITY)], [valuesGetUpperBound({ doubleValue: NaN })], @@ -576,6 +626,10 @@ describe('Values', () => { [wrap(date1)], [valuesGetUpperBound({ timestampValue: {} })], + // bson timestamps + [wrap(bsonTimestamp(4294967295, 4294967295))], // largest bson timestamp value + [valuesGetUpperBound(MIN_BSON_TIMESTAMP_VALUE)], + // strings [wrap('\u0000')], [valuesGetUpperBound({ stringValue: '' })], @@ -584,20 +638,39 @@ describe('Values', () => { [wrap(blob(255))], [valuesGetUpperBound({ bytesValue: '' })], + // bson binary data + [wrap(bsonBinaryData(128, new Uint8Array([255, 255, 255])))], + [valuesGetUpperBound(MIN_BSON_BINARY_VALUE)], + // resource names [refValue(dbId('', ''), key('a/a'))], [valuesGetUpperBound({ referenceValue: '' })], + // bson object ids + [wrap(bsonObjectId('foo'))], + [valuesGetUpperBound(MIN_BSON_OBJECT_ID_VALUE)], + // geo points [wrap(new GeoPoint(90, 180))], [valuesGetUpperBound({ geoPointValue: {} })], + // regular expressions + [wrap(regex('a', 'i'))], + [valuesGetUpperBound(MIN_REGEX_VALUE)], + // arrays [wrap([false])], [valuesGetUpperBound({ arrayValue: {} })], + // vectors + [wrap(vector([1, 2, 3]))], + [valuesGetUpperBound(MIN_VECTOR_VALUE)], + // objects - [wrap({ 'a': 'b' })] + [wrap({ 'a': 'b' })], + + // MaxKey + [wrap(maxKey())] ]; expectCorrectComparisonGroups( From 82f32ca7de9508352e26be16ad7b1ab2f0eb0d46 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Fri, 14 Mar 2025 13:44:38 -0400 Subject: [PATCH 03/23] rename BsonTimestampValue class to BsonTimestamp (#333) --- packages/firestore/lite/index.ts | 2 +- .../src/lite-api/bson_timestamp_value.ts | 12 ++++++------ .../firestore/src/lite-api/field_value_impl.ts | 10 +++++----- .../firestore/src/lite-api/user_data_reader.ts | 8 ++++---- .../firestore/src/lite-api/user_data_writer.ts | 18 ++++++++---------- .../firestore/test/unit/model/values.test.ts | 4 ++-- 6 files changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/firestore/lite/index.ts b/packages/firestore/lite/index.ts index 6d1d6c01998..48e0bdae068 100644 --- a/packages/firestore/lite/index.ts +++ b/packages/firestore/lite/index.ts @@ -156,7 +156,7 @@ export { BsonBinaryData } from '../src/lite-api/bson_binary_data'; export { BsonObjectId } from '../src/lite-api/bson_object_Id'; -export { BsonTimestampValue } from '../src/lite-api/bson_timestamp_value'; +export { BsonTimestamp } from '../src/lite-api/bson_timestamp_value'; export { MinKey } from '../src/lite-api/min_key'; diff --git a/packages/firestore/src/lite-api/bson_timestamp_value.ts b/packages/firestore/src/lite-api/bson_timestamp_value.ts index 60b48157906..0b317f9042c 100644 --- a/packages/firestore/src/lite-api/bson_timestamp_value.ts +++ b/packages/firestore/src/lite-api/bson_timestamp_value.ts @@ -18,18 +18,18 @@ /** * Represents a BSON Timestamp type in Firestore documents. * - * @class BsonTimestampValue + * @class BsonTimestamp */ -export class BsonTimestampValue { +export class BsonTimestamp { constructor(readonly seconds: number, readonly increment: number) {} /** - * Returns true if this `BsonTimestampValue` is equal to the provided one. + * Returns true if this `BsonTimestamp` is equal to the provided one. * - * @param other - The `BsonTimestampValue` to compare against. - * @return 'true' if this `BsonTimestampValue` is equal to the provided one. + * @param other - The `BsonTimestamp` to compare against. + * @return 'true' if this `BsonTimestamp` is equal to the provided one. */ - isEqual(other: BsonTimestampValue): boolean { + isEqual(other: BsonTimestamp): boolean { return this.seconds === other.seconds && this.increment === other.increment; } } diff --git a/packages/firestore/src/lite-api/field_value_impl.ts b/packages/firestore/src/lite-api/field_value_impl.ts index 2cc1e3522b0..ade0656e0d3 100644 --- a/packages/firestore/src/lite-api/field_value_impl.ts +++ b/packages/firestore/src/lite-api/field_value_impl.ts @@ -17,7 +17,7 @@ import { BsonBinaryData } from './bson_binary_data'; import { BsonObjectId } from './bson_object_Id'; -import { BsonTimestampValue } from './bson_timestamp_value'; +import { BsonTimestamp } from './bson_timestamp_value'; import { FieldValue } from './field_value'; import { Int32Value } from './int32_value'; import { MaxKey } from './max_key'; @@ -167,18 +167,18 @@ export function bsonObjectId(value: string): BsonObjectId { } /** - * Creates a new `BsonTimestampValue` constructed with the given seconds and increment. + * Creates a new `BsonTimestamp` constructed with the given seconds and increment. * * @param seconds - The underlying unsigned 32-bit integer for seconds. * @param seconds - The underlying unsigned 32-bit integer for increment. * - * @returns A new `BsonTimestampValue` constructed with the given seconds and increment. + * @returns A new `BsonTimestamp` constructed with the given seconds and increment. */ export function bsonTimestamp( seconds: number, increment: number -): BsonTimestampValue { - return new BsonTimestampValue(seconds, increment); +): BsonTimestamp { + return new BsonTimestamp(seconds, increment); } /** diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index 3d0ce031599..9d7e6fa79f1 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -77,7 +77,7 @@ import { Dict, forEach, isEmpty } from '../util/obj'; import { BsonBinaryData } from './bson_binary_data'; import { BsonObjectId } from './bson_object_Id'; -import { BsonTimestampValue } from './bson_timestamp_value'; +import { BsonTimestamp } from './bson_timestamp_value'; import { Bytes } from './bytes'; import { Firestore } from './database'; import { FieldPath } from './field_path'; @@ -934,7 +934,7 @@ function parseScalarValue( return parseBsonObjectId(value); } else if (value instanceof Int32Value) { return parseInt32Value(value); - } else if (value instanceof BsonTimestampValue) { + } else if (value instanceof BsonTimestamp) { return parseBsonTimestamp(value); } else if (value instanceof BsonBinaryData) { return parseBsonBinaryData(context.serializer, value); @@ -1043,7 +1043,7 @@ export function parseInt32Value(value: Int32Value): ProtoValue { return { mapValue }; } -export function parseBsonTimestamp(value: BsonTimestampValue): ProtoValue { +export function parseBsonTimestamp(value: BsonTimestamp): ProtoValue { const mapValue: ProtoMapValue = { fields: { [RESERVED_BSON_TIMESTAMP_KEY]: { @@ -1105,7 +1105,7 @@ function looksLikeJsonObject(input: unknown): boolean { !(input instanceof Int32Value) && !(input instanceof RegexValue) && !(input instanceof BsonObjectId) && - !(input instanceof BsonTimestampValue) && + !(input instanceof BsonTimestamp) && !(input instanceof BsonBinaryData) ); } diff --git a/packages/firestore/src/lite-api/user_data_writer.ts b/packages/firestore/src/lite-api/user_data_writer.ts index 012b04874c3..e4719591b4c 100644 --- a/packages/firestore/src/lite-api/user_data_writer.ts +++ b/packages/firestore/src/lite-api/user_data_writer.ts @@ -60,7 +60,7 @@ import { forEach } from '../util/obj'; import { BsonBinaryData } from './bson_binary_data'; import { BsonObjectId } from './bson_object_Id'; -import { BsonTimestampValue } from './bson_timestamp_value'; +import { BsonTimestamp } from './bson_timestamp_value'; import { maxKey, minKey } from './field_value_impl'; import { GeoPoint } from './geo_point'; import { Int32Value } from './int32_value'; @@ -112,11 +112,11 @@ export abstract class AbstractUserDataWriter { case TypeOrder.RegexValue: return this.convertToRegexValue(value.mapValue!); case TypeOrder.BsonObjectIdValue: - return this.convertToBsonObjectIdValue(value.mapValue!); + return this.convertToBsonObjectId(value.mapValue!); case TypeOrder.BsonBinaryValue: - return this.convertToBsonBinaryValue(value.mapValue!); + return this.convertToBsonBinaryData(value.mapValue!); case TypeOrder.BsonTimestampValue: - return this.convertToBsonTimestampValue(value.mapValue!); + return this.convertToBsonTimestamp(value.mapValue!); case TypeOrder.MaxKeyValue: return maxKey(); case TypeOrder.MinKeyValue: @@ -160,13 +160,13 @@ export abstract class AbstractUserDataWriter { return new VectorValue(values); } - private convertToBsonObjectIdValue(mapValue: ProtoMapValue): BsonObjectId { + private convertToBsonObjectId(mapValue: ProtoMapValue): BsonObjectId { const oid = mapValue!.fields?.[RESERVED_BSON_OBJECT_ID_KEY]?.stringValue ?? ''; return new BsonObjectId(oid); } - private convertToBsonBinaryValue(mapValue: ProtoMapValue): BsonBinaryData { + private convertToBsonBinaryData(mapValue: ProtoMapValue): BsonBinaryData { const fields = mapValue!.fields?.[RESERVED_BSON_BINARY_KEY]; const subtypeAndData = fields?.bytesValue; if (!subtypeAndData) { @@ -182,9 +182,7 @@ export abstract class AbstractUserDataWriter { return new BsonBinaryData(Number(subtype), data); } - private convertToBsonTimestampValue( - mapValue: ProtoMapValue - ): BsonTimestampValue { + private convertToBsonTimestamp(mapValue: ProtoMapValue): BsonTimestamp { const fields = mapValue!.fields?.[RESERVED_BSON_TIMESTAMP_KEY]; const seconds = Number( fields?.mapValue?.fields?.[RESERVED_BSON_TIMESTAMP_SECONDS_KEY] @@ -194,7 +192,7 @@ export abstract class AbstractUserDataWriter { fields?.mapValue?.fields?.[RESERVED_BSON_TIMESTAMP_INCREMENT_KEY] ?.integerValue ); - return new BsonTimestampValue(seconds, increment); + return new BsonTimestamp(seconds, increment); } private convertToRegexValue(mapValue: ProtoMapValue): RegexValue { diff --git a/packages/firestore/test/unit/model/values.test.ts b/packages/firestore/test/unit/model/values.test.ts index 7a2558d2461..4054dd6481d 100644 --- a/packages/firestore/test/unit/model/values.test.ts +++ b/packages/firestore/test/unit/model/values.test.ts @@ -21,7 +21,7 @@ import { GeoPoint, Timestamp } from '../../../src'; import { DatabaseId } from '../../../src/core/database_info'; import { BsonBinaryData } from '../../../src/lite-api/bson_binary_data'; import { BsonObjectId } from '../../../src/lite-api/bson_object_Id'; -import { BsonTimestampValue } from '../../../src/lite-api/bson_timestamp_value'; +import { BsonTimestamp } from '../../../src/lite-api/bson_timestamp_value'; import { vector, regex, @@ -118,7 +118,7 @@ describe('Values', () => { [wrap(vector([]))], [wrap(vector([1, 2.3, -4.0]))], [wrap(regex('^foo', 'i')), wrap(new RegexValue('^foo', 'i'))], - [wrap(bsonTimestamp(57, 4)), wrap(new BsonTimestampValue(57, 4))], + [wrap(bsonTimestamp(57, 4)), wrap(new BsonTimestamp(57, 4))], [ wrap(bsonBinaryData(128, Uint8Array.from([7, 8, 9]))), wrap(new BsonBinaryData(128, Uint8Array.from([7, 8, 9]))), From 1df3d26fbfb4db24b74d5d779825017e9ec40eaa Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Fri, 4 Apr 2025 11:55:48 -0700 Subject: [PATCH 04/23] Fix Data Connect Types (#8898) --- .changeset/hungry-snails-drive.md | 5 +++++ common/api-review/data-connect.api.md | 18 +++++++++++++++++- packages/data-connect/src/api/index.ts | 1 + packages/data-connect/src/core/error.ts | 1 - 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .changeset/hungry-snails-drive.md diff --git a/.changeset/hungry-snails-drive.md b/.changeset/hungry-snails-drive.md new file mode 100644 index 00000000000..1a29782a04d --- /dev/null +++ b/.changeset/hungry-snails-drive.md @@ -0,0 +1,5 @@ +--- +"@firebase/data-connect": patch +--- + +Fix DataConnectOperationError. diff --git a/common/api-review/data-connect.api.md b/common/api-review/data-connect.api.md index 786714361af..9e3d2424876 100644 --- a/common/api-review/data-connect.api.md +++ b/common/api-review/data-connect.api.md @@ -24,6 +24,20 @@ export const CallerSdkTypeEnum: { readonly GeneratedAngular: "GeneratedAngular"; }; +// @public (undocumented) +export type Code = DataConnectErrorCode; + +// @public (undocumented) +export const Code: { + OTHER: DataConnectErrorCode; + ALREADY_INITIALIZED: DataConnectErrorCode; + NOT_INITIALIZED: DataConnectErrorCode; + NOT_SUPPORTED: DataConnectErrorCode; + INVALID_ARGUMENT: DataConnectErrorCode; + PARTIAL_ERROR: DataConnectErrorCode; + UNAUTHORIZED: DataConnectErrorCode; +}; + // @public export function connectDataConnectEmulator(dc: DataConnect, host: string, port?: number, sslEnabled?: boolean): void; @@ -54,7 +68,9 @@ export class DataConnect { // @public export class DataConnectError extends FirebaseError { - } + /* Excluded from this release type: name */ + constructor(code: Code, message: string); +} // @public (undocumented) export type DataConnectErrorCode = 'other' | 'already-initialized' | 'not-initialized' | 'not-supported' | 'invalid-argument' | 'partial-error' | 'unauthorized'; diff --git a/packages/data-connect/src/api/index.ts b/packages/data-connect/src/api/index.ts index dcd48485571..72ee8b313e5 100644 --- a/packages/data-connect/src/api/index.ts +++ b/packages/data-connect/src/api/index.ts @@ -24,6 +24,7 @@ export { setLogLevel } from '../logger'; export { validateArgs } from '../util/validateArgs'; export { DataConnectErrorCode, + Code, DataConnectError, DataConnectOperationError, DataConnectOperationFailureResponse, diff --git a/packages/data-connect/src/core/error.ts b/packages/data-connect/src/core/error.ts index b1246969e48..bbf1e299e0d 100644 --- a/packages/data-connect/src/core/error.ts +++ b/packages/data-connect/src/core/error.ts @@ -43,7 +43,6 @@ export class DataConnectError extends FirebaseError { /** @internal */ readonly name: string = 'DataConnectError'; - /** @hideconstructor */ constructor(code: Code, message: string) { super(code, message); From 66a09f247603519768c6b6a3e1c13732a48781f8 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 8 Apr 2025 13:17:02 -0400 Subject: [PATCH 05/23] test(vertexai): update mock responses to v8 (#8904) --- scripts/update_vertexai_responses.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update_vertexai_responses.sh b/scripts/update_vertexai_responses.sh index 0d1f1a2c6f6..de55ac176ce 100755 --- a/scripts/update_vertexai_responses.sh +++ b/scripts/update_vertexai_responses.sh @@ -17,7 +17,7 @@ # This script replaces mock response files for Vertex AI unit tests with a fresh # clone of the shared repository of Vertex AI test data. -RESPONSES_VERSION='v7.*' # The major version of mock responses to use +RESPONSES_VERSION='v8.*' # The major version of mock responses to use REPO_NAME="vertexai-sdk-test-data" REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git" From b3328250d516142c8bec4f5c9bfe8663523ffcb4 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:26:26 -0400 Subject: [PATCH 06/23] Fix: display WebChannel error message (#8907) --- .../firestore/src/platform/browser/webchannel_connection.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/firestore/src/platform/browser/webchannel_connection.ts b/packages/firestore/src/platform/browser/webchannel_connection.ts index 6813b88f65a..206e5829c41 100644 --- a/packages/firestore/src/platform/browser/webchannel_connection.ts +++ b/packages/firestore/src/platform/browser/webchannel_connection.ts @@ -326,8 +326,10 @@ export class WebChannelConnection extends RestConnection { closed = true; logWarn( LOG_TAG, - `RPC '${rpcName}' stream ${streamId} transport errored:`, - err + `RPC '${rpcName}' stream ${streamId} transport errored. Name:`, + err.name, + 'Message:', + err.message ); streamBridge.callOnClose( new FirestoreError( From 4e0f630e714cd63e1e39ffb0b56918a88951fe8e Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Wed, 9 Apr 2025 14:19:49 -0400 Subject: [PATCH 07/23] test(vertexai): add `backendName` param to mock response getters (#8906) `backendName` can be either `googleAI` or `vertexAI`. This can be passed to `getMockResponse` or `getMockResponseStreaming` to specify whether the mock response lookup should be done in the set of vertexAI or googleAI mock files. Modified the `convert-mocks.ts` script to read mock responses from the 'developerapi' directory, and add an export to the generated file for the new lookup object with those mock responses. --- .../vertexai/src/methods/count-tokens.test.ts | 12 +- .../src/methods/generate-content.test.ts | 26 +++- .../src/models/generative-model.test.ts | 11 +- .../vertexai/src/models/imagen-model.test.ts | 3 + .../vertexai/src/requests/request.test.ts | 1 + .../src/requests/response-helpers.test.ts | 5 + .../src/requests/stream-reader.test.ts | 16 ++- packages/vertexai/test-utils/convert-mocks.ts | 131 +++++++++++++----- packages/vertexai/test-utils/mock-response.ts | 27 +++- 9 files changed, 186 insertions(+), 46 deletions(-) diff --git a/packages/vertexai/src/methods/count-tokens.test.ts b/packages/vertexai/src/methods/count-tokens.test.ts index a3d7c99b4ba..9eccbf702fe 100644 --- a/packages/vertexai/src/methods/count-tokens.test.ts +++ b/packages/vertexai/src/methods/count-tokens.test.ts @@ -45,7 +45,10 @@ describe('countTokens()', () => { restore(); }); it('total tokens', async () => { - const mockResponse = getMockResponse('unary-success-total-tokens.json'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-total-tokens.json' + ); const makeRequestStub = stub(request, 'makeRequest').resolves( mockResponse as Response ); @@ -69,6 +72,7 @@ describe('countTokens()', () => { }); it('total tokens with modality details', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-detailed-token-response.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -96,6 +100,7 @@ describe('countTokens()', () => { }); it('total tokens no billable characters', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-no-billable-characters.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -120,7 +125,10 @@ describe('countTokens()', () => { ); }); it('model not found', async () => { - const mockResponse = getMockResponse('unary-failure-model-not-found.json'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-model-not-found.json' + ); const mockFetch = stub(globalThis, 'fetch').resolves({ ok: false, status: 404, diff --git a/packages/vertexai/src/methods/generate-content.test.ts b/packages/vertexai/src/methods/generate-content.test.ts index 426bd5176db..1d15632f828 100644 --- a/packages/vertexai/src/methods/generate-content.test.ts +++ b/packages/vertexai/src/methods/generate-content.test.ts @@ -61,6 +61,7 @@ describe('generateContent()', () => { }); it('short response', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-basic-reply-short.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -84,7 +85,10 @@ describe('generateContent()', () => { ); }); it('long response', async () => { - const mockResponse = getMockResponse('unary-success-basic-reply-long.json'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-long.json' + ); const makeRequestStub = stub(request, 'makeRequest').resolves( mockResponse as Response ); @@ -105,6 +109,7 @@ describe('generateContent()', () => { }); it('long response with token details', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-basic-response-long-usage-metadata.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -138,7 +143,10 @@ describe('generateContent()', () => { ); }); it('citations', async () => { - const mockResponse = getMockResponse('unary-success-citations.json'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-citations.json' + ); const makeRequestStub = stub(request, 'makeRequest').resolves( mockResponse as Response ); @@ -163,6 +171,7 @@ describe('generateContent()', () => { }); it('blocked prompt', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-failure-prompt-blocked-safety.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -184,6 +193,7 @@ describe('generateContent()', () => { }); it('finishReason safety', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-failure-finish-reason-safety.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -204,7 +214,10 @@ describe('generateContent()', () => { ); }); it('empty content', async () => { - const mockResponse = getMockResponse('unary-failure-empty-content.json'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-empty-content.json' + ); const makeRequestStub = stub(request, 'makeRequest').resolves( mockResponse as Response ); @@ -224,6 +237,7 @@ describe('generateContent()', () => { }); it('unknown enum - should ignore', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-unknown-enum-safety-ratings.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -244,7 +258,10 @@ describe('generateContent()', () => { ); }); it('image rejected (400)', async () => { - const mockResponse = getMockResponse('unary-failure-image-rejected.json'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-image-rejected.json' + ); const mockFetch = stub(globalThis, 'fetch').resolves({ ok: false, status: 400, @@ -257,6 +274,7 @@ describe('generateContent()', () => { }); it('api not enabled (403)', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-failure-firebasevertexai-api-not-enabled.json' ); const mockFetch = stub(globalThis, 'fetch').resolves({ diff --git a/packages/vertexai/src/models/generative-model.test.ts b/packages/vertexai/src/models/generative-model.test.ts index 26dff4e04c6..987f9b115e2 100644 --- a/packages/vertexai/src/models/generative-model.test.ts +++ b/packages/vertexai/src/models/generative-model.test.ts @@ -60,6 +60,7 @@ describe('GenerativeModel', () => { ); expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-basic-reply-short.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -89,6 +90,7 @@ describe('GenerativeModel', () => { }); expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-basic-reply-short.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -129,6 +131,7 @@ describe('GenerativeModel', () => { ); expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-basic-reply-short.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -177,6 +180,7 @@ describe('GenerativeModel', () => { ); expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-basic-reply-short.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -206,6 +210,7 @@ describe('GenerativeModel', () => { }); expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-basic-reply-short.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -239,6 +244,7 @@ describe('GenerativeModel', () => { ); expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-basic-reply-short.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -277,7 +283,10 @@ describe('GenerativeModel', () => { }); it('calls countTokens', async () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model' }); - const mockResponse = getMockResponse('unary-success-total-tokens.json'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-total-tokens.json' + ); const makeRequestStub = stub(request, 'makeRequest').resolves( mockResponse as Response ); diff --git a/packages/vertexai/src/models/imagen-model.test.ts b/packages/vertexai/src/models/imagen-model.test.ts index c566a88e5b0..9e534f2195a 100644 --- a/packages/vertexai/src/models/imagen-model.test.ts +++ b/packages/vertexai/src/models/imagen-model.test.ts @@ -47,6 +47,7 @@ const fakeVertexAI: VertexAI = { describe('ImagenModel', () => { it('generateImages makes a request to predict with default parameters', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-generate-images-base64.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -90,6 +91,7 @@ describe('ImagenModel', () => { }); const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-generate-images-base64.json' ); const makeRequestStub = stub(request, 'makeRequest').resolves( @@ -133,6 +135,7 @@ describe('ImagenModel', () => { }); it('throws if prompt blocked', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-failure-generate-images-prompt-blocked.json' ); diff --git a/packages/vertexai/src/requests/request.test.ts b/packages/vertexai/src/requests/request.test.ts index 499f06c848b..cd39a0f8ae5 100644 --- a/packages/vertexai/src/requests/request.test.ts +++ b/packages/vertexai/src/requests/request.test.ts @@ -414,6 +414,7 @@ describe('request methods', () => { }); it('Network error, API not enabled', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-failure-firebasevertexai-api-not-enabled.json' ); const fetchStub = stub(globalThis, 'fetch').resolves( diff --git a/packages/vertexai/src/requests/response-helpers.test.ts b/packages/vertexai/src/requests/response-helpers.test.ts index 4cab8cde047..5371d040253 100644 --- a/packages/vertexai/src/requests/response-helpers.test.ts +++ b/packages/vertexai/src/requests/response-helpers.test.ts @@ -257,6 +257,7 @@ describe('response-helpers methods', () => { describe('handlePredictResponse', () => { it('returns base64 images', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-generate-images-base64.json' ) as Response; const res = await handlePredictResponse(mockResponse); @@ -270,6 +271,7 @@ describe('response-helpers methods', () => { }); it('returns GCS images', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-success-generate-images-gcs.json' ) as Response; const res = await handlePredictResponse(mockResponse); @@ -284,6 +286,7 @@ describe('response-helpers methods', () => { }); it('has filtered reason and no images if all images were filtered', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-failure-generate-images-all-filtered.json' ) as Response; const res = await handlePredictResponse(mockResponse); @@ -294,6 +297,7 @@ describe('response-helpers methods', () => { }); it('has filtered reason and no images if all base64 images were filtered', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-failure-generate-images-base64-some-filtered.json' ) as Response; const res = await handlePredictResponse(mockResponse); @@ -308,6 +312,7 @@ describe('response-helpers methods', () => { }); it('has filtered reason and no images if all GCS images were filtered', async () => { const mockResponse = getMockResponse( + 'vertexAI', 'unary-failure-generate-images-gcs-some-filtered.json' ) as Response; const res = await handlePredictResponse(mockResponse); diff --git a/packages/vertexai/src/requests/stream-reader.test.ts b/packages/vertexai/src/requests/stream-reader.test.ts index b68c2423066..bf959276a93 100644 --- a/packages/vertexai/src/requests/stream-reader.test.ts +++ b/packages/vertexai/src/requests/stream-reader.test.ts @@ -72,6 +72,7 @@ describe('processStream', () => { }); it('streaming response - short', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-success-basic-reply-short.txt' ); const result = processStream(fakeResponse as Response); @@ -83,6 +84,7 @@ describe('processStream', () => { }); it('streaming response - long', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-success-basic-reply-long.txt' ); const result = processStream(fakeResponse as Response); @@ -95,6 +97,7 @@ describe('processStream', () => { }); it('streaming response - long - big chunk', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-success-basic-reply-long.txt', 1e6 ); @@ -107,7 +110,10 @@ describe('processStream', () => { expect(aggregatedResponse.text()).to.include('to their owners.'); }); it('streaming response - utf8', async () => { - const fakeResponse = getMockResponseStreaming('streaming-success-utf8.txt'); + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-utf8.txt' + ); const result = processStream(fakeResponse as Response); for await (const response of result.stream) { expect(response.text()).to.not.be.empty; @@ -118,6 +124,7 @@ describe('processStream', () => { }); it('streaming response - functioncall', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-success-function-call-short.txt' ); const result = processStream(fakeResponse as Response); @@ -141,6 +148,7 @@ describe('processStream', () => { }); it('candidate had finishReason', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-failure-finish-reason-safety.txt' ); const result = processStream(fakeResponse as Response); @@ -153,6 +161,7 @@ describe('processStream', () => { }); it('prompt was blocked', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-failure-prompt-blocked-safety.txt' ); const result = processStream(fakeResponse as Response); @@ -165,6 +174,7 @@ describe('processStream', () => { }); it('empty content', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-failure-empty-content.txt' ); const result = processStream(fakeResponse as Response); @@ -176,6 +186,7 @@ describe('processStream', () => { }); it('unknown enum - should ignore', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-success-unknown-safety-enum.txt' ); const result = processStream(fakeResponse as Response); @@ -187,6 +198,7 @@ describe('processStream', () => { }); it('recitation ending with a missing content field', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-failure-recitation-no-content.txt' ); const result = processStream(fakeResponse as Response); @@ -205,6 +217,7 @@ describe('processStream', () => { }); it('handles citations', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-success-citations.txt' ); const result = processStream(fakeResponse as Response); @@ -224,6 +237,7 @@ describe('processStream', () => { }); it('removes empty text parts', async () => { const fakeResponse = getMockResponseStreaming( + 'vertexAI', 'streaming-success-empty-text-part.txt' ); const result = processStream(fakeResponse as Response); diff --git a/packages/vertexai/test-utils/convert-mocks.ts b/packages/vertexai/test-utils/convert-mocks.ts index c306bec312f..851d6017b6d 100644 --- a/packages/vertexai/test-utils/convert-mocks.ts +++ b/packages/vertexai/test-utils/convert-mocks.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2024 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,42 +15,111 @@ * limitations under the License. */ +/* eslint-disable @typescript-eslint/no-require-imports */ +const { readdirSync, readFileSync, writeFileSync } = require('node:fs'); +const { join } = require('node:path'); + +const MOCK_RESPONSES_DIR_PATH = join( + __dirname, + 'vertexai-sdk-test-data', + 'mock-responses' +); +const MOCK_LOOKUP_OUTPUT_PATH = join(__dirname, 'mocks-lookup.ts'); + +type BackendName = 'vertexAI' | 'googleAI'; + +const mockDirs: Record = { + vertexAI: join(MOCK_RESPONSES_DIR_PATH, 'vertexai'), + // Note: the dirname is developerapi is legacy. It should be updated to googleai. + googleAI: join(MOCK_RESPONSES_DIR_PATH, 'developerapi') +}; + /** - * Converts mock text files into a js file that karma can read without - * using fs. + * Generates a JS file that exports maps from filenames to JSON mock responses (as strings) + * for each backend. + * + * This allows tests that run in a browser to access the mock responses without having to + * read from local disk and requiring 'fs'. */ +function generateMockLookupFile(): void { + console.log('Generating mock lookup file...'); + const vertexAIMockLookupText = generateMockLookup('vertexAI'); + const googleAIMockLookupText = generateMockLookup('googleAI'); -// eslint-disable-next-line @typescript-eslint/no-require-imports -const fs = require('fs'); -// eslint-disable-next-line @typescript-eslint/no-require-imports -const { join } = require('path'); + const fileText = ` +/** + * DO NOT EDIT - This file was generated by the packages/vertexai/test-utils/convert-mocks.ts script. + * + * These objects map mock response filenames to their JSON contents. + * + * Mock response files are pulled from https://github.com/FirebaseExtended/vertexai-sdk-test-data. + */ -const mockResponseDir = join( - __dirname, - 'vertexai-sdk-test-data/mock-responses/vertexai' -); +// Automatically generated at: ${new Date().toISOString()} + +${vertexAIMockLookupText} -async function main(): Promise { - const list = fs.readdirSync(mockResponseDir); +${googleAIMockLookupText} +`; + try { + writeFileSync(MOCK_LOOKUP_OUTPUT_PATH, fileText, 'utf-8'); + console.log( + `Successfully generated mock lookup file at: ${MOCK_LOOKUP_OUTPUT_PATH}` + ); + } catch (err) { + console.error( + `Error writing mock lookup file to ${MOCK_LOOKUP_OUTPUT_PATH}:`, + err + ); + process.exit(1); + } +} + +/** + * Given a directory that contains mock response files, reads through all the files, + * maps file names to file contents, and returns a string of typescript code + * that exports that map as an object. + */ +function generateMockLookup(backendName: BackendName): string { const lookup: Record = {}; - // eslint-disable-next-line guard-for-in - for (const fileName of list) { - const fullText = fs.readFileSync(join(mockResponseDir, fileName), 'utf-8'); - lookup[fileName] = fullText; + const mockDir = mockDirs[backendName]; + let mockFilenames: string[]; + + console.log( + `Processing mocks for "${backendName}" from directory: ${mockDir}` + ); + + try { + mockFilenames = readdirSync(mockDir); + } catch (err) { + console.error( + `Error reading directory ${mockDir} for ${backendName}:`, + err + ); + return `export const ${backendName}MocksLookup: Record = {};`; } - let fileText = `// Generated from mocks text files.`; - - fileText += '\n\n'; - fileText += `export const mocksLookup: Record = ${JSON.stringify( - lookup, - null, - 2 - )}`; - fileText += ';\n'; - fs.writeFileSync(join(__dirname, 'mocks-lookup.ts'), fileText, 'utf-8'); + + if (mockFilenames.length === 0) { + console.warn(`No .json files found in ${mockDir} for ${backendName}.`); + } + + for (const mockFilename of mockFilenames) { + const mockFilepath = `${mockDir}/${mockFilename}`; + try { + const fullText = readFileSync(mockFilepath, 'utf-8'); + lookup[mockFilename] = fullText; + } catch (err) { + console.error( + `Error reading mock file ${mockFilepath} for ${backendName}:`, + err + ); + } + } + + // Use JSON.stringify with indentation for readable output in the generated file + const lookupJsonString = JSON.stringify(lookup, null, 2); + + return `export const ${backendName}MocksLookup: Record = ${lookupJsonString};`; } -main().catch(e => { - console.error(e); - process.exit(1); -}); +generateMockLookupFile(); diff --git a/packages/vertexai/test-utils/mock-response.ts b/packages/vertexai/test-utils/mock-response.ts index 9b42c93427b..5128ddabe74 100644 --- a/packages/vertexai/test-utils/mock-response.ts +++ b/packages/vertexai/test-utils/mock-response.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2024 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,12 @@ * limitations under the License. */ -import { mocksLookup } from './mocks-lookup'; +import { vertexAIMocksLookup, googleAIMocksLookup } from './mocks-lookup'; + +const mockSetMaps: Record> = { + 'vertexAI': vertexAIMocksLookup, + 'googleAI': googleAIMocksLookup +}; /** * Mock native Response.body @@ -45,25 +50,33 @@ export function getChunkedStream( return stream; } + export function getMockResponseStreaming( + backendName: BackendName, filename: string, chunkLength: number = 20 ): Partial { - if (!(filename in mocksLookup)) { - throw Error(`Mock response file '${filename}' not found.`); + const mocksMap = mockSetMaps[backendName]; + if (!(filename in mocksMap)) { + throw Error(`${backendName} mock response file '${filename}' not found.`); } - const fullText = mocksLookup[filename]; + const fullText = mocksMap[filename]; return { body: getChunkedStream(fullText, chunkLength) }; } -export function getMockResponse(filename: string): Partial { +export function getMockResponse( + backendName: BackendName, + filename: string +): Partial { + const mocksLookup = mockSetMaps[backendName]; if (!(filename in mocksLookup)) { - throw Error(`Mock response file '${filename}' not found.`); + throw Error(`${backendName} mock response file '${filename}' not found.`); } const fullText = mocksLookup[filename]; + return { ok: true, json: () => Promise.resolve(JSON.parse(fullText)) From ed0803a29791cc0cecd0153f95e814ddcee7efd8 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:16:52 -0400 Subject: [PATCH 08/23] fix: remove `null` value inclusion from `!=` and `not-in` filter results (#8915) --- .changeset/cyan-frogs-relate.md | 6 ++ packages/firestore/src/core/filter.ts | 7 +- .../test/integration/api/query.test.ts | 94 +++++++++++++++++++ .../firestore/test/unit/core/query.test.ts | 4 +- 4 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 .changeset/cyan-frogs-relate.md diff --git a/.changeset/cyan-frogs-relate.md b/.changeset/cyan-frogs-relate.md new file mode 100644 index 00000000000..08af27593a9 --- /dev/null +++ b/.changeset/cyan-frogs-relate.md @@ -0,0 +1,6 @@ +--- +'@firebase/firestore': patch +'firebase': patch +--- + +Fixed the `null` value handling in `!=` and `not-in` filters. diff --git a/packages/firestore/src/core/filter.ts b/packages/firestore/src/core/filter.ts index 06e2740c315..12b57729f81 100644 --- a/packages/firestore/src/core/filter.ts +++ b/packages/firestore/src/core/filter.ts @@ -141,6 +141,7 @@ export class FieldFilter extends Filter { if (this.op === Operator.NOT_EQUAL) { return ( other !== null && + other.nullValue === undefined && this.matchesComparison(valueCompare(other!, this.value)) ); } @@ -495,7 +496,11 @@ export class NotInFilter extends FieldFilter { return false; } const other = doc.data.field(this.field); - return other !== null && !arrayValueContains(this.value.arrayValue!, other); + return ( + other !== null && + other.nullValue === undefined && + !arrayValueContains(this.value.arrayValue!, other) + ); } } diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index 01fd0e47e35..5871607eb03 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -1751,6 +1751,100 @@ apiDescribe('Queries', persistence => { ); }); }); + + it('sdk uses != filter same as backend', async () => { + const testDocs = { + a: { zip: Number.NaN }, + b: { zip: 91102 }, + c: { zip: 98101 }, + d: { zip: '98101' }, + e: { zip: [98101] }, + f: { zip: [98101, 98102] }, + g: { zip: ['98101', { zip: 98101 }] }, + h: { zip: { code: 500 } }, + i: { zip: null }, + j: { code: 500 } + }; + + await withTestCollection(persistence, testDocs, async coll => { + // populate cache with all documents first to ensure getDocsFromCache() scans all docs + await getDocs(coll); + + let testQuery = query(coll, where('zip', '!=', 98101)); + await checkOnlineAndOfflineResultsMatch( + testQuery, + 'a', + 'b', + 'd', + 'e', + 'f', + 'g', + 'h' + ); + + testQuery = query(coll, where('zip', '!=', Number.NaN)); + await checkOnlineAndOfflineResultsMatch( + testQuery, + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h' + ); + + testQuery = query(coll, where('zip', '!=', null)); + await checkOnlineAndOfflineResultsMatch( + testQuery, + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h' + ); + }); + }); + + it('sdk uses not-in filter same as backend', async () => { + const testDocs = { + a: { zip: Number.NaN }, + b: { zip: 91102 }, + c: { zip: 98101 }, + d: { zip: '98101' }, + e: { zip: [98101] }, + f: { zip: [98101, 98102] }, + g: { zip: ['98101', { zip: 98101 }] }, + h: { zip: { code: 500 } }, + i: { zip: null }, + j: { code: 500 } + }; + + await withTestCollection(persistence, testDocs, async coll => { + // populate cache with all documents first to ensure getDocsFromCache() scans all docs + await getDocs(coll); + + let testQuery = query( + coll, + where('zip', 'not-in', [98101, 98103, [98101, 98102]]) + ); + await checkOnlineAndOfflineResultsMatch( + testQuery, + 'a', + 'b', + 'd', + 'e', + 'g', + 'h' + ); + + testQuery = query(coll, where('zip', 'not-in', [null])); + await checkOnlineAndOfflineResultsMatch(testQuery); + }); + }); }); // Reproduces https://github.com/firebase/firebase-js-sdk/issues/5873 diff --git a/packages/firestore/test/unit/core/query.test.ts b/packages/firestore/test/unit/core/query.test.ts index fd0e6884c66..e589e7bf027 100644 --- a/packages/firestore/test/unit/core/query.test.ts +++ b/packages/firestore/test/unit/core/query.test.ts @@ -256,7 +256,7 @@ describe('Query', () => { document = doc('collection/1', 0, { zip: null }); - expect(queryMatches(query1, document)).to.be.true; + expect(queryMatches(query1, document)).to.be.false; // NaN match. document = doc('collection/1', 0, { @@ -354,7 +354,7 @@ describe('Query', () => { expect(queryMatches(query2, doc3)).to.equal(true); expect(queryMatches(query2, doc4)).to.equal(true); expect(queryMatches(query2, doc5)).to.equal(true); - expect(queryMatches(query2, doc6)).to.equal(true); + expect(queryMatches(query2, doc6)).to.equal(false); }); it('matches null for filters', () => { From e055e9057caab4d9f73734307fe4e0be2098249b Mon Sep 17 00:00:00 2001 From: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Date: Mon, 14 Apr 2025 10:52:23 -0600 Subject: [PATCH 09/23] Add assertion IDs that will be included in production log statements for fail and hardAsserts (#8313) --- .changeset/odd-wolves-sit.md | 5 + packages/firestore/package.json | 10 +- .../firestore/scripts/assertion-id-tool.js | 17 + .../firestore/scripts/assertion-id-tool.ts | 381 ++++++++++++++++++ packages/firestore/scripts/build-bundle.js | 2 +- packages/firestore/scripts/build-bundle.ts | 26 +- packages/firestore/scripts/extract-api.js | 2 +- packages/firestore/scripts/remove-asserts.js | 2 +- packages/firestore/scripts/remove-asserts.ts | 4 +- .../firestore/scripts/rename-internals.js | 2 +- packages/firestore/scripts/run-tests.js | 17 + packages/firestore/scripts/run-tests.ts | 42 +- packages/firestore/src/api/credentials.ts | 22 +- packages/firestore/src/api/snapshot.ts | 2 +- .../firestore/src/core/component_provider.ts | 1 + packages/firestore/src/core/filter.ts | 6 +- packages/firestore/src/core/query.ts | 2 +- .../firestore/src/core/sync_engine_impl.ts | 9 +- packages/firestore/src/core/transaction.ts | 4 +- packages/firestore/src/core/view.ts | 2 +- packages/firestore/src/core/view_snapshot.ts | 10 +- .../src/index/firestore_index_value_writer.ts | 2 +- .../firestore/src/lite-api/reference_impl.ts | 6 +- .../firestore/src/lite-api/transaction.ts | 8 +- .../src/lite-api/user_data_reader.ts | 4 +- .../src/lite-api/user_data_writer.ts | 8 +- .../src/local/encoded_resource_path.ts | 10 +- .../src/local/indexeddb_index_manager.ts | 3 +- .../local/indexeddb_mutation_batch_impl.ts | 7 +- .../src/local/indexeddb_mutation_queue.ts | 49 ++- .../local/indexeddb_remote_document_cache.ts | 2 +- .../src/local/indexeddb_schema_converter.ts | 6 +- .../src/local/indexeddb_sentinels.ts | 2 +- .../src/local/indexeddb_target_cache.ts | 3 +- .../firestore/src/local/local_serializer.ts | 4 +- .../firestore/src/local/local_store_impl.ts | 7 +- .../src/local/memory_mutation_queue.ts | 1 + .../firestore/src/local/memory_persistence.ts | 5 +- .../src/local/memory_remote_document_cache.ts | 2 +- .../src/local/persistence_promise.ts | 2 +- .../src/local/shared_client_state.ts | 4 +- packages/firestore/src/model/document.ts | 5 +- packages/firestore/src/model/mutation.ts | 8 +- .../firestore/src/model/mutation_batch.ts | 10 +- packages/firestore/src/model/normalize.ts | 6 +- packages/firestore/src/model/path.ts | 10 +- .../src/model/target_index_matcher.ts | 1 + packages/firestore/src/model/values.ts | 14 +- .../platform/browser/webchannel_connection.ts | 20 +- .../src/platform/node/grpc_connection.ts | 1 + packages/firestore/src/remote/datastore.ts | 5 +- .../firestore/src/remote/persistent_stream.ts | 3 + packages/firestore/src/remote/rpc_error.ts | 8 +- packages/firestore/src/remote/serializer.ts | 63 ++- packages/firestore/src/remote/watch_change.ts | 17 +- packages/firestore/src/util/assert.ts | 95 ++++- .../firestore/src/util/async_queue_impl.ts | 4 +- .../firestore/src/util/input_validation.ts | 2 +- packages/firestore/src/util/logic_utils.ts | 10 + packages/firestore/src/util/sorted_map.ts | 22 +- .../unit/index/ordered_code_writer.test.ts | 9 +- .../unit/local/indexeddb_persistence.test.ts | 2 +- .../test/unit/local/simple_db.test.ts | 2 +- .../firestore/test/unit/specs/spec_builder.ts | 14 +- .../test/unit/specs/spec_test_components.ts | 2 +- .../test/unit/specs/spec_test_runner.ts | 4 +- .../firestore/test/unit/util/assert.test.ts | 88 ++++ .../test/unit/util/async_queue.test.ts | 4 +- packages/firestore/test/util/helpers.ts | 2 + .../firestore/test/util/spec_test_helpers.ts | 7 +- packages/firestore/test/util/test_platform.ts | 2 +- 71 files changed, 955 insertions(+), 188 deletions(-) create mode 100644 .changeset/odd-wolves-sit.md create mode 100644 packages/firestore/scripts/assertion-id-tool.js create mode 100644 packages/firestore/scripts/assertion-id-tool.ts create mode 100644 packages/firestore/scripts/run-tests.js create mode 100644 packages/firestore/test/unit/util/assert.test.ts diff --git a/.changeset/odd-wolves-sit.md b/.changeset/odd-wolves-sit.md new file mode 100644 index 00000000000..fc63acc005e --- /dev/null +++ b/.changeset/odd-wolves-sit.md @@ -0,0 +1,5 @@ +--- +"@firebase/firestore": patch +--- + +Add unique IDs and state information into fatal error messages instead of the generic "unexpected state" message. diff --git a/packages/firestore/package.json b/packages/firestore/package.json index d9cb6c263b5..0c9ddeee843 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -20,14 +20,14 @@ "dev": "rollup -c -w", "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "prettier": "prettier --write '*.js' '@(lite|src|test)/**/*.ts' 'test/unit/remote/bloom_filter_golden_test_data/*.json'", + "prettier": "prettier --write '*.js' '@(lite|src|test|scripts)/**/*.ts' 'test/unit/remote/bloom_filter_golden_test_data/*.json'", "test:lite": "ts-node ./scripts/run-tests.ts --emulator --platform node_lite --main=lite/index.ts 'test/lite/**/*.test.ts'", "test:lite:prod": "ts-node ./scripts/run-tests.ts --platform node_lite --main=lite/index.ts 'test/lite/**/*.test.ts'", "test:lite:prod:nameddb": "ts-node ./scripts/run-tests.ts --platform node_lite --databaseId=test-db --main=lite/index.ts 'test/lite/**/*.test.ts'", "test:lite:browser": "karma start --lite", "test:lite:browser:nameddb": "karma start --lite --databaseId=test-db", "test:lite:browser:debug": "karma start --browsers=Chrome --lite --auto-watch", - "test": "run-s --npm-path npm lint test:all", + "test": "run-s --npm-path npm lint assertion-id:check test:all", "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all:ci", "test:all:ci": "run-s --npm-path npm test:browser test:travis test:lite:browser test:browser:prod:nameddb test:lite:browser:nameddb", "test:all": "run-p --npm-path npm test:browser test:lite:browser test:travis test:minified test:browser:prod:nameddb test:lite:browser:nameddb", @@ -52,7 +52,11 @@ "api-report:api-json": "rm -rf temp && api-extractor run --local --verbose", "api-report": "run-s --npm-path npm api-report:main api-report:lite && yarn api-report:api-json", "doc": "api-documenter markdown --input temp --output docs", - "typings:public": "node ../../scripts/build/use_typings.js ./dist/index.d.ts" + "typings:public": "node ../../scripts/build/use_typings.js ./dist/index.d.ts", + "assertion-id:check": "ts-node scripts/assertion-id-tool.ts --dir=src --check", + "assertion-id:new": "ts-node scripts/assertion-id-tool.ts --dir=src --new", + "assertion-id:list": "ts-node scripts/assertion-id-tool.ts --dir=src --list", + "assertion-id:find": "ts-node scripts/assertion-id-tool.ts --dir=src --find" }, "exports": { ".": { diff --git a/packages/firestore/scripts/assertion-id-tool.js b/packages/firestore/scripts/assertion-id-tool.js new file mode 100644 index 00000000000..f29d88a1bf2 --- /dev/null +++ b/packages/firestore/scripts/assertion-id-tool.js @@ -0,0 +1,17 @@ +"use strict"; +/** + * @license + * Copyright 2025 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. + */var __awaiter=this&&this.__awaiter||function(thisArg,_arguments,P,generator){function adopt(value){return value instanceof P?value:new P((function(resolve){resolve(value)}))}return new(P||(P=Promise))((function(resolve,reject){function fulfilled(value){try{step(generator.next(value))}catch(e){reject(e)}}function rejected(value){try{step(generator["throw"](value))}catch(e){reject(e)}}function step(result){result.done?resolve(result.value):adopt(result.value).then(fulfilled,rejected)}step((generator=generator.apply(thisArg,_arguments||[])).next())}))};var __generator=this&&this.__generator||function(thisArg,body){var _={label:0,sent:function(){if(t[0]&1)throw t[1];return t[1]},trys:[],ops:[]},f,y,t,g;return g={next:verb(0),throw:verb(1),return:verb(2)},typeof Symbol==="function"&&(g[Symbol.iterator]=function(){return this}),g;function verb(n){return function(v){return step([n,v])}}function step(op){if(f)throw new TypeError("Generator is already executing.");while(g&&(g=0,op[0]&&(_=0)),_)try{if(f=1,y&&(t=op[0]&2?y["return"]:op[0]?y["throw"]||((t=y["return"])&&t.call(y),0):y.next)&&!(t=t.call(y,op[1])).done)return t;if(y=0,t)op=[op[0]&2,t.value];switch(op[0]){case 0:case 1:t=op;break;case 4:_.label++;return{value:op[1],done:false};case 5:_.label++;y=op[1];op=[0];continue;case 7:op=_.ops.pop();_.trys.pop();continue;default:if(!(t=_.trys,t=t.length>0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]0){node.arguments.forEach((function(arg){argsText_1.push(arg.getText(sourceFile_1));if(ts.isStringLiteral(arg)){errorMessage_1=arg.getText(sourceFile_1)}else if(ts.isNumericLiteral(arg)){assertionId_1=parseInt(arg.getText(sourceFile_1),10)}}))}foundCalls.push({fileName:filePath,functionName:functionName,line:line+1,character:character+1,argumentsText:argsText_1,errorMessage:errorMessage_1,assertionId:assertionId_1!==null&&assertionId_1!==void 0?assertionId_1:-1})}}ts.forEachChild(node,visit_1)};visit_1(sourceFile_1)}catch(error){console.error("Error processing file ".concat(filePath,": ").concat(error.message))}};for(var _i=0,filePaths_1=filePaths;_i1){duplicatesFound=true;console.error('\nDuplicate assertion id "'.concat(code,'" found at ').concat(locations.length," locations:"));locations.forEach((function(loc){var relativePath=path.relative(process.cwd(),loc.fileName);console.error("- ".concat(relativePath,":").concat(loc.line,":").concat(loc.character))}))}}));if(!duplicatesFound){log("No duplicate assertion ids found.")}else{process.exit(1)}}function handleNew(occurrences){var maxCode=0;occurrences.forEach((function(occ){if(occ.assertionId>maxCode){maxCode=occ.assertionId}}));if(occurrences.length===0){log("0");return}var newCode=maxCode+1;console.log(newCode)}function main(){return __awaiter(this,void 0,void 0,(function(){var argv,targetDirectory,stats,filesToScan,allOccurrences;return __generator(this,(function(_a){argv=(0,yargs_1.default)((0,helpers_1.hideBin)(process.argv)).usage("Usage: $0 [options]").option("dir",{alias:"D",describe:"Directory to scan recursively for TS files",type:"string",demandOption:true}).option("verbose",{alias:"V",describe:"verbose",type:"boolean"}).option("find",{alias:"F",describe:"Find locations of a specific {assertionId}",type:"string",nargs:1}).option("list",{alias:"L",describe:"List all unique assertion ids found (default action)",type:"boolean"}).option("new",{alias:"N",describe:"Suggest a new assertion id based on existing ones",type:"boolean"}).option("check",{alias:"C",describe:"Check for duplicate usage of assertion ids",type:"boolean"}).check((function(argv){var options=[argv.F,argv.L,argv.N,argv.C].filter(Boolean).length;if(options>1){throw new Error("Options -F, -L, -N, -C are mutually exclusive.")}return true})).help().alias("help","h").strict().parse();targetDirectory=path.resolve(argv["dir"]);isVerbose=!!argv["verbose"];try{stats=fs.statSync(targetDirectory);if(!stats.isDirectory()){console.error("Error: Provided path is not a directory: ".concat(targetDirectory));process.exit(1)}}catch(error){console.error("Error accessing directory ".concat(targetDirectory,": ").concat(error.message));process.exit(1)}log("Scanning directory: ".concat(targetDirectory));filesToScan=getTsFilesRecursive(targetDirectory);if(filesToScan.length===0){log("No relevant .ts or .tsx files found.");process.exit(0)}log("Found ".concat(filesToScan.length," files. Analyzing for assertion ids..."));allOccurrences=findFunctionCalls(filesToScan);log("Scan complete. Found ".concat(allOccurrences.length," potential assertion id occurrences."));if(argv["find"]){handleFind(allOccurrences,argv["find"])}else if(argv["new"]){handleNew(allOccurrences)}else if(argv["check"]){handleCheck(allOccurrences)}else{handleList(allOccurrences)}return[2]}))}))}main().catch((function(error){console.error("\nAn unexpected error occurred:");console.error(error);process.exit(1)})); \ No newline at end of file diff --git a/packages/firestore/scripts/assertion-id-tool.ts b/packages/firestore/scripts/assertion-id-tool.ts new file mode 100644 index 00000000000..8bde791cc6d --- /dev/null +++ b/packages/firestore/scripts/assertion-id-tool.ts @@ -0,0 +1,381 @@ +/** + * @license + * Copyright 2025 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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console */ + +import * as fs from 'fs'; +import { getRandomValues } from 'node:crypto'; +import * as path from 'path'; + +import * as ts from 'typescript'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +let isVerbose: boolean = false; + +function log(message: any): void { + if (isVerbose) { + console.log(message); + } +} + +// Define the names of the functions we are looking for +const targetFunctionNames: Set = new Set(['fail', 'hardAssert']); + +// Interface to store information about found call sites +interface CallSiteInfo { + fileName: string; + functionName: string; + line: number; + character: number; + argumentsText: string[]; // Added to store argument text + errorMessage: string | undefined; + assertionId: string; +} + +/** + * Recursively finds all files with .ts extensions in a directory. + * @param dirPath The absolute path to the directory to scan. + * @returns An array of absolute paths to the found TypeScript files. + */ +function getTsFilesRecursive(dirPath: string): string[] { + let tsFiles: string[] = []; + try { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isDirectory()) { + // Ignore node_modules for performance and relevance + if (entry.name === 'node_modules') { + continue; + } + // Recursively scan subdirectories + tsFiles = tsFiles.concat(getTsFilesRecursive(fullPath)); + } else if (entry.isFile() && entry.name.endsWith('.ts')) { + // Exclude declaration files (.d.ts) as they usually don't contain implementation + if (!entry.name.endsWith('.d.ts')) { + tsFiles.push(fullPath); + } + } + } + } catch (error: any) { + console.error(`Error reading directory ${dirPath}: ${error.message}`); + throw error; + } + return tsFiles; +} + +/** + * Analyzes TypeScript source files to find calls to specific functions. + * @param filePaths An array of absolute paths to the TypeScript files to scan. + * @returns An array of objects detailing the found call sites. + */ +function findFunctionCalls(filePaths: string[]): CallSiteInfo[] { + const foundCalls: CallSiteInfo[] = []; + + for (const filePath of filePaths) { + // Read the file content + const sourceText = fs.readFileSync(filePath, 'utf8'); + + // Create the SourceFile AST node + const sourceFile = ts.createSourceFile( + path.basename(filePath), // Use basename for AST node name + sourceText, + ts.ScriptTarget.ESNext, + true, // Set parent pointers + ts.ScriptKind.Unknown // Detect TS vs TSX automatically + ); + + // Define the visitor function + const visit = (node: ts.Node): void => { + // Check if the node is a CallExpression (e.g., myFunction(...)) + if (ts.isCallExpression(node)) { + let functionName: string | null = null; + const expression = node.expression; + + // Check if the call is directly to an identifier (e.g., fail()) + if (ts.isIdentifier(expression)) { + functionName = expression.text; + } + + // If we found a function name, and it's one we're looking for + if (functionName && targetFunctionNames.has(functionName)) { + // Get line and character number + const { line, character } = ts.getLineAndCharacterOfPosition( + sourceFile, + node.getStart() // Get start position of the call expression + ); + + // --- Extract Arguments --- + const argsText: string[] = []; + let errorMessage: string | undefined; + let assertionId: string | undefined; + if (node.arguments && node.arguments.length > 0) { + node.arguments.forEach((arg: ts.Expression) => { + // Get the source text of the argument node + argsText.push(arg.getText(sourceFile)); + + if (ts.isStringLiteral(arg)) { + errorMessage = arg.getText(sourceFile); + } else if (ts.isNumericLiteral(arg)) { + assertionId = arg.getText(sourceFile); + } + }); + } + + // Store the information (add 1 to line/char for 1-based indexing) + foundCalls.push({ + fileName: filePath, // Store the full path + functionName, + line: line + 1, + character: character + 1, + argumentsText: argsText, // Store the extracted arguments, + errorMessage, + assertionId: assertionId ?? 'INVALID' + }); + } + } + + // Continue traversing down the AST + ts.forEachChild(node, visit); + }; + + // Start traversal from the root SourceFile node + visit(sourceFile); + } // End loop through filePaths + + return foundCalls; +} + +// --- Action Handlers --- + +function handleList(occurrences: CallSiteInfo[]): void { + if (occurrences.length === 0) { + log('No assertion ids found.'); + return; + } + + occurrences + .sort((a, b) => a.assertionId.localeCompare(b.assertionId)) + .forEach(call => { + console.log( + `ID: ${call.assertionId}; MESSAGE: ${call.errorMessage}; SOURCE: '${ + call.functionName + }' call at ${path.relative(process.cwd(), call.fileName)}:${ + call.line + }:${call.character}` + ); + }); +} + +function find( + occurrences: CallSiteInfo[], + targetId: string | number +): CallSiteInfo[] { + const target = + typeof targetId === 'number' ? targetId.toString(16) : targetId; + return occurrences.filter(o => String(o.assertionId) === String(target)); +} + +function handleFind( + occurrences: CallSiteInfo[], + targetId: string | number +): void { + const foundLocations = find(occurrences, targetId); + + if (foundLocations.length === 0) { + log(`Assertion id "${targetId}" not found.`); + process.exit(1); + } + + handleList(foundLocations); +} + +function handleCheck(occurrences: CallSiteInfo[]): void { + if (occurrences.length === 0) { + log('No assertion ids found to check for duplicates.'); + return; + } + const idCounts: { [id: string]: CallSiteInfo[] } = {}; + + occurrences.forEach(occ => { + // Count ID occurrences + const codeStr = String(occ.assertionId); // Use string representation as key + if (!idCounts[codeStr]) { + idCounts[codeStr] = []; + } + idCounts[codeStr].push(occ); + + // validate formats + if (!/^0x[0-9a-f]{4}$/.test(occ.assertionId)) { + console.error( + `Invalid assertion ID '${occ.assertionId}'. Must match /^0x[0-9a-f]{4}$/` + ); + + const relativePath = path.relative(process.cwd(), occ.fileName); + console.error(`- at '${relativePath}:${occ.line}:${occ.character}`); + } + }); + + let duplicatesFound = false; + log('Checking for duplicate assertion id usage:'); + Object.entries(idCounts).forEach(([code, locations]) => { + if (locations.length > 1) { + duplicatesFound = true; + console.error( + `\nDuplicate assertion id "${code}" found at ${locations.length} locations:` + ); + locations.forEach(loc => { + const relativePath = path.relative(process.cwd(), loc.fileName); + console.error(`- ${relativePath}:${loc.line}:${loc.character}`); + }); + } + }); + + if (!duplicatesFound) { + log('No duplicate assertion ids found.'); + } else { + process.exit(1); + } +} + +function randomId(): string { + const randomBytes = new Uint8Array(2); + getRandomValues(randomBytes); + + return ( + '0x' + + Array.from(randomBytes) + .map(byte => byte.toString(16).padStart(2, '0')) + .join('') + ); +} + +function handleNew(occurrences: CallSiteInfo[]): void { + let newCode: string = randomId(); + + // If we find this code already is used, regenerate it. + while (find(occurrences, newCode).length > 0) { + newCode = randomId(); + } + + console.log(newCode); +} + +// --- Main Execution --- +async function main(): Promise { + const argv = yargs(hideBin(process.argv)) + .usage('Usage: $0 [options]') + .option('dir', { + alias: 'D', + describe: 'Directory to scan recursively for TS files', + type: 'string', + demandOption: true + }) + .option('verbose', { + alias: 'V', + describe: 'verbose', + type: 'boolean' + }) + .option('find', { + alias: 'F', + describe: 'Find locations of a specific {assertionId}', + type: 'string', + nargs: 1 + }) + .option('list', { + alias: 'L', + describe: 'List all unique assertion ids found (default action)', + type: 'boolean' + }) + .option('new', { + alias: 'N', + describe: 'Suggest a new assertion id based on existing ones', + type: 'boolean' + }) + .option('check', { + alias: 'C', + describe: 'Check for duplicate usage of assertion ids', + type: 'boolean' + }) + .check(argv => { + // Enforce mutual exclusivity among options *within* the scan command + const options = [argv.F, argv.L, argv.N, argv.C].filter(Boolean).length; + if (options > 1) { + throw new Error('Options -F, -L, -N, -C are mutually exclusive.'); + } + return true; + }) + .help() + .alias('help', 'h') + .strict() + .parse(); // Execute parsing + + // Extract directory path (safe due to demandOption) + const targetDirectory = path.resolve(argv['dir'] as string); + + // set verbosity + isVerbose = !!argv['verbose']; + + // Validate directory + try { + const stats = fs.statSync(targetDirectory); + if (!stats.isDirectory()) { + console.error( + `Error: Provided path is not a directory: ${targetDirectory}` + ); + process.exit(1); + } + } catch (error: any) { + console.error( + `Error accessing directory ${targetDirectory}: ${error.message}` + ); + process.exit(1); + } + + log(`Scanning directory: ${targetDirectory}`); + const filesToScan = getTsFilesRecursive(targetDirectory); + + if (filesToScan.length === 0) { + log('No relevant .ts or .tsx files found.'); + process.exit(0); + } + log(`Found ${filesToScan.length} files. Analyzing for assertion ids...`); + + const allOccurrences = findFunctionCalls(filesToScan); + log( + `Scan complete. Found ${allOccurrences.length} potential assertion id occurrences.` + ); + + // Determine action based on flags + if (argv['find']) { + handleFind(allOccurrences, argv['find']); + } else if (argv['new']) { + handleNew(allOccurrences); + } else if (argv['check']) { + handleCheck(allOccurrences); + } else { + // Default action is List (-L or no flag) + handleList(allOccurrences); + } +} + +// Run the main function +void main(); diff --git a/packages/firestore/scripts/build-bundle.js b/packages/firestore/scripts/build-bundle.js index b44311571e3..f8ba283a5a8 100644 --- a/packages/firestore/scripts/build-bundle.js +++ b/packages/firestore/scripts/build-bundle.js @@ -14,4 +14,4 @@ * 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. - */var __awaiter=this&&this.__awaiter||function(thisArg,_arguments,P,generator){function adopt(value){return value instanceof P?value:new P((function(resolve){resolve(value)}))}return new(P||(P=Promise))((function(resolve,reject){function fulfilled(value){try{step(generator.next(value))}catch(e){reject(e)}}function rejected(value){try{step(generator["throw"](value))}catch(e){reject(e)}}function step(result){result.done?resolve(result.value):adopt(result.value).then(fulfilled,rejected)}step((generator=generator.apply(thisArg,_arguments||[])).next())}))};var __generator=this&&this.__generator||function(thisArg,body){var _={label:0,sent:function(){if(t[0]&1)throw t[1];return t[1]},trys:[],ops:[]},f,y,t,g;return g={next:verb(0),throw:verb(1),return:verb(2)},typeof Symbol==="function"&&(g[Symbol.iterator]=function(){return this}),g;function verb(n){return function(v){return step([n,v])}}function step(op){if(f)throw new TypeError("Generator is already executing.");while(g&&(g=0,op[0]&&(_=0)),_)try{if(f=1,y&&(t=op[0]&2?y["return"]:op[0]?y["throw"]||((t=y["return"])&&t.call(y),0):y.next)&&!(t=t.call(y,op[1])).done)return t;if(y=0,t)op=[op[0]&2,t.value];switch(op[0]){case 0:case 1:t=op;break;case 4:_.label++;return{value:op[1],done:false};case 5:_.label++;y=op[1];op=[0];continue;case 7:op=_.ops.pop();_.trys.pop();continue;default:if(!(t=_.trys,t=t.length>0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]=0){var method=declaration.name.text;if(method==="debugAssert"){updatedNode=ts.factory.createOmittedExpression()}else if(method==="hardAssert"){updatedNode=ts.factory.createCallExpression(declaration.name,undefined,[node.arguments[0]])}else if(method==="fail"){updatedNode=ts.factory.createCallExpression(declaration.name,undefined,[])}}}}if(updatedNode){ts.setSourceMapRange(updatedNode,ts.getSourceMapRange(node));return updatedNode}else{return node}};return RemoveAsserts}(); \ No newline at end of file + */Object.defineProperty(exports,"__esModule",{value:true});exports.removeAsserts=removeAsserts;var ts=require("typescript");var ASSERT_LOCATION="packages/firestore/src/util/assert.ts";function removeAsserts(program){var removeAsserts=new RemoveAsserts(program.getTypeChecker());return function(context){return function(file){return removeAsserts.visitNodeAndChildren(file,context)}}}var RemoveAsserts=function(){function RemoveAsserts(typeChecker){this.typeChecker=typeChecker}RemoveAsserts.prototype.visitNodeAndChildren=function(node,context){var _this=this;return ts.visitEachChild(this.visitNode(node),(function(childNode){return _this.visitNodeAndChildren(childNode,context)}),context)};RemoveAsserts.prototype.visitNode=function(node){var updatedNode=null;if(ts.isCallExpression(node)){var signature=this.typeChecker.getResolvedSignature(node);if(signature&&signature.declaration&&signature.declaration.kind===ts.SyntaxKind.FunctionDeclaration){var declaration=signature.declaration;if(declaration&&declaration.getSourceFile().fileName.indexOf(ASSERT_LOCATION)>=0){var method=declaration.name.text;if(method==="debugAssert"){updatedNode=ts.factory.createOmittedExpression()}else if(method==="hardAssert"){updatedNode=ts.factory.createCallExpression(declaration.name,undefined,node.arguments.filter((function(value){return!ts.isStringLiteral(value)})))}else if(method==="fail"){updatedNode=ts.factory.createCallExpression(declaration.name,undefined,node.arguments.filter((function(value){return!ts.isStringLiteral(value)})))}}}}if(updatedNode){ts.setSourceMapRange(updatedNode,ts.getSourceMapRange(node));return updatedNode}else{return node}};return RemoveAsserts}(); \ No newline at end of file diff --git a/packages/firestore/scripts/remove-asserts.ts b/packages/firestore/scripts/remove-asserts.ts index fb3e2af6a34..f55907ec152 100644 --- a/packages/firestore/scripts/remove-asserts.ts +++ b/packages/firestore/scripts/remove-asserts.ts @@ -70,14 +70,14 @@ class RemoveAsserts { updatedNode = ts.factory.createCallExpression( declaration.name!, /*typeArgs*/ undefined, - [node.arguments[0]] + node.arguments.filter(value => !ts.isStringLiteral(value)) ); } else if (method === 'fail') { // Remove the log message updatedNode = ts.factory.createCallExpression( declaration.name!, /*typeArgs*/ undefined, - [] + node.arguments.filter(value => !ts.isStringLiteral(value)) ); } } diff --git a/packages/firestore/scripts/rename-internals.js b/packages/firestore/scripts/rename-internals.js index fa8394a2261..4d5ebf1a838 100644 --- a/packages/firestore/scripts/rename-internals.js +++ b/packages/firestore/scripts/rename-internals.js @@ -14,4 +14,4 @@ * 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. - */exports.__esModule=true;exports.renameInternals=void 0;var ts=require("typescript");var ignoredIdentifiers=["undefined"];var RenameInternals=function(){function RenameInternals(publicApi,prefix){this.publicApi=publicApi;this.prefix=prefix}RenameInternals.prototype.visitNodeAndChildren=function(node,context){var _this=this;return ts.visitEachChild(this.visitNode(node),(function(childNode){return _this.visitNodeAndChildren(childNode,context)}),context)};RenameInternals.prototype.visitNode=function(node){if(ts.isIdentifier(node)){var name_1=node.escapedText.toString();if(!this.publicApi.has(name_1)&&ignoredIdentifiers.indexOf(node.escapedText.toString())===-1){var newIdentifier=ts.factory.createIdentifier(this.prefix+name_1);ts.setSourceMapRange(newIdentifier,ts.getSourceMapRange(node));return newIdentifier}}return node};return RenameInternals}();var DEFAULT_PREFIX="_";function renameInternals(program,config){var _a;var prefix=(_a=config.prefix)!==null&&_a!==void 0?_a:DEFAULT_PREFIX;var renamer=new RenameInternals(config.publicIdentifiers,prefix);return function(context){return function(file){return renamer.visitNodeAndChildren(file,context)}}}exports.renameInternals=renameInternals; \ No newline at end of file + */Object.defineProperty(exports,"__esModule",{value:true});exports.renameInternals=renameInternals;var ts=require("typescript");var ignoredIdentifiers=["undefined"];var RenameInternals=function(){function RenameInternals(publicApi,prefix){this.publicApi=publicApi;this.prefix=prefix}RenameInternals.prototype.visitNodeAndChildren=function(node,context){var _this=this;return ts.visitEachChild(this.visitNode(node),(function(childNode){return _this.visitNodeAndChildren(childNode,context)}),context)};RenameInternals.prototype.visitNode=function(node){if(ts.isIdentifier(node)){var name_1=node.escapedText.toString();if(!this.publicApi.has(name_1)&&ignoredIdentifiers.indexOf(node.escapedText.toString())===-1){var newIdentifier=ts.factory.createIdentifier(this.prefix+name_1);ts.setSourceMapRange(newIdentifier,ts.getSourceMapRange(node));return newIdentifier}}return node};return RenameInternals}();var DEFAULT_PREFIX="_";function renameInternals(program,config){var _a;var prefix=(_a=config.prefix)!==null&&_a!==void 0?_a:DEFAULT_PREFIX;var renamer=new RenameInternals(config.publicIdentifiers,prefix);return function(context){return function(file){return renamer.visitNodeAndChildren(file,context)}}} \ No newline at end of file diff --git a/packages/firestore/scripts/run-tests.js b/packages/firestore/scripts/run-tests.js new file mode 100644 index 00000000000..3a2e171b649 --- /dev/null +++ b/packages/firestore/scripts/run-tests.js @@ -0,0 +1,17 @@ +"use strict"; +/** + * @license + * Copyright 2020 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. + */Object.defineProperty(exports,"__esModule",{value:true});var path_1=require("path");var child_process_promise_1=require("child-process-promise");var yargs=require("yargs");var argv=yargs.options({main:{type:"string",demandOption:true},platform:{type:"string",default:"node"},emulator:{type:"boolean"},persistence:{type:"boolean"},databaseId:{type:"string"}}).parseSync();var nyc=(0,path_1.resolve)(__dirname,"../../../node_modules/.bin/nyc");var mocha=(0,path_1.resolve)(__dirname,"../../../node_modules/.bin/mocha");var babel=(0,path_1.resolve)(__dirname,"../babel-register.js");process.env.NO_TS_NODE="true";process.env.TEST_PLATFORM=argv.platform;var args=["--reporter","lcovonly",mocha,"--require",babel,"--require",argv.main,"--config","../../config/mocharc.node.js"];if(argv.emulator){process.env.FIRESTORE_TARGET_BACKEND="emulator"}if(argv.persistence){process.env.USE_MOCK_PERSISTENCE="YES";args.push("--require","test/util/node_persistence.ts")}if(argv.databaseId){process.env.FIRESTORE_TARGET_DB_ID=argv.databaseId}args=args.concat(argv._);var childProcess=(0,child_process_promise_1.spawn)(nyc,args,{stdio:"inherit",cwd:process.cwd()}).childProcess;process.once("exit",(function(){return childProcess.kill()}));process.once("SIGINT",(function(){return childProcess.kill("SIGINT")}));process.once("SIGTERM",(function(){return childProcess.kill("SIGTERM")})); \ No newline at end of file diff --git a/packages/firestore/scripts/run-tests.ts b/packages/firestore/scripts/run-tests.ts index 7e5cdf8fc80..bd721944647 100644 --- a/packages/firestore/scripts/run-tests.ts +++ b/packages/firestore/scripts/run-tests.ts @@ -20,32 +20,34 @@ import { resolve } from 'path'; import { spawn } from 'child-process-promise'; import * as yargs from 'yargs'; -const argv = yargs.options({ - main: { - type: 'string', - demandOption: true - }, - platform: { - type: 'string', - default: 'node' - }, - emulator: { - type: 'boolean' - }, - persistence: { - type: 'boolean' - }, - databaseId: { - type: 'string' - } -}).parseSync(); +const argv = yargs + .options({ + main: { + type: 'string', + demandOption: true + }, + platform: { + type: 'string', + default: 'node' + }, + emulator: { + type: 'boolean' + }, + persistence: { + type: 'boolean' + }, + databaseId: { + type: 'string' + } + }) + .parseSync(); const nyc = resolve(__dirname, '../../../node_modules/.bin/nyc'); const mocha = resolve(__dirname, '../../../node_modules/.bin/mocha'); const babel = resolve(__dirname, '../babel-register.js'); // used in '../../config/mocharc.node.js' to disable ts-node -process.env.NO_TS_NODE = "true"; +process.env.NO_TS_NODE = 'true'; process.env.TEST_PLATFORM = argv.platform; let args = [ diff --git a/packages/firestore/src/api/credentials.ts b/packages/firestore/src/api/credentials.ts index b542ec80b91..c1b10d61057 100644 --- a/packages/firestore/src/api/credentials.ts +++ b/packages/firestore/src/api/credentials.ts @@ -207,7 +207,9 @@ export class LiteAuthCredentialsProvider implements CredentialsProvider { if (tokenData) { hardAssert( typeof tokenData.accessToken === 'string', - 'Invalid tokenData returned from getToken():' + tokenData + 0xa539, + 'Invalid tokenData returned from getToken()', + { tokenData } ); return new OAuthToken( tokenData.accessToken, @@ -259,6 +261,7 @@ export class FirebaseAuthCredentialsProvider ): void { hardAssert( this.tokenListener === undefined, + 0xa540, 'Token listener already added' ); let lastTokenId = this.tokenCounter; @@ -357,7 +360,9 @@ export class FirebaseAuthCredentialsProvider if (tokenData) { hardAssert( typeof tokenData.accessToken === 'string', - 'Invalid tokenData returned from getToken():' + tokenData + 0x7c5d, + 'Invalid tokenData returned from getToken()', + { tokenData } ); return new OAuthToken(tokenData.accessToken, this.currentUser); } else { @@ -386,7 +391,9 @@ export class FirebaseAuthCredentialsProvider const currentUid = this.auth && this.auth.getUid(); hardAssert( currentUid === null || typeof currentUid === 'string', - 'Received invalid UID: ' + currentUid + 0x0807, + 'Received invalid UID', + { currentUid } ); return new User(currentUid); } @@ -513,6 +520,7 @@ export class FirebaseAppCheckTokenProvider ): void { hardAssert( this.tokenListener === undefined, + 0x0db8, 'Token listener already added' ); @@ -588,7 +596,9 @@ export class FirebaseAppCheckTokenProvider if (tokenResult) { hardAssert( typeof tokenResult.token === 'string', - 'Invalid tokenResult returned from getToken():' + tokenResult + 0xae0e, + 'Invalid tokenResult returned from getToken()', + { tokenResult } ); this.latestAppCheckToken = tokenResult.token; return new AppCheckToken(tokenResult.token); @@ -659,7 +669,9 @@ export class LiteAppCheckTokenProvider implements CredentialsProvider { if (tokenResult) { hardAssert( typeof tokenResult.token === 'string', - 'Invalid tokenResult returned from getToken():' + tokenResult + 0x0d8e, + 'Invalid tokenResult returned from getToken()', + { tokenResult } ); return new AppCheckToken(tokenResult.token); } else { diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 29e1616b61c..669ac26cafe 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -749,7 +749,7 @@ export function resultChangeType(type: ChangeType): DocumentChangeType { case ChangeType.Removed: return 'removed'; default: - return fail('Unknown change type: ' + type); + return fail(0xf03d, 'Unknown change type', { type }); } } diff --git a/packages/firestore/src/core/component_provider.ts b/packages/firestore/src/core/component_provider.ts index 8a63509232c..183a1046cc9 100644 --- a/packages/firestore/src/core/component_provider.ts +++ b/packages/firestore/src/core/component_provider.ts @@ -197,6 +197,7 @@ export class LruGcMemoryOfflineComponentProvider extends MemoryOfflineComponentP ): Scheduler | null { hardAssert( this.persistence.referenceDelegate instanceof MemoryLruDelegate, + 0xb743, 'referenceDelegate is expected to be an instance of MemoryLruDelegate.' ); diff --git a/packages/firestore/src/core/filter.ts b/packages/firestore/src/core/filter.ts index 12b57729f81..b3ae3d0619b 100644 --- a/packages/firestore/src/core/filter.ts +++ b/packages/firestore/src/core/filter.ts @@ -169,7 +169,9 @@ export class FieldFilter extends Filter { case Operator.GREATER_THAN_OR_EQUAL: return comparison >= 0; default: - return fail('Unknown FieldFilter operator: ' + this.op); + return fail(0xb8a2, 'Unknown FieldFilter operator', { + operator: this.op + }); } } @@ -316,7 +318,7 @@ export function filterEquals(f1: Filter, f2: Filter): boolean { } else if (f1 instanceof CompositeFilter) { return compositeFilterEquals(f1, f2); } else { - fail('Only FieldFilters and CompositeFilters can be compared'); + fail(0x4bef, 'Only FieldFilters and CompositeFilters can be compared'); } } diff --git a/packages/firestore/src/core/query.ts b/packages/firestore/src/core/query.ts index b13296ad7ee..ca875bbb14d 100644 --- a/packages/firestore/src/core/query.ts +++ b/packages/firestore/src/core/query.ts @@ -578,6 +578,6 @@ export function compareDocs( case Direction.DESCENDING: return -1 * comparison; default: - return fail('Unknown direction: ' + orderBy.dir); + return fail(0x4d4e, 'Unknown direction', { direction: orderBy.dir }); } } diff --git a/packages/firestore/src/core/sync_engine_impl.ts b/packages/firestore/src/core/sync_engine_impl.ts index f96cbea0f00..404d4663a47 100644 --- a/packages/firestore/src/core/sync_engine_impl.ts +++ b/packages/firestore/src/core/sync_engine_impl.ts @@ -579,6 +579,7 @@ export async function syncEngineApplyRemoteEvent( targetChange.modifiedDocuments.size + targetChange.removedDocuments.size <= 1, + 0x5858, 'Limbo resolution for single document contains multiple changes.' ); if (targetChange.addedDocuments.size > 0) { @@ -586,11 +587,13 @@ export async function syncEngineApplyRemoteEvent( } else if (targetChange.modifiedDocuments.size > 0) { hardAssert( limboResolution.receivedDocument, + 0x390f, 'Received change for limbo target document without add.' ); } else if (targetChange.removedDocuments.size > 0) { hardAssert( limboResolution.receivedDocument, + 0xa4f3, 'Received remove for limbo target document without add.' ); limboResolution.receivedDocument = false; @@ -994,7 +997,7 @@ function updateTrackedLimbos( removeLimboTarget(syncEngineImpl, limboChange.key); } } else { - fail('Unknown limbo change: ' + JSON.stringify(limboChange)); + fail(0x4d4f, 'Unknown limbo change', { limboChange }); } } } @@ -1317,7 +1320,7 @@ export async function syncEngineApplyBatchState( batchId ); } else { - fail(`Unknown batchState: ${batchState}`); + fail(0x1a40, `Unknown batchState`, { batchState }); } await syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, documents); @@ -1560,7 +1563,7 @@ export async function syncEngineApplyTargetState( break; } default: - fail('Unexpected target state: ' + state); + fail(0xfa9b, 'Unexpected target state', state); } } } diff --git a/packages/firestore/src/core/transaction.ts b/packages/firestore/src/core/transaction.ts index 471c64e13bd..d3dc6ee8a89 100644 --- a/packages/firestore/src/core/transaction.ts +++ b/packages/firestore/src/core/transaction.ts @@ -124,7 +124,9 @@ export class Transaction { // Represent a deleted doc using SnapshotVersion.min(). docVersion = SnapshotVersion.min(); } else { - throw fail('Document in a transaction was a ' + doc.constructor.name); + throw fail(0xc542, 'Document in a transaction was a ', { + documentName: doc.constructor.name + }); } const existingVersion = this.readVersions.get(doc.key.toString()); diff --git a/packages/firestore/src/core/view.ts b/packages/firestore/src/core/view.ts index b0a07bd783c..e0909de938f 100644 --- a/packages/firestore/src/core/view.ts +++ b/packages/firestore/src/core/view.ts @@ -505,7 +505,7 @@ function compareChangeType(c1: ChangeType, c2: ChangeType): number { case ChangeType.Removed: return 0; default: - return fail('Unknown ChangeType: ' + change); + return fail(0x4f35, 'Unknown ChangeType', { change }); } }; diff --git a/packages/firestore/src/core/view_snapshot.ts b/packages/firestore/src/core/view_snapshot.ts index f15c5ccb409..3bb6e8ae134 100644 --- a/packages/firestore/src/core/view_snapshot.ts +++ b/packages/firestore/src/core/view_snapshot.ts @@ -118,10 +118,12 @@ export class DocumentChangeSet { // Metadata->Added // Removed->Metadata fail( - 'unsupported combination of changes: ' + - JSON.stringify(change) + - ' after ' + - JSON.stringify(oldChange) + 0xf76d, + 'unsupported combination of changes: `change` after `oldChange`', + { + change, + oldChange + } ); } } diff --git a/packages/firestore/src/index/firestore_index_value_writer.ts b/packages/firestore/src/index/firestore_index_value_writer.ts index dfdb3836578..b76ca7a930a 100644 --- a/packages/firestore/src/index/firestore_index_value_writer.ts +++ b/packages/firestore/src/index/firestore_index_value_writer.ts @@ -136,7 +136,7 @@ export class FirestoreIndexValueWriter { this.writeIndexArray(indexValue.arrayValue!, encoder); this.writeTruncationMarker(encoder); } else { - fail('unknown index value type ' + indexValue); + fail(0x4a4e, 'unknown index value type', { indexValue }); } } diff --git a/packages/firestore/src/lite-api/reference_impl.ts b/packages/firestore/src/lite-api/reference_impl.ts index 6876ed0a877..6d92ccab479 100644 --- a/packages/firestore/src/lite-api/reference_impl.ts +++ b/packages/firestore/src/lite-api/reference_impl.ts @@ -133,7 +133,11 @@ export function getDoc( return invokeBatchGetDocumentsRpc(datastore, [reference._key]).then( result => { - hardAssert(result.length === 1, 'Expected a single document result'); + hardAssert( + result.length === 1, + 0x3d02, + 'Expected a single document result' + ); const document = result[0]; return new DocumentSnapshot( reference.firestore, diff --git a/packages/firestore/src/lite-api/transaction.ts b/packages/firestore/src/lite-api/transaction.ts index 10a07ef252c..9a405feebea 100644 --- a/packages/firestore/src/lite-api/transaction.ts +++ b/packages/firestore/src/lite-api/transaction.ts @@ -94,7 +94,7 @@ export class Transaction { const userDataWriter = new LiteUserDataWriter(this._firestore); return this._transaction.lookup([ref._key]).then(docs => { if (!docs || docs.length !== 1) { - return fail('Mismatch in docs returned from document lookup.'); + return fail(0x5de9, 'Mismatch in docs returned from document lookup.'); } const doc = docs[0]; if (doc.isFoundDocument()) { @@ -115,7 +115,11 @@ export class Transaction { ); } else { throw fail( - `BatchGetDocumentsRequest returned unexpected document: ${doc}` + 0x4801, + 'BatchGetDocumentsRequest returned unexpected document', + { + doc + } ); } }); diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index ebd4b49085f..aa5f9eeb5bf 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -175,7 +175,9 @@ function isWrite(dataSource: UserDataSource): boolean { case UserDataSource.ArrayArgument: return false; default: - throw fail(`Unexpected case for UserDataSource: ${dataSource}`); + throw fail(0x9c4b, 'Unexpected case for UserDataSource', { + dataSource + }); } } diff --git a/packages/firestore/src/lite-api/user_data_writer.ts b/packages/firestore/src/lite-api/user_data_writer.ts index e903991cb58..070c71c7832 100644 --- a/packages/firestore/src/lite-api/user_data_writer.ts +++ b/packages/firestore/src/lite-api/user_data_writer.ts @@ -89,7 +89,9 @@ export abstract class AbstractUserDataWriter { case TypeOrder.VectorValue: return this.convertVectorValue(value.mapValue!); default: - throw fail('Invalid value type: ' + JSON.stringify(value)); + throw fail(0xf2a2, 'Invalid value type', { + value + }); } } @@ -173,7 +175,9 @@ export abstract class AbstractUserDataWriter { const resourcePath = ResourcePath.fromString(name); hardAssert( isValidResourceName(resourcePath), - 'ReferenceValue is not valid ' + name + 0x25d8, + 'ReferenceValue is not valid', + { name } ); const databaseId = new DatabaseId(resourcePath.get(1), resourcePath.get(3)); const key = new DocumentKey(resourcePath.popFirst(5)); diff --git a/packages/firestore/src/local/encoded_resource_path.ts b/packages/firestore/src/local/encoded_resource_path.ts index b52b8bd6805..497a65fdf8c 100644 --- a/packages/firestore/src/local/encoded_resource_path.ts +++ b/packages/firestore/src/local/encoded_resource_path.ts @@ -118,11 +118,13 @@ export function decodeResourcePath(path: EncodedResourcePath): ResourcePath { // Event the empty path must encode as a path of at least length 2. A path // with exactly 2 must be the empty path. const length = path.length; - hardAssert(length >= 2, 'Invalid path ' + path); + hardAssert(length >= 2, 0xfb98, 'Invalid path', { path }); if (length === 2) { hardAssert( path.charAt(0) === escapeChar && path.charAt(1) === encodedSeparatorChar, - 'Non-empty path ' + path + ' had length 2' + 0xdb51, + 'Non-empty path had length 2', + { path } ); return ResourcePath.emptyPath(); } @@ -139,7 +141,7 @@ export function decodeResourcePath(path: EncodedResourcePath): ResourcePath { // there must be an end to this segment. const end = path.indexOf(escapeChar, start); if (end < 0 || end > lastReasonableEscapeIndex) { - fail('Invalid encoded resource path: "' + path + '"'); + fail(0xc553, 'Invalid encoded resource path', { path }); } const next = path.charAt(end + 1); @@ -167,7 +169,7 @@ export function decodeResourcePath(path: EncodedResourcePath): ResourcePath { segmentBuilder += path.substring(start, end + 1); break; default: - fail('Invalid encoded resource path: "' + path + '"'); + fail(0xeeef, 'Invalid encoded resource path', { path }); } start = end + 2; diff --git a/packages/firestore/src/local/indexeddb_index_manager.ts b/packages/firestore/src/local/indexeddb_index_manager.ts index 04a380601b3..d2b8bc47163 100644 --- a/packages/firestore/src/local/indexeddb_index_manager.ts +++ b/packages/firestore/src/local/indexeddb_index_manager.ts @@ -1066,7 +1066,7 @@ export class IndexedDbIndexManager implements IndexManager { this.getSubTargets(target), (subTarget: Target) => this.getFieldIndex(transaction, subTarget).next(index => - index ? index : fail('Target cannot be served from index') + index ? index : fail(0xad8a, 'Target cannot be served from index') ) ).next(getMinOffsetFromFieldIndexes); } @@ -1118,6 +1118,7 @@ function indexStateStore( function getMinOffsetFromFieldIndexes(fieldIndexes: FieldIndex[]): IndexOffset { hardAssert( fieldIndexes.length !== 0, + 0x7099, 'Found empty index group when looking for least recent index offset.' ); diff --git a/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts b/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts index 16b157accb2..64b6df20b7e 100644 --- a/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts +++ b/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts @@ -64,8 +64,9 @@ export function removeMutationBatch( removePromise.next(() => { hardAssert( numDeleted === 1, - 'Dangling document-mutation reference found: Missing batch ' + - batch.batchId + 0xb7de, + 'Dangling document-mutation reference found: Missing batch', + { batchId: batch.batchId } ); }) ); @@ -100,7 +101,7 @@ export function dbDocumentSize( } else if (doc.noDocument) { value = doc.noDocument; } else { - throw fail('Unknown remote document type'); + throw fail(0x398b, 'Unknown remote document type'); } return JSON.stringify(value).length; } diff --git a/packages/firestore/src/local/indexeddb_mutation_queue.ts b/packages/firestore/src/local/indexeddb_mutation_queue.ts index 0aedf650769..bcdafa6aa36 100644 --- a/packages/firestore/src/local/indexeddb_mutation_queue.ts +++ b/packages/firestore/src/local/indexeddb_mutation_queue.ts @@ -105,7 +105,7 @@ export class IndexedDbMutationQueue implements MutationQueue { // In particular, are there any reserved characters? are empty ids allowed? // For the moment store these together in the same mutations table assuming // that empty userIDs aren't allowed. - hardAssert(user.uid !== '', 'UserID must not be an empty string.'); + hardAssert(user.uid !== '', 0xfb83, 'UserID must not be an empty string.'); const userId = user.isAuthenticated() ? user.uid! : ''; return new IndexedDbMutationQueue( userId, @@ -154,6 +154,7 @@ export class IndexedDbMutationQueue implements MutationQueue { return mutationStore.add({} as any).next(batchId => { hardAssert( typeof batchId === 'number', + 0xbf7b, 'Auto-generated key is not a number' ); @@ -206,7 +207,12 @@ export class IndexedDbMutationQueue implements MutationQueue { if (dbBatch) { hardAssert( dbBatch.userId === this.userId, - `Unexpected user '${dbBatch.userId}' for mutation batch ${batchId}` + 0x0030, + `Unexpected user for mutation batch`, + { + userId: dbBatch.userId, + batchId + } ); return fromDbMutationBatch(this.serializer, dbBatch); } @@ -257,7 +263,9 @@ export class IndexedDbMutationQueue implements MutationQueue { if (dbBatch.userId === this.userId) { hardAssert( dbBatch.batchId >= nextBatchId, - 'Should have found mutation after ' + nextBatchId + 0xb9a4, + 'Should have found mutation after `nextBatchId`', + { nextBatchId } ); foundBatch = fromDbMutationBatch(this.serializer, dbBatch); } @@ -336,15 +344,22 @@ export class IndexedDbMutationQueue implements MutationQueue { .next(mutation => { if (!mutation) { throw fail( - 'Dangling document-mutation reference found: ' + - indexKey + - ' which points to ' + + 0xf028, + 'Dangling document-mutation reference found: `indexKey` which points to `batchId`', + { + indexKey, batchId + } ); } hardAssert( mutation.userId === this.userId, - `Unexpected user '${mutation.userId}' for mutation batch ${batchId}` + 0x2907, + `Unexpected user for mutation batch`, + { + userId: mutation.userId, + batchId + } ); results.push(fromDbMutationBatch(this.serializer, mutation)); }); @@ -468,14 +483,18 @@ export class IndexedDbMutationQueue implements MutationQueue { .next(mutation => { if (mutation === null) { throw fail( - 'Dangling document-mutation reference found, ' + - 'which points to ' + + 0x89ca, + 'Dangling document-mutation reference found, which points to `batchId`', + { batchId + } ); } hardAssert( mutation.userId === this.userId, - `Unexpected user '${mutation.userId}' for mutation batch ${batchId}` + 0x2614, + `Unexpected user for mutation batch`, + { userId: mutation.userId, batchId } ); results.push(fromDbMutationBatch(this.serializer, mutation)); }) @@ -549,9 +568,13 @@ export class IndexedDbMutationQueue implements MutationQueue { .next(() => { hardAssert( danglingMutationReferences.length === 0, - 'Document leak -- detected dangling mutation references when queue is empty. ' + - 'Dangling keys: ' + - danglingMutationReferences.map(p => p.canonicalString()) + 0xdd90, + 'Document leak -- detected dangling mutation references when queue is empty.', + { + danglingKeys: danglingMutationReferences.map(p => + p.canonicalString() + ) + } ); }); }); diff --git a/packages/firestore/src/local/indexeddb_remote_document_cache.ts b/packages/firestore/src/local/indexeddb_remote_document_cache.ts index 9b23c64fcf5..fffe935c4f9 100644 --- a/packages/firestore/src/local/indexeddb_remote_document_cache.ts +++ b/packages/firestore/src/local/indexeddb_remote_document_cache.ts @@ -381,7 +381,7 @@ class IndexedDbRemoteDocumentCacheImpl implements IndexedDbRemoteDocumentCache { return documentGlobalStore(txn) .get(DbRemoteDocumentGlobalKey) .next(metadata => { - hardAssert(!!metadata, 'Missing document cache metadata'); + hardAssert(!!metadata, 0x4e35, 'Missing document cache metadata'); return metadata!; }); } diff --git a/packages/firestore/src/local/indexeddb_schema_converter.ts b/packages/firestore/src/local/indexeddb_schema_converter.ts index 9d7485f4a92..7446ae7ae20 100644 --- a/packages/firestore/src/local/indexeddb_schema_converter.ts +++ b/packages/firestore/src/local/indexeddb_schema_converter.ts @@ -326,7 +326,9 @@ export class SchemaConverter implements SimpleDbSchemaConverter { (dbBatch: DbMutationBatch) => { hardAssert( dbBatch.userId === queue.userId, - `Cannot process batch ${dbBatch.batchId} from unexpected user` + 0x48da, + `Cannot process batch from unexpected user`, + { batchId: dbBatch.batchId } ); const batch = fromDbMutationBatch(this.serializer, dbBatch); @@ -772,6 +774,6 @@ function extractKey(remoteDoc: DbRemoteDocumentLegacy): DocumentKey { } else if (remoteDoc.unknownDocument) { return DocumentKey.fromSegments(remoteDoc.unknownDocument.path); } else { - return fail('Unexpected DbRemoteDocument'); + return fail(0x8faf, 'Unexpected DbRemoteDocument'); } } diff --git a/packages/firestore/src/local/indexeddb_sentinels.ts b/packages/firestore/src/local/indexeddb_sentinels.ts index e1e3ead3aa2..cb6ebcb664a 100644 --- a/packages/firestore/src/local/indexeddb_sentinels.ts +++ b/packages/firestore/src/local/indexeddb_sentinels.ts @@ -450,6 +450,6 @@ export function getObjectStores(schemaVersion: number): string[] { } else if (schemaVersion === 11) { return V11_STORES; } else { - fail('Only schema version 11 and 12 and 13 are supported'); + fail(0xeb55, 'Only schema version 11 and 12 and 13 are supported'); } } diff --git a/packages/firestore/src/local/indexeddb_target_cache.ts b/packages/firestore/src/local/indexeddb_target_cache.ts index 9e93cc68838..1d5ed8f0c8b 100644 --- a/packages/firestore/src/local/indexeddb_target_cache.ts +++ b/packages/firestore/src/local/indexeddb_target_cache.ts @@ -144,6 +144,7 @@ export class IndexedDbTargetCache implements TargetCache { .next(metadata => { hardAssert( metadata.targetCount > 0, + 0x1f81, 'Removing from an empty target cache' ); metadata.targetCount -= 1; @@ -197,7 +198,7 @@ export class IndexedDbTargetCache implements TargetCache { return globalTargetStore(transaction) .get(DbTargetGlobalKey) .next(metadata => { - hardAssert(metadata !== null, 'Missing metadata row.'); + hardAssert(metadata !== null, 0x0b48, 'Missing metadata row.'); return metadata; }); } diff --git a/packages/firestore/src/local/local_serializer.ts b/packages/firestore/src/local/local_serializer.ts index b8916608711..bb1658caa52 100644 --- a/packages/firestore/src/local/local_serializer.ts +++ b/packages/firestore/src/local/local_serializer.ts @@ -101,7 +101,7 @@ export function fromDbRemoteDocument( const version = fromDbTimestamp(remoteDoc.unknownDocument.version); doc = MutableDocument.newUnknownDocument(key, version); } else { - return fail('Unexpected DbRemoteDocument'); + return fail(0xdd85, 'Unexpected DbRemoteDocument'); } if (remoteDoc.readTime) { @@ -138,7 +138,7 @@ export function toDbRemoteDocument( version: toDbTimestamp(document.version) }; } else { - return fail('Unexpected Document ' + document); + return fail(0xe230, 'Unexpected Document', { document }); } return remoteDoc; } diff --git a/packages/firestore/src/local/local_store_impl.ts b/packages/firestore/src/local/local_store_impl.ts index 56f2b96f8d1..31d2a46c326 100644 --- a/packages/firestore/src/local/local_store_impl.ts +++ b/packages/firestore/src/local/local_store_impl.ts @@ -494,7 +494,11 @@ export function localStoreRejectBatch( return localStoreImpl.mutationQueue .lookupMutationBatch(txn, batchId) .next((batch: MutationBatch | null) => { - hardAssert(batch !== null, 'Attempt to reject nonexistent batch!'); + hardAssert( + batch !== null, + 0x90f9, + 'Attempt to reject nonexistent batch!' + ); affectedKeys = batch.keys(); return localStoreImpl.mutationQueue.removeMutationBatch(txn, batch); }) @@ -1137,6 +1141,7 @@ function applyWriteToRemoteDocuments( const ackVersion = batchResult.docVersions.get(docKey); hardAssert( ackVersion !== null, + 0xbd9d, 'ackVersions should contain every doc in the write.' ); if (doc.version.compareTo(ackVersion!) < 0) { diff --git a/packages/firestore/src/local/memory_mutation_queue.ts b/packages/firestore/src/local/memory_mutation_queue.ts index e3902cc96fc..f136fb7ad15 100644 --- a/packages/firestore/src/local/memory_mutation_queue.ts +++ b/packages/firestore/src/local/memory_mutation_queue.ts @@ -246,6 +246,7 @@ export class MemoryMutationQueue implements MutationQueue { const batchIndex = this.indexOfExistingBatchId(batch.batchId, 'removed'); hardAssert( batchIndex === 0, + 0xd6db, 'Can only remove the first entry of the mutation queue' ); this.mutationQueue.shift(); diff --git a/packages/firestore/src/local/memory_persistence.ts b/packages/firestore/src/local/memory_persistence.ts index 30d4f2bd19a..fcb6db42059 100644 --- a/packages/firestore/src/local/memory_persistence.ts +++ b/packages/firestore/src/local/memory_persistence.ts @@ -235,7 +235,10 @@ export class MemoryEagerDelegate implements MemoryReferenceDelegate { private get orphanedDocuments(): Set { if (!this._orphanedDocuments) { - throw fail('orphanedDocuments is only valid during a transaction.'); + throw fail( + 0xee44, + 'orphanedDocuments is only valid during a transaction.' + ); } else { return this._orphanedDocuments; } diff --git a/packages/firestore/src/local/memory_remote_document_cache.ts b/packages/firestore/src/local/memory_remote_document_cache.ts index 42a0010d4ac..0daf80b6a19 100644 --- a/packages/firestore/src/local/memory_remote_document_cache.ts +++ b/packages/firestore/src/local/memory_remote_document_cache.ts @@ -219,7 +219,7 @@ class MemoryRemoteDocumentCacheImpl implements MemoryRemoteDocumentCache { ): PersistencePromise { // This method should only be called from the IndexBackfiller if persistence // is enabled. - fail('getAllFromCollectionGroup() is not supported.'); + fail(0x251c, 'getAllFromCollectionGroup() is not supported.'); } forEachDocumentKey( diff --git a/packages/firestore/src/local/persistence_promise.ts b/packages/firestore/src/local/persistence_promise.ts index 4678650fa84..812cc0fca85 100644 --- a/packages/firestore/src/local/persistence_promise.ts +++ b/packages/firestore/src/local/persistence_promise.ts @@ -86,7 +86,7 @@ export class PersistencePromise { catchFn?: RejectedHandler ): PersistencePromise { if (this.callbackAttached) { - fail('Called next() or catch() twice for PersistencePromise'); + fail(0xe830, 'Called next() or catch() twice for PersistencePromise'); } this.callbackAttached = true; if (this.isDone) { diff --git a/packages/firestore/src/local/shared_client_state.ts b/packages/firestore/src/local/shared_client_state.ts index 7c033cedb41..1000e63a0f6 100644 --- a/packages/firestore/src/local/shared_client_state.ts +++ b/packages/firestore/src/local/shared_client_state.ts @@ -1085,7 +1085,9 @@ function fromWebStorageSequenceNumber( const parsed = JSON.parse(seqString); hardAssert( typeof parsed === 'number', - 'Found non-numeric sequence number' + 0x77ac, + 'Found non-numeric sequence number', + { seqString } ); sequenceNumber = parsed; } catch (e) { diff --git a/packages/firestore/src/model/document.ts b/packages/firestore/src/model/document.ts index 830983aec43..ac454704776 100644 --- a/packages/firestore/src/model/document.ts +++ b/packages/firestore/src/model/document.ts @@ -397,6 +397,9 @@ export function compareDocumentsByField( if (v1 !== null && v2 !== null) { return valueCompare(v1, v2); } else { - return fail("Trying to compare documents on fields that don't exist"); + return fail( + 0xa786, + "Trying to compare documents on fields that don't exist" + ); } } diff --git a/packages/firestore/src/model/mutation.ts b/packages/firestore/src/model/mutation.ts index 119e9b9731b..0bcd1345b01 100644 --- a/packages/firestore/src/model/mutation.ts +++ b/packages/firestore/src/model/mutation.ts @@ -623,8 +623,12 @@ function serverTransformResults( const transformResults = new Map(); hardAssert( fieldTransforms.length === serverTransformResults.length, - `server transform result count (${serverTransformResults.length}) ` + - `should match field transform count (${fieldTransforms.length})` + 0x7f90, + 'server transform result count should match field transform count', + { + serverTransformResultCount: serverTransformResults.length, + fieldTransformCount: fieldTransforms.length + } ); for (let i = 0; i < serverTransformResults.length; i++) { diff --git a/packages/firestore/src/model/mutation_batch.ts b/packages/firestore/src/model/mutation_batch.ts index 56d5f4d2cd3..703623da01a 100644 --- a/packages/firestore/src/model/mutation_batch.ts +++ b/packages/firestore/src/model/mutation_batch.ts @@ -219,10 +219,12 @@ export class MutationBatchResult { ): MutationBatchResult { hardAssert( batch.mutations.length === results.length, - 'Mutations sent ' + - batch.mutations.length + - ' must equal results received ' + - results.length + 0xe5da, + 'Mutations sent must equal results received', + { + mutationsSent: batch.mutations.length, + resultsReceived: results.length + } ); let versionMap = documentVersionMap(); diff --git a/packages/firestore/src/model/normalize.ts b/packages/firestore/src/model/normalize.ts index 2061601d23e..986eeed1e48 100644 --- a/packages/firestore/src/model/normalize.ts +++ b/packages/firestore/src/model/normalize.ts @@ -32,7 +32,7 @@ export function normalizeTimestamp(date: Timestamp): { seconds: number; nanos: number; } { - hardAssert(!!date, 'Cannot normalize null or undefined timestamp.'); + hardAssert(!!date, 0x986a, 'Cannot normalize null or undefined timestamp.'); // The json interface (for the browser) will return an iso timestamp string, // while the proto js library (for node) will return a @@ -44,7 +44,9 @@ export function normalizeTimestamp(date: Timestamp): { // Parse the nanos right out of the string. let nanos = 0; const fraction = ISO_TIMESTAMP_REG_EXP.exec(date); - hardAssert(!!fraction, 'invalid timestamp: ' + date); + hardAssert(!!fraction, 0xb5de, 'invalid timestamp', { + timestamp: date + }); if (fraction[1]) { // Pad the fraction out to 9 digits (nanos). let nanoStr = fraction[1]; diff --git a/packages/firestore/src/model/path.ts b/packages/firestore/src/model/path.ts index c375b4c56d2..0f4581da8d8 100644 --- a/packages/firestore/src/model/path.ts +++ b/packages/firestore/src/model/path.ts @@ -35,13 +35,19 @@ abstract class BasePath> { if (offset === undefined) { offset = 0; } else if (offset > segments.length) { - fail('offset ' + offset + ' out of range ' + segments.length); + fail(0x027d, 'offset out of range', { + offset, + range: segments.length + }); } if (length === undefined) { length = segments.length - offset; } else if (length > segments.length - offset) { - fail('length ' + length + ' out of range ' + (segments.length - offset)); + fail(0x06d2, 'length out of range', { + length, + range: segments.length - offset + }); } this.segments = segments; this.offset = offset; diff --git a/packages/firestore/src/model/target_index_matcher.ts b/packages/firestore/src/model/target_index_matcher.ts index df80ffa419a..407eae337c7 100644 --- a/packages/firestore/src/model/target_index_matcher.ts +++ b/packages/firestore/src/model/target_index_matcher.ts @@ -111,6 +111,7 @@ export class TargetIndexMatcher { servedByIndex(index: FieldIndex): boolean { hardAssert( index.collectionGroup === this.collectionId, + 0xc07f, 'Collection IDs do not match' ); diff --git a/packages/firestore/src/model/values.ts b/packages/firestore/src/model/values.ts index 30d8688b776..1ef54a98ad6 100644 --- a/packages/firestore/src/model/values.ts +++ b/packages/firestore/src/model/values.ts @@ -93,7 +93,7 @@ export function typeOrder(value: Value): TypeOrder { } return TypeOrder.ObjectValue; } else { - return fail('Invalid value type: ' + JSON.stringify(value)); + return fail(0x6e87, 'Invalid value type', { value }); } } @@ -140,7 +140,7 @@ export function valueEquals(left: Value, right: Value): boolean { case TypeOrder.MaxValue: return true; default: - return fail('Unexpected value type: ' + JSON.stringify(left)); + return fail(0xcbf8, 'Unexpected value type', { left }); } } @@ -269,7 +269,7 @@ export function valueCompare(left: Value, right: Value): number { case TypeOrder.ObjectValue: return compareMaps(left.mapValue!, right.mapValue!); default: - throw fail('Invalid value type: ' + leftType); + throw fail(0x5ae0, 'Invalid value type', { leftType }); } } @@ -449,7 +449,7 @@ function canonifyValue(value: Value): string { } else if ('mapValue' in value) { return canonifyMap(value.mapValue!); } else { - return fail('Invalid value type: ' + JSON.stringify(value)); + return fail(0xee4d, 'Invalid value type', { value }); } } @@ -541,7 +541,7 @@ export function estimateByteSize(value: Value): number { case TypeOrder.ObjectValue: return estimateMapByteSize(value.mapValue!); default: - throw fail('Invalid value type: ' + JSON.stringify(value)); + throw fail(0x34ae, 'Invalid value type', { value }); } } @@ -701,7 +701,7 @@ export function valuesGetLowerBound(value: Value): Value { } return { mapValue: {} }; } else { - return fail('Invalid value type: ' + JSON.stringify(value)); + return fail(0x8c66, 'Invalid value type', { value }); } } @@ -731,7 +731,7 @@ export function valuesGetUpperBound(value: Value): Value { } return MAX_VALUE; } else { - return fail('Invalid value type: ' + JSON.stringify(value)); + return fail(0xf207, 'Invalid value type', { value }); } } diff --git a/packages/firestore/src/platform/browser/webchannel_connection.ts b/packages/firestore/src/platform/browser/webchannel_connection.ts index 206e5829c41..5223285c5a4 100644 --- a/packages/firestore/src/platform/browser/webchannel_connection.ts +++ b/packages/firestore/src/platform/browser/webchannel_connection.ts @@ -142,12 +142,14 @@ export class WebChannelConnection extends RestConnection { break; default: fail( - `RPC '${rpcName}' ${streamId} ` + - 'failed with unanticipated webchannel error: ' + - xhr.getLastErrorCode() + - ': ' + - xhr.getLastError() + - ', giving up.' + 0x235f, + 'RPC failed with unanticipated webchannel error. Giving up.', + { + rpcName, + streamId, + lastErrorCode: xhr.getLastErrorCode(), + lastError: xhr.getLastError() + } ); } } finally { @@ -353,7 +355,11 @@ export class WebChannelConnection extends RestConnection { msg => { if (!closed) { const msgData = msg.data[0]; - hardAssert(!!msgData, 'Got a webchannel message without data.'); + hardAssert( + !!msgData, + 0x3fdd, + 'Got a webchannel message without data.' + ); // TODO(b/35143891): There is a bug in One Platform that caused errors // (and only errors) to be wrapped in an extra array. To be forward // compatible with the bug we need to check either condition. The latter diff --git a/packages/firestore/src/platform/node/grpc_connection.ts b/packages/firestore/src/platform/node/grpc_connection.ts index dec3137af76..d50a3149416 100644 --- a/packages/firestore/src/platform/node/grpc_connection.ts +++ b/packages/firestore/src/platform/node/grpc_connection.ts @@ -48,6 +48,7 @@ function createMetadata( ): grpc.Metadata { hardAssert( authToken === null || authToken.type === 'OAuth', + 0x9048, 'If provided, token must be OAuth' ); const metadata = new grpc.Metadata(); diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index ac47f0cb931..f790ede0d5c 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -228,7 +228,9 @@ export async function invokeBatchGetDocumentsRpc( const result: Document[] = []; keys.forEach(key => { const doc = docs.get(key.toString()); - hardAssert(!!doc, 'Missing entity in write response for ' + key); + hardAssert(!!doc, 0xd7c2, 'Missing entity in write response for `key`', { + key + }); result.push(doc); }); return result; @@ -290,6 +292,7 @@ export async function invokeRunAggregationQueryRpc( hardAssert( filteredResult.length === 1, + 0xfcd7, 'Aggregation fields are missing from result.' ); debugAssert( diff --git a/packages/firestore/src/remote/persistent_stream.ts b/packages/firestore/src/remote/persistent_stream.ts index f9f64bec7f6..4f3b91652ad 100644 --- a/packages/firestore/src/remote/persistent_stream.ts +++ b/packages/firestore/src/remote/persistent_stream.ts @@ -809,6 +809,7 @@ export class PersistentWriteStream extends PersistentStream< // Always capture the last stream token. hardAssert( !!responseProto.streamToken, + 0x7a5a, 'Got a write handshake response without a stream token' ); this.lastStreamToken = responseProto.streamToken; @@ -816,6 +817,7 @@ export class PersistentWriteStream extends PersistentStream< // The first response is always the handshake response hardAssert( !responseProto.writeResults || responseProto.writeResults.length === 0, + 0xda08, 'Got mutation results for handshake' ); return this.listener!.onHandshakeComplete(); @@ -825,6 +827,7 @@ export class PersistentWriteStream extends PersistentStream< // Always capture the last stream token. hardAssert( !!responseProto.streamToken, + 0x3186, 'Got a write response without a stream token' ); this.lastStreamToken = responseProto.streamToken; diff --git a/packages/firestore/src/remote/rpc_error.ts b/packages/firestore/src/remote/rpc_error.ts index 479ceea36c9..2efee40f223 100644 --- a/packages/firestore/src/remote/rpc_error.ts +++ b/packages/firestore/src/remote/rpc_error.ts @@ -58,7 +58,7 @@ enum RpcCode { export function isPermanentError(code: Code): boolean { switch (code) { case Code.OK: - return fail('Treated status OK as error'); + return fail(0xfdaa, 'Treated status OK as error'); case Code.CANCELLED: case Code.UNKNOWN: case Code.DEADLINE_EXCEEDED: @@ -83,7 +83,7 @@ export function isPermanentError(code: Code): boolean { case Code.DATA_LOSS: return true; default: - return fail('Unknown status code: ' + code); + return fail(0x3c6b, 'Unknown status code', { code }); } } @@ -171,7 +171,7 @@ export function mapCodeFromRpcCode(code: number | undefined): Code { case RpcCode.DATA_LOSS: return Code.DATA_LOSS; default: - return fail('Unknown status code: ' + code); + return fail(0x999b, 'Unknown status code', { code }); } } @@ -220,7 +220,7 @@ export function mapRpcCodeFromCode(code: Code | undefined): number { case Code.DATA_LOSS: return RpcCode.DATA_LOSS; default: - return fail('Unknown status code: ' + code); + return fail(0x3019, 'Unknown status code', { code }); } } diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 811c2ac4df6..aabdb263c1a 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -257,6 +257,7 @@ export function fromBytes( if (serializer.useProto3Json) { hardAssert( value === undefined || typeof value === 'string', + 0xe30b, 'value must be undefined or a string when using proto3 Json' ); return ByteString.fromBase64String(value ? value : ''); @@ -269,6 +270,7 @@ export function fromBytes( // does not indicate that it extends Uint8Array. value instanceof Buffer || value instanceof Uint8Array, + 0x3f41, 'value must be undefined, Buffer, or Uint8Array' ); return ByteString.fromUint8Array(value ? value : new Uint8Array()); @@ -283,7 +285,7 @@ export function toVersion( } export function fromVersion(version: ProtoTimestamp): SnapshotVersion { - hardAssert(!!version, "Trying to deserialize version that isn't set"); + hardAssert(!!version, 0xc050, "Trying to deserialize version that isn't set"); return SnapshotVersion.fromTimestamp(fromTimestamp(version)); } @@ -306,7 +308,9 @@ function fromResourceName(name: string): ResourcePath { const resource = ResourcePath.fromString(name); hardAssert( isValidResourceName(resource), - 'Tried to deserialize invalid key ' + resource.toString() + 0x27ce, + 'Tried to deserialize invalid key', + { key: resource.toString() } ); return resource; } @@ -389,7 +393,9 @@ function extractLocalPathFromResourceName( ): ResourcePath { hardAssert( resourceName.length > 4 && resourceName.get(4) === 'documents', - 'tried to deserialize invalid key ' + resourceName.toString() + 0x71a3, + 'tried to deserialize invalid key', + { key: resourceName.toString() } ); return resourceName.popFirst(5); } @@ -454,6 +460,7 @@ function fromFound( ): MutableDocument { hardAssert( !!doc.found, + 0xaa33, 'Tried to deserialize a found document from a missing document.' ); assertPresent(doc.found.name, 'doc.found.name'); @@ -473,10 +480,12 @@ function fromMissing( ): MutableDocument { hardAssert( !!result.missing, + 0x0f36, 'Tried to deserialize a missing document from a found document.' ); hardAssert( !!result.readTime, + 0x5995, 'Tried to deserialize a missing document without a read time.' ); const key = fromName(serializer, result.missing); @@ -493,7 +502,7 @@ export function fromBatchGetDocumentsResponse( } else if ('missing' in result) { return fromMissing(serializer, result); } - return fail('invalid batch get response: ' + JSON.stringify(result)); + return fail(0x1c42, 'invalid batch get response', { result }); } export function fromWatchChange( @@ -578,7 +587,7 @@ export function fromWatchChange( const targetId = filter.targetId; watchChange = new ExistenceFilterChange(targetId, existenceFilter); } else { - return fail('Unknown change type ' + JSON.stringify(change)); + return fail(0x2d51, 'Unknown change type', { change }); } return watchChange; } @@ -597,7 +606,7 @@ function fromWatchTargetChangeState( } else if (state === 'RESET') { return WatchTargetChangeState.Reset; } else { - return fail('Got unexpected TargetChange.state: ' + state); + return fail(0x9991, 'Got unexpected TargetChange.state', { state }); } } @@ -641,7 +650,9 @@ export function toMutation( verify: toName(serializer, mutation.key) }; } else { - return fail('Unknown mutation type ' + mutation.type); + return fail(0x40d7, 'Unknown mutation type', { + mutationType: mutation.type + }); } if (mutation.fieldTransforms.length > 0) { @@ -697,7 +708,7 @@ export function fromMutation( const key = fromName(serializer, proto.verify); return new VerifyMutation(key, precondition); } else { - return fail('unknown mutation proto: ' + JSON.stringify(proto)); + return fail(0x05b7, 'unknown mutation proto', { proto }); } } @@ -713,7 +724,7 @@ function toPrecondition( } else if (precondition.exists !== undefined) { return { exists: precondition.exists }; } else { - return fail('Unknown precondition'); + return fail(0x6b69, 'Unknown precondition'); } } @@ -755,6 +766,7 @@ export function fromWriteResults( if (protos && protos.length > 0) { hardAssert( commitTime !== undefined, + 0x3811, 'Received a write result without a commit time' ); return protos.map(proto => fromWriteResult(proto, commitTime)); @@ -793,7 +805,9 @@ function toFieldTransform( increment: transform.operand }; } else { - throw fail('Unknown transform: ' + fieldTransform.transform); + throw fail(0x51c2, 'Unknown transform', { + transform: fieldTransform.transform + }); } } @@ -805,7 +819,9 @@ function fromFieldTransform( if ('setToServerValue' in proto) { hardAssert( proto.setToServerValue === 'REQUEST_TIME', - 'Unknown server value transform proto: ' + JSON.stringify(proto) + 0x40f6, + 'Unknown server value transform proto', + { proto } ); transform = new ServerTimestampTransform(); } else if ('appendMissingElements' in proto) { @@ -820,7 +836,7 @@ function fromFieldTransform( proto.increment! ); } else { - fail('Unknown transform proto: ' + JSON.stringify(proto)); + fail(0x40c8, 'Unknown transform proto', { proto }); } const fieldPath = FieldPath.fromServerFormat(proto.fieldPath!); return new FieldTransform(fieldPath, transform!); @@ -839,7 +855,11 @@ export function fromDocumentsTarget( const count = documentsTarget.documents!.length; hardAssert( count === 1, - 'DocumentsTarget contained other than 1 document: ' + count + 0x07ae, + 'DocumentsTarget contained other than 1 document', + { + count + } ); const name = documentsTarget.documents![0]; return queryToTarget(newQueryForPath(fromQueryPath(name))); @@ -969,6 +989,7 @@ export function convertQueryTargetToQuery(target: ProtoQueryTarget): Query { if (fromCount > 0) { hardAssert( fromCount === 1, + 0xfe26, 'StructuredQuery.from with more than one collection is not supported.' ); const from = query.from![0]; @@ -1045,7 +1066,7 @@ export function toLabel(purpose: TargetPurpose): string | null { case TargetPurpose.LimboResolution: return 'limbo-document'; default: - return fail('Unrecognized query purpose: ' + purpose); + return fail(0x713b, 'Unrecognized query purpose', { purpose }); } } @@ -1116,7 +1137,7 @@ function fromFilter(filter: ProtoFilter): Filter { } else if (filter.compositeFilter !== undefined) { return fromCompositeFilter(filter); } else { - return fail('Unknown filter: ' + JSON.stringify(filter)); + return fail(0x7591, 'Unknown filter', { filter }); } } @@ -1210,9 +1231,9 @@ export function fromOperatorName(op: ProtoFieldFilterOp): Operator { case 'ARRAY_CONTAINS_ANY': return Operator.ARRAY_CONTAINS_ANY; case 'OPERATOR_UNSPECIFIED': - return fail('Unspecified operator'); + return fail(0xe2fe, 'Unspecified operator'); default: - return fail('Unknown operator'); + return fail(0xc54a, 'Unknown operator'); } } @@ -1225,7 +1246,7 @@ export function fromCompositeOperatorName( case 'OR': return CompositeOperator.OR; default: - return fail('Unknown operator'); + return fail(0x0402, 'Unknown operator'); } } @@ -1261,7 +1282,7 @@ export function toFilter(filter: Filter): ProtoFilter { } else if (filter instanceof CompositeFilter) { return toCompositeFilter(filter); } else { - return fail('Unrecognized filter type ' + JSON.stringify(filter)); + return fail(0xd65d, 'Unrecognized filter type', { filter }); } } @@ -1346,9 +1367,9 @@ export function fromUnaryFilter(filter: ProtoFilter): Filter { nullValue: 'NULL_VALUE' }); case 'OPERATOR_UNSPECIFIED': - return fail('Unspecified filter'); + return fail(0xef81, 'Unspecified filter'); default: - return fail('Unknown filter'); + return fail(0xed36, 'Unknown filter'); } } diff --git a/packages/firestore/src/remote/watch_change.ts b/packages/firestore/src/remote/watch_change.ts index 0c69163095f..a656d8fdf6e 100644 --- a/packages/firestore/src/remote/watch_change.ts +++ b/packages/firestore/src/remote/watch_change.ts @@ -203,7 +203,7 @@ class TargetState { removedDocuments = removedDocuments.add(key); break; default: - fail('Encountered invalid change type: ' + changeType); + fail(0x9481, 'Encountered invalid change type', { changeType }); } }); @@ -242,10 +242,9 @@ class TargetState { this.pendingResponses -= 1; hardAssert( this.pendingResponses >= 0, - '`pendingResponses` is less than 0. Actual value: ' + - this.pendingResponses + - '. This indicates that the SDK received more target acks from the ' + - 'server than expected. The SDK should not continue to operate.' + 0x0ca9, + '`pendingResponses` is less than 0. This indicates that the SDK received more target acks from the server than expected. The SDK should not continue to operate.', + { pendingResponses: this.pendingResponses } ); } @@ -378,7 +377,9 @@ export class WatchChangeAggregator { } break; default: - fail('Unknown target watch change state: ' + targetChange.state); + fail(0xddd6, 'Unknown target watch change state', { + state: targetChange.state + }); } }); } @@ -432,7 +433,9 @@ export class WatchChangeAggregator { } else { hardAssert( expectedCount === 1, - 'Single document existence filter with count: ' + expectedCount + 0x4e2d, + 'Single document existence filter with count', + { expectedCount } ); } } else { diff --git a/packages/firestore/src/util/assert.ts b/packages/firestore/src/util/assert.ts index 6d65e6cd19b..07ebe775e9c 100644 --- a/packages/firestore/src/util/assert.ts +++ b/packages/firestore/src/util/assert.ts @@ -26,12 +26,61 @@ import { logError } from './log'; * Returns `never` and can be used in expressions: * @example * let futureVar = fail('not implemented yet'); + * + * @param code generate a new unique value with `yarn assertion-id:generate` + * Search for an existing value using `yarn assertion-id:find X` + */ +export function fail( + code: number, + message: string, + context?: Record +): never; + +/** + * Unconditionally fails, throwing an Error with the given message. + * Messages are stripped in production builds. + * + * Returns `never` and can be used in expressions: + * @example + * let futureVar = fail('not implemented yet'); + * + * @param id generate a new unique value with `yarn assertion-id:generate` + * Search for an existing value using `yarn assertion-id:find X` */ -export function fail(failure: string = 'Unexpected state'): never { +export function fail(id: number, context?: Record): never; + +export function fail( + id: number, + messageOrContext?: string | Record, + context?: Record +): never { + let message = 'Unexpected state'; + if (typeof messageOrContext === 'string') { + message = messageOrContext; + } else { + context = messageOrContext; + } + _fail(id, message, context); +} + +function _fail( + id: number, + failure: string, + context?: Record +): never { // Log the failure in addition to throw an exception, just in case the // exception is swallowed. - const message = - `FIRESTORE (${SDK_VERSION}) INTERNAL ASSERTION FAILED: ` + failure; + let message = `FIRESTORE (${SDK_VERSION}) INTERNAL ASSERTION FAILED: ${failure} (ID: ${id.toString( + 16 + )})`; + if (context !== undefined) { + try { + const stringContext = JSON.stringify(context); + message += ' CONTEXT: ' + stringContext; + } catch (e) { + message += ' CONTEXT: ' + context; + } + } logError(message); // NOTE: We don't use FirestoreError here because these are internal failures @@ -45,13 +94,47 @@ export function fail(failure: string = 'Unexpected state'): never { * given message if it did. * * Messages are stripped in production builds. + * + * @param id generate a new unique value with `yarn assertion-idgenerate`. + * Search for an existing value using `yarn assertion-id:find X` + */ +export function hardAssert( + assertion: boolean, + id: number, + message: string, + context?: Record +): asserts assertion; + +/** + * Fails if the given assertion condition is false, throwing an Error with the + * given message if it did. + * + * Messages are stripped in production builds. + * + * @param id generate a new unique value with `yarn assertion-id:generate`. + * Search for an existing value using `yarn assertion-id:find X` */ export function hardAssert( assertion: boolean, - message?: string + id: number, + context?: Record +): asserts assertion; + +export function hardAssert( + assertion: boolean, + id: number, + messageOrContext?: string | Record, + context?: Record ): asserts assertion { + let message = 'Unexpected state'; + if (typeof messageOrContext === 'string') { + message = messageOrContext; + } else { + context = messageOrContext; + } + if (!assertion) { - fail(message); + _fail(id, message, context); } } @@ -70,7 +153,7 @@ export function debugAssert( message: string ): asserts assertion { if (!assertion) { - fail(message); + fail(0xdeb6, message); } } diff --git a/packages/firestore/src/util/async_queue_impl.ts b/packages/firestore/src/util/async_queue_impl.ts index 79eb6c23850..f8c7a995761 100644 --- a/packages/firestore/src/util/async_queue_impl.ts +++ b/packages/firestore/src/util/async_queue_impl.ts @@ -236,7 +236,9 @@ export class AsyncQueueImpl implements AsyncQueue { private verifyNotFailed(): void { if (this.failure) { - fail('AsyncQueue is already failed: ' + getMessageOrStack(this.failure)); + fail(0xb815, 'AsyncQueue is already failed', { + messageOrStack: getMessageOrStack(this.failure) + }); } } diff --git a/packages/firestore/src/util/input_validation.ts b/packages/firestore/src/util/input_validation.ts index 37e349ce910..7fd9967b5a0 100644 --- a/packages/firestore/src/util/input_validation.ts +++ b/packages/firestore/src/util/input_validation.ts @@ -128,7 +128,7 @@ export function valueDescription(input: unknown): string { } else if (typeof input === 'function') { return 'a function'; } else { - return fail('Unknown wrong type: ' + typeof input); + return fail(0x3029, 'Unknown wrong type', { type: typeof input }); } } diff --git a/packages/firestore/src/util/logic_utils.ts b/packages/firestore/src/util/logic_utils.ts index 3c3a6b19fd8..b2167c385e9 100644 --- a/packages/firestore/src/util/logic_utils.ts +++ b/packages/firestore/src/util/logic_utils.ts @@ -44,6 +44,7 @@ import { hardAssert } from './assert'; export function computeInExpansion(filter: Filter): Filter { hardAssert( filter instanceof FieldFilter || filter instanceof CompositeFilter, + 0x4e2c, 'Only field filters and composite filters are accepted.' ); @@ -90,6 +91,7 @@ export function getDnfTerms(filter: CompositeFilter): Filter[] { hardAssert( isDisjunctiveNormalForm(result), + 0x1cdf, 'computeDistributedNormalForm did not result in disjunctive normal form' ); @@ -157,6 +159,7 @@ function isDisjunctionOfFieldFiltersAndFlatConjunctions( export function computeDistributedNormalForm(filter: Filter): Filter { hardAssert( filter instanceof FieldFilter || filter instanceof CompositeFilter, + 0x84e2, 'Only field filters and composite filters are accepted.' ); @@ -182,14 +185,17 @@ export function computeDistributedNormalForm(filter: Filter): Filter { hardAssert( newFilter instanceof CompositeFilter, + 0xfbf2, 'field filters are already in DNF form' ); hardAssert( compositeFilterIsConjunction(newFilter), + 0x9d3b, 'Disjunction of filters all of which are already in DNF form is itself in DNF form.' ); hardAssert( newFilter.filters.length > 1, + 0xe247, 'Single-filter composite filters are already in DNF form.' ); @@ -201,10 +207,12 @@ export function computeDistributedNormalForm(filter: Filter): Filter { export function applyDistribution(lhs: Filter, rhs: Filter): Filter { hardAssert( lhs instanceof FieldFilter || lhs instanceof CompositeFilter, + 0x95f4, 'Only field filters and composite filters are accepted.' ); hardAssert( rhs instanceof FieldFilter || rhs instanceof CompositeFilter, + 0x6381, 'Only field filters and composite filters are accepted.' ); @@ -245,6 +253,7 @@ function applyDistributionCompositeFilters( ): Filter { hardAssert( lhs.filters.length > 0 && rhs.filters.length > 0, + 0xbb85, 'Found an empty composite filter' ); @@ -306,6 +315,7 @@ function applyDistributionFieldAndCompositeFilters( export function applyAssociation(filter: Filter): Filter { hardAssert( filter instanceof FieldFilter || filter instanceof CompositeFilter, + 0x2e4a, 'Only field filters and composite filters are accepted.' ); diff --git a/packages/firestore/src/util/sorted_map.ts b/packages/firestore/src/util/sorted_map.ts index a24cf8802ca..023354173d3 100644 --- a/packages/firestore/src/util/sorted_map.ts +++ b/packages/firestore/src/util/sorted_map.ts @@ -511,14 +511,20 @@ export class LLRBNode { // leaves is equal on both sides. This function verifies that or asserts. protected check(): number { if (this.isRed() && this.left.isRed()) { - throw fail('Red node has red child(' + this.key + ',' + this.value + ')'); + throw fail(0xaad2, 'Red node has red child', { + key: this.key, + value: this.value + }); } if (this.right.isRed()) { - throw fail('Right child of (' + this.key + ',' + this.value + ') is red'); + throw fail(0x3721, 'Right child of (`key`, `value`) is red', { + key: this.key, + value: this.value + }); } const blackDepth = (this.left as LLRBNode).check(); if (blackDepth !== (this.right as LLRBNode).check()) { - throw fail('Black depths differ'); + throw fail(0x6d2d, 'Black depths differ'); } else { return blackDepth + (this.isRed() ? 0 : 1); } @@ -528,19 +534,19 @@ export class LLRBNode { // Represents an empty node (a leaf node in the Red-Black Tree). export class LLRBEmptyNode { get key(): never { - throw fail('LLRBEmptyNode has no key.'); + throw fail(0xe1a6, 'LLRBEmptyNode has no key.'); } get value(): never { - throw fail('LLRBEmptyNode has no value.'); + throw fail(0x3f0d, 'LLRBEmptyNode has no value.'); } get color(): never { - throw fail('LLRBEmptyNode has no color.'); + throw fail(0x4157, 'LLRBEmptyNode has no color.'); } get left(): never { - throw fail('LLRBEmptyNode has no left child.'); + throw fail(0x741e, 'LLRBEmptyNode has no left child.'); } get right(): never { - throw fail('LLRBEmptyNode has no right child.'); + throw fail(0x901e, 'LLRBEmptyNode has no right child.'); } size = 0; diff --git a/packages/firestore/test/unit/index/ordered_code_writer.test.ts b/packages/firestore/test/unit/index/ordered_code_writer.test.ts index 6d87ddb4849..27956c730ee 100644 --- a/packages/firestore/test/unit/index/ordered_code_writer.test.ts +++ b/packages/firestore/test/unit/index/ordered_code_writer.test.ts @@ -248,7 +248,14 @@ function getBytes(val: unknown): { asc: Uint8Array; desc: Uint8Array } { ascWriter.writeUtf8Ascending(val); descWriter.writeUtf8Descending(val); } else { - hardAssert(val instanceof Uint8Array); + hardAssert( + val instanceof Uint8Array, + 0xa10f, + 'val is not instance of Uint8Array', + { + val + } + ); ascWriter.writeBytesAscending(ByteString.fromUint8Array(val)); descWriter.writeBytesDescending(ByteString.fromUint8Array(val)); } diff --git a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts index e44bb73e47b..1240c977cee 100644 --- a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts +++ b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts @@ -1609,6 +1609,6 @@ function toLegacyDbRemoteDocument( parentPath }; } else { - return fail('Unexpected Document ' + document); + return fail(0x6bb7, 'Unexpected Document ', { document }); } } diff --git a/packages/firestore/test/unit/local/simple_db.test.ts b/packages/firestore/test/unit/local/simple_db.test.ts index b2b7ed3f95a..207e454fb5b 100644 --- a/packages/firestore/test/unit/local/simple_db.test.ts +++ b/packages/firestore/test/unit/local/simple_db.test.ts @@ -363,7 +363,7 @@ describe('SimpleDb', () => { iterated.push(value); return PersistencePromise.reject(new Error('Expected error')); }) - .next(() => fail('Promise not rejected')) + .next(() => fail(0xb9b3, 'Promise not rejected')) .catch(err => { expect(err.message).to.eq('Expected error'); expect(iterated).to.deep.equal([testData[0]]); diff --git a/packages/firestore/test/unit/specs/spec_builder.ts b/packages/firestore/test/unit/specs/spec_builder.ts index 80dcd6519de..3e52c5873b9 100644 --- a/packages/firestore/test/unit/specs/spec_builder.ts +++ b/packages/firestore/test/unit/specs/spec_builder.ts @@ -674,7 +674,7 @@ export class SpecBuilder { } else if (doc.isNoDocument()) { // Don't send any updates } else { - fail('Unknown parameter: ' + doc); + fail(0xd71c, 'Unknown parameter', { doc }); } this.watchCurrents(query, 'resume-token-' + version); this.watchSnapshots(version); @@ -1149,7 +1149,7 @@ export class SpecBuilder { userDataWriter.convertValue(filter.value) ] as SpecQueryFilter; } else { - return fail('Unknown filter: ' + filter); + return fail(0x0e51, 'Unknown filter', { filter }); } }); } @@ -1210,7 +1210,10 @@ export class SpecBuilder { targetPurpose?: TargetPurpose ): void { if (!(resume?.resumeToken || resume?.readTime) && resume?.expectedCount) { - fail('Expected count is present without a resume token or read time.'); + fail( + 0xc9a1, + 'Expected count is present without a resume token or read time.' + ); } if (this.activeTargets[targetId]) { @@ -1273,7 +1276,10 @@ export class SpecBuilder { if (queryTargetId && limboTargetId) { // TODO(dimond): add support for query for doc and limbo doc at the same // time? - fail('Found both query and limbo doc with target ID, not supported yet'); + fail( + 0x6e17, + 'Found both query and limbo doc with target ID, not supported yet' + ); } const targetId = queryTargetId || limboTargetId; debugAssert( diff --git a/packages/firestore/test/unit/specs/spec_test_components.ts b/packages/firestore/test/unit/specs/spec_test_components.ts index 2a2e480de63..017afe1d924 100644 --- a/packages/firestore/test/unit/specs/spec_test_components.ts +++ b/packages/firestore/test/unit/specs/spec_test_components.ts @@ -415,7 +415,7 @@ export class MockConnection implements Connection { } else if (request.removeTarget) { delete this.activeTargets[request.removeTarget]; } else { - fail('Invalid listen request'); + fail(0x782d, 'Invalid listen request'); } }, closeFn: () => { diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index b34421d9e0a..ee0af0b8bf8 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -477,7 +477,7 @@ abstract class TestRunner { ? this.doFailDatabase(step.failDatabase!) : this.doRecoverDatabase(); } else { - return fail('Unknown step: ' + JSON.stringify(step)); + return fail(0x6bb3, 'Unknown step: ' + JSON.stringify(step)); } } @@ -724,7 +724,7 @@ abstract class TestRunner { ); return this.doWatchEvent(change); } else { - return fail('Either doc or docs must be set'); + return fail(0xdcc3, 'Either doc or docs must be set'); } } diff --git a/packages/firestore/test/unit/util/assert.test.ts b/packages/firestore/test/unit/util/assert.test.ts new file mode 100644 index 00000000000..e865337a3a3 --- /dev/null +++ b/packages/firestore/test/unit/util/assert.test.ts @@ -0,0 +1,88 @@ +/** + * @license + * Copyright 2025 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. + */ + +import { expect } from 'chai'; + +import { fail, hardAssert } from '../../../src/util/assert'; + +describe('hardAssert', () => { + it('includes the error code as hex', () => { + expect(() => hardAssert(false, 0x1234, 'a message here')).to.throw('1234'); + }); + + it('includes the context', () => { + expect(() => + hardAssert(false, 0x1234, 'a message here', { foo: 'bar baz' }) + ).to.throw('bar baz'); + }); + + it('includes the message', () => { + expect(() => + hardAssert(false, 0x1234, 'a message here', { foo: 'bar baz' }) + ).to.throw('a message here'); + }); + + describe('without message', () => { + it('includes the error code as hex', () => { + expect(() => hardAssert(false, 0x1234)).to.throw('1234'); + }); + + it('includes the context', () => { + expect(() => hardAssert(false, 0x1234, { foo: 'bar baz' })).to.throw( + 'bar baz' + ); + }); + it('includes a default message', () => { + expect(() => hardAssert(false, 0x1234, { foo: 'bar baz' })).to.throw( + 'Unexpected state' + ); + }); + }); +}); + +describe('fail', () => { + it('includes the error code as hex', () => { + expect(() => fail(0x1234, 'a message here')).to.throw('1234'); + }); + + it('includes the context', () => { + expect(() => fail(0x1234, 'a message here', { foo: 'bar baz' })).to.throw( + 'bar baz' + ); + }); + + it('includes the message', () => { + expect(() => fail(0x1234, 'a message here', { foo: 'bar baz' })).to.throw( + 'a message here' + ); + }); + + describe('without message', () => { + it('includes the error code as hex', () => { + expect(() => fail(0x1234)).to.throw('1234'); + }); + + it('includes the context', () => { + expect(() => fail(0x1234, { foo: 'bar baz' })).to.throw('bar baz'); + }); + it('includes a default message', () => { + expect(() => fail(0x1234, { foo: 'bar baz' })).to.throw( + 'Unexpected state' + ); + }); + }); +}); diff --git a/packages/firestore/test/unit/util/async_queue.test.ts b/packages/firestore/test/unit/util/async_queue.test.ts index cc55879c88b..48aa02fe298 100644 --- a/packages/firestore/test/unit/util/async_queue.test.ts +++ b/packages/firestore/test/unit/util/async_queue.test.ts @@ -139,7 +139,7 @@ describe('AsyncQueue', () => { Promise.reject('dummyOp should not be run'); expect(() => { queue.enqueueAndForget(dummyOp); - }).to.throw(/already failed:.*Simulated Error/); + }).to.throw(/already failed.*Simulated Error/); // Finally, restore log level. setLogLevel(oldLogLevel as unknown as LogLevelString); @@ -247,7 +247,7 @@ describe('AsyncQueue', () => { const deferred = new Deferred(); queue.enqueueRetryable(async () => { deferred.resolve(); - throw fail('Simulated test failure'); + throw fail(0x1576, 'Simulated test failure'); }); await deferred.promise; await expect( diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index c5865c3e0f7..1ddaf174b19 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -867,11 +867,13 @@ export function expectEqual(left: any, right: any, message?: string): void { message = message || ''; if (typeof left.isEqual !== 'function') { return fail( + 0x8004, JSON.stringify(left) + ' does not support isEqual (left) ' + message ); } if (typeof right.isEqual !== 'function') { return fail( + 0xebc9, JSON.stringify(right) + ' does not support isEqual (right) ' + message ); } diff --git a/packages/firestore/test/util/spec_test_helpers.ts b/packages/firestore/test/util/spec_test_helpers.ts index 7e2f77da438..73378a361a3 100644 --- a/packages/firestore/test/util/spec_test_helpers.ts +++ b/packages/firestore/test/util/spec_test_helpers.ts @@ -100,7 +100,10 @@ export function encodeWatchChange( } }; } - return fail('Unrecognized watch change: ' + JSON.stringify(watchChange)); + return fail( + 0xf8e5, + 'Unrecognized watch change: ' + JSON.stringify(watchChange) + ); } function encodeTargetChangeTargetChangeType( @@ -118,6 +121,6 @@ function encodeTargetChangeTargetChangeType( case WatchTargetChangeState.Reset: return 'RESET'; default: - return fail('Unknown WatchTargetChangeState: ' + state); + return fail(0x368b, 'Unknown WatchTargetChangeState: ' + state); } } diff --git a/packages/firestore/test/util/test_platform.ts b/packages/firestore/test/util/test_platform.ts index 7803a2fb3c6..a0eac93cbc9 100644 --- a/packages/firestore/test/util/test_platform.ts +++ b/packages/firestore/test/util/test_platform.ts @@ -64,7 +64,7 @@ export class FakeWindow implements WindowLike { // listeners. break; default: - fail(`MockWindow doesn't support events of type '${type}'`); + fail(0xe53d, `MockWindow doesn't support events of type '${type}'`); } } From 8bb66c28b3b1a2486ae2574250e6f738bec671d1 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 15 Apr 2025 12:11:50 -0400 Subject: [PATCH 10/23] docs(vertexai): remove HTML `` tags (#8900) We wrap links that reference public APIs with `` tags so that they can be rendered as code in devsite. Unfortunately, VSCode's hover feature can not render these links at all. Since there are no other alternatives for creating code-formatted links, we should remove the problematic HTML `` tags. --- docs-devsite/vertexai.chatsession.md | 8 +-- docs-devsite/vertexai.citationmetadata.md | 2 +- docs-devsite/vertexai.counttokensrequest.md | 4 +- docs-devsite/vertexai.customerrordata.md | 4 +- docs-devsite/vertexai.filedatapart.md | 2 +- docs-devsite/vertexai.functioncall.md | 2 +- docs-devsite/vertexai.functioncallpart.md | 2 +- .../vertexai.functiondeclarationstool.md | 4 +- docs-devsite/vertexai.functionresponse.md | 2 +- docs-devsite/vertexai.functionresponsepart.md | 2 +- .../vertexai.generatecontentcandidate.md | 2 +- docs-devsite/vertexai.generationconfig.md | 4 +- docs-devsite/vertexai.generativemodel.md | 8 +-- docs-devsite/vertexai.imagengcsimage.md | 4 +- .../vertexai.imagengenerationconfig.md | 16 ++--- .../vertexai.imagengenerationresponse.md | 4 +- docs-devsite/vertexai.imagenimageformat.md | 14 ++--- docs-devsite/vertexai.imageninlineimage.md | 4 +- docs-devsite/vertexai.imagenmodel.md | 6 +- docs-devsite/vertexai.imagenmodelparams.md | 2 +- docs-devsite/vertexai.md | 60 +++++++++---------- docs-devsite/vertexai.modelparams.md | 2 +- .../vertexai.objectschemainterface.md | 2 +- docs-devsite/vertexai.requestoptions.md | 2 +- docs-devsite/vertexai.safetyrating.md | 2 +- docs-devsite/vertexai.schemainterface.md | 2 +- docs-devsite/vertexai.schemaparams.md | 2 +- docs-devsite/vertexai.schemarequest.md | 2 +- docs-devsite/vertexai.schemashared.md | 2 +- docs-devsite/vertexai.usagemetadata.md | 2 +- docs-devsite/vertexai.vertexai.md | 4 +- docs-devsite/vertexai.vertexaierror.md | 2 +- packages/vertexai/src/api.ts | 6 +- packages/vertexai/src/errors.ts | 2 +- packages/vertexai/src/methods/chat-session.ts | 4 +- .../vertexai/src/models/generative-model.ts | 4 +- packages/vertexai/src/models/imagen-model.ts | 6 +- packages/vertexai/src/public-types.ts | 2 +- .../src/requests/imagen-image-format.ts | 10 ++-- .../vertexai/src/requests/request-helpers.ts | 2 +- .../vertexai/src/requests/response-helpers.ts | 2 +- packages/vertexai/src/types/content.ts | 12 ++-- packages/vertexai/src/types/error.ts | 4 +- .../vertexai/src/types/imagen/requests.ts | 14 ++--- .../vertexai/src/types/imagen/responses.ts | 10 ++-- packages/vertexai/src/types/requests.ts | 14 ++--- packages/vertexai/src/types/responses.ts | 8 +-- packages/vertexai/src/types/schema.ts | 12 ++-- 48 files changed, 146 insertions(+), 146 deletions(-) diff --git a/docs-devsite/vertexai.chatsession.md b/docs-devsite/vertexai.chatsession.md index cc5a75ace16..ed359f7e08c 100644 --- a/docs-devsite/vertexai.chatsession.md +++ b/docs-devsite/vertexai.chatsession.md @@ -37,8 +37,8 @@ export declare class ChatSession | Method | Modifiers | Description | | --- | --- | --- | | [getHistory()](./vertexai.chatsession.md#chatsessiongethistory) | | Gets the chat history so far. Blocked prompts are not added to history. Neither blocked candidates nor the prompts that generated them are added to history. | -| [sendMessage(request)](./vertexai.chatsession.md#chatsessionsendmessage) | | Sends a chat message and receives a non-streaming [GenerateContentResult](./vertexai.generatecontentresult.md#generatecontentresult_interface) | -| [sendMessageStream(request)](./vertexai.chatsession.md#chatsessionsendmessagestream) | | Sends a chat message and receives the response as a [GenerateContentStreamResult](./vertexai.generatecontentstreamresult.md#generatecontentstreamresult_interface) containing an iterable stream and a response promise. | +| [sendMessage(request)](./vertexai.chatsession.md#chatsessionsendmessage) | | Sends a chat message and receives a non-streaming [GenerateContentResult](./vertexai.generatecontentresult.md#generatecontentresult_interface) | +| [sendMessageStream(request)](./vertexai.chatsession.md#chatsessionsendmessagestream) | | Sends a chat message and receives the response as a [GenerateContentStreamResult](./vertexai.generatecontentstreamresult.md#generatecontentstreamresult_interface) containing an iterable stream and a response promise. | ## ChatSession.(constructor) @@ -98,7 +98,7 @@ Promise<[Content](./vertexai.content.md#content_interface)\[\]> ## ChatSession.sendMessage() -Sends a chat message and receives a non-streaming [GenerateContentResult](./vertexai.generatecontentresult.md#generatecontentresult_interface) +Sends a chat message and receives a non-streaming [GenerateContentResult](./vertexai.generatecontentresult.md#generatecontentresult_interface) Signature: @@ -118,7 +118,7 @@ Promise<[GenerateContentResult](./vertexai.generatecontentresult.md#generatec ## ChatSession.sendMessageStream() -Sends a chat message and receives the response as a [GenerateContentStreamResult](./vertexai.generatecontentstreamresult.md#generatecontentstreamresult_interface) containing an iterable stream and a response promise. +Sends a chat message and receives the response as a [GenerateContentStreamResult](./vertexai.generatecontentstreamresult.md#generatecontentstreamresult_interface) containing an iterable stream and a response promise. Signature: diff --git a/docs-devsite/vertexai.citationmetadata.md b/docs-devsite/vertexai.citationmetadata.md index e3d41a37d98..c317160e64f 100644 --- a/docs-devsite/vertexai.citationmetadata.md +++ b/docs-devsite/vertexai.citationmetadata.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # CitationMetadata interface -Citation metadata that may be found on a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface). +Citation metadata that may be found on a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface). Signature: diff --git a/docs-devsite/vertexai.counttokensrequest.md b/docs-devsite/vertexai.counttokensrequest.md index f5875564588..740ae5feed4 100644 --- a/docs-devsite/vertexai.counttokensrequest.md +++ b/docs-devsite/vertexai.counttokensrequest.md @@ -25,7 +25,7 @@ export interface CountTokensRequest | [contents](./vertexai.counttokensrequest.md#counttokensrequestcontents) | [Content](./vertexai.content.md#content_interface)\[\] | | | [generationConfig](./vertexai.counttokensrequest.md#counttokensrequestgenerationconfig) | [GenerationConfig](./vertexai.generationconfig.md#generationconfig_interface) | Configuration options that control how the model generates a response. | | [systemInstruction](./vertexai.counttokensrequest.md#counttokensrequestsysteminstruction) | string \| [Part](./vertexai.md#part) \| [Content](./vertexai.content.md#content_interface) | Instructions that direct the model to behave a certain way. | -| [tools](./vertexai.counttokensrequest.md#counttokensrequesttools) | [Tool](./vertexai.md#tool)\[\] | [Tool](./vertexai.md#tool) configuration. | +| [tools](./vertexai.counttokensrequest.md#counttokensrequesttools) | [Tool](./vertexai.md#tool)\[\] | [Tool](./vertexai.md#tool) configuration. | ## CountTokensRequest.contents @@ -57,7 +57,7 @@ systemInstruction?: string | Part | Content; ## CountTokensRequest.tools -[Tool](./vertexai.md#tool) configuration. +[Tool](./vertexai.md#tool) configuration. Signature: diff --git a/docs-devsite/vertexai.customerrordata.md b/docs-devsite/vertexai.customerrordata.md index 701b1b84c49..100f4a85fd9 100644 --- a/docs-devsite/vertexai.customerrordata.md +++ b/docs-devsite/vertexai.customerrordata.md @@ -23,7 +23,7 @@ export interface CustomErrorData | Property | Type | Description | | --- | --- | --- | | [errorDetails](./vertexai.customerrordata.md#customerrordataerrordetails) | [ErrorDetails](./vertexai.errordetails.md#errordetails_interface)\[\] | Optional additional details about the error. | -| [response](./vertexai.customerrordata.md#customerrordataresponse) | [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface) | Response from a [GenerateContentRequest](./vertexai.generatecontentrequest.md#generatecontentrequest_interface) | +| [response](./vertexai.customerrordata.md#customerrordataresponse) | [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface) | Response from a [GenerateContentRequest](./vertexai.generatecontentrequest.md#generatecontentrequest_interface) | | [status](./vertexai.customerrordata.md#customerrordatastatus) | number | HTTP status code of the error response. | | [statusText](./vertexai.customerrordata.md#customerrordatastatustext) | string | HTTP status text of the error response. | @@ -39,7 +39,7 @@ errorDetails?: ErrorDetails[]; ## CustomErrorData.response -Response from a [GenerateContentRequest](./vertexai.generatecontentrequest.md#generatecontentrequest_interface) +Response from a [GenerateContentRequest](./vertexai.generatecontentrequest.md#generatecontentrequest_interface) Signature: diff --git a/docs-devsite/vertexai.filedatapart.md b/docs-devsite/vertexai.filedatapart.md index 74512fa6d29..76162227526 100644 --- a/docs-devsite/vertexai.filedatapart.md +++ b/docs-devsite/vertexai.filedatapart.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # FileDataPart interface -Content part interface if the part represents [FileData](./vertexai.filedata.md#filedata_interface) +Content part interface if the part represents [FileData](./vertexai.filedata.md#filedata_interface) Signature: diff --git a/docs-devsite/vertexai.functioncall.md b/docs-devsite/vertexai.functioncall.md index ca7bc015438..299fb7130f4 100644 --- a/docs-devsite/vertexai.functioncall.md +++ b/docs-devsite/vertexai.functioncall.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # FunctionCall interface -A predicted [FunctionCall](./vertexai.functioncall.md#functioncall_interface) returned from the model that contains a string representing the [FunctionDeclaration.name](./vertexai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing the parameters and their values. +A predicted [FunctionCall](./vertexai.functioncall.md#functioncall_interface) returned from the model that contains a string representing the [FunctionDeclaration.name](./vertexai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing the parameters and their values. Signature: diff --git a/docs-devsite/vertexai.functioncallpart.md b/docs-devsite/vertexai.functioncallpart.md index af8ccf1109a..58fe0f5fa97 100644 --- a/docs-devsite/vertexai.functioncallpart.md +++ b/docs-devsite/vertexai.functioncallpart.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # FunctionCallPart interface -Content part interface if the part represents a [FunctionCall](./vertexai.functioncall.md#functioncall_interface). +Content part interface if the part represents a [FunctionCall](./vertexai.functioncall.md#functioncall_interface). Signature: diff --git a/docs-devsite/vertexai.functiondeclarationstool.md b/docs-devsite/vertexai.functiondeclarationstool.md index 5e728046639..2eff3138d8d 100644 --- a/docs-devsite/vertexai.functiondeclarationstool.md +++ b/docs-devsite/vertexai.functiondeclarationstool.md @@ -22,11 +22,11 @@ export declare interface FunctionDeclarationsTool | Property | Type | Description | | --- | --- | --- | -| [functionDeclarations](./vertexai.functiondeclarationstool.md#functiondeclarationstoolfunctiondeclarations) | [FunctionDeclaration](./vertexai.functiondeclaration.md#functiondeclaration_interface)\[\] | Optional. One or more function declarations to be passed to the model along with the current user query. Model may decide to call a subset of these functions by populating [FunctionCall](./vertexai.functioncall.md#functioncall_interface) in the response. User should provide a [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface) for each function call in the next turn. Based on the function responses, the model will generate the final response back to the user. Maximum 64 function declarations can be provided. | +| [functionDeclarations](./vertexai.functiondeclarationstool.md#functiondeclarationstoolfunctiondeclarations) | [FunctionDeclaration](./vertexai.functiondeclaration.md#functiondeclaration_interface)\[\] | Optional. One or more function declarations to be passed to the model along with the current user query. Model may decide to call a subset of these functions by populating [FunctionCall](./vertexai.functioncall.md#functioncall_interface) in the response. User should provide a [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface) for each function call in the next turn. Based on the function responses, the model will generate the final response back to the user. Maximum 64 function declarations can be provided. | ## FunctionDeclarationsTool.functionDeclarations -Optional. One or more function declarations to be passed to the model along with the current user query. Model may decide to call a subset of these functions by populating [FunctionCall](./vertexai.functioncall.md#functioncall_interface) in the response. User should provide a [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface) for each function call in the next turn. Based on the function responses, the model will generate the final response back to the user. Maximum 64 function declarations can be provided. +Optional. One or more function declarations to be passed to the model along with the current user query. Model may decide to call a subset of these functions by populating [FunctionCall](./vertexai.functioncall.md#functioncall_interface) in the response. User should provide a [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface) for each function call in the next turn. Based on the function responses, the model will generate the final response back to the user. Maximum 64 function declarations can be provided. Signature: diff --git a/docs-devsite/vertexai.functionresponse.md b/docs-devsite/vertexai.functionresponse.md index 0ca553e0e6b..072a08b3486 100644 --- a/docs-devsite/vertexai.functionresponse.md +++ b/docs-devsite/vertexai.functionresponse.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # FunctionResponse interface -The result output from a [FunctionCall](./vertexai.functioncall.md#functioncall_interface) that contains a string representing the [FunctionDeclaration.name](./vertexai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing any output from the function is used as context to the model. This should contain the result of a [FunctionCall](./vertexai.functioncall.md#functioncall_interface) made based on model prediction. +The result output from a [FunctionCall](./vertexai.functioncall.md#functioncall_interface) that contains a string representing the [FunctionDeclaration.name](./vertexai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing any output from the function is used as context to the model. This should contain the result of a [FunctionCall](./vertexai.functioncall.md#functioncall_interface) made based on model prediction. Signature: diff --git a/docs-devsite/vertexai.functionresponsepart.md b/docs-devsite/vertexai.functionresponsepart.md index 1905c98cdc7..ffbf2ad0517 100644 --- a/docs-devsite/vertexai.functionresponsepart.md +++ b/docs-devsite/vertexai.functionresponsepart.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # FunctionResponsePart interface -Content part interface if the part represents [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface). +Content part interface if the part represents [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface). Signature: diff --git a/docs-devsite/vertexai.generatecontentcandidate.md b/docs-devsite/vertexai.generatecontentcandidate.md index a30eef55485..e5fd9eacbbe 100644 --- a/docs-devsite/vertexai.generatecontentcandidate.md +++ b/docs-devsite/vertexai.generatecontentcandidate.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # GenerateContentCandidate interface -A candidate returned as part of a [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). +A candidate returned as part of a [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). Signature: diff --git a/docs-devsite/vertexai.generationconfig.md b/docs-devsite/vertexai.generationconfig.md index 3c3d0a14ffa..d3e9879f937 100644 --- a/docs-devsite/vertexai.generationconfig.md +++ b/docs-devsite/vertexai.generationconfig.md @@ -27,7 +27,7 @@ export interface GenerationConfig | [maxOutputTokens](./vertexai.generationconfig.md#generationconfigmaxoutputtokens) | number | | | [presencePenalty](./vertexai.generationconfig.md#generationconfigpresencepenalty) | number | | | [responseMimeType](./vertexai.generationconfig.md#generationconfigresponsemimetype) | string | Output response MIME type of the generated candidate text. Supported MIME types are text/plain (default, text output), application/json (JSON response in the candidates), and text/x.enum. | -| [responseSchema](./vertexai.generationconfig.md#generationconfigresponseschema) | [TypedSchema](./vertexai.md#typedschema) \| [SchemaRequest](./vertexai.schemarequest.md#schemarequest_interface) | Output response schema of the generated candidate text. This value can be a class generated with a [Schema](./vertexai.schema.md#schema_class) static method like Schema.string() or Schema.object() or it can be a plain JS object matching the [SchemaRequest](./vertexai.schemarequest.md#schemarequest_interface) interface.
Note: This only applies when the specified responseMIMEType supports a schema; currently this is limited to application/json and text/x.enum. | +| [responseSchema](./vertexai.generationconfig.md#generationconfigresponseschema) | [TypedSchema](./vertexai.md#typedschema) \| [SchemaRequest](./vertexai.schemarequest.md#schemarequest_interface) | Output response schema of the generated candidate text. This value can be a class generated with a [Schema](./vertexai.schema.md#schema_class) static method like Schema.string() or Schema.object() or it can be a plain JS object matching the [SchemaRequest](./vertexai.schemarequest.md#schemarequest_interface) interface.
Note: This only applies when the specified responseMIMEType supports a schema; currently this is limited to application/json and text/x.enum. | | [stopSequences](./vertexai.generationconfig.md#generationconfigstopsequences) | string\[\] | | | [temperature](./vertexai.generationconfig.md#generationconfigtemperature) | number | | | [topK](./vertexai.generationconfig.md#generationconfigtopk) | number | | @@ -77,7 +77,7 @@ responseMimeType?: string; ## GenerationConfig.responseSchema -Output response schema of the generated candidate text. This value can be a class generated with a [Schema](./vertexai.schema.md#schema_class) static method like `Schema.string()` or `Schema.object()` or it can be a plain JS object matching the [SchemaRequest](./vertexai.schemarequest.md#schemarequest_interface) interface.
Note: This only applies when the specified `responseMIMEType` supports a schema; currently this is limited to `application/json` and `text/x.enum`. +Output response schema of the generated candidate text. This value can be a class generated with a [Schema](./vertexai.schema.md#schema_class) static method like `Schema.string()` or `Schema.object()` or it can be a plain JS object matching the [SchemaRequest](./vertexai.schemarequest.md#schemarequest_interface) interface.
Note: This only applies when the specified `responseMIMEType` supports a schema; currently this is limited to `application/json` and `text/x.enum`. Signature: diff --git a/docs-devsite/vertexai.generativemodel.md b/docs-devsite/vertexai.generativemodel.md index b734e241e78..e4a238b0af5 100644 --- a/docs-devsite/vertexai.generativemodel.md +++ b/docs-devsite/vertexai.generativemodel.md @@ -41,9 +41,9 @@ export declare class GenerativeModel extends VertexAIModel | Method | Modifiers | Description | | --- | --- | --- | | [countTokens(request)](./vertexai.generativemodel.md#generativemodelcounttokens) | | Counts the tokens in the provided request. | -| [generateContent(request)](./vertexai.generativemodel.md#generativemodelgeneratecontent) | | Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | +| [generateContent(request)](./vertexai.generativemodel.md#generativemodelgeneratecontent) | | Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | | [generateContentStream(request)](./vertexai.generativemodel.md#generativemodelgeneratecontentstream) | | Makes a single streaming call to the model and returns an object containing an iterable stream that iterates over all chunks in the streaming response as well as a promise that returns the final aggregated response. | -| [startChat(startChatParams)](./vertexai.generativemodel.md#generativemodelstartchat) | | Gets a new [ChatSession](./vertexai.chatsession.md#chatsession_class) instance which can be used for multi-turn chats. | +| [startChat(startChatParams)](./vertexai.generativemodel.md#generativemodelstartchat) | | Gets a new [ChatSession](./vertexai.chatsession.md#chatsession_class) instance which can be used for multi-turn chats. | ## GenerativeModel.(constructor) @@ -133,7 +133,7 @@ Promise<[CountTokensResponse](./vertexai.counttokensresponse.md#counttokensre ## GenerativeModel.generateContent() -Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). +Makes a single non-streaming call to the model and returns an object containing a single [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). Signature: @@ -173,7 +173,7 @@ Promise<[GenerateContentStreamResult](./vertexai.generatecontentstreamresult. ## GenerativeModel.startChat() -Gets a new [ChatSession](./vertexai.chatsession.md#chatsession_class) instance which can be used for multi-turn chats. +Gets a new [ChatSession](./vertexai.chatsession.md#chatsession_class) instance which can be used for multi-turn chats. Signature: diff --git a/docs-devsite/vertexai.imagengcsimage.md b/docs-devsite/vertexai.imagengcsimage.md index b094e63c1d8..23770192b3b 100644 --- a/docs-devsite/vertexai.imagengcsimage.md +++ b/docs-devsite/vertexai.imagengcsimage.md @@ -25,7 +25,7 @@ export interface ImagenGCSImage | Property | Type | Description | | --- | --- | --- | | [gcsURI](./vertexai.imagengcsimage.md#imagengcsimagegcsuri) | string | The URI of the file stored in a Cloud Storage for Firebase bucket. | -| [mimeType](./vertexai.imagengcsimage.md#imagengcsimagemimetype) | string | The MIME type of the image; either "image/png" or "image/jpeg".To request a different format, set the imageFormat property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). | +| [mimeType](./vertexai.imagengcsimage.md#imagengcsimagemimetype) | string | The MIME type of the image; either "image/png" or "image/jpeg".To request a different format, set the imageFormat property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). | ## ImagenGCSImage.gcsURI @@ -45,7 +45,7 @@ gcsURI: string; The MIME type of the image; either `"image/png"` or `"image/jpeg"`. -To request a different format, set the `imageFormat` property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). +To request a different format, set the `imageFormat` property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). Signature: diff --git a/docs-devsite/vertexai.imagengenerationconfig.md b/docs-devsite/vertexai.imagengenerationconfig.md index cee7734f789..b6785b9b2bb 100644 --- a/docs-devsite/vertexai.imagengenerationconfig.md +++ b/docs-devsite/vertexai.imagengenerationconfig.md @@ -27,11 +27,11 @@ export interface ImagenGenerationConfig | Property | Type | Description | | --- | --- | --- | -| [addWatermark](./vertexai.imagengenerationconfig.md#imagengenerationconfigaddwatermark) | boolean | (Public Preview) Whether to add an invisible watermark to generated images.If set to true, an invisible SynthID watermark is embedded in generated images to indicate that they are AI generated. If set to false, watermarking will be disabled.For Imagen 3 models, the default value is true; see the addWatermark documentation for more details. | -| [aspectRatio](./vertexai.imagengenerationconfig.md#imagengenerationconfigaspectratio) | [ImagenAspectRatio](./vertexai.md#imagenaspectratio) | (Public Preview) The aspect ratio of the generated images. The default value is square 1:1. Supported aspect ratios depend on the Imagen model, see [ImagenAspectRatio](./vertexai.md#imagenaspectratio) for more details. | -| [imageFormat](./vertexai.imagengenerationconfig.md#imagengenerationconfigimageformat) | [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) | (Public Preview) The image format of the generated images. The default is PNG.See [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for more details. | +| [addWatermark](./vertexai.imagengenerationconfig.md#imagengenerationconfigaddwatermark) | boolean | (Public Preview) Whether to add an invisible watermark to generated images.If set to true, an invisible SynthID watermark is embedded in generated images to indicate that they are AI generated. If set to false, watermarking will be disabled.For Imagen 3 models, the default value is true; see the addWatermark documentation for more details. | +| [aspectRatio](./vertexai.imagengenerationconfig.md#imagengenerationconfigaspectratio) | [ImagenAspectRatio](./vertexai.md#imagenaspectratio) | (Public Preview) The aspect ratio of the generated images. The default value is square 1:1. Supported aspect ratios depend on the Imagen model, see [ImagenAspectRatio](./vertexai.md#imagenaspectratio) for more details. | +| [imageFormat](./vertexai.imagengenerationconfig.md#imagengenerationconfigimageformat) | [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) | (Public Preview) The image format of the generated images. The default is PNG.See [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for more details. | | [negativePrompt](./vertexai.imagengenerationconfig.md#imagengenerationconfignegativeprompt) | string | (Public Preview) A description of what should be omitted from the generated images.Support for negative prompts depends on the Imagen model.See the [documentation](http://firebase.google.com/docs/vertex-ai/model-parameters#imagen) for more details. | -| [numberOfImages](./vertexai.imagengenerationconfig.md#imagengenerationconfignumberofimages) | number | (Public Preview) The number of images to generate. The default value is 1.The number of sample images that may be generated in each request depends on the model (typically up to 4); see the sampleCount documentation for more details. | +| [numberOfImages](./vertexai.imagengenerationconfig.md#imagengenerationconfignumberofimages) | number | (Public Preview) The number of images to generate. The default value is 1.The number of sample images that may be generated in each request depends on the model (typically up to 4); see the sampleCount documentation for more details. | ## ImagenGenerationConfig.addWatermark @@ -42,7 +42,7 @@ Whether to add an invisible watermark to generated images. If set to `true`, an invisible SynthID watermark is embedded in generated images to indicate that they are AI generated. If set to `false`, watermarking will be disabled. -For Imagen 3 models, the default value is `true`; see the addWatermark documentation for more details. +For Imagen 3 models, the default value is `true`; see the addWatermark documentation for more details. Signature: @@ -55,7 +55,7 @@ addWatermark?: boolean; > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -The aspect ratio of the generated images. The default value is square 1:1. Supported aspect ratios depend on the Imagen model, see [ImagenAspectRatio](./vertexai.md#imagenaspectratio) for more details. +The aspect ratio of the generated images. The default value is square 1:1. Supported aspect ratios depend on the Imagen model, see [ImagenAspectRatio](./vertexai.md#imagenaspectratio) for more details. Signature: @@ -70,7 +70,7 @@ aspectRatio?: ImagenAspectRatio; The image format of the generated images. The default is PNG. -See [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for more details. +See [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for more details. Signature: @@ -102,7 +102,7 @@ negativePrompt?: string; The number of images to generate. The default value is 1. -The number of sample images that may be generated in each request depends on the model (typically up to 4); see the sampleCount documentation for more details. +The number of sample images that may be generated in each request depends on the model (typically up to 4); see the sampleCount documentation for more details. Signature: diff --git a/docs-devsite/vertexai.imagengenerationresponse.md b/docs-devsite/vertexai.imagengenerationresponse.md index 32ed69718f9..d8de93df3ec 100644 --- a/docs-devsite/vertexai.imagengenerationresponse.md +++ b/docs-devsite/vertexai.imagengenerationresponse.md @@ -25,7 +25,7 @@ export interface ImagenGenerationResponse(Public Preview)
The reason that images were filtered out. This property will only be defined if one or more images were filtered.Images may be filtered out due to the [ImagenSafetyFilterLevel](./vertexai.md#imagensafetyfilterlevel), [ImagenPersonFilterLevel](./vertexai.md#imagenpersonfilterlevel), or filtering included in the model. The filter levels may be adjusted in your [ImagenSafetySettings](./vertexai.imagensafetysettings.md#imagensafetysettings_interface).See the [Responsible AI and usage guidelines for Imagen](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen) for more details. | +| [filteredReason](./vertexai.imagengenerationresponse.md#imagengenerationresponsefilteredreason) | string | (Public Preview) The reason that images were filtered out. This property will only be defined if one or more images were filtered.Images may be filtered out due to the [ImagenSafetyFilterLevel](./vertexai.md#imagensafetyfilterlevel), [ImagenPersonFilterLevel](./vertexai.md#imagenpersonfilterlevel), or filtering included in the model. The filter levels may be adjusted in your [ImagenSafetySettings](./vertexai.imagensafetysettings.md#imagensafetysettings_interface).See the [Responsible AI and usage guidelines for Imagen](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen) for more details. | | [images](./vertexai.imagengenerationresponse.md#imagengenerationresponseimages) | T\[\] | (Public Preview) The images generated by Imagen.The number of images generated may be fewer than the number requested if one or more were filtered out; see filteredReason. | ## ImagenGenerationResponse.filteredReason @@ -35,7 +35,7 @@ export interface ImagenGenerationResponse[ImagenSafetyFilterLevel](./vertexai.md#imagensafetyfilterlevel), [ImagenPersonFilterLevel](./vertexai.md#imagenpersonfilterlevel), or filtering included in the model. The filter levels may be adjusted in your [ImagenSafetySettings](./vertexai.imagensafetysettings.md#imagensafetysettings_interface). +Images may be filtered out due to the [ImagenSafetyFilterLevel](./vertexai.md#imagensafetyfilterlevel), [ImagenPersonFilterLevel](./vertexai.md#imagenpersonfilterlevel), or filtering included in the model. The filter levels may be adjusted in your [ImagenSafetySettings](./vertexai.imagensafetysettings.md#imagensafetysettings_interface). See the [Responsible AI and usage guidelines for Imagen](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen) for more details. diff --git a/docs-devsite/vertexai.imagenimageformat.md b/docs-devsite/vertexai.imagenimageformat.md index 785c7c726fc..68db8bbdae0 100644 --- a/docs-devsite/vertexai.imagenimageformat.md +++ b/docs-devsite/vertexai.imagenimageformat.md @@ -15,7 +15,7 @@ https://github.com/firebase/firebase-js-sdk Defines the image format for images generated by Imagen. -Use this class to specify the desired format (JPEG or PNG) and compression quality for images generated by Imagen. This is typically included as part of [ImagenModelParams](./vertexai.imagenmodelparams.md#imagenmodelparams_interface). +Use this class to specify the desired format (JPEG or PNG) and compression quality for images generated by Imagen. This is typically included as part of [ImagenModelParams](./vertexai.imagenmodelparams.md#imagenmodelparams_interface). Signature: @@ -34,8 +34,8 @@ export declare class ImagenImageFormat | Method | Modifiers | Description | | --- | --- | --- | -| [jpeg(compressionQuality)](./vertexai.imagenimageformat.md#imagenimageformatjpeg) | static | (Public Preview) Creates an [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for a JPEG image. | -| [png()](./vertexai.imagenimageformat.md#imagenimageformatpng) | static | (Public Preview) Creates an [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for a PNG image. | +| [jpeg(compressionQuality)](./vertexai.imagenimageformat.md#imagenimageformatjpeg) | static | (Public Preview) Creates an [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for a JPEG image. | +| [png()](./vertexai.imagenimageformat.md#imagenimageformatpng) | static | (Public Preview) Creates an [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for a PNG image. | ## ImagenImageFormat.compressionQuality @@ -68,7 +68,7 @@ mimeType: string; > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -Creates an [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for a JPEG image. +Creates an [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for a JPEG image. Signature: @@ -86,14 +86,14 @@ static jpeg(compressionQuality?: number): ImagenImageFormat; [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) -An [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) object for a JPEG image. +An [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) object for a JPEG image. ## ImagenImageFormat.png() > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -Creates an [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for a PNG image. +Creates an [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) for a PNG image. Signature: @@ -104,7 +104,7 @@ static png(): ImagenImageFormat; [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) -An [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) object for a PNG image. +An [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) object for a PNG image. ### Example diff --git a/docs-devsite/vertexai.imageninlineimage.md b/docs-devsite/vertexai.imageninlineimage.md index 19fe8a67764..a72937b5e5d 100644 --- a/docs-devsite/vertexai.imageninlineimage.md +++ b/docs-devsite/vertexai.imageninlineimage.md @@ -26,7 +26,7 @@ export interface ImagenInlineImage | Property | Type | Description | | --- | --- | --- | | [bytesBase64Encoded](./vertexai.imageninlineimage.md#imageninlineimagebytesbase64encoded) | string | (Public Preview) The base64-encoded image data. | -| [mimeType](./vertexai.imageninlineimage.md#imageninlineimagemimetype) | string | (Public Preview) The MIME type of the image; either "image/png" or "image/jpeg".To request a different format, set the imageFormat property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). | +| [mimeType](./vertexai.imageninlineimage.md#imageninlineimagemimetype) | string | (Public Preview) The MIME type of the image; either "image/png" or "image/jpeg".To request a different format, set the imageFormat property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). | ## ImagenInlineImage.bytesBase64Encoded @@ -48,7 +48,7 @@ bytesBase64Encoded: string; The MIME type of the image; either `"image/png"` or `"image/jpeg"`. -To request a different format, set the `imageFormat` property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). +To request a different format, set the `imageFormat` property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). Signature: diff --git a/docs-devsite/vertexai.imagenmodel.md b/docs-devsite/vertexai.imagenmodel.md index 63e15ff133a..ed40dc8f578 100644 --- a/docs-devsite/vertexai.imagenmodel.md +++ b/docs-devsite/vertexai.imagenmodel.md @@ -28,7 +28,7 @@ export declare class ImagenModel extends VertexAIModel | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(vertexAI, modelParams, requestOptions)](./vertexai.imagenmodel.md#imagenmodelconstructor) | | (Public Preview) Constructs a new instance of the [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class. | +| [(constructor)(vertexAI, modelParams, requestOptions)](./vertexai.imagenmodel.md#imagenmodelconstructor) | | (Public Preview) Constructs a new instance of the [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class. | ## Properties @@ -49,7 +49,7 @@ export declare class ImagenModel extends VertexAIModel > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -Constructs a new instance of the [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class. +Constructs a new instance of the [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class. Signature: @@ -131,7 +131,7 @@ generateImages(prompt: string): Promise<[ImagenInlineImage](./vertexai.imageninlineimage.md#imageninlineimage_interface)>> -A promise that resolves to an [ImagenGenerationResponse](./vertexai.imagengenerationresponse.md#imagengenerationresponse_interface) object containing the generated images. +A promise that resolves to an [ImagenGenerationResponse](./vertexai.imagengenerationresponse.md#imagengenerationresponse_interface) object containing the generated images. #### Exceptions diff --git a/docs-devsite/vertexai.imagenmodelparams.md b/docs-devsite/vertexai.imagenmodelparams.md index 66c4bb0bfe6..5396a36e4d1 100644 --- a/docs-devsite/vertexai.imagenmodelparams.md +++ b/docs-devsite/vertexai.imagenmodelparams.md @@ -13,7 +13,7 @@ https://github.com/firebase/firebase-js-sdk > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -Parameters for configuring an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class). +Parameters for configuring an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class). Signature: diff --git a/docs-devsite/vertexai.md b/docs-devsite/vertexai.md index fca51b42f4f..f67254eef20 100644 --- a/docs-devsite/vertexai.md +++ b/docs-devsite/vertexai.md @@ -17,10 +17,10 @@ The Vertex AI in Firebase Web SDK. | Function | Description | | --- | --- | | function(app, ...) | -| [getVertexAI(app, options)](./vertexai.md#getvertexai_04094cf) | Returns a [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance for the given app. | +| [getVertexAI(app, options)](./vertexai.md#getvertexai_04094cf) | Returns a [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance for the given app. | | function(vertexAI, ...) | -| [getGenerativeModel(vertexAI, modelParams, requestOptions)](./vertexai.md#getgenerativemodel_e3037c9) | Returns a [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. | -| [getImagenModel(vertexAI, modelParams, requestOptions)](./vertexai.md#getimagenmodel_812c375) | (Public Preview) Returns an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen.Only Imagen 3 models (named imagen-3.0-*) are supported. | +| [getGenerativeModel(vertexAI, modelParams, requestOptions)](./vertexai.md#getgenerativemodel_e3037c9) | Returns a [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. | +| [getImagenModel(vertexAI, modelParams, requestOptions)](./vertexai.md#getimagenmodel_812c375) | (Public Preview) Returns an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen.Only Imagen 3 models (named imagen-3.0-*) are supported. | ## Classes @@ -30,7 +30,7 @@ The Vertex AI in Firebase Web SDK. | [BooleanSchema](./vertexai.booleanschema.md#booleanschema_class) | Schema class for "boolean" types. | | [ChatSession](./vertexai.chatsession.md#chatsession_class) | ChatSession class that enables sending chat messages and stores history of sent and received messages so far. | | [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) | Class for generative model APIs. | -| [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) | (Public Preview) Defines the image format for images generated by Imagen.Use this class to specify the desired format (JPEG or PNG) and compression quality for images generated by Imagen. This is typically included as part of [ImagenModelParams](./vertexai.imagenmodelparams.md#imagenmodelparams_interface). | +| [ImagenImageFormat](./vertexai.imagenimageformat.md#imagenimageformat_class) | (Public Preview) Defines the image format for images generated by Imagen.Use this class to specify the desired format (JPEG or PNG) and compression quality for images generated by Imagen. This is typically included as part of [ImagenModelParams](./vertexai.imagenmodelparams.md#imagenmodelparams_interface). | | [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) | (Public Preview) Class for Imagen model APIs.This class provides methods for generating images using the Imagen model. | | [IntegerSchema](./vertexai.integerschema.md#integerschema_class) | Schema class for "integer" types. | | [NumberSchema](./vertexai.numberschema.md#numberschema_class) | Schema class for "number" types. | @@ -52,12 +52,12 @@ The Vertex AI in Firebase Web SDK. | [HarmCategory](./vertexai.md#harmcategory) | Harm categories that would cause prompts or candidates to be blocked. | | [HarmProbability](./vertexai.md#harmprobability) | Probability that a prompt or candidate matches a harm category. | | [HarmSeverity](./vertexai.md#harmseverity) | Harm severity levels. | -| [ImagenAspectRatio](./vertexai.md#imagenaspectratio) | (Public Preview) Aspect ratios for Imagen images.To specify an aspect ratio for generated images, set the aspectRatio property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface).See the the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. | -| [ImagenPersonFilterLevel](./vertexai.md#imagenpersonfilterlevel) | (Public Preview) A filter level controlling whether generation of images containing people or faces is allowed.See the personGeneration documentation for more details. | +| [ImagenAspectRatio](./vertexai.md#imagenaspectratio) | (Public Preview) Aspect ratios for Imagen images.To specify an aspect ratio for generated images, set the aspectRatio property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface).See the the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. | +| [ImagenPersonFilterLevel](./vertexai.md#imagenpersonfilterlevel) | (Public Preview) A filter level controlling whether generation of images containing people or faces is allowed.See the personGeneration documentation for more details. | | [ImagenSafetyFilterLevel](./vertexai.md#imagensafetyfilterlevel) | (Public Preview) A filter level controlling how aggressively to filter sensitive content.Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI are assessed against a list of safety filters, which include 'harmful categories' (for example, violence, sexual, derogatory, and toxic). This filter level controls how aggressively to filter out potentially harmful content from responses. See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) and the [Responsible AI and usage guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) for more details. | | [Modality](./vertexai.md#modality) | Content part modality. | | [SchemaType](./vertexai.md#schematype) | Contains the list of OpenAPI data types as defined by the [OpenAPI specification](https://swagger.io/docs/specification/data-models/data-types/) | -| [VertexAIErrorCode](./vertexai.md#vertexaierrorcode) | Standardized error codes that [VertexAIError](./vertexai.vertexaierror.md#vertexaierror_class) can have. | +| [VertexAIErrorCode](./vertexai.md#vertexaierrorcode) | Standardized error codes that [VertexAIError](./vertexai.vertexaierror.md#vertexaierror_class) can have. | ## Interfaces @@ -65,7 +65,7 @@ The Vertex AI in Firebase Web SDK. | --- | --- | | [BaseParams](./vertexai.baseparams.md#baseparams_interface) | Base parameters for a number of methods. | | [Citation](./vertexai.citation.md#citation_interface) | A single citation. | -| [CitationMetadata](./vertexai.citationmetadata.md#citationmetadata_interface) | Citation metadata that may be found on a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface). | +| [CitationMetadata](./vertexai.citationmetadata.md#citationmetadata_interface) | Citation metadata that may be found on a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface). | | [Content](./vertexai.content.md#content_interface) | Content type for both prompts and response candidates. | | [CountTokensRequest](./vertexai.counttokensrequest.md#counttokensrequest_interface) | Params for calling [GenerativeModel.countTokens()](./vertexai.generativemodel.md#generativemodelcounttokens) | | [CountTokensResponse](./vertexai.counttokensresponse.md#counttokensresponse_interface) | Response from calling [GenerativeModel.countTokens()](./vertexai.generativemodel.md#generativemodelcounttokens). | @@ -74,15 +74,15 @@ The Vertex AI in Firebase Web SDK. | [EnhancedGenerateContentResponse](./vertexai.enhancedgeneratecontentresponse.md#enhancedgeneratecontentresponse_interface) | Response object wrapped with helper methods. | | [ErrorDetails](./vertexai.errordetails.md#errordetails_interface) | Details object that may be included in an error response. | | [FileData](./vertexai.filedata.md#filedata_interface) | Data pointing to a file uploaded on Google Cloud Storage. | -| [FileDataPart](./vertexai.filedatapart.md#filedatapart_interface) | Content part interface if the part represents [FileData](./vertexai.filedata.md#filedata_interface) | -| [FunctionCall](./vertexai.functioncall.md#functioncall_interface) | A predicted [FunctionCall](./vertexai.functioncall.md#functioncall_interface) returned from the model that contains a string representing the [FunctionDeclaration.name](./vertexai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing the parameters and their values. | +| [FileDataPart](./vertexai.filedatapart.md#filedatapart_interface) | Content part interface if the part represents [FileData](./vertexai.filedata.md#filedata_interface) | +| [FunctionCall](./vertexai.functioncall.md#functioncall_interface) | A predicted [FunctionCall](./vertexai.functioncall.md#functioncall_interface) returned from the model that contains a string representing the [FunctionDeclaration.name](./vertexai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing the parameters and their values. | | [FunctionCallingConfig](./vertexai.functioncallingconfig.md#functioncallingconfig_interface) | | -| [FunctionCallPart](./vertexai.functioncallpart.md#functioncallpart_interface) | Content part interface if the part represents a [FunctionCall](./vertexai.functioncall.md#functioncall_interface). | +| [FunctionCallPart](./vertexai.functioncallpart.md#functioncallpart_interface) | Content part interface if the part represents a [FunctionCall](./vertexai.functioncall.md#functioncall_interface). | | [FunctionDeclaration](./vertexai.functiondeclaration.md#functiondeclaration_interface) | Structured representation of a function declaration as defined by the [OpenAPI 3.0 specification](https://spec.openapis.org/oas/v3.0.3). Included in this declaration are the function name and parameters. This FunctionDeclaration is a representation of a block of code that can be used as a Tool by the model and executed by the client. | | [FunctionDeclarationsTool](./vertexai.functiondeclarationstool.md#functiondeclarationstool_interface) | A FunctionDeclarationsTool is a piece of code that enables the system to interact with external systems to perform an action, or set of actions, outside of knowledge and scope of the model. | -| [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface) | The result output from a [FunctionCall](./vertexai.functioncall.md#functioncall_interface) that contains a string representing the [FunctionDeclaration.name](./vertexai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing any output from the function is used as context to the model. This should contain the result of a [FunctionCall](./vertexai.functioncall.md#functioncall_interface) made based on model prediction. | -| [FunctionResponsePart](./vertexai.functionresponsepart.md#functionresponsepart_interface) | Content part interface if the part represents [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface). | -| [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface) | A candidate returned as part of a [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | +| [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface) | The result output from a [FunctionCall](./vertexai.functioncall.md#functioncall_interface) that contains a string representing the [FunctionDeclaration.name](./vertexai.functiondeclaration.md#functiondeclarationname) and a structured JSON object containing any output from the function is used as context to the model. This should contain the result of a [FunctionCall](./vertexai.functioncall.md#functioncall_interface) made based on model prediction. | +| [FunctionResponsePart](./vertexai.functionresponsepart.md#functionresponsepart_interface) | Content part interface if the part represents [FunctionResponse](./vertexai.functionresponse.md#functionresponse_interface). | +| [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface) | A candidate returned as part of a [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | | [GenerateContentRequest](./vertexai.generatecontentrequest.md#generatecontentrequest_interface) | Request sent through [GenerativeModel.generateContent()](./vertexai.generativemodel.md#generativemodelgeneratecontent) | | [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface) | Individual response from [GenerativeModel.generateContent()](./vertexai.generativemodel.md#generativemodelgeneratecontent) and [GenerativeModel.generateContentStream()](./vertexai.generativemodel.md#generativemodelgeneratecontentstream). generateContentStream() will return one in each chunk until the stream is done. | | [GenerateContentResult](./vertexai.generatecontentresult.md#generatecontentresult_interface) | Result object returned from [GenerativeModel.generateContent()](./vertexai.generativemodel.md#generativemodelgeneratecontent) call. | @@ -95,26 +95,26 @@ The Vertex AI in Firebase Web SDK. | [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface) | (Public Preview) Configuration options for generating images with Imagen.See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images-imagen) for more details. | | [ImagenGenerationResponse](./vertexai.imagengenerationresponse.md#imagengenerationresponse_interface) | (Public Preview) The response from a request to generate images with Imagen. | | [ImagenInlineImage](./vertexai.imageninlineimage.md#imageninlineimage_interface) | (Public Preview) An image generated by Imagen, represented as inline data. | -| [ImagenModelParams](./vertexai.imagenmodelparams.md#imagenmodelparams_interface) | (Public Preview) Parameters for configuring an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class). | +| [ImagenModelParams](./vertexai.imagenmodelparams.md#imagenmodelparams_interface) | (Public Preview) Parameters for configuring an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class). | | [ImagenSafetySettings](./vertexai.imagensafetysettings.md#imagensafetysettings_interface) | (Public Preview) Settings for controlling the aggressiveness of filtering out sensitive content.See the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details. | | [InlineDataPart](./vertexai.inlinedatapart.md#inlinedatapart_interface) | Content part interface if the part represents an image. | | [ModalityTokenCount](./vertexai.modalitytokencount.md#modalitytokencount_interface) | Represents token counting info for a single modality. | -| [ModelParams](./vertexai.modelparams.md#modelparams_interface) | Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9). | -| [ObjectSchemaInterface](./vertexai.objectschemainterface.md#objectschemainterface_interface) | Interface for [ObjectSchema](./vertexai.objectschema.md#objectschema_class) class. | +| [ModelParams](./vertexai.modelparams.md#modelparams_interface) | Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9). | +| [ObjectSchemaInterface](./vertexai.objectschemainterface.md#objectschemainterface_interface) | Interface for [ObjectSchema](./vertexai.objectschema.md#objectschema_class) class. | | [PromptFeedback](./vertexai.promptfeedback.md#promptfeedback_interface) | If the prompt was blocked, this will be populated with blockReason and the relevant safetyRatings. | -| [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9). | +| [RequestOptions](./vertexai.requestoptions.md#requestoptions_interface) | Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9). | | [RetrievedContextAttribution](./vertexai.retrievedcontextattribution.md#retrievedcontextattribution_interface) | | -| [SafetyRating](./vertexai.safetyrating.md#safetyrating_interface) | A safety rating associated with a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface) | +| [SafetyRating](./vertexai.safetyrating.md#safetyrating_interface) | A safety rating associated with a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface) | | [SafetySetting](./vertexai.safetysetting.md#safetysetting_interface) | Safety setting that can be sent as part of request parameters. | -| [SchemaInterface](./vertexai.schemainterface.md#schemainterface_interface) | Interface for [Schema](./vertexai.schema.md#schema_class) class. | -| [SchemaParams](./vertexai.schemaparams.md#schemaparams_interface) | Params passed to [Schema](./vertexai.schema.md#schema_class) static methods to create specific [Schema](./vertexai.schema.md#schema_class) classes. | -| [SchemaRequest](./vertexai.schemarequest.md#schemarequest_interface) | Final format for [Schema](./vertexai.schema.md#schema_class) params passed to backend requests. | -| [SchemaShared](./vertexai.schemashared.md#schemashared_interface) | Basic [Schema](./vertexai.schema.md#schema_class) properties shared across several Schema-related types. | +| [SchemaInterface](./vertexai.schemainterface.md#schemainterface_interface) | Interface for [Schema](./vertexai.schema.md#schema_class) class. | +| [SchemaParams](./vertexai.schemaparams.md#schemaparams_interface) | Params passed to [Schema](./vertexai.schema.md#schema_class) static methods to create specific [Schema](./vertexai.schema.md#schema_class) classes. | +| [SchemaRequest](./vertexai.schemarequest.md#schemarequest_interface) | Final format for [Schema](./vertexai.schema.md#schema_class) params passed to backend requests. | +| [SchemaShared](./vertexai.schemashared.md#schemashared_interface) | Basic [Schema](./vertexai.schema.md#schema_class) properties shared across several Schema-related types. | | [Segment](./vertexai.segment.md#segment_interface) | | | [StartChatParams](./vertexai.startchatparams.md#startchatparams_interface) | Params for [GenerativeModel.startChat()](./vertexai.generativemodel.md#generativemodelstartchat). | | [TextPart](./vertexai.textpart.md#textpart_interface) | Content part interface if the part represents a text string. | | [ToolConfig](./vertexai.toolconfig.md#toolconfig_interface) | Tool config. This config is shared for all tools provided in the request. | -| [UsageMetadata](./vertexai.usagemetadata.md#usagemetadata_interface) | Usage metadata about a [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | +| [UsageMetadata](./vertexai.usagemetadata.md#usagemetadata_interface) | Usage metadata about a [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). | | [VertexAI](./vertexai.vertexai.md#vertexai_interface) | An instance of the Vertex AI in Firebase SDK. | | [VertexAIOptions](./vertexai.vertexaioptions.md#vertexaioptions_interface) | Options when initializing the Vertex AI in Firebase SDK. | | [VideoMetadata](./vertexai.videometadata.md#videometadata_interface) | Describes the input video content. | @@ -139,7 +139,7 @@ The Vertex AI in Firebase Web SDK. ### getVertexAI(app, options) {:#getvertexai_04094cf} -Returns a [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance for the given app. +Returns a [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance for the given app. Signature: @@ -162,7 +162,7 @@ export declare function getVertexAI(app?: FirebaseApp, options?: VertexAIOptions ### getGenerativeModel(vertexAI, modelParams, requestOptions) {:#getgenerativemodel_e3037c9} -Returns a [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. +Returns a [GenerativeModel](./vertexai.generativemodel.md#generativemodel_class) class with methods for inference and other functionality. Signature: @@ -187,7 +187,7 @@ export declare function getGenerativeModel(vertexAI: VertexAI, modelParams: Mode > This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment. > -Returns an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen. +Returns an [ImagenModel](./vertexai.imagenmodel.md#imagenmodel_class) class with methods for using Imagen. Only Imagen 3 models (named `imagen-3.0-*`) are supported. @@ -422,7 +422,7 @@ export declare enum HarmSeverity Aspect ratios for Imagen images. -To specify an aspect ratio for generated images, set the `aspectRatio` property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). +To specify an aspect ratio for generated images, set the `aspectRatio` property in your [ImagenGenerationConfig](./vertexai.imagengenerationconfig.md#imagengenerationconfig_interface). See the the [documentation](http://firebase.google.com/docs/vertex-ai/generate-images) for more details and examples of the supported aspect ratios. @@ -449,7 +449,7 @@ export declare enum ImagenAspectRatio A filter level controlling whether generation of images containing people or faces is allowed. -See the personGeneration documentation for more details. +See the personGeneration documentation for more details. Signature: @@ -533,7 +533,7 @@ export declare enum SchemaType ## VertexAIErrorCode -Standardized error codes that [VertexAIError](./vertexai.vertexaierror.md#vertexaierror_class) can have. +Standardized error codes that [VertexAIError](./vertexai.vertexaierror.md#vertexaierror_class) can have. Signature: diff --git a/docs-devsite/vertexai.modelparams.md b/docs-devsite/vertexai.modelparams.md index 590bc14e435..d3963d240eb 100644 --- a/docs-devsite/vertexai.modelparams.md +++ b/docs-devsite/vertexai.modelparams.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # ModelParams interface -Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9). +Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9). Signature: diff --git a/docs-devsite/vertexai.objectschemainterface.md b/docs-devsite/vertexai.objectschemainterface.md index 6a4e052d183..4eb7a5d80e7 100644 --- a/docs-devsite/vertexai.objectschemainterface.md +++ b/docs-devsite/vertexai.objectschemainterface.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # ObjectSchemaInterface interface -Interface for [ObjectSchema](./vertexai.objectschema.md#objectschema_class) class. +Interface for [ObjectSchema](./vertexai.objectschema.md#objectschema_class) class. Signature: diff --git a/docs-devsite/vertexai.requestoptions.md b/docs-devsite/vertexai.requestoptions.md index 6d074775520..dcd0c552ecb 100644 --- a/docs-devsite/vertexai.requestoptions.md +++ b/docs-devsite/vertexai.requestoptions.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # RequestOptions interface -Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9). +Params passed to [getGenerativeModel()](./vertexai.md#getgenerativemodel_e3037c9). Signature: diff --git a/docs-devsite/vertexai.safetyrating.md b/docs-devsite/vertexai.safetyrating.md index b5f204bef2c..28493bafef0 100644 --- a/docs-devsite/vertexai.safetyrating.md +++ b/docs-devsite/vertexai.safetyrating.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # SafetyRating interface -A safety rating associated with a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface) +A safety rating associated with a [GenerateContentCandidate](./vertexai.generatecontentcandidate.md#generatecontentcandidate_interface) Signature: diff --git a/docs-devsite/vertexai.schemainterface.md b/docs-devsite/vertexai.schemainterface.md index 3992c9e5116..c14b561193b 100644 --- a/docs-devsite/vertexai.schemainterface.md +++ b/docs-devsite/vertexai.schemainterface.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # SchemaInterface interface -Interface for [Schema](./vertexai.schema.md#schema_class) class. +Interface for [Schema](./vertexai.schema.md#schema_class) class. Signature: diff --git a/docs-devsite/vertexai.schemaparams.md b/docs-devsite/vertexai.schemaparams.md index 3c6d9f385fd..8e4a41f6bdc 100644 --- a/docs-devsite/vertexai.schemaparams.md +++ b/docs-devsite/vertexai.schemaparams.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # SchemaParams interface -Params passed to [Schema](./vertexai.schema.md#schema_class) static methods to create specific [Schema](./vertexai.schema.md#schema_class) classes. +Params passed to [Schema](./vertexai.schema.md#schema_class) static methods to create specific [Schema](./vertexai.schema.md#schema_class) classes. Signature: diff --git a/docs-devsite/vertexai.schemarequest.md b/docs-devsite/vertexai.schemarequest.md index f12259b1608..c382c2a6297 100644 --- a/docs-devsite/vertexai.schemarequest.md +++ b/docs-devsite/vertexai.schemarequest.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # SchemaRequest interface -Final format for [Schema](./vertexai.schema.md#schema_class) params passed to backend requests. +Final format for [Schema](./vertexai.schema.md#schema_class) params passed to backend requests. Signature: diff --git a/docs-devsite/vertexai.schemashared.md b/docs-devsite/vertexai.schemashared.md index 50cc6464ecf..0764a53bdc0 100644 --- a/docs-devsite/vertexai.schemashared.md +++ b/docs-devsite/vertexai.schemashared.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # SchemaShared interface -Basic [Schema](./vertexai.schema.md#schema_class) properties shared across several Schema-related types. +Basic [Schema](./vertexai.schema.md#schema_class) properties shared across several Schema-related types. Signature: diff --git a/docs-devsite/vertexai.usagemetadata.md b/docs-devsite/vertexai.usagemetadata.md index 5f886dd29f2..176878235d5 100644 --- a/docs-devsite/vertexai.usagemetadata.md +++ b/docs-devsite/vertexai.usagemetadata.md @@ -10,7 +10,7 @@ https://github.com/firebase/firebase-js-sdk {% endcomment %} # UsageMetadata interface -Usage metadata about a [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). +Usage metadata about a [GenerateContentResponse](./vertexai.generatecontentresponse.md#generatecontentresponse_interface). Signature: diff --git a/docs-devsite/vertexai.vertexai.md b/docs-devsite/vertexai.vertexai.md index 4797bf8bada..d30d0f7113e 100644 --- a/docs-devsite/vertexai.vertexai.md +++ b/docs-devsite/vertexai.vertexai.md @@ -22,12 +22,12 @@ export interface VertexAI | Property | Type | Description | | --- | --- | --- | -| [app](./vertexai.vertexai.md#vertexaiapp) | [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) | The [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) this [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance is associated with. | +| [app](./vertexai.vertexai.md#vertexaiapp) | [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) | The [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) this [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance is associated with. | | [location](./vertexai.vertexai.md#vertexailocation) | string | | ## VertexAI.app -The [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) this [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance is associated with. +The [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface) this [VertexAI](./vertexai.vertexai.md#vertexai_interface) instance is associated with. Signature: diff --git a/docs-devsite/vertexai.vertexaierror.md b/docs-devsite/vertexai.vertexaierror.md index 86532ac6018..31f527e59b3 100644 --- a/docs-devsite/vertexai.vertexaierror.md +++ b/docs-devsite/vertexai.vertexaierror.md @@ -46,7 +46,7 @@ constructor(code: VertexAIErrorCode, message: string, customErrorData?: CustomEr | Parameter | Type | Description | | --- | --- | --- | -| code | [VertexAIErrorCode](./vertexai.md#vertexaierrorcode) | The error code from [VertexAIErrorCode](./vertexai.md#vertexaierrorcode). | +| code | [VertexAIErrorCode](./vertexai.md#vertexaierrorcode) | The error code from [VertexAIErrorCode](./vertexai.md#vertexaierrorcode). | | message | string | A human-readable message describing the error. | | customErrorData | [CustomErrorData](./vertexai.customerrordata.md#customerrordata_interface) \| undefined | Optional error data. | diff --git a/packages/vertexai/src/api.ts b/packages/vertexai/src/api.ts index 752e75c7e23..7843a5bdeee 100644 --- a/packages/vertexai/src/api.ts +++ b/packages/vertexai/src/api.ts @@ -43,7 +43,7 @@ declare module '@firebase/component' { } /** - * Returns a {@link VertexAI} instance for the given app. + * Returns a {@link VertexAI} instance for the given app. * * @public * @@ -63,7 +63,7 @@ export function getVertexAI( } /** - * Returns a {@link GenerativeModel} class with methods for inference + * Returns a {@link GenerativeModel} class with methods for inference * and other functionality. * * @public @@ -83,7 +83,7 @@ export function getGenerativeModel( } /** - * Returns an {@link ImagenModel} class with methods for using Imagen. + * Returns an {@link ImagenModel} class with methods for using Imagen. * * Only Imagen 3 models (named `imagen-3.0-*`) are supported. * diff --git a/packages/vertexai/src/errors.ts b/packages/vertexai/src/errors.ts index b643a5f552d..ad3f9b72f5a 100644 --- a/packages/vertexai/src/errors.ts +++ b/packages/vertexai/src/errors.ts @@ -28,7 +28,7 @@ export class VertexAIError extends FirebaseError { /** * Constructs a new instance of the `VertexAIError` class. * - * @param code - The error code from {@link VertexAIErrorCode}. + * @param code - The error code from {@link VertexAIErrorCode}. * @param message - A human-readable message describing the error. * @param customErrorData - Optional error data. */ diff --git a/packages/vertexai/src/methods/chat-session.ts b/packages/vertexai/src/methods/chat-session.ts index dd22b29a7c8..60794001e37 100644 --- a/packages/vertexai/src/methods/chat-session.ts +++ b/packages/vertexai/src/methods/chat-session.ts @@ -72,7 +72,7 @@ export class ChatSession { /** * Sends a chat message and receives a non-streaming - * {@link GenerateContentResult} + * {@link GenerateContentResult} */ async sendMessage( request: string | Array @@ -126,7 +126,7 @@ export class ChatSession { /** * Sends a chat message and receives the response as a - * {@link GenerateContentStreamResult} containing an iterable stream + * {@link GenerateContentStreamResult} containing an iterable stream * and a response promise. */ async sendMessageStream( diff --git a/packages/vertexai/src/models/generative-model.ts b/packages/vertexai/src/models/generative-model.ts index b4cf464f025..983118bf6ff 100644 --- a/packages/vertexai/src/models/generative-model.ts +++ b/packages/vertexai/src/models/generative-model.ts @@ -74,7 +74,7 @@ export class GenerativeModel extends VertexAIModel { /** * Makes a single non-streaming call to the model - * and returns an object containing a single {@link GenerateContentResponse}. + * and returns an object containing a single {@link GenerateContentResponse}. */ async generateContent( request: GenerateContentRequest | string | Array @@ -121,7 +121,7 @@ export class GenerativeModel extends VertexAIModel { } /** - * Gets a new {@link ChatSession} instance which can be used for + * Gets a new {@link ChatSession} instance which can be used for * multi-turn chats. */ startChat(startChatParams?: StartChatParams): ChatSession { diff --git a/packages/vertexai/src/models/imagen-model.ts b/packages/vertexai/src/models/imagen-model.ts index 89c740852a3..04514ef6ffd 100644 --- a/packages/vertexai/src/models/imagen-model.ts +++ b/packages/vertexai/src/models/imagen-model.ts @@ -63,7 +63,7 @@ export class ImagenModel extends VertexAIModel { safetySettings?: ImagenSafetySettings; /** - * Constructs a new instance of the {@link ImagenModel} class. + * Constructs a new instance of the {@link ImagenModel} class. * * @param vertexAI - An instance of the Vertex AI in Firebase SDK. * @param modelParams - Parameters to use when making requests to Imagen. @@ -88,7 +88,7 @@ export class ImagenModel extends VertexAIModel { * base64-encoded strings. * * @param prompt - A text prompt describing the image(s) to generate. - * @returns A promise that resolves to an {@link ImagenGenerationResponse} + * @returns A promise that resolves to an {@link ImagenGenerationResponse} * object containing the generated images. * * @throws If the request to generate images fails. This happens if the @@ -127,7 +127,7 @@ export class ImagenModel extends VertexAIModel { * @param prompt - A text prompt describing the image(s) to generate. * @param gcsURI - The URI of file stored in a Cloud Storage for Firebase bucket. * This should be a directory. For example, `gs://my-bucket/my-directory/`. - * @returns A promise that resolves to an {@link ImagenGenerationResponse} + * @returns A promise that resolves to an {@link ImagenGenerationResponse} * object containing the URLs of the generated images. * * @throws If the request fails to generate images fails. This happens if diff --git a/packages/vertexai/src/public-types.ts b/packages/vertexai/src/public-types.ts index 280fee9d1cd..fbc5d51084d 100644 --- a/packages/vertexai/src/public-types.ts +++ b/packages/vertexai/src/public-types.ts @@ -25,7 +25,7 @@ export * from './types'; */ export interface VertexAI { /** - * The {@link @firebase/app#FirebaseApp} this {@link VertexAI} instance is associated with. + * The {@link @firebase/app#FirebaseApp} this {@link VertexAI} instance is associated with. */ app: FirebaseApp; location: string; diff --git a/packages/vertexai/src/requests/imagen-image-format.ts b/packages/vertexai/src/requests/imagen-image-format.ts index 283dc80bfaf..b9690a7d39b 100644 --- a/packages/vertexai/src/requests/imagen-image-format.ts +++ b/packages/vertexai/src/requests/imagen-image-format.ts @@ -22,7 +22,7 @@ import { logger } from '../logger'; * * Use this class to specify the desired format (JPEG or PNG) and compression quality * for images generated by Imagen. This is typically included as part of - * {@link ImagenModelParams}. + * {@link ImagenModelParams}. * * @example * ```javascript @@ -49,10 +49,10 @@ export class ImagenImageFormat { } /** - * Creates an {@link ImagenImageFormat} for a JPEG image. + * Creates an {@link ImagenImageFormat} for a JPEG image. * * @param compressionQuality - The level of compression (a number between 0 and 100). - * @returns An {@link ImagenImageFormat} object for a JPEG image. + * @returns An {@link ImagenImageFormat} object for a JPEG image. * * @beta */ @@ -69,9 +69,9 @@ export class ImagenImageFormat { } /** - * Creates an {@link ImagenImageFormat} for a PNG image. + * Creates an {@link ImagenImageFormat} for a PNG image. * - * @returns An {@link ImagenImageFormat} object for a PNG image. + * @returns An {@link ImagenImageFormat} object for a PNG image. * * @beta */ diff --git a/packages/vertexai/src/requests/request-helpers.ts b/packages/vertexai/src/requests/request-helpers.ts index f69c88fca92..fd2cd04e0fd 100644 --- a/packages/vertexai/src/requests/request-helpers.ts +++ b/packages/vertexai/src/requests/request-helpers.ts @@ -127,7 +127,7 @@ export function formatGenerateContentInput( } /** - * Convert the user-defined parameters in {@link ImagenGenerationParams} to the format + * Convert the user-defined parameters in {@link ImagenGenerationParams} to the format * that is expected from the REST API. * * @internal diff --git a/packages/vertexai/src/requests/response-helpers.ts b/packages/vertexai/src/requests/response-helpers.ts index 1ba0986f3f9..6d0e3bf2a0a 100644 --- a/packages/vertexai/src/requests/response-helpers.ts +++ b/packages/vertexai/src/requests/response-helpers.ts @@ -144,7 +144,7 @@ export function getText(response: GenerateContentResponse): string { } /** - * Returns {@link FunctionCall}s associated with first candidate. + * Returns {@link FunctionCall}s associated with first candidate. */ export function getFunctionCalls( response: GenerateContentResponse diff --git a/packages/vertexai/src/types/content.ts b/packages/vertexai/src/types/content.ts index abf5d29222a..ad2906671e4 100644 --- a/packages/vertexai/src/types/content.ts +++ b/packages/vertexai/src/types/content.ts @@ -82,7 +82,7 @@ export interface VideoMetadata { } /** - * Content part interface if the part represents a {@link FunctionCall}. + * Content part interface if the part represents a {@link FunctionCall}. * @public */ export interface FunctionCallPart { @@ -93,7 +93,7 @@ export interface FunctionCallPart { } /** - * Content part interface if the part represents {@link FunctionResponse}. + * Content part interface if the part represents {@link FunctionResponse}. * @public */ export interface FunctionResponsePart { @@ -104,7 +104,7 @@ export interface FunctionResponsePart { } /** - * Content part interface if the part represents {@link FileData} + * Content part interface if the part represents {@link FileData} * @public */ export interface FileDataPart { @@ -116,7 +116,7 @@ export interface FileDataPart { } /** - * A predicted {@link FunctionCall} returned from the model + * A predicted {@link FunctionCall} returned from the model * that contains a string representing the {@link FunctionDeclaration.name} * and a structured JSON object containing the parameters and their values. * @public @@ -127,11 +127,11 @@ export interface FunctionCall { } /** - * The result output from a {@link FunctionCall} that contains a string + * The result output from a {@link FunctionCall} that contains a string * representing the {@link FunctionDeclaration.name} * and a structured JSON object containing any output * from the function is used as context to the model. - * This should contain the result of a {@link FunctionCall} + * This should contain the result of a {@link FunctionCall} * made based on model prediction. * @public */ diff --git a/packages/vertexai/src/types/error.ts b/packages/vertexai/src/types/error.ts index c249320a39e..b1f075101a6 100644 --- a/packages/vertexai/src/types/error.ts +++ b/packages/vertexai/src/types/error.ts @@ -50,7 +50,7 @@ export interface CustomErrorData { /** HTTP status text of the error response. */ statusText?: string; - /** Response from a {@link GenerateContentRequest} */ + /** Response from a {@link GenerateContentRequest} */ response?: GenerateContentResponse; /** Optional additional details about the error. */ @@ -58,7 +58,7 @@ export interface CustomErrorData { } /** - * Standardized error codes that {@link VertexAIError} can have. + * Standardized error codes that {@link VertexAIError} can have. * * @public */ diff --git a/packages/vertexai/src/types/imagen/requests.ts b/packages/vertexai/src/types/imagen/requests.ts index ac37488dfb5..70ae182238e 100644 --- a/packages/vertexai/src/types/imagen/requests.ts +++ b/packages/vertexai/src/types/imagen/requests.ts @@ -18,7 +18,7 @@ import { ImagenImageFormat } from '../../requests/imagen-image-format'; /** - * Parameters for configuring an {@link ImagenModel}. + * Parameters for configuring an {@link ImagenModel}. * * @beta */ @@ -64,20 +64,20 @@ export interface ImagenGenerationConfig { * The number of images to generate. The default value is 1. * * The number of sample images that may be generated in each request depends on the model - * (typically up to 4); see the sampleCount + * (typically up to 4); see the sampleCount * documentation for more details. */ numberOfImages?: number; /** * The aspect ratio of the generated images. The default value is square 1:1. - * Supported aspect ratios depend on the Imagen model, see {@link ImagenAspectRatio} + * Supported aspect ratios depend on the Imagen model, see {@link ImagenAspectRatio} * for more details. */ aspectRatio?: ImagenAspectRatio; /** * The image format of the generated images. The default is PNG. * - * See {@link ImagenImageFormat} for more details. + * See {@link ImagenImageFormat} for more details. */ imageFormat?: ImagenImageFormat; /** @@ -86,7 +86,7 @@ export interface ImagenGenerationConfig { * If set to `true`, an invisible SynthID watermark is embedded in generated images to indicate * that they are AI generated. If set to `false`, watermarking will be disabled. * - * For Imagen 3 models, the default value is `true`; see the addWatermark + * For Imagen 3 models, the default value is `true`; see the addWatermark * documentation for more details. */ addWatermark?: boolean; @@ -129,7 +129,7 @@ export enum ImagenSafetyFilterLevel { /** * A filter level controlling whether generation of images containing people or faces is allowed. * - * See the personGeneration + * See the personGeneration * documentation for more details. * * @beta @@ -181,7 +181,7 @@ export interface ImagenSafetySettings { * Aspect ratios for Imagen images. * * To specify an aspect ratio for generated images, set the `aspectRatio` property in your - * {@link ImagenGenerationConfig}. + * {@link ImagenGenerationConfig}. * * See the the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } * for more details and examples of the supported aspect ratios. diff --git a/packages/vertexai/src/types/imagen/responses.ts b/packages/vertexai/src/types/imagen/responses.ts index c5cf5dd9057..4b093fd550f 100644 --- a/packages/vertexai/src/types/imagen/responses.ts +++ b/packages/vertexai/src/types/imagen/responses.ts @@ -24,7 +24,7 @@ export interface ImagenInlineImage { /** * The MIME type of the image; either `"image/png"` or `"image/jpeg"`. * - * To request a different format, set the `imageFormat` property in your {@link ImagenGenerationConfig}. + * To request a different format, set the `imageFormat` property in your {@link ImagenGenerationConfig}. */ mimeType: string; /** @@ -42,7 +42,7 @@ export interface ImagenGCSImage { /** * The MIME type of the image; either `"image/png"` or `"image/jpeg"`. * - * To request a different format, set the `imageFormat` property in your {@link ImagenGenerationConfig}. + * To request a different format, set the `imageFormat` property in your {@link ImagenGenerationConfig}. */ mimeType: string; /** @@ -72,9 +72,9 @@ export interface ImagenGenerationResponse< * The reason that images were filtered out. This property will only be defined if one * or more images were filtered. * - * Images may be filtered out due to the {@link ImagenSafetyFilterLevel}, - * {@link ImagenPersonFilterLevel}, or filtering included in the model. - * The filter levels may be adjusted in your {@link ImagenSafetySettings}. + * Images may be filtered out due to the {@link ImagenSafetyFilterLevel}, + * {@link ImagenPersonFilterLevel}, or filtering included in the model. + * The filter levels may be adjusted in your {@link ImagenSafetySettings}. * * See the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen | Responsible AI and usage guidelines for Imagen} * for more details. diff --git a/packages/vertexai/src/types/requests.ts b/packages/vertexai/src/types/requests.ts index 5058b457365..c15258b06d0 100644 --- a/packages/vertexai/src/types/requests.ts +++ b/packages/vertexai/src/types/requests.ts @@ -35,7 +35,7 @@ export interface BaseParams { } /** - * Params passed to {@link getGenerativeModel}. + * Params passed to {@link getGenerativeModel}. * @public */ export interface ModelParams extends BaseParams { @@ -88,9 +88,9 @@ export interface GenerationConfig { responseMimeType?: string; /** * Output response schema of the generated candidate text. This - * value can be a class generated with a {@link Schema} static method + * value can be a class generated with a {@link Schema} static method * like `Schema.string()` or `Schema.object()` or it can be a plain - * JS object matching the {@link SchemaRequest} interface. + * JS object matching the {@link SchemaRequest} interface. *
Note: This only applies when the specified `responseMIMEType` supports a schema; currently * this is limited to `application/json` and `text/x.enum`. */ @@ -119,7 +119,7 @@ export interface CountTokensRequest { */ systemInstruction?: string | Part | Content; /** - * {@link Tool} configuration. + * {@link Tool} configuration. */ tools?: Tool[]; /** @@ -129,7 +129,7 @@ export interface CountTokensRequest { } /** - * Params passed to {@link getGenerativeModel}. + * Params passed to {@link getGenerativeModel}. * @public */ export interface RequestOptions { @@ -189,8 +189,8 @@ export declare interface FunctionDeclarationsTool { * Optional. One or more function declarations * to be passed to the model along with the current user query. Model may * decide to call a subset of these functions by populating - * {@link FunctionCall} in the response. User should - * provide a {@link FunctionResponse} for each + * {@link FunctionCall} in the response. User should + * provide a {@link FunctionResponse} for each * function call in the next turn. Based on the function responses, the model will * generate the final response back to the user. Maximum 64 function * declarations can be provided. diff --git a/packages/vertexai/src/types/responses.ts b/packages/vertexai/src/types/responses.ts index 437d33e9a47..7f68df1e679 100644 --- a/packages/vertexai/src/types/responses.ts +++ b/packages/vertexai/src/types/responses.ts @@ -76,7 +76,7 @@ export interface GenerateContentResponse { } /** - * Usage metadata about a {@link GenerateContentResponse}. + * Usage metadata about a {@link GenerateContentResponse}. * * @public */ @@ -112,7 +112,7 @@ export interface PromptFeedback { } /** - * A candidate returned as part of a {@link GenerateContentResponse}. + * A candidate returned as part of a {@link GenerateContentResponse}. * @public */ export interface GenerateContentCandidate { @@ -126,7 +126,7 @@ export interface GenerateContentCandidate { } /** - * Citation metadata that may be found on a {@link GenerateContentCandidate}. + * Citation metadata that may be found on a {@link GenerateContentCandidate}. * @public */ export interface CitationMetadata { @@ -206,7 +206,7 @@ export interface Date { } /** - * A safety rating associated with a {@link GenerateContentCandidate} + * A safety rating associated with a {@link GenerateContentCandidate} * @public */ export interface SafetyRating { diff --git a/packages/vertexai/src/types/schema.ts b/packages/vertexai/src/types/schema.ts index fca213431ad..5c23655be0e 100644 --- a/packages/vertexai/src/types/schema.ts +++ b/packages/vertexai/src/types/schema.ts @@ -37,7 +37,7 @@ export enum SchemaType { } /** - * Basic {@link Schema} properties shared across several Schema-related + * Basic {@link Schema} properties shared across several Schema-related * types. * @public */ @@ -62,14 +62,14 @@ export interface SchemaShared { } /** - * Params passed to {@link Schema} static methods to create specific - * {@link Schema} classes. + * Params passed to {@link Schema} static methods to create specific + * {@link Schema} classes. * @public */ export interface SchemaParams extends SchemaShared {} /** - * Final format for {@link Schema} params passed to backend requests. + * Final format for {@link Schema} params passed to backend requests. * @public */ export interface SchemaRequest extends SchemaShared { @@ -83,7 +83,7 @@ export interface SchemaRequest extends SchemaShared { } /** - * Interface for {@link Schema} class. + * Interface for {@link Schema} class. * @public */ export interface SchemaInterface extends SchemaShared { @@ -95,7 +95,7 @@ export interface SchemaInterface extends SchemaShared { } /** - * Interface for {@link ObjectSchema} class. + * Interface for {@link ObjectSchema} class. * @public */ export interface ObjectSchemaInterface extends SchemaInterface { From 0cbff6bc7a62f77346ce76c9602b3d572a9af6c0 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 15 Apr 2025 12:12:12 -0400 Subject: [PATCH 11/23] test(vertexai): update developerapi mock response dir to googleai (#8921) --- packages/vertexai/test-utils/convert-mocks.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/vertexai/test-utils/convert-mocks.ts b/packages/vertexai/test-utils/convert-mocks.ts index 851d6017b6d..8690af4ac72 100644 --- a/packages/vertexai/test-utils/convert-mocks.ts +++ b/packages/vertexai/test-utils/convert-mocks.ts @@ -30,8 +30,7 @@ type BackendName = 'vertexAI' | 'googleAI'; const mockDirs: Record = { vertexAI: join(MOCK_RESPONSES_DIR_PATH, 'vertexai'), - // Note: the dirname is developerapi is legacy. It should be updated to googleai. - googleAI: join(MOCK_RESPONSES_DIR_PATH, 'developerapi') + googleAI: join(MOCK_RESPONSES_DIR_PATH, 'googleai') }; /** From 1363ecc533de0ba5bfcae206a831acc33f9020a6 Mon Sep 17 00:00:00 2001 From: Pavan Shankar Date: Wed, 16 Apr 2025 10:06:31 +0530 Subject: [PATCH 12/23] Fix languageCode parameter in action_code_url (#8912) * Fix languageCode parameter in action_code_url * Add changeset --- .changeset/great-cheetahs-invite.md | 5 +++++ packages/auth/src/core/action_code_url.test.ts | 14 +++++++------- packages/auth/src/core/action_code_url.ts | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 .changeset/great-cheetahs-invite.md diff --git a/.changeset/great-cheetahs-invite.md b/.changeset/great-cheetahs-invite.md new file mode 100644 index 00000000000..f6c8dbd182e --- /dev/null +++ b/.changeset/great-cheetahs-invite.md @@ -0,0 +1,5 @@ +--- +'@firebase/auth': patch +--- + +Fixed: `ActionCodeURL` not populating `languageCode` from the url. diff --git a/packages/auth/src/core/action_code_url.test.ts b/packages/auth/src/core/action_code_url.test.ts index 1f85fd94cc4..1432361e221 100644 --- a/packages/auth/src/core/action_code_url.test.ts +++ b/packages/auth/src/core/action_code_url.test.ts @@ -30,7 +30,7 @@ describe('core/action_code_url', () => { 'oobCode=CODE&mode=signIn&apiKey=API_KEY&' + 'continueUrl=' + encodeURIComponent(continueUrl) + - '&languageCode=en&tenantId=TENANT_ID&state=bla'; + '&lang=en&tenantId=TENANT_ID&state=bla'; const actionCodeUrl = ActionCodeURL.parseLink(actionLink); expect(actionCodeUrl!.operation).to.eq(ActionCodeOperation.EMAIL_SIGNIN); expect(actionCodeUrl!.code).to.eq('CODE'); @@ -46,7 +46,7 @@ describe('core/action_code_url', () => { const actionLink = 'https://www.example.com/finishSignIn?' + 'oobCode=CODE&mode=signIn&apiKey=API_KEY&' + - 'languageCode=en'; + 'lang=en'; const actionCodeUrl = ActionCodeURL.parseLink(actionLink); expect(actionCodeUrl!.operation).to.eq( ActionCodeOperation.EMAIL_SIGNIN @@ -57,7 +57,7 @@ describe('core/action_code_url', () => { const actionLink = 'https://www.example.com/finishSignIn?' + 'oobCode=CODE&mode=verifyAndChangeEmail&apiKey=API_KEY&' + - 'languageCode=en'; + 'lang=en'; const actionCodeUrl = ActionCodeURL.parseLink(actionLink); expect(actionCodeUrl!.operation).to.eq( ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL @@ -68,7 +68,7 @@ describe('core/action_code_url', () => { const actionLink = 'https://www.example.com/finishSignIn?' + 'oobCode=CODE&mode=verifyEmail&apiKey=API_KEY&' + - 'languageCode=en'; + 'lang=en'; const actionCodeUrl = ActionCodeURL.parseLink(actionLink); expect(actionCodeUrl!.operation).to.eq( ActionCodeOperation.VERIFY_EMAIL @@ -79,7 +79,7 @@ describe('core/action_code_url', () => { const actionLink = 'https://www.example.com/finishSignIn?' + 'oobCode=CODE&mode=recoverEmail&apiKey=API_KEY&' + - 'languageCode=en'; + 'lang=en'; const actionCodeUrl = ActionCodeURL.parseLink(actionLink); expect(actionCodeUrl!.operation).to.eq( ActionCodeOperation.RECOVER_EMAIL @@ -90,7 +90,7 @@ describe('core/action_code_url', () => { const actionLink = 'https://www.example.com/finishSignIn?' + 'oobCode=CODE&mode=resetPassword&apiKey=API_KEY&' + - 'languageCode=en'; + 'lang=en'; const actionCodeUrl = ActionCodeURL.parseLink(actionLink); expect(actionCodeUrl!.operation).to.eq( ActionCodeOperation.PASSWORD_RESET @@ -101,7 +101,7 @@ describe('core/action_code_url', () => { const actionLink = 'https://www.example.com/finishSignIn?' + 'oobCode=CODE&mode=revertSecondFactorAddition&apiKey=API_KEY&' + - 'languageCode=en'; + 'lang=en'; const actionCodeUrl = ActionCodeURL.parseLink(actionLink); expect(actionCodeUrl!.operation).to.eq( ActionCodeOperation.REVERT_SECOND_FACTOR_ADDITION diff --git a/packages/auth/src/core/action_code_url.ts b/packages/auth/src/core/action_code_url.ts index f3d5c69bc1f..b7778766a9e 100644 --- a/packages/auth/src/core/action_code_url.ts +++ b/packages/auth/src/core/action_code_url.ts @@ -29,7 +29,7 @@ const enum QueryField { API_KEY = 'apiKey', CODE = 'oobCode', CONTINUE_URL = 'continueUrl', - LANGUAGE_CODE = 'languageCode', + LANGUAGE_CODE = 'lang', MODE = 'mode', TENANT_ID = 'tenantId' } From 20b45d3ab12bfae3ac8761fdf32cb613cc2f3641 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Wed, 23 Apr 2025 14:02:07 -0400 Subject: [PATCH 13/23] test(vertexai): update mock responses to v10 (#8959) --- scripts/update_vertexai_responses.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update_vertexai_responses.sh b/scripts/update_vertexai_responses.sh index de55ac176ce..bf55a645a66 100755 --- a/scripts/update_vertexai_responses.sh +++ b/scripts/update_vertexai_responses.sh @@ -17,7 +17,7 @@ # This script replaces mock response files for Vertex AI unit tests with a fresh # clone of the shared repository of Vertex AI test data. -RESPONSES_VERSION='v8.*' # The major version of mock responses to use +RESPONSES_VERSION='v10.*' # The major version of mock responses to use REPO_NAME="vertexai-sdk-test-data" REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git" From b204e7126bf09cdc551993be6427e2e5dd478fc9 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 24 Apr 2025 08:32:02 -0700 Subject: [PATCH 14/23] Version Packages (#8956) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/cyan-frogs-relate.md | 6 ------ .changeset/gentle-rocks-repeat.md | 6 ------ .changeset/great-cheetahs-invite.md | 5 ----- .changeset/hungry-snails-drive.md | 5 ----- .changeset/odd-wolves-sit.md | 5 ----- .changeset/slow-students-fry.md | 6 ------ integration/compat-interop/package.json | 8 ++++---- integration/firestore/package.json | 4 ++-- integration/messaging/package.json | 2 +- packages/analytics-compat/package.json | 2 +- packages/analytics/package.json | 2 +- packages/app-check-compat/package.json | 2 +- packages/app-check/package.json | 2 +- packages/app-compat/CHANGELOG.md | 7 +++++++ packages/app-compat/package.json | 4 ++-- packages/app/CHANGELOG.md | 6 ++++++ packages/app/package.json | 2 +- packages/auth-compat/CHANGELOG.md | 7 +++++++ packages/auth-compat/package.json | 6 +++--- packages/auth/CHANGELOG.md | 6 ++++++ packages/auth/package.json | 4 ++-- packages/data-connect/CHANGELOG.md | 6 ++++++ packages/data-connect/package.json | 4 ++-- packages/database-compat/package.json | 2 +- packages/database/package.json | 2 +- packages/firebase/CHANGELOG.md | 19 +++++++++++++++++++ packages/firebase/package.json | 16 ++++++++-------- packages/firestore-compat/CHANGELOG.md | 7 +++++++ packages/firestore-compat/package.json | 6 +++--- packages/firestore/CHANGELOG.md | 12 ++++++++++++ packages/firestore/package.json | 8 ++++---- packages/functions-compat/package.json | 2 +- packages/functions/package.json | 2 +- packages/installations-compat/package.json | 2 +- packages/installations/package.json | 2 +- packages/messaging-compat/package.json | 2 +- packages/messaging/package.json | 2 +- packages/performance-compat/package.json | 2 +- packages/performance/package.json | 2 +- packages/remote-config-compat/package.json | 2 +- packages/remote-config/package.json | 2 +- packages/storage-compat/package.json | 4 ++-- packages/storage/package.json | 4 ++-- packages/template/package.json | 2 +- packages/vertexai/package.json | 2 +- repo-scripts/size-analysis/package.json | 2 +- 46 files changed, 125 insertions(+), 88 deletions(-) delete mode 100644 .changeset/cyan-frogs-relate.md delete mode 100644 .changeset/gentle-rocks-repeat.md delete mode 100644 .changeset/great-cheetahs-invite.md delete mode 100644 .changeset/hungry-snails-drive.md delete mode 100644 .changeset/odd-wolves-sit.md delete mode 100644 .changeset/slow-students-fry.md diff --git a/.changeset/cyan-frogs-relate.md b/.changeset/cyan-frogs-relate.md deleted file mode 100644 index 08af27593a9..00000000000 --- a/.changeset/cyan-frogs-relate.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@firebase/firestore': patch -'firebase': patch ---- - -Fixed the `null` value handling in `!=` and `not-in` filters. diff --git a/.changeset/gentle-rocks-repeat.md b/.changeset/gentle-rocks-repeat.md deleted file mode 100644 index 462e36659b8..00000000000 --- a/.changeset/gentle-rocks-repeat.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@firebase/firestore': patch -'firebase': patch ---- - -Fix 'window is not defined' error when calling `clearIndexedDbPersistence` from a service worker diff --git a/.changeset/great-cheetahs-invite.md b/.changeset/great-cheetahs-invite.md deleted file mode 100644 index f6c8dbd182e..00000000000 --- a/.changeset/great-cheetahs-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@firebase/auth': patch ---- - -Fixed: `ActionCodeURL` not populating `languageCode` from the url. diff --git a/.changeset/hungry-snails-drive.md b/.changeset/hungry-snails-drive.md deleted file mode 100644 index 1a29782a04d..00000000000 --- a/.changeset/hungry-snails-drive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@firebase/data-connect": patch ---- - -Fix DataConnectOperationError. diff --git a/.changeset/odd-wolves-sit.md b/.changeset/odd-wolves-sit.md deleted file mode 100644 index fc63acc005e..00000000000 --- a/.changeset/odd-wolves-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@firebase/firestore": patch ---- - -Add unique IDs and state information into fatal error messages instead of the generic "unexpected state" message. diff --git a/.changeset/slow-students-fry.md b/.changeset/slow-students-fry.md deleted file mode 100644 index 45f3cf7e576..00000000000 --- a/.changeset/slow-students-fry.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@firebase/firestore': patch -'firebase': patch ---- - -Fix issue where Firestore would produce `undefined` for document snapshot data if using IndexedDB persistence and "clear site data" (or equivalent) button was pressed in the web browser. diff --git a/integration/compat-interop/package.json b/integration/compat-interop/package.json index 547862a7b99..4cd59d2626d 100644 --- a/integration/compat-interop/package.json +++ b/integration/compat-interop/package.json @@ -8,12 +8,12 @@ "test:debug": "karma start --browsers Chrome --auto-watch" }, "dependencies": { - "@firebase/app": "0.11.4", - "@firebase/app-compat": "0.2.53", + "@firebase/app": "0.11.5", + "@firebase/app-compat": "0.2.54", "@firebase/analytics": "0.10.12", "@firebase/analytics-compat": "0.2.18", - "@firebase/auth": "1.10.0", - "@firebase/auth-compat": "0.5.20", + "@firebase/auth": "1.10.1", + "@firebase/auth-compat": "0.5.21", "@firebase/functions": "0.12.3", "@firebase/functions-compat": "0.3.20", "@firebase/messaging": "0.12.17", diff --git a/integration/firestore/package.json b/integration/firestore/package.json index 6f0829cc16e..9ca8917ab4c 100644 --- a/integration/firestore/package.json +++ b/integration/firestore/package.json @@ -14,8 +14,8 @@ "test:memory:debug": "yarn build:memory; karma start --auto-watch --browsers Chrome" }, "dependencies": { - "@firebase/app": "0.11.4", - "@firebase/firestore": "4.7.10" + "@firebase/app": "0.11.5", + "@firebase/firestore": "4.7.11" }, "devDependencies": { "@types/mocha": "9.1.1", diff --git a/integration/messaging/package.json b/integration/messaging/package.json index 4ba2bef35b8..a86c4b1f7b5 100644 --- a/integration/messaging/package.json +++ b/integration/messaging/package.json @@ -9,7 +9,7 @@ "test:manual": "mocha --exit" }, "devDependencies": { - "firebase": "11.6.0", + "firebase": "11.6.1", "chai": "4.5.0", "chromedriver": "119.0.1", "express": "4.21.2", diff --git a/packages/analytics-compat/package.json b/packages/analytics-compat/package.json index 0dfbb6e431b..1064125d5f6 100644 --- a/packages/analytics-compat/package.json +++ b/packages/analytics-compat/package.json @@ -22,7 +22,7 @@ "@firebase/app-compat": "0.x" }, "devDependencies": { - "@firebase/app-compat": "0.2.53", + "@firebase/app-compat": "0.2.54", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 6b73106cd07..d56f6f7c61e 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -47,7 +47,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/app-check-compat/package.json b/packages/app-check-compat/package.json index e0c922a311c..630e7a8d234 100644 --- a/packages/app-check-compat/package.json +++ b/packages/app-check-compat/package.json @@ -43,7 +43,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app-compat": "0.2.53", + "@firebase/app-compat": "0.2.54", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/app-check/package.json b/packages/app-check/package.json index 31d2c734de1..ae6555da970 100644 --- a/packages/app-check/package.json +++ b/packages/app-check/package.json @@ -44,7 +44,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/app-compat/CHANGELOG.md b/packages/app-compat/CHANGELOG.md index 35e8bd7fd36..d505c7b6240 100644 --- a/packages/app-compat/CHANGELOG.md +++ b/packages/app-compat/CHANGELOG.md @@ -1,5 +1,12 @@ # @firebase/app-compat +## 0.2.54 + +### Patch Changes + +- Updated dependencies []: + - @firebase/app@0.11.5 + ## 0.2.53 ### Patch Changes diff --git a/packages/app-compat/package.json b/packages/app-compat/package.json index 6437d895d4f..e113c708c74 100644 --- a/packages/app-compat/package.json +++ b/packages/app-compat/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/app-compat", - "version": "0.2.53", + "version": "0.2.54", "description": "The primary entrypoint to the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -37,7 +37,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "@firebase/util": "1.11.0", "@firebase/logger": "0.4.4", "@firebase/component": "0.6.13", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 8e1b0766095..3528eee556d 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,11 @@ # @firebase/app +## 0.11.5 + +### Patch Changes + +- Update SDK_VERSION. + ## 0.11.4 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index dce420d3b30..848919067bd 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/app", - "version": "0.11.4", + "version": "0.11.5", "description": "The primary entrypoint to the Firebase JS SDK", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", diff --git a/packages/auth-compat/CHANGELOG.md b/packages/auth-compat/CHANGELOG.md index 81cb295aabc..66b1e0d6e28 100644 --- a/packages/auth-compat/CHANGELOG.md +++ b/packages/auth-compat/CHANGELOG.md @@ -1,5 +1,12 @@ # @firebase/auth-compat +## 0.5.21 + +### Patch Changes + +- Updated dependencies [[`1363ecc`](https://github.com/firebase/firebase-js-sdk/commit/1363ecc533de0ba5bfcae206a831acc33f9020a6)]: + - @firebase/auth@1.10.1 + ## 0.5.20 ### Patch Changes diff --git a/packages/auth-compat/package.json b/packages/auth-compat/package.json index a10dc65173b..fa69e3f3679 100644 --- a/packages/auth-compat/package.json +++ b/packages/auth-compat/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/auth-compat", - "version": "0.5.20", + "version": "0.5.21", "description": "FirebaseAuth compatibility package that uses API style compatible with Firebase@8 and prior versions", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", @@ -49,7 +49,7 @@ "@firebase/app-compat": "0.x" }, "dependencies": { - "@firebase/auth": "1.10.0", + "@firebase/auth": "1.10.1", "@firebase/auth-types": "0.13.0", "@firebase/component": "0.6.13", "@firebase/util": "1.11.0", @@ -57,7 +57,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app-compat": "0.2.53", + "@firebase/app-compat": "0.2.54", "@rollup/plugin-json": "6.1.0", "rollup": "2.79.2", "rollup-plugin-replace": "2.2.0", diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index 3eb66ffa508..5a52929a128 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -1,5 +1,11 @@ # @firebase/auth +## 1.10.1 + +### Patch Changes + +- [`1363ecc`](https://github.com/firebase/firebase-js-sdk/commit/1363ecc533de0ba5bfcae206a831acc33f9020a6) [#8912](https://github.com/firebase/firebase-js-sdk/pull/8912) - Fixed: `ActionCodeURL` not populating `languageCode` from the url. + ## 1.10.0 ### Minor Changes diff --git a/packages/auth/package.json b/packages/auth/package.json index dde545bb198..6a704a4d4b6 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/auth", - "version": "1.10.0", + "version": "1.10.1", "description": "The Firebase Authenticaton component of the Firebase JS SDK.", "author": "Firebase (https://firebase.google.com/)", "main": "dist/node/index.js", @@ -131,7 +131,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-strip": "2.1.0", "@types/express": "4.17.21", diff --git a/packages/data-connect/CHANGELOG.md b/packages/data-connect/CHANGELOG.md index da401509f5d..03e17644a77 100644 --- a/packages/data-connect/CHANGELOG.md +++ b/packages/data-connect/CHANGELOG.md @@ -1,5 +1,11 @@ ## Unreleased +## 0.3.4 + +### Patch Changes + +- [`1df3d26`](https://github.com/firebase/firebase-js-sdk/commit/1df3d26fbfb4db24b74d5d779825017e9ec40eaa) [#8898](https://github.com/firebase/firebase-js-sdk/pull/8898) - Fix DataConnectOperationError. + ## 0.3.3 ### Patch Changes diff --git a/packages/data-connect/package.json b/packages/data-connect/package.json index 5b792b1bfe1..00f9bb492cc 100644 --- a/packages/data-connect/package.json +++ b/packages/data-connect/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/data-connect", - "version": "0.3.3", + "version": "0.3.4", "description": "", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", @@ -55,7 +55,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4" diff --git a/packages/database-compat/package.json b/packages/database-compat/package.json index 4233e735858..65deedb34e2 100644 --- a/packages/database-compat/package.json +++ b/packages/database-compat/package.json @@ -57,7 +57,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.2.53", + "@firebase/app-compat": "0.2.54", "typescript": "5.5.4" }, "repository": { diff --git a/packages/database/package.json b/packages/database/package.json index 2c86f94adbd..d6f5ddc1707 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -57,7 +57,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4" diff --git a/packages/firebase/CHANGELOG.md b/packages/firebase/CHANGELOG.md index ca4de9bb639..e71a10d560f 100644 --- a/packages/firebase/CHANGELOG.md +++ b/packages/firebase/CHANGELOG.md @@ -1,5 +1,24 @@ # firebase +## 11.6.1 + +### Patch Changes + +- [`ed0803a`](https://github.com/firebase/firebase-js-sdk/commit/ed0803a29791cc0cecd0153f95e814ddcee7efd8) [#8915](https://github.com/firebase/firebase-js-sdk/pull/8915) - Fixed the `null` value handling in `!=` and `not-in` filters. + +- [`88a8055`](https://github.com/firebase/firebase-js-sdk/commit/88a8055808bdbd1c75011a94d11062460027d931) [#8888](https://github.com/firebase/firebase-js-sdk/pull/8888) (fixes [#6465](https://github.com/firebase/firebase-js-sdk/issues/6465)) - Fix 'window is not defined' error when calling `clearIndexedDbPersistence` from a service worker + +- [`195d943`](https://github.com/firebase/firebase-js-sdk/commit/195d943103795a50bb3fc5c56ef2bb64610006a1) [#8871](https://github.com/firebase/firebase-js-sdk/pull/8871) (fixes [#8593](https://github.com/firebase/firebase-js-sdk/issues/8593)) - Fix issue where Firestore would produce `undefined` for document snapshot data if using IndexedDB persistence and "clear site data" (or equivalent) button was pressed in the web browser. + +- Updated dependencies [[`ed0803a`](https://github.com/firebase/firebase-js-sdk/commit/ed0803a29791cc0cecd0153f95e814ddcee7efd8), [`88a8055`](https://github.com/firebase/firebase-js-sdk/commit/88a8055808bdbd1c75011a94d11062460027d931), [`1363ecc`](https://github.com/firebase/firebase-js-sdk/commit/1363ecc533de0ba5bfcae206a831acc33f9020a6), [`1df3d26`](https://github.com/firebase/firebase-js-sdk/commit/1df3d26fbfb4db24b74d5d779825017e9ec40eaa), [`e055e90`](https://github.com/firebase/firebase-js-sdk/commit/e055e9057caab4d9f73734307fe4e0be2098249b), [`195d943`](https://github.com/firebase/firebase-js-sdk/commit/195d943103795a50bb3fc5c56ef2bb64610006a1)]: + - @firebase/app@0.11.5 + - @firebase/firestore@4.7.11 + - @firebase/auth@1.10.1 + - @firebase/data-connect@0.3.4 + - @firebase/app-compat@0.2.54 + - @firebase/firestore-compat@0.3.46 + - @firebase/auth-compat@0.5.21 + ## 11.6.0 ### Minor Changes diff --git a/packages/firebase/package.json b/packages/firebase/package.json index 0a108875770..3a4bec301e3 100644 --- a/packages/firebase/package.json +++ b/packages/firebase/package.json @@ -1,6 +1,6 @@ { "name": "firebase", - "version": "11.6.0", + "version": "11.6.1", "description": "Firebase JavaScript library for web and Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -399,16 +399,16 @@ "trusted-type-check": "tsec -p tsconfig.json --noEmit" }, "dependencies": { - "@firebase/app": "0.11.4", - "@firebase/app-compat": "0.2.53", + "@firebase/app": "0.11.5", + "@firebase/app-compat": "0.2.54", "@firebase/app-types": "0.9.3", - "@firebase/auth": "1.10.0", - "@firebase/auth-compat": "0.5.20", - "@firebase/data-connect": "0.3.3", + "@firebase/auth": "1.10.1", + "@firebase/auth-compat": "0.5.21", + "@firebase/data-connect": "0.3.4", "@firebase/database": "1.0.14", "@firebase/database-compat": "2.0.5", - "@firebase/firestore": "4.7.10", - "@firebase/firestore-compat": "0.3.45", + "@firebase/firestore": "4.7.11", + "@firebase/firestore-compat": "0.3.46", "@firebase/functions": "0.12.3", "@firebase/functions-compat": "0.3.20", "@firebase/installations": "0.6.13", diff --git a/packages/firestore-compat/CHANGELOG.md b/packages/firestore-compat/CHANGELOG.md index 87b800344b4..aadd8c532b3 100644 --- a/packages/firestore-compat/CHANGELOG.md +++ b/packages/firestore-compat/CHANGELOG.md @@ -1,5 +1,12 @@ # @firebase/firestore-compat +## 0.3.46 + +### Patch Changes + +- Updated dependencies [[`ed0803a`](https://github.com/firebase/firebase-js-sdk/commit/ed0803a29791cc0cecd0153f95e814ddcee7efd8), [`88a8055`](https://github.com/firebase/firebase-js-sdk/commit/88a8055808bdbd1c75011a94d11062460027d931), [`e055e90`](https://github.com/firebase/firebase-js-sdk/commit/e055e9057caab4d9f73734307fe4e0be2098249b), [`195d943`](https://github.com/firebase/firebase-js-sdk/commit/195d943103795a50bb3fc5c56ef2bb64610006a1)]: + - @firebase/firestore@4.7.11 + ## 0.3.45 ### Patch Changes diff --git a/packages/firestore-compat/package.json b/packages/firestore-compat/package.json index 35415667824..58210252655 100644 --- a/packages/firestore-compat/package.json +++ b/packages/firestore-compat/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/firestore-compat", - "version": "0.3.45", + "version": "0.3.46", "description": "The Cloud Firestore component of the Firebase JS SDK.", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.node.cjs.js", @@ -47,13 +47,13 @@ }, "dependencies": { "@firebase/component": "0.6.13", - "@firebase/firestore": "4.7.10", + "@firebase/firestore": "4.7.11", "@firebase/util": "1.11.0", "@firebase/firestore-types": "3.0.3", "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.2.53", + "@firebase/app-compat": "0.2.54", "@types/eslint": "7.29.0", "rollup": "2.79.2", "rollup-plugin-sourcemaps": "0.6.3", diff --git a/packages/firestore/CHANGELOG.md b/packages/firestore/CHANGELOG.md index 26128e8d56a..421f33ed1d5 100644 --- a/packages/firestore/CHANGELOG.md +++ b/packages/firestore/CHANGELOG.md @@ -1,5 +1,17 @@ # @firebase/firestore +## 4.7.11 + +### Patch Changes + +- [`ed0803a`](https://github.com/firebase/firebase-js-sdk/commit/ed0803a29791cc0cecd0153f95e814ddcee7efd8) [#8915](https://github.com/firebase/firebase-js-sdk/pull/8915) - Fixed the `null` value handling in `!=` and `not-in` filters. + +- [`88a8055`](https://github.com/firebase/firebase-js-sdk/commit/88a8055808bdbd1c75011a94d11062460027d931) [#8888](https://github.com/firebase/firebase-js-sdk/pull/8888) (fixes [#6465](https://github.com/firebase/firebase-js-sdk/issues/6465)) - Fix 'window is not defined' error when calling `clearIndexedDbPersistence` from a service worker + +- [`e055e90`](https://github.com/firebase/firebase-js-sdk/commit/e055e9057caab4d9f73734307fe4e0be2098249b) [#8313](https://github.com/firebase/firebase-js-sdk/pull/8313) - Add unique IDs and state information into fatal error messages instead of the generic "unexpected state" message. + +- [`195d943`](https://github.com/firebase/firebase-js-sdk/commit/195d943103795a50bb3fc5c56ef2bb64610006a1) [#8871](https://github.com/firebase/firebase-js-sdk/pull/8871) (fixes [#8593](https://github.com/firebase/firebase-js-sdk/issues/8593)) - Fix issue where Firestore would produce `undefined` for document snapshot data if using IndexedDB persistence and "clear site data" (or equivalent) button was pressed in the web browser. + ## 4.7.10 ### Patch Changes diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 0c9ddeee843..8cc7e5e18f5 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/firestore", - "version": "4.7.10", + "version": "4.7.11", "engines": { "node": ">=18.0.0" }, @@ -110,9 +110,9 @@ "@firebase/app": "0.x" }, "devDependencies": { - "@firebase/app": "0.11.4", - "@firebase/app-compat": "0.2.53", - "@firebase/auth": "1.10.0", + "@firebase/app": "0.11.5", + "@firebase/app-compat": "0.2.54", + "@firebase/auth": "1.10.1", "@rollup/plugin-alias": "5.1.1", "@rollup/plugin-json": "6.1.0", "@types/eslint": "7.29.0", diff --git a/packages/functions-compat/package.json b/packages/functions-compat/package.json index 5fe4e7e85ce..c2757fcf130 100644 --- a/packages/functions-compat/package.json +++ b/packages/functions-compat/package.json @@ -29,7 +29,7 @@ "@firebase/app-compat": "0.x" }, "devDependencies": { - "@firebase/app-compat": "0.2.53", + "@firebase/app-compat": "0.2.54", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/functions/package.json b/packages/functions/package.json index 477fd599ac0..4ddf15ac556 100644 --- a/packages/functions/package.json +++ b/packages/functions/package.json @@ -49,7 +49,7 @@ "@firebase/app": "0.x" }, "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/installations-compat/package.json b/packages/installations-compat/package.json index 1814656c070..0f5203dd5d1 100644 --- a/packages/installations-compat/package.json +++ b/packages/installations-compat/package.json @@ -44,7 +44,7 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "@firebase/app-compat": "0.2.53", + "@firebase/app-compat": "0.2.54", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/installations/package.json b/packages/installations/package.json index cf367ff7954..83db977a6b6 100644 --- a/packages/installations/package.json +++ b/packages/installations/package.json @@ -49,7 +49,7 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "rollup": "2.79.2", "@rollup/plugin-commonjs": "21.1.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/messaging-compat/package.json b/packages/messaging-compat/package.json index 5e02d85a7d4..388670eb5ab 100644 --- a/packages/messaging-compat/package.json +++ b/packages/messaging-compat/package.json @@ -44,7 +44,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.2.53", + "@firebase/app-compat": "0.2.54", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", "ts-essentials": "9.4.2", diff --git a/packages/messaging/package.json b/packages/messaging/package.json index 93300081e57..5e25b2b1ca0 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -60,7 +60,7 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "@rollup/plugin-json": "6.1.0", diff --git a/packages/performance-compat/package.json b/packages/performance-compat/package.json index ea04ce4dda3..69c24f13465 100644 --- a/packages/performance-compat/package.json +++ b/packages/performance-compat/package.json @@ -51,7 +51,7 @@ "rollup-plugin-replace": "2.2.0", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4", - "@firebase/app-compat": "0.2.53" + "@firebase/app-compat": "0.2.54" }, "repository": { "directory": "packages/performance-compat", diff --git a/packages/performance/package.json b/packages/performance/package.json index 0fca12f70f9..07e8e60d054 100644 --- a/packages/performance/package.json +++ b/packages/performance/package.json @@ -47,7 +47,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/remote-config-compat/package.json b/packages/remote-config-compat/package.json index 2e840c85238..1055c892435 100644 --- a/packages/remote-config-compat/package.json +++ b/packages/remote-config-compat/package.json @@ -50,7 +50,7 @@ "rollup-plugin-replace": "2.2.0", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4", - "@firebase/app-compat": "0.2.53" + "@firebase/app-compat": "0.2.54" }, "repository": { "directory": "packages/remote-config-compat", diff --git a/packages/remote-config/package.json b/packages/remote-config/package.json index e0252a59bca..4262488b0fb 100644 --- a/packages/remote-config/package.json +++ b/packages/remote-config/package.json @@ -48,7 +48,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4" diff --git a/packages/storage-compat/package.json b/packages/storage-compat/package.json index 1380b70185b..d829a79cbc5 100644 --- a/packages/storage-compat/package.json +++ b/packages/storage-compat/package.json @@ -44,8 +44,8 @@ "tslib": "^2.1.0" }, "devDependencies": { - "@firebase/app-compat": "0.2.53", - "@firebase/auth-compat": "0.5.20", + "@firebase/app-compat": "0.2.54", + "@firebase/auth-compat": "0.5.21", "rollup": "2.79.2", "@rollup/plugin-json": "6.1.0", "rollup-plugin-typescript2": "0.36.0", diff --git a/packages/storage/package.json b/packages/storage/package.json index 57b58d0dda3..0a7a8af9b93 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -54,8 +54,8 @@ "@firebase/app": "0.x" }, "devDependencies": { - "@firebase/app": "0.11.4", - "@firebase/auth": "1.10.0", + "@firebase/app": "0.11.5", + "@firebase/auth": "1.10.1", "rollup": "2.79.2", "@rollup/plugin-alias": "5.1.1", "@rollup/plugin-json": "6.1.0", diff --git a/packages/template/package.json b/packages/template/package.json index e9f19f330e2..80500aa1392 100644 --- a/packages/template/package.json +++ b/packages/template/package.json @@ -48,7 +48,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "rollup": "2.79.2", "rollup-plugin-typescript2": "0.36.0", "typescript": "5.5.4" diff --git a/packages/vertexai/package.json b/packages/vertexai/package.json index 9faf562a535..e3472e733f8 100644 --- a/packages/vertexai/package.json +++ b/packages/vertexai/package.json @@ -56,7 +56,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "@rollup/plugin-json": "6.1.0", "rollup": "2.79.2", "rollup-plugin-replace": "2.2.0", diff --git a/repo-scripts/size-analysis/package.json b/repo-scripts/size-analysis/package.json index 44a870c3905..f2a4a35bd53 100644 --- a/repo-scripts/size-analysis/package.json +++ b/repo-scripts/size-analysis/package.json @@ -20,7 +20,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.11.4", + "@firebase/app": "0.11.5", "@firebase/logger": "0.4.4", "@firebase/util": "1.11.0", "@rollup/plugin-commonjs": "21.1.0", From fbf72228d6a3fbb9eab9786c0e2fe2605aa9fa35 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Fri, 25 Apr 2025 14:39:31 -0400 Subject: [PATCH 15/23] Port BSON code updates from other sdks (#341) --- common/api-review/firestore-lite.api.md | 10 +- common/api-review/firestore.api.md | 10 +- packages/firestore/src/api.ts | 2 +- .../src/index/firestore_index_value_writer.ts | 36 +- packages/firestore/src/model/values.ts | 135 ++- .../test/integration/api/database.test.ts | 328 +++++-- .../test/integration/util/helpers.ts | 90 +- .../test/unit/local/index_manager.test.ts | 80 +- .../unit/local/local_store_indexeddb.test.ts | 832 +++++++++++++++++- .../firestore/test/unit/model/values.test.ts | 2 +- 10 files changed, 1226 insertions(+), 299 deletions(-) diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index 04faa9c47c6..394fd0b33ce 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -88,18 +88,18 @@ export class BsonObjectId { export function bsonObjectId(value: string): BsonObjectId; // @public -export function bsonTimestamp(seconds: number, increment: number): BsonTimestampValue; - -// @public -export class BsonTimestampValue { +export class BsonTimestamp { constructor(seconds: number, increment: number); // (undocumented) readonly increment: number; - isEqual(other: BsonTimestampValue): boolean; + isEqual(other: BsonTimestamp): boolean; // (undocumented) readonly seconds: number; } +// @public +export function bsonTimestamp(seconds: number, increment: number): BsonTimestamp; + // @public export class Bytes { static fromBase64String(base64: string): Bytes; diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 5d3c2286859..39dcdb6df59 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -88,18 +88,18 @@ export class BsonObjectId { export function bsonObjectId(value: string): BsonObjectId; // @public -export function bsonTimestamp(seconds: number, increment: number): BsonTimestampValue; - -// @public -export class BsonTimestampValue { +export class BsonTimestamp { constructor(seconds: number, increment: number); // (undocumented) readonly increment: number; - isEqual(other: BsonTimestampValue): boolean; + isEqual(other: BsonTimestamp): boolean; // (undocumented) readonly seconds: number; } +// @public +export function bsonTimestamp(seconds: number, increment: number): BsonTimestamp; + // @public export class Bytes { static fromBase64String(base64: string): Bytes; diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index 46fb1b3bba3..ec6fdd2c4ea 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -193,7 +193,7 @@ export { BsonBinaryData } from './lite-api/bson_binary_data'; export { BsonObjectId } from './lite-api/bson_object_Id'; -export { BsonTimestampValue } from './lite-api/bson_timestamp_value'; +export { BsonTimestamp } from './lite-api/bson_timestamp_value'; export { MinKey } from './lite-api/min_key'; diff --git a/packages/firestore/src/index/firestore_index_value_writer.ts b/packages/firestore/src/index/firestore_index_value_writer.ts index d02a07313fe..6949ad99b31 100644 --- a/packages/firestore/src/index/firestore_index_value_writer.ts +++ b/packages/firestore/src/index/firestore_index_value_writer.ts @@ -23,12 +23,12 @@ import { } from '../model/normalize'; import { VECTOR_MAP_VECTORS_KEY, - detectSpecialMapType, + detectMapRepresentation, RESERVED_BSON_TIMESTAMP_KEY, RESERVED_REGEX_KEY, RESERVED_BSON_OBJECT_ID_KEY, RESERVED_BSON_BINARY_KEY, - SpecialMapValueType, + MapRepresentation, RESERVED_REGEX_PATTERN_KEY, RESERVED_REGEX_OPTIONS_KEY, RESERVED_INT32_KEY @@ -42,6 +42,10 @@ import { DirectionalIndexByteEncoder } from './directional_index_byte_encoder'; // Note: This file is copied from the backend. Code that is not used by // Firestore was removed. Code that has different behavior was modified. +// The client SDK only supports references to documents from the same database. We can skip the +// first five segments. +const DOCUMENT_NAME_OFFSET = 5; + const INDEX_TYPE_NULL = 5; const INDEX_TYPE_MIN_KEY = 7; const INDEX_TYPE_BOOLEAN = 10; @@ -60,7 +64,7 @@ const INDEX_TYPE_ARRAY = 50; const INDEX_TYPE_VECTOR = 53; const INDEX_TYPE_MAP = 55; const INDEX_TYPE_REFERENCE_SEGMENT = 60; -const INDEX_TYPE_MAX_VALUE = 999; +const INDEX_TYPE_MAX_KEY = 999; // A terminator that indicates that a truncatable value was not truncated. // This must be smaller than all other type labels. @@ -137,24 +141,24 @@ export class FirestoreIndexValueWriter { encoder.writeNumber(geoPoint.latitude || 0); encoder.writeNumber(geoPoint.longitude || 0); } else if ('mapValue' in indexValue) { - const type = detectSpecialMapType(indexValue); - if (type === SpecialMapValueType.INTERNAL_MAX) { + const type = detectMapRepresentation(indexValue); + if (type === MapRepresentation.INTERNAL_MAX) { this.writeValueTypeLabel(encoder, Number.MAX_SAFE_INTEGER); - } else if (type === SpecialMapValueType.VECTOR) { + } else if (type === MapRepresentation.VECTOR) { this.writeIndexVector(indexValue.mapValue!, encoder); - } else if (type === SpecialMapValueType.MAX_KEY) { - this.writeValueTypeLabel(encoder, INDEX_TYPE_MAX_VALUE); - } else if (type === SpecialMapValueType.MIN_KEY) { + } else if (type === MapRepresentation.MAX_KEY) { + this.writeValueTypeLabel(encoder, INDEX_TYPE_MAX_KEY); + } else if (type === MapRepresentation.MIN_KEY) { this.writeValueTypeLabel(encoder, INDEX_TYPE_MIN_KEY); - } else if (type === SpecialMapValueType.BSON_BINARY) { + } else if (type === MapRepresentation.BSON_BINARY) { this.writeIndexBsonBinaryData(indexValue.mapValue!, encoder); - } else if (type === SpecialMapValueType.REGEX) { + } else if (type === MapRepresentation.REGEX) { this.writeIndexRegex(indexValue.mapValue!, encoder); - } else if (type === SpecialMapValueType.BSON_TIMESTAMP) { + } else if (type === MapRepresentation.BSON_TIMESTAMP) { this.writeIndexBsonTimestamp(indexValue.mapValue!, encoder); - } else if (type === SpecialMapValueType.BSON_OBJECT_ID) { + } else if (type === MapRepresentation.BSON_OBJECT_ID) { this.writeIndexBsonObjectId(indexValue.mapValue!, encoder); - } else if (type === SpecialMapValueType.INT32) { + } else if (type === MapRepresentation.INT32) { this.writeValueTypeLabel(encoder, INDEX_TYPE_NUMBER); encoder.writeNumber( normalizeNumber( @@ -237,7 +241,9 @@ export class FirestoreIndexValueWriter { const segments: string[] = referenceValue .split('/') .filter(segment => segment.length > 0); - const path = DocumentKey.fromSegments(segments.slice(5)).path; + const path = DocumentKey.fromSegments( + segments.slice(DOCUMENT_NAME_OFFSET) + ).path; path.forEach(segment => { this.writeValueTypeLabel(encoder, INDEX_TYPE_REFERENCE_SEGMENT); this.writeUnlabeledIndexString(segment, encoder); diff --git a/packages/firestore/src/model/values.ts b/packages/firestore/src/model/values.ts index 02404130a77..15408892725 100644 --- a/packages/firestore/src/model/values.ts +++ b/packages/firestore/src/model/values.ts @@ -160,7 +160,7 @@ export const MIN_BSON_BINARY_VALUE: Value = { } }; -export enum SpecialMapValueType { +export enum MapRepresentation { REGEX = 'regexValue', BSON_OBJECT_ID = 'bsonObjectIdValue', INT32 = 'int32Value', @@ -195,27 +195,27 @@ export function typeOrder(value: Value): TypeOrder { } else if ('arrayValue' in value) { return TypeOrder.ArrayValue; } else if ('mapValue' in value) { - const valueType = detectSpecialMapType(value); + const valueType = detectMapRepresentation(value); switch (valueType) { - case SpecialMapValueType.SERVER_TIMESTAMP: + case MapRepresentation.SERVER_TIMESTAMP: return TypeOrder.ServerTimestampValue; - case SpecialMapValueType.INTERNAL_MAX: + case MapRepresentation.INTERNAL_MAX: return TypeOrder.MaxValue; - case SpecialMapValueType.VECTOR: + case MapRepresentation.VECTOR: return TypeOrder.VectorValue; - case SpecialMapValueType.REGEX: + case MapRepresentation.REGEX: return TypeOrder.RegexValue; - case SpecialMapValueType.BSON_OBJECT_ID: + case MapRepresentation.BSON_OBJECT_ID: return TypeOrder.BsonObjectIdValue; - case SpecialMapValueType.INT32: + case MapRepresentation.INT32: return TypeOrder.NumberValue; - case SpecialMapValueType.BSON_TIMESTAMP: + case MapRepresentation.BSON_TIMESTAMP: return TypeOrder.BsonTimestampValue; - case SpecialMapValueType.BSON_BINARY: + case MapRepresentation.BSON_BINARY: return TypeOrder.BsonBinaryValue; - case SpecialMapValueType.MIN_KEY: + case MapRepresentation.MIN_KEY: return TypeOrder.MinKeyValue; - case SpecialMapValueType.MAX_KEY: + case MapRepresentation.MAX_KEY: return TypeOrder.MaxKeyValue; default: return TypeOrder.ObjectValue; @@ -319,8 +319,8 @@ function blobEquals(left: Value, right: Value): boolean { export function numberEquals(left: Value, right: Value): boolean { if ( ('integerValue' in left && 'integerValue' in right) || - (detectSpecialMapType(left) === SpecialMapValueType.INT32 && - detectSpecialMapType(right) === SpecialMapValueType.INT32) + (detectMapRepresentation(left) === MapRepresentation.INT32 && + detectMapRepresentation(right) === MapRepresentation.INT32) ) { return extractNumber(left) === extractNumber(right); } else if ('doubleValue' in left && 'doubleValue' in right) { @@ -427,7 +427,7 @@ export function valueCompare(left: Value, right: Value): number { export function extractNumber(value: Value): number { let numberValue; - if (detectSpecialMapType(value) === SpecialMapValueType.INT32) { + if (detectMapRepresentation(value) === MapRepresentation.INT32) { numberValue = value.mapValue!.fields![RESERVED_INT32_KEY].integerValue!; } else { numberValue = value.integerValue || value.doubleValue; @@ -686,10 +686,6 @@ function canonifyValue(value: Value): string { } else if ('arrayValue' in value) { return canonifyArray(value.arrayValue!); } else if ('mapValue' in value) { - // BsonBinaryValue contains an array of bytes, and needs to extract `subtype` and `data` from it before canonifying. - if (detectSpecialMapType(value) === SpecialMapValueType.BSON_BINARY) { - return canonifyBsonBinaryData(value.mapValue!); - } return canonifyMap(value.mapValue!); } else { return fail('Invalid value type: ' + JSON.stringify(value)); @@ -713,19 +709,6 @@ function canonifyReference(referenceValue: string): string { return DocumentKey.fromName(referenceValue).toString(); } -function canonifyBsonBinaryData(mapValue: MapValue): string { - const fields = mapValue!.fields?.[RESERVED_BSON_BINARY_KEY]; - const subtypeAndData = fields?.bytesValue; - if (!subtypeAndData) { - throw new Error('Received incorrect bytesValue for BsonBinaryData'); - } - // Normalize the bytesValue to Uint8Array before extracting subtype and data. - const bytes = normalizeByteString(subtypeAndData).toUint8Array(); - return `{__binary__:{subType:${bytes.at(0)},data:${canonifyByteString( - bytes.slice(1) - )}}}`; -} - function canonifyMap(mapValue: MapValue): string { // Iteration order in JavaScript is not guaranteed. To ensure that we generate // matching canonical IDs for identical maps, we need to sort the keys. @@ -886,9 +869,9 @@ export function isMapValue( return !!value && 'mapValue' in value; } -export function detectSpecialMapType(value: Value): SpecialMapValueType { +export function detectMapRepresentation(value: Value): MapRepresentation { if (!value || !value.mapValue || !value.mapValue.fields) { - return SpecialMapValueType.REGULAR_MAP; // Not a special map type + return MapRepresentation.REGULAR_MAP; // Not a special map type } const fields = value.mapValue.fields; @@ -896,25 +879,31 @@ export function detectSpecialMapType(value: Value): SpecialMapValueType { // Check for type-based mappings const type = fields[TYPE_KEY]?.stringValue; if (type) { - const typeMap: Record = { - [RESERVED_VECTOR_KEY]: SpecialMapValueType.VECTOR, - [RESERVED_MAX_KEY]: SpecialMapValueType.INTERNAL_MAX, - [RESERVED_SERVER_TIMESTAMP_KEY]: SpecialMapValueType.SERVER_TIMESTAMP + const typeMap: Record = { + [RESERVED_VECTOR_KEY]: MapRepresentation.VECTOR, + [RESERVED_MAX_KEY]: MapRepresentation.INTERNAL_MAX, + [RESERVED_SERVER_TIMESTAMP_KEY]: MapRepresentation.SERVER_TIMESTAMP }; if (typeMap[type]) { return typeMap[type]; } } + if (objectSize(fields) !== 1) { + // All BSON types have 1 key in the map. To improve performance, we can + // return early if the number of keys in the map is not 1. + return MapRepresentation.REGULAR_MAP; + } + // Check for BSON-related mappings - const bsonMap: Record = { - [RESERVED_REGEX_KEY]: SpecialMapValueType.REGEX, - [RESERVED_BSON_OBJECT_ID_KEY]: SpecialMapValueType.BSON_OBJECT_ID, - [RESERVED_INT32_KEY]: SpecialMapValueType.INT32, - [RESERVED_BSON_TIMESTAMP_KEY]: SpecialMapValueType.BSON_TIMESTAMP, - [RESERVED_BSON_BINARY_KEY]: SpecialMapValueType.BSON_BINARY, - [RESERVED_MIN_KEY]: SpecialMapValueType.MIN_KEY, - [RESERVED_MAX_KEY]: SpecialMapValueType.MAX_KEY + const bsonMap: Record = { + [RESERVED_REGEX_KEY]: MapRepresentation.REGEX, + [RESERVED_BSON_OBJECT_ID_KEY]: MapRepresentation.BSON_OBJECT_ID, + [RESERVED_INT32_KEY]: MapRepresentation.INT32, + [RESERVED_BSON_TIMESTAMP_KEY]: MapRepresentation.BSON_TIMESTAMP, + [RESERVED_BSON_BINARY_KEY]: MapRepresentation.BSON_BINARY, + [RESERVED_MIN_KEY]: MapRepresentation.MIN_KEY, + [RESERVED_MAX_KEY]: MapRepresentation.MAX_KEY }; for (const key in bsonMap) { @@ -923,20 +912,20 @@ export function detectSpecialMapType(value: Value): SpecialMapValueType { } } - return SpecialMapValueType.REGULAR_MAP; + return MapRepresentation.REGULAR_MAP; } export function isBsonType(value: Value): boolean { const bsonTypes = new Set([ - SpecialMapValueType.REGEX, - SpecialMapValueType.BSON_OBJECT_ID, - SpecialMapValueType.INT32, - SpecialMapValueType.BSON_TIMESTAMP, - SpecialMapValueType.BSON_BINARY, - SpecialMapValueType.MIN_KEY, - SpecialMapValueType.MAX_KEY + MapRepresentation.REGEX, + MapRepresentation.BSON_OBJECT_ID, + MapRepresentation.INT32, + MapRepresentation.BSON_TIMESTAMP, + MapRepresentation.BSON_BINARY, + MapRepresentation.MIN_KEY, + MapRepresentation.MAX_KEY ]); - return bsonTypes.has(detectSpecialMapType(value)); + return bsonTypes.has(detectMapRepresentation(value)); } /** Creates a deep copy of `source`. */ @@ -987,23 +976,23 @@ export function valuesGetLowerBound(value: Value): Value { } else if ('arrayValue' in value) { return { arrayValue: {} }; } else if ('mapValue' in value) { - const type = detectSpecialMapType(value); - if (type === SpecialMapValueType.VECTOR) { + const type = detectMapRepresentation(value); + if (type === MapRepresentation.VECTOR) { return MIN_VECTOR_VALUE; - } else if (type === SpecialMapValueType.BSON_OBJECT_ID) { + } else if (type === MapRepresentation.BSON_OBJECT_ID) { return MIN_BSON_OBJECT_ID_VALUE; - } else if (type === SpecialMapValueType.BSON_TIMESTAMP) { + } else if (type === MapRepresentation.BSON_TIMESTAMP) { return MIN_BSON_TIMESTAMP_VALUE; - } else if (type === SpecialMapValueType.BSON_BINARY) { + } else if (type === MapRepresentation.BSON_BINARY) { return MIN_BSON_BINARY_VALUE; - } else if (type === SpecialMapValueType.REGEX) { + } else if (type === MapRepresentation.REGEX) { return MIN_REGEX_VALUE; - } else if (type === SpecialMapValueType.INT32) { + } else if (type === MapRepresentation.INT32) { // int32Value is treated the same as integerValue and doubleValue return { doubleValue: NaN }; - } else if (type === SpecialMapValueType.MIN_KEY) { + } else if (type === MapRepresentation.MIN_KEY) { return MIN_KEY_VALUE; - } else if (type === SpecialMapValueType.MAX_KEY) { + } else if (type === MapRepresentation.MAX_KEY) { return MAX_KEY_VALUE; } return { mapValue: {} }; @@ -1033,23 +1022,23 @@ export function valuesGetUpperBound(value: Value): Value { } else if ('arrayValue' in value) { return MIN_VECTOR_VALUE; } else if ('mapValue' in value) { - const type = detectSpecialMapType(value); - if (type === SpecialMapValueType.VECTOR) { + const type = detectMapRepresentation(value); + if (type === MapRepresentation.VECTOR) { return { mapValue: {} }; - } else if (type === SpecialMapValueType.BSON_OBJECT_ID) { + } else if (type === MapRepresentation.BSON_OBJECT_ID) { return { geoPointValue: { latitude: -90, longitude: -180 } }; - } else if (type === SpecialMapValueType.BSON_TIMESTAMP) { + } else if (type === MapRepresentation.BSON_TIMESTAMP) { return { stringValue: '' }; - } else if (type === SpecialMapValueType.BSON_BINARY) { + } else if (type === MapRepresentation.BSON_BINARY) { return refValue(DatabaseId.empty(), DocumentKey.empty()); - } else if (type === SpecialMapValueType.REGEX) { + } else if (type === MapRepresentation.REGEX) { return { arrayValue: {} }; - } else if (type === SpecialMapValueType.INT32) { + } else if (type === MapRepresentation.INT32) { // int32Value is treated the same as integerValue and doubleValue return { timestampValue: { seconds: Number.MIN_SAFE_INTEGER } }; - } else if (type === SpecialMapValueType.MIN_KEY) { + } else if (type === MapRepresentation.MIN_KEY) { return { booleanValue: false }; - } else if (type === SpecialMapValueType.MAX_KEY) { + } else if (type === MapRepresentation.MAX_KEY) { return INTERNAL_MAX_VALUE; } return MAX_KEY_VALUE; diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index d4091768e7c..2071b786f24 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -20,7 +20,6 @@ import { Deferred } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import { AutoId } from '../../../src/util/misc'; import { EventsAccumulator } from '../util/events_accumulator'; import { addDoc, @@ -93,7 +92,7 @@ import { checkOnlineAndOfflineResultsMatch, toIds, withTestProjectIdAndCollectionSettings, - checkCacheRoundTrip + assertSDKQueryResultsConsistentWithBackend } from '../util/helpers'; import { DEFAULT_SETTINGS, DEFAULT_PROJECT_ID } from '../util/settings'; @@ -2439,7 +2438,8 @@ apiDescribe('Database', persistence => { }); describe('BSON types', () => { - // TODO(Mila/BSON): simplify the test setup once prod support BSON + // TODO(Mila/BSON): simplify the test setup once prod support BSON and + // remove the cache population after the test helper is updated const NIGHTLY_PROJECT_ID = 'firestore-sdk-nightly'; const settings = { ...DEFAULT_SETTINGS, @@ -2548,7 +2548,10 @@ apiDescribe('Database', persistence => { NIGHTLY_PROJECT_ID, settings, testDocs, - async (coll, db) => { + async coll => { + // Populate the cache with all docs first + await getDocs(coll); + let orderedQuery = query( coll, where('key', '>', bsonObjectId('507f191e810c19729de860ea')), @@ -2560,7 +2563,11 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['b'] ]); - await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + orderedQuery, + testDocs, + toIds(snapshot) + ); orderedQuery = query( coll, @@ -2576,7 +2583,11 @@ apiDescribe('Database', persistence => { testDocs['b'], testDocs['a'] ]); - await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + orderedQuery, + testDocs, + toIds(snapshot) + ); } ); }); @@ -2592,7 +2603,10 @@ apiDescribe('Database', persistence => { NIGHTLY_PROJECT_ID, settings, testDocs, - async (coll, db) => { + async coll => { + // Populate the cache with all docs first + await getDocs(coll); + let orderedQuery = query( coll, where('key', '>=', int32(1)), @@ -2604,7 +2618,11 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['b'] ]); - await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + orderedQuery, + testDocs, + toIds(snapshot) + ); orderedQuery = query( coll, @@ -2617,7 +2635,11 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['a'] ]); - await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + orderedQuery, + testDocs, + toIds(snapshot) + ); } ); }); @@ -2633,7 +2655,10 @@ apiDescribe('Database', persistence => { NIGHTLY_PROJECT_ID, settings, testDocs, - async (coll, db) => { + async coll => { + // Populate the cache with all docs first + await getDocs(coll); + let orderedQuery = query( coll, where('key', '>', bsonTimestamp(1, 1)), @@ -2645,7 +2670,11 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['b'] ]); - await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + orderedQuery, + testDocs, + toIds(snapshot) + ); orderedQuery = query( coll, @@ -2658,7 +2687,11 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['b'] ]); - await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + orderedQuery, + testDocs, + toIds(snapshot) + ); } ); }); @@ -2674,7 +2707,10 @@ apiDescribe('Database', persistence => { NIGHTLY_PROJECT_ID, settings, testDocs, - async (coll, db) => { + async coll => { + // Populate the cache with all docs first + await getDocs(coll); + let orderedQuery = query( coll, where('key', '>', bsonBinaryData(1, new Uint8Array([1, 2, 3]))), @@ -2686,7 +2722,11 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['b'] ]); - await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + orderedQuery, + testDocs, + toIds(snapshot) + ); orderedQuery = query( coll, @@ -2700,7 +2740,11 @@ apiDescribe('Database', persistence => { testDocs['b'], testDocs['a'] ]); - await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + orderedQuery, + testDocs, + toIds(snapshot) + ); } ); }); @@ -2716,7 +2760,10 @@ apiDescribe('Database', persistence => { NIGHTLY_PROJECT_ID, settings, testDocs, - async (coll, db) => { + async coll => { + // Populate the cache with all docs first + await getDocs(coll); + const orderedQuery = query( coll, or( @@ -2731,7 +2778,11 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['a'] ]); - await checkCacheRoundTrip(orderedQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + orderedQuery, + testDocs, + toIds(snapshot) + ); } ); }); @@ -2749,22 +2800,34 @@ apiDescribe('Database', persistence => { NIGHTLY_PROJECT_ID, settings, testDocs, - async (coll, db) => { + async coll => { + // Populate the cache with all docs first + await getDocs(coll); + let filteredQuery = query(coll, where('key', '==', minKey())); let snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ testDocs['a'], testDocs['b'] ]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); - filteredQuery = query(coll, where('key', '!=', minKey())); - snapshot = await getDocs(filteredQuery); - expect(toDataArray(snapshot)).to.deep.equal([ - testDocs['d'], - testDocs['e'] - ]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + // TODO(Mila/BSON): uncomment after the null inclusion bug + // filteredQuery = query(coll, where('key', '!=', minKey())); + // snapshot = await getDocs(filteredQuery); + // expect(toDataArray(snapshot)).to.deep.equal([ + // testDocs['d'], + // testDocs['e'] + // ]); + // await assertSDKQueryResultsConsistentWithBackend( + // filteredQuery, + // testDocs, + // toIds(snapshot) + // ); filteredQuery = query(coll, where('key', '>=', minKey())); snapshot = await getDocs(filteredQuery); @@ -2772,7 +2835,11 @@ apiDescribe('Database', persistence => { testDocs['a'], testDocs['b'] ]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '<=', minKey())); snapshot = await getDocs(filteredQuery); @@ -2780,22 +2847,38 @@ apiDescribe('Database', persistence => { testDocs['a'], testDocs['b'] ]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '>', minKey())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '<', minKey())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '<', 1)); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); } ); }); @@ -2813,22 +2896,34 @@ apiDescribe('Database', persistence => { NIGHTLY_PROJECT_ID, settings, testDocs, - async (coll, db) => { + async coll => { + // Populate the cache with all docs first + await getDocs(coll); + let filteredQuery = query(coll, where('key', '==', maxKey())); let snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ testDocs['c'], testDocs['d'] ]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); - filteredQuery = query(coll, where('key', '!=', maxKey())); - snapshot = await getDocs(filteredQuery); - expect(toDataArray(snapshot)).to.deep.equal([ - testDocs['a'], - testDocs['b'] - ]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + // TODO(Mila/BSON): uncomment after the null inclusion bug + // filteredQuery = query(coll, where('key', '!=', maxKey())); + // snapshot = await getDocs(filteredQuery); + // expect(toDataArray(snapshot)).to.deep.equal([ + // testDocs['a'], + // testDocs['b'] + // ]); + // await assertSDKQueryResultsConsistentWithBackend( + // filteredQuery, + // testDocs, + // toIds(snapshot) + // ); filteredQuery = query(coll, where('key', '>=', maxKey())); snapshot = await getDocs(filteredQuery); @@ -2836,7 +2931,11 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['d'] ]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '<=', maxKey())); snapshot = await getDocs(filteredQuery); @@ -2844,22 +2943,38 @@ apiDescribe('Database', persistence => { testDocs['c'], testDocs['d'] ]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '>', maxKey())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '<', maxKey())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '>', 1)); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); } ); }); @@ -2878,14 +2993,21 @@ apiDescribe('Database', persistence => { NIGHTLY_PROJECT_ID, settings, testDocs, - async (coll, db) => { + async coll => { + // Populate the cache with all docs first + await getDocs(coll); + let filteredQuery = query(coll, where('key', '==', null)); let snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ testDocs['b'], testDocs['c'] ]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '!=', null)); snapshot = await getDocs(filteredQuery); @@ -2894,7 +3016,11 @@ apiDescribe('Database', persistence => { testDocs['d'], testDocs['e'] ]); - await checkCacheRoundTrip(filteredQuery, db, toDataArray(snapshot)); + await assertSDKQueryResultsConsistentWithBackend( + filteredQuery, + testDocs, + toIds(snapshot) + ); } ); }); @@ -2913,7 +3039,7 @@ apiDescribe('Database', persistence => { NIGHTLY_PROJECT_ID, settings, testDocs, - async (coll, db) => { + async coll => { const orderedQuery = query(coll, orderBy('key', 'asc')); const storeEvent = new EventsAccumulator(); @@ -2955,18 +3081,17 @@ apiDescribe('Database', persistence => { b: { key: regex('^foo', 'i') }, c: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) } }; - return withTestDbsSettings( + return withTestProjectIdAndCollectionSettings( persistence, NIGHTLY_PROJECT_ID, settings, - 1, - async dbs => { - const coll = collection(dbs[0], AutoId.newId()); + {}, + async (coll, db) => { const docA = await addDoc(coll, testDocs['a']); const docB = await addDoc(coll, { key: 'place holder' }); const docC = await addDoc(coll, testDocs['c']); - await runTransaction(dbs[0], async transaction => { + await runTransaction(db, async transaction => { const docSnapshot = await transaction.get(docA); expect(docSnapshot.data()).to.deep.equal(testDocs['a']); transaction.set(docB, testDocs['b']); @@ -3018,30 +3143,37 @@ apiDescribe('Database', persistence => { // TODO(Mila/BSON): remove after prod supports bson, and use `ref` helper function instead const docRef = doc(coll, 'doc'); await setDoc(doc(coll, 'm'), { key: docRef }); + testDocs['m'] = { key: docRef }; + + // Populate the cache with all docs first + await getDocs(coll); const orderedQuery = query(coll, orderBy('key', 'desc')); - await checkOnlineAndOfflineResultsMatch( + await assertSDKQueryResultsConsistentWithBackend( orderedQuery, - 't', - 's', - 'r', - 'q', - 'p', - 'o', - 'n', - 'm', - 'l', - 'k', - 'j', - 'i', - 'h', - 'g', - 'f', - 'e', - 'd', - 'c', - 'b', - 'a' + testDocs, + [ + 't', + 's', + 'r', + 'q', + 'p', + 'o', + 'n', + 'm', + 'l', + 'k', + 'j', + 'i', + 'h', + 'g', + 'f', + 'e', + 'd', + 'c', + 'b', + 'a' + ] ); } ); @@ -3076,28 +3208,34 @@ apiDescribe('Database', persistence => { settings, testDocs, async coll => { + // Populate the cache with all docs first + await getDocs(coll); + const orderedQuery = query(coll, orderBy('key')); - await checkOnlineAndOfflineResultsMatch( + await assertSDKQueryResultsConsistentWithBackend( orderedQuery, - 'r', - 's', - 'd', - 'e', - 'c', - 'f', - 'h', - 'g', - 'j', - 'i', - 'k', - 'n', - 'm', - 'l', - 'q', - 'o', - 'p', - 'a', - 'b' + testDocs, + [ + 'r', + 's', + 'd', + 'e', + 'c', + 'f', + 'h', + 'g', + 'j', + 'i', + 'k', + 'n', + 'm', + 'l', + 'q', + 'o', + 'p', + 'a', + 'b' + ] ); } ); diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index 2c789ec9151..092721e113c 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -18,6 +18,7 @@ import { isIndexedDBAvailable } from '@firebase/util'; import { expect } from 'chai'; +import { EventsAccumulator } from './events_accumulator'; import { clearIndexedDbPersistence, collection, @@ -45,8 +46,7 @@ import { getDocsFromServer, getDocsFromCache, _AutoId, - disableNetwork, - enableNetwork + onSnapshot } from './firebase_export'; import { ALT_PROJECT_ID, @@ -596,32 +596,74 @@ export async function checkOnlineAndOfflineResultsMatch( } /** - * Checks that documents fetched from the server and stored in the cache can be - * successfully retrieved from the cache and matches the expected documents. - * - * This function performs the following steps: - * 1. Fetch documents from the server for provided query and populate the cache. - * 2. Disables the network connection to simulate offline mode. - * 3. Retrieves the documents from the cache using the same query. - * 4. Compares the cached documents with the expected documents. - * - * @param query The query to check. - * @param db The Firestore database instance. - * @param expectedDocs Optional ordered list of document data that are expected to be retrieved from the cache. + * Asserts that the given query produces the expected result for all of the + * following scenarios: + * 1. Performing the given query using source=server, compare with expected result and populate + * cache. + * 2. Performing the given query using source=cache, compare with server result and expected + * result. + * 3. Using a snapshot listener to raise snapshots from cache and server, compare them with + * expected result. + * @param {firebase.firestore.Query} query The query to test. + * @param {Object>} allData A map of document IDs to their data. + * @param {string[]} expectedDocIds An array of expected document IDs in the result. + * @returns {Promise} A Promise that resolves when the assertions are complete. */ -export async function checkCacheRoundTrip( +export async function assertSDKQueryResultsConsistentWithBackend( query: Query, - db: Firestore, - expectedDocs: DocumentData[] + allData: { [key: string]: DocumentData }, + expectedDocIds: string[] ): Promise { - await getDocsFromServer(query); + // Check the cache round trip first to make sure cache is properly populated, otherwise the + // snapshot listener below will return partial results from previous + // "assertSDKQueryResultsConsistentWithBackend" calls if it is called multiple times in one test + await checkOnlineAndOfflineResultsMatch(query, ...expectedDocIds); + + const eventAccumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + query, + { includeMetadataChanges: true }, + eventAccumulator.storeEvent + ); + let watchSnapshots; + try { + watchSnapshots = await eventAccumulator.awaitEvents(2); + } finally { + unsubscribe(); + } - await disableNetwork(db); - const docsFromCache = await getDocsFromCache(query); + expect(watchSnapshots[0].metadata.fromCache).to.be.true; + verifySnapshot(watchSnapshots[0], allData, expectedDocIds); + expect(watchSnapshots[1].metadata.fromCache).to.be.false; + verifySnapshot(watchSnapshots[1], allData, expectedDocIds); +} - if (expectedDocs.length !== 0) { - expect(expectedDocs).to.deep.equal(toDataArray(docsFromCache)); +/** + * Verifies that a QuerySnapshot matches the expected data and document IDs. + * @param {firebase.firestore.QuerySnapshot} snapshot The QuerySnapshot to verify. + * @param {Object>} allData A map of document IDs to their data. + * @param {string[]} expectedDocIds An array of expected document IDs in the result. + */ +function verifySnapshot( + snapshot: QuerySnapshot, + allData: { [key: string]: DocumentData }, + expectedDocIds: string[] +): void { + const snapshotDocIds = toIds(snapshot); + expect( + expectedDocIds.length === snapshotDocIds.length, + `Did not get the same document size. Expected doc size: ${expectedDocIds.length}, Actual doc size: ${snapshotDocIds.length} ` + ).to.be.true; + + expect( + expectedDocIds.every((id, index) => id === snapshotDocIds[index]), + `Did not get the expected document IDs. Expected doc IDs: ${expectedDocIds}, Actual doc IDs: ${snapshotDocIds} ` + ).to.be.true; + + const actualDocs = toDataMap(snapshot); + for (const docId of expectedDocIds) { + const expectedDoc = allData[docId]; + const actualDoc = actualDocs[docId]; + expect(expectedDoc).to.deep.equal(actualDoc); } - - await enableNetwork(db); } diff --git a/packages/firestore/test/unit/local/index_manager.test.ts b/packages/firestore/test/unit/local/index_manager.test.ts index 51bef76b31e..1097f5e682e 100644 --- a/packages/firestore/test/unit/local/index_manager.test.ts +++ b/packages/firestore/test/unit/local/index_manager.test.ts @@ -2371,64 +2371,64 @@ describe('IndexedDbIndexManager', async () => { await indexManager.addFieldIndex( fieldIndex('coll', { fields: [['key', IndexKind.DESCENDING]] }) ); - await addDoc('coll/a', { + await addDoc('coll/doc1', { key: null }); - await addDoc('coll/b', { + await addDoc('coll/doc2', { key: minKey() }); - await addDoc('coll/c', { + await addDoc('coll/doc3', { key: true }); - await addDoc('coll/d', { + await addDoc('coll/doc4', { key: NaN }); - await addDoc('coll/e', { + await addDoc('coll/doc5', { key: int32(1) }); - await addDoc('coll/f', { + await addDoc('coll/doc6', { key: 2.0 }); - await addDoc('coll/g', { + await addDoc('coll/doc7', { key: 3 }); - await addDoc('coll/h', { + await addDoc('coll/doc8', { key: new Timestamp(100, 123456000) }); - await addDoc('coll/i', { + await addDoc('coll/doc9', { key: bsonTimestamp(1, 2) }); - await addDoc('coll/j', { + await addDoc('coll/doc10', { key: 'string' }); - await addDoc('coll/k', { + await addDoc('coll/doc11', { key: Bytes.fromUint8Array(new Uint8Array([0, 1, 255])) as Bytes }); - await addDoc('coll/l', { + await addDoc('coll/doc12', { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }); - await addDoc('coll/m', { + await addDoc('coll/doc13', { key: ref('coll/doc') }); - await addDoc('coll/n', { + await addDoc('coll/doc14', { key: bsonObjectId('507f191e810c19729de860ea') }); - await addDoc('coll/o', { + await addDoc('coll/doc15', { key: new GeoPoint(0, 1) }); - await addDoc('coll/p', { + await addDoc('coll/doc16', { key: regex('^foo', 'i') }); - await addDoc('coll/q', { + await addDoc('coll/doc17', { key: [1, 2] }); - await addDoc('coll/r', { + await addDoc('coll/doc18', { key: vector([1, 2]) }); - await addDoc('coll/s', { + await addDoc('coll/doc19', { key: { a: 1 } }); - await addDoc('coll/t', { + await addDoc('coll/doc20', { key: maxKey() }); @@ -2438,26 +2438,26 @@ describe('IndexedDbIndexManager', async () => { const q = queryWithAddedOrderBy(query('coll'), orderBy('key', 'desc')); await verifyResults( q, - 'coll/t', - 'coll/s', - 'coll/r', - 'coll/q', - 'coll/p', - 'coll/o', - 'coll/n', - 'coll/m', - 'coll/l', - 'coll/k', - 'coll/j', - 'coll/i', - 'coll/h', - 'coll/g', - 'coll/f', - 'coll/e', - 'coll/d', - 'coll/c', - 'coll/b', - 'coll/a' + 'coll/doc20', + 'coll/doc19', + 'coll/doc18', + 'coll/doc17', + 'coll/doc16', + 'coll/doc15', + 'coll/doc14', + 'coll/doc13', + 'coll/doc12', + 'coll/doc11', + 'coll/doc10', + 'coll/doc9', + 'coll/doc8', + 'coll/doc7', + 'coll/doc6', + 'coll/doc5', + 'coll/doc4', + 'coll/doc3', + 'coll/doc2', + 'coll/doc1' ); }); diff --git a/packages/firestore/test/unit/local/local_store_indexeddb.test.ts b/packages/firestore/test/unit/local/local_store_indexeddb.test.ts index 6f0275ab4ad..0e5afbc2914 100644 --- a/packages/firestore/test/unit/local/local_store_indexeddb.test.ts +++ b/packages/firestore/test/unit/local/local_store_indexeddb.test.ts @@ -18,17 +18,28 @@ import { isIndexedDBAvailable } from '@firebase/util'; import { expect } from 'chai'; -import { serverTimestamp, Timestamp } from '../../../src'; +import { serverTimestamp, Timestamp, GeoPoint } from '../../../src'; import { User } from '../../../src/auth/user'; import { BundleConverterImpl } from '../../../src/core/bundle_impl'; import { LimitType, + newQueryComparator, Query, queryToTarget, queryWithLimit } from '../../../src/core/query'; import { Target } from '../../../src/core/target'; import { TargetId } from '../../../src/core/types'; +import { + bsonBinaryData, + bsonObjectId, + bsonTimestamp, + int32, + maxKey, + minKey, + regex, + vector +} from '../../../src/lite-api/field_value_impl'; import { IndexBackfiller } from '../../../src/local/index_backfiller'; import { LocalStore } from '../../../src/local/local_store'; import { @@ -44,6 +55,7 @@ import { } from '../../../src/local/local_store_impl'; import { Persistence } from '../../../src/local/persistence'; import { DocumentMap } from '../../../src/model/collections'; +import { Document } from '../../../src/model/document'; import { DocumentKey } from '../../../src/model/document_key'; import { FieldIndex, @@ -53,6 +65,7 @@ import { import { Mutation, MutationType } from '../../../src/model/mutation'; import { MutationBatch } from '../../../src/model/mutation_batch'; import { RemoteEvent } from '../../../src/remote/remote_event'; +import { SortedSet } from '../../../src/util/sorted_set'; import { deletedDoc, deleteMutation, @@ -65,8 +78,10 @@ import { orderBy, orFilter, query, + ref, setMutation, - version + version, + blob } from '../../util/helpers'; import { CountingQueryEngine } from './counting_query_engine'; @@ -208,11 +223,20 @@ class AsyncLocalStoreTester { } } - assertQueryReturned(...keys: string[]): void { + assertQueryReturned(query: Query, ...keys: string[]): void { expect(this.lastChanges).to.exist; - for (const k of keys) { - expect(this.lastChanges?.get(key(k))).to.exist; - } + expect(this.lastChanges?.size === keys.length).to.be.true; + + // lastChanges is a DocumentMap sorted by document keys. Re-sort the documents by the query comparator. + let returnedDocs = new SortedSet(newQueryComparator(query)); + this.lastChanges!.forEach((key, doc) => { + returnedDocs = returnedDocs.add(doc); + }); + + let i = 0; + returnedDocs.forEach(doc => { + expect(keys[i++]).to.equal(doc.key.path.toString()); + }); } async backfillIndexes(config?: { @@ -331,7 +355,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(queryMatches); test.assertRemoteDocumentsRead(1, 0); - test.assertQueryReturned('coll/a'); + test.assertQueryReturned(queryMatches, 'coll/a'); await test.applyRemoteEvent( docUpdateRemoteEvent(deletedDoc('coll/a', 0), [targetId]) @@ -340,7 +364,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { // No backfill needed for deleted document. await test.executeQuery(queryMatches); test.assertRemoteDocumentsRead(0, 0); - test.assertQueryReturned(); + test.assertQueryReturned(queryMatches); }); it('Uses Indexes', async () => { @@ -360,7 +384,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(queryMatches); test.assertRemoteDocumentsRead(1, 0); - test.assertQueryReturned('coll/a'); + test.assertQueryReturned(queryMatches, 'coll/a'); }); it('Uses Partially Indexed Remote Documents When Available', async () => { @@ -384,7 +408,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(queryMatches); test.assertRemoteDocumentsRead(1, 1); - test.assertQueryReturned('coll/a', 'coll/b'); + test.assertQueryReturned(queryMatches, 'coll/a', 'coll/b'); }); it('Uses Partially Indexed Overlays When Available', async () => { @@ -405,7 +429,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { [key('coll/a').toString()]: MutationType.Set, [key('coll/b').toString()]: MutationType.Set }); - test.assertQueryReturned('coll/a', 'coll/b'); + test.assertQueryReturned(queryMatches, 'coll/a', 'coll/b'); }); it('Does Not Use Limit When Index Is Outdated', async () => { @@ -443,7 +467,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { test.assertOverlaysRead(5, 1, { [key('coll/b').toString()]: MutationType.Delete }); - test.assertQueryReturned('coll/a', 'coll/c'); + test.assertQueryReturned(queryCount, 'coll/a', 'coll/c'); }); it('Uses Index For Limit Query When Index Is Updated', async () => { @@ -476,7 +500,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(queryCount); test.assertRemoteDocumentsRead(2, 0); test.assertOverlaysRead(2, 0, {}); - test.assertQueryReturned('coll/a', 'coll/c'); + test.assertQueryReturned(queryCount, 'coll/a', 'coll/c'); }); it('Indexes Server Timestamps', async () => { @@ -496,7 +520,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { test.assertOverlaysRead(1, 0, { [key('coll/a').toString()]: MutationType.Set }); - test.assertQueryReturned('coll/a'); + test.assertQueryReturned(queryTime, 'coll/a'); }); it('can auto-create indexes', async () => { @@ -522,7 +546,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { // Full matched index should be created. await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); await test.backfillIndexes(); @@ -532,7 +556,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(query_); test.assertRemoteDocumentsRead(2, 1); - test.assertQueryReturned('coll/a', 'coll/e', 'coll/f'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e', 'coll/f'); }); it('can auto-create indexes works with or query', async () => { @@ -561,7 +585,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { // Full matched index should be created. await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); await test.backfillIndexes(); @@ -571,7 +595,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(query_); test.assertRemoteDocumentsRead(2, 1); - test.assertQueryReturned('coll/a', 'coll/e', 'coll/f'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e', 'coll/f'); }); it('does not auto-create indexes for small collections', async () => { @@ -597,7 +621,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { // SDK will not create indexes since collection size is too small. await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/e', 'coll/a'); await test.backfillIndexes(); @@ -607,7 +631,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 3); - test.assertQueryReturned('coll/a', 'coll/e', 'coll/f'); + test.assertQueryReturned(query_, 'coll/e', 'coll/f', 'coll/a'); }); it('does not auto create indexes when index lookup is expensive', async () => { @@ -632,7 +656,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { // SDK will not create indexes since relative read cost is too large. await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); await test.backfillIndexes(); @@ -642,7 +666,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 3); - test.assertQueryReturned('coll/a', 'coll/e', 'coll/f'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e', 'coll/f'); }); it('index auto creation works when backfiller runs halfway', async () => { @@ -680,7 +704,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { // Full matched index should be created. await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); await test.backfillIndexes({ maxDocumentsToProcess: 2 }); @@ -692,7 +716,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(query_); test.assertRemoteDocumentsRead(1, 2); - test.assertQueryReturned('coll/a', 'coll/e', 'coll/f'); + test.assertQueryReturned(query_, 'coll/a', 'coll/f', 'coll/e'); }); it('index created by index auto creation exists after turn off auto creation', async () => { @@ -718,7 +742,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { // Full matched index should be created. await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/e', 'coll/a'); test.configureIndexAutoCreation({ isEnabled: false }); await test.backfillIndexes(); @@ -729,7 +753,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(query_); test.assertRemoteDocumentsRead(2, 1); - test.assertQueryReturned('coll/a', 'coll/e', 'coll/f'); + test.assertQueryReturned(query_, 'coll/e', 'coll/a', 'coll/f'); }); it('disable index auto creation works', async () => { @@ -757,13 +781,13 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { // Full matched index should be created. await test.executeQuery(query1); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query1, 'coll/a', 'coll/e'); test.configureIndexAutoCreation({ isEnabled: false }); await test.backfillIndexes(); await test.executeQuery(query1); test.assertRemoteDocumentsRead(2, 0); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query1, 'coll/a', 'coll/e'); const targetId2 = await test.allocateQuery(query2); await test.applyRemoteEvents( @@ -776,14 +800,14 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(query2); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('foo/a', 'foo/e'); + test.assertQueryReturned(query2, 'foo/a', 'foo/e'); await test.backfillIndexes(); // Run the query in second time, test index won't be created await test.executeQuery(query2); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('foo/a', 'foo/e'); + test.assertQueryReturned(query2, 'foo/a', 'foo/e'); }); it('index auto creation works with mutation', async () => { @@ -811,7 +835,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); await test.writeMutations(deleteMutation('coll/e')); await test.backfillIndexes(); @@ -820,7 +844,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(query_); test.assertRemoteDocumentsRead(1, 0); test.assertOverlaysRead(1, 1); - test.assertQueryReturned('coll/a', 'coll/f'); + test.assertQueryReturned(query_, 'coll/a', 'coll/f'); }); it('delete all indexes works with index auto creation', async () => { @@ -847,24 +871,24 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { // Full matched index should be created. await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); await test.backfillIndexes(); await test.executeQuery(query_); test.assertRemoteDocumentsRead(2, 0); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); await test.deleteAllFieldIndexes(); await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); // Field index is created again. await test.backfillIndexes(); await test.executeQuery(query_); test.assertRemoteDocumentsRead(2, 0); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); }); it('delete all indexes works with manual added indexes', async () => { @@ -884,13 +908,13 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.executeQuery(query_); test.assertRemoteDocumentsRead(1, 0); - test.assertQueryReturned('coll/a'); + test.assertQueryReturned(query_, 'coll/a'); await test.deleteAllFieldIndexes(); await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 1); - test.assertQueryReturned('coll/a'); + test.assertQueryReturned(query_, 'coll/a'); }); it('index auto creation does not work with multiple inequality', async () => { @@ -930,11 +954,739 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { // support multiple inequality. await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); await test.backfillIndexes(); await test.executeQuery(query_); test.assertRemoteDocumentsRead(0, 2); - test.assertQueryReturned('coll/a', 'coll/e'); + test.assertQueryReturned(query_, 'coll/a', 'coll/e'); + }); + + describe('BSON type indexing', () => { + it('Indexes BSON ObjectId fields', async () => { + const index = fieldIndex('coll', { + id: 1, + fields: [['key', IndexKind.ASCENDING]] + }); + await test.configureFieldsIndexes(index); + + await test.writeMutations( + setMutation('coll/a', { + key: bsonObjectId('507f191e810c19729de860ea') + }), + setMutation('coll/b', { + key: bsonObjectId('507f191e810c19729de860eb') + }), + setMutation('coll/c', { key: bsonObjectId('507f191e810c19729de860ec') }) + ); + await test.backfillIndexes(); + + let query_ = query('coll', orderBy('key', 'asc')); + await test.executeQuery(query_); + test.assertOverlaysRead(3, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b', 'coll/c'); + + query_ = query( + 'coll', + filter('key', '==', bsonObjectId('507f191e810c19729de860ea')) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(1, 0, { + [key('coll/a').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a'); + + query_ = query( + 'coll', + filter('key', '!=', bsonObjectId('507f191e810c19729de860ea')) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/b', 'coll/c'); + + query_ = query( + 'coll', + filter('key', '>=', bsonObjectId('507f191e810c19729de860eb')) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/b', 'coll/c'); + + query_ = query( + 'coll', + filter('key', '<', bsonObjectId('507f191e810c19729de860ea')) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(0, 0); + test.assertQueryReturned(query_); + + query_ = query( + 'coll', + filter('key', 'in', [ + bsonObjectId('507f191e810c19729de860ea'), + bsonObjectId('507f191e810c19729de860eb') + ]) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b'); + + query_ = query( + 'coll', + filter('key', 'not-in', [ + bsonObjectId('507f191e810c19729de860ea'), + bsonObjectId('507f191e810c19729de860eb') + ]) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(1, 0, { + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/c'); + }); + + it('Indexes BSON Timestamp fields', async () => { + const index = fieldIndex('coll', { + id: 1, + fields: [['key', IndexKind.ASCENDING]] + }); + await test.configureFieldsIndexes(index); + await test.writeMutations( + setMutation('coll/a', { key: bsonTimestamp(1000, 1000) }), + setMutation('coll/b', { key: bsonTimestamp(1001, 1000) }), + setMutation('coll/c', { key: bsonTimestamp(1000, 1001) }) + ); + await test.backfillIndexes(); + + let query_ = query('coll', orderBy('key', 'asc')); + await test.executeQuery(query_); + test.assertOverlaysRead(3, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/c', 'coll/b'); + + query_ = query('coll', filter('key', '==', bsonTimestamp(1000, 1000))); + await test.executeQuery(query_); + test.assertOverlaysRead(1, 0, { + [key('coll/a').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a'); + + query_ = query('coll', filter('key', '!=', bsonTimestamp(1000, 1000))); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/c', 'coll/b'); + + query_ = query('coll', filter('key', '>=', bsonTimestamp(1000, 1001))); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/c', 'coll/b'); + + query_ = query('coll', filter('key', '<', bsonTimestamp(1000, 1000))); + await test.executeQuery(query_); + test.assertOverlaysRead(0, 0); + test.assertQueryReturned(query_); + + query_ = query( + 'coll', + filter('key', 'in', [ + bsonTimestamp(1000, 1000), + bsonTimestamp(1001, 1000) + ]) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b'); + + query_ = query( + 'coll', + filter('key', 'not-in', [ + bsonTimestamp(1000, 1000), + bsonTimestamp(1001, 1000) + ]) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(1, 0, { + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/c'); + }); + + it('Indexes BSON Binary Data fields', async () => { + const index = fieldIndex('coll', { + id: 1, + fields: [['key', IndexKind.ASCENDING]] + }); + await test.configureFieldsIndexes(index); + await test.writeMutations( + setMutation('coll/a', { + key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + }), + setMutation('coll/b', { + key: bsonBinaryData(1, new Uint8Array([1, 2])) + }), + setMutation('coll/c', { + key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) + }), + setMutation('coll/d', { + key: bsonBinaryData(2, new Uint8Array([1, 2])) + }) + ); + await test.backfillIndexes(); + + let query_ = query('coll', orderBy('key', 'asc')); + await test.executeQuery(query_); + test.assertOverlaysRead(4, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set, + [key('coll/d').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/b', 'coll/a', 'coll/c', 'coll/d'); + + query_ = query( + 'coll', + filter('key', '==', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(1, 0, { + [key('coll/a').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a'); + + query_ = query( + 'coll', + filter('key', '!=', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(3, 0, { + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set, + [key('coll/d').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/b', 'coll/c', 'coll/d'); + + query_ = query( + 'coll', + filter('key', '>=', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(3, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set, + [key('coll/d').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/c', 'coll/d'); + + query_ = query( + 'coll', + filter('key', '<', bsonBinaryData(1, new Uint8Array([1, 2]))) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(0, 0); + test.assertQueryReturned(query_); + + query_ = query( + 'coll', + filter('key', 'in', [ + bsonBinaryData(1, new Uint8Array([1, 2, 3])), + bsonBinaryData(1, new Uint8Array([1, 2])) + ]) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set + }); + // Note that `in` does not add implicit ordering, so the result is ordered by keys + test.assertQueryReturned(query_, 'coll/a', 'coll/b'); + + query_ = query( + 'coll', + filter('key', 'not-in', [ + bsonBinaryData(1, new Uint8Array([1, 2, 3])), + bsonBinaryData(1, new Uint8Array([1, 2])) + ]) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/c').toString()]: MutationType.Set, + [key('coll/d').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/c', 'coll/d'); + }); + + it('Indexes BSON Int32 fields', async () => { + const index = fieldIndex('coll', { + id: 1, + fields: [['key', IndexKind.ASCENDING]] + }); + await test.configureFieldsIndexes(index); + await test.writeMutations( + setMutation('coll/a', { key: int32(-1) }), + setMutation('coll/b', { key: int32(0) }), + setMutation('coll/c', { key: int32(1) }) + ); + await test.backfillIndexes(); + + let query_ = query('coll', orderBy('key', 'asc')); + await test.executeQuery(query_); + test.assertOverlaysRead(3, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b', 'coll/c'); + + query_ = query('coll', filter('key', '==', int32(0))); + await test.executeQuery(query_); + test.assertOverlaysRead(1, 0, { + [key('coll/b').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/b'); + + query_ = query('coll', filter('key', '!=', int32(0))); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/c'); + + query_ = query('coll', filter('key', '>=', int32(0))); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/b', 'coll/c'); + + query_ = query('coll', filter('key', '<', int32(-1))); + await test.executeQuery(query_); + test.assertOverlaysRead(0, 0); + test.assertQueryReturned(query_); + + query_ = query('coll', filter('key', 'in', [int32(0), int32(1)])); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/b', 'coll/c'); + + query_ = query('coll', filter('key', 'not-in', [int32(0), int32(1)])); + await test.executeQuery(query_); + test.assertOverlaysRead(1, 0, { + [key('coll/a').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a'); + }); + + it('Indexes BSON Regex fields', async () => { + const index = fieldIndex('coll', { + id: 1, + fields: [['key', IndexKind.ASCENDING]] + }); + await test.configureFieldsIndexes(index); + await test.writeMutations( + setMutation('coll/a', { key: regex('a', 'i') }), + setMutation('coll/b', { key: regex('a', 'm') }), + setMutation('coll/c', { key: regex('b', 'i') }) + ); + await test.backfillIndexes(); + + let query_ = query('coll', orderBy('key', 'asc')); + await test.executeQuery(query_); + test.assertOverlaysRead(3, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b', 'coll/c'); + + query_ = query('coll', filter('key', '==', regex('a', 'i'))); + await test.executeQuery(query_); + test.assertOverlaysRead(1, 0, { + [key('coll/a').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a'); + + query_ = query('coll', filter('key', '!=', regex('a', 'i'))); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/b', 'coll/c'); + + query_ = query('coll', filter('key', '>=', regex('a', 'm'))); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/b', 'coll/c'); + + query_ = query('coll', filter('key', '<', regex('a', 'i'))); + await test.executeQuery(query_); + test.assertOverlaysRead(0, 0); + test.assertQueryReturned(query_); + + query_ = query( + 'coll', + filter('key', 'in', [regex('a', 'i'), regex('a', 'm')]) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b'); + + query_ = query( + 'coll', + filter('key', 'not-in', [regex('a', 'i'), regex('a', 'm')]) + ); + await test.executeQuery(query_); + test.assertOverlaysRead(1, 0, { + [key('coll/c').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/c'); + }); + + it('Indexes BSON minKey fields', async () => { + const index = fieldIndex('coll', { + id: 1, + fields: [['key', IndexKind.ASCENDING]] + }); + await test.configureFieldsIndexes(index); + await test.writeMutations( + setMutation('coll/a', { key: minKey() }), + setMutation('coll/b', { key: minKey() }), + setMutation('coll/c', { key: null }), + setMutation('coll/d', { key: 1 }), + setMutation('coll/e', { key: maxKey() }) + ); + await test.backfillIndexes(); + + let query_ = query('coll', orderBy('key', 'asc')); + await test.executeQuery(query_); + test.assertOverlaysRead(5, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set, + [key('coll/d').toString()]: MutationType.Set, + [key('coll/e').toString()]: MutationType.Set + }); + test.assertQueryReturned( + query_, + 'coll/c', + 'coll/a', + 'coll/b', + 'coll/d', + 'coll/e' + ); + + query_ = query('coll', filter('key', '==', minKey())); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b'); + + query_ = query('coll', filter('key', '!=', minKey())); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/d').toString()]: MutationType.Set, + [key('coll/e').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/d', 'coll/e'); + + query_ = query('coll', filter('key', '>=', minKey())); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b'); + + query_ = query('coll', filter('key', '<', minKey())); + await test.executeQuery(query_); + test.assertOverlaysRead(0, 0, {}); + test.assertQueryReturned(query_); + + query_ = query('coll', filter('key', 'in', [minKey()])); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b'); + + query_ = query('coll', filter('key', 'not-in', [minKey()])); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/d').toString()]: MutationType.Set, + [key('coll/e').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/d', 'coll/e'); + }); + + it('Indexes BSON maxKey fields', async () => { + const index = fieldIndex('coll', { + id: 1, + fields: [['key', IndexKind.ASCENDING]] + }); + await test.configureFieldsIndexes(index); + await test.writeMutations( + setMutation('coll/a', { key: maxKey() }), + setMutation('coll/b', { key: maxKey() }), + setMutation('coll/c', { key: null }), + setMutation('coll/d', { key: 1 }), + setMutation('coll/e', { key: minKey() }) + ); + await test.backfillIndexes(); + + let query_ = query('coll', orderBy('key', 'asc')); + await test.executeQuery(query_); + test.assertOverlaysRead(5, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set, + [key('coll/d').toString()]: MutationType.Set, + [key('coll/e').toString()]: MutationType.Set + }); + test.assertQueryReturned( + query_, + 'coll/c', + 'coll/e', + 'coll/d', + 'coll/a', + 'coll/b' + ); + + query_ = query('coll', filter('key', '==', maxKey())); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b'); + + query_ = query('coll', filter('key', '!=', maxKey())); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/d').toString()]: MutationType.Set, + [key('coll/e').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/e', 'coll/d'); + + query_ = query('coll', filter('key', '<=', maxKey())); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b'); + + query_ = query('coll', filter('key', '>', maxKey())); + await test.executeQuery(query_); + test.assertOverlaysRead(0, 0, {}); + test.assertQueryReturned(query_); + + query_ = query('coll', filter('key', '<', maxKey())); + await test.executeQuery(query_); + test.assertOverlaysRead(0, 0, {}); + test.assertQueryReturned(query_); + + query_ = query('coll', filter('key', 'in', [maxKey()])); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/a', 'coll/b'); + + query_ = query('coll', filter('key', 'not-in', [maxKey()])); + await test.executeQuery(query_); + test.assertOverlaysRead(2, 0, { + [key('coll/d').toString()]: MutationType.Set, + [key('coll/e').toString()]: MutationType.Set + }); + test.assertQueryReturned(query_, 'coll/e', 'coll/d'); + }); + + it('Indexes multiple BSON types together', async () => { + const index = fieldIndex('coll', { + id: 1, + fields: [['key', IndexKind.DESCENDING]] + }); + await test.configureFieldsIndexes(index); + + await test.writeMutations( + setMutation('coll/a', { key: minKey() }), + setMutation('coll/b', { key: int32(2) }), + setMutation('coll/c', { key: int32(1) }), + setMutation('coll/d', { key: bsonTimestamp(1000, 1001) }), + setMutation('coll/e', { key: bsonTimestamp(1000, 1000) }), + setMutation('coll/f', { + key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) + }), + setMutation('coll/g', { + key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + }), + setMutation('coll/h', { + key: bsonObjectId('507f191e810c19729de860eb') + }), + setMutation('coll/i', { + key: bsonObjectId('507f191e810c19729de860ea') + }), + setMutation('coll/j', { key: regex('^bar', 'm') }), + setMutation('coll/k', { key: regex('^bar', 'i') }), + setMutation('coll/l', { key: maxKey() }) + ); + await test.backfillIndexes(); + + const query_ = query('coll', orderBy('key', 'desc')); + await test.executeQuery(query_); + test.assertOverlaysRead(12, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set, + [key('coll/d').toString()]: MutationType.Set, + [key('coll/e').toString()]: MutationType.Set, + [key('coll/f').toString()]: MutationType.Set, + [key('coll/g').toString()]: MutationType.Set, + [key('coll/h').toString()]: MutationType.Set, + [key('coll/i').toString()]: MutationType.Set, + [key('coll/j').toString()]: MutationType.Set, + [key('coll/k').toString()]: MutationType.Set, + [key('coll/l').toString()]: MutationType.Set + }); + test.assertQueryReturned( + query_, + 'coll/l', + 'coll/j', + 'coll/k', + 'coll/h', + 'coll/i', + 'coll/f', + 'coll/g', + 'coll/d', + 'coll/e', + 'coll/b', + 'coll/c', + 'coll/a' + ); + }); + + it('Indexes all types together', async () => { + const index = fieldIndex('coll', { + id: 1, + fields: [['key', IndexKind.ASCENDING]] + }); + await test.configureFieldsIndexes(index); + + await test.writeMutations( + setMutation('coll/a', { key: null }), + setMutation('coll/b', { key: minKey() }), + setMutation('coll/c', { key: true }), + setMutation('coll/d', { key: NaN }), + setMutation('coll/e', { key: int32(1) }), + setMutation('coll/f', { key: 2.0 }), + setMutation('coll/g', { key: 3 }), + setMutation('coll/h', { key: new Timestamp(100, 123456000) }), + setMutation('coll/i', { key: bsonTimestamp(1, 2) }), + setMutation('coll/j', { key: 'string' }), + setMutation('coll/k', { key: blob(1, 2, 3) }), + setMutation('coll/l', { + key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + }), + setMutation('coll/m', { key: ref('foo/bar') }), + setMutation('coll/n', { + key: bsonObjectId('507f191e810c19729de860ea') + }), + setMutation('coll/o', { key: new GeoPoint(1, 2) }), + setMutation('coll/p', { key: regex('^bar', 'm') }), + setMutation('coll/q', { key: [2, 'foo'] }), + setMutation('coll/r', { key: vector([1, 2, 3]) }), + setMutation('coll/s', { key: { bar: 1, foo: 2 } }), + setMutation('coll/t', { key: maxKey() }) + ); + await test.backfillIndexes(); + + const query_ = query('coll', orderBy('key', 'asc')); + await test.executeQuery(query_); + test.assertOverlaysRead(20, 0, { + [key('coll/a').toString()]: MutationType.Set, + [key('coll/b').toString()]: MutationType.Set, + [key('coll/c').toString()]: MutationType.Set, + [key('coll/d').toString()]: MutationType.Set, + [key('coll/e').toString()]: MutationType.Set, + [key('coll/f').toString()]: MutationType.Set, + [key('coll/g').toString()]: MutationType.Set, + [key('coll/h').toString()]: MutationType.Set, + [key('coll/i').toString()]: MutationType.Set, + [key('coll/j').toString()]: MutationType.Set, + [key('coll/k').toString()]: MutationType.Set, + [key('coll/l').toString()]: MutationType.Set, + [key('coll/m').toString()]: MutationType.Set, + [key('coll/n').toString()]: MutationType.Set, + [key('coll/o').toString()]: MutationType.Set, + [key('coll/p').toString()]: MutationType.Set, + [key('coll/q').toString()]: MutationType.Set, + [key('coll/r').toString()]: MutationType.Set, + [key('coll/s').toString()]: MutationType.Set, + [key('coll/t').toString()]: MutationType.Set + }); + test.assertQueryReturned( + query_, + 'coll/a', + 'coll/b', + 'coll/c', + 'coll/d', + 'coll/e', + 'coll/f', + 'coll/g', + 'coll/h', + 'coll/i', + 'coll/j', + 'coll/k', + 'coll/l', + 'coll/m', + 'coll/n', + 'coll/o', + 'coll/p', + 'coll/q', + 'coll/r', + 'coll/s', + 'coll/t' + ); + }); }); }); diff --git a/packages/firestore/test/unit/model/values.test.ts b/packages/firestore/test/unit/model/values.test.ts index 4054dd6481d..0d93d335ded 100644 --- a/packages/firestore/test/unit/model/values.test.ts +++ b/packages/firestore/test/unit/model/values.test.ts @@ -722,7 +722,7 @@ describe('Values', () => { expect(canonicalId(wrap(int32(1)))).to.equal('{__int__:1}'); expect( canonicalId(wrap(bsonBinaryData(1, new Uint8Array([1, 2, 3])))) - ).to.equal('{__binary__:{subType:1,data:AQID}}'); + ).to.equal('{__binary__:AQECAw==}'); expect(canonicalId(wrap(minKey()))).to.equal('{__min__:null}'); expect(canonicalId(wrap(maxKey()))).to.equal('{__max__:null}'); }); From 0e2558a96b1408fa7047b63f3d506939ff1f28ff Mon Sep 17 00:00:00 2001 From: Ehsan Date: Mon, 28 Apr 2025 13:13:42 -0700 Subject: [PATCH 16/23] Improve the integration test coverage for online vs offline comparisons. (#8975) * Improve the integration test coverage for online vs offline comparisons. * prettier. * Add missing cases. --- .../api/composite_index_query.test.ts | 12 ++++++ .../test/integration/api/database.test.ts | 40 ++++++++++++++++--- .../test/integration/api/query.test.ts | 38 +++++++++++++++++- .../util/composite_index_test_helper.ts | 2 + .../test/integration/util/helpers.ts | 29 ++++++++++++-- 5 files changed, 111 insertions(+), 10 deletions(-) diff --git a/packages/firestore/test/integration/api/composite_index_query.test.ts b/packages/firestore/test/integration/api/composite_index_query.test.ts index 04cdafe7169..d08cc77bde9 100644 --- a/packages/firestore/test/integration/api/composite_index_query.test.ts +++ b/packages/firestore/test/integration/api/composite_index_query.test.ts @@ -73,6 +73,7 @@ apiDescribe('Composite Index Queries', persistence => { return testHelper.withTestDocs(persistence, testDocs, async coll => { // a == 1, limit 2, b - desc await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.query( coll, where('a', '==', 1), @@ -97,6 +98,7 @@ apiDescribe('Composite Index Queries', persistence => { return testHelper.withTestDocs(persistence, testDocs, async coll => { // with one inequality: a>2 || b==1. await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.compositeQuery( coll, or(where('a', '>', 2), where('b', '==', 1)) @@ -108,6 +110,7 @@ apiDescribe('Composite Index Queries', persistence => { // Test with limits (implicit order by ASC): (a==1) || (b > 0) LIMIT 2 await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.compositeQuery( coll, or(where('a', '==', 1), where('b', '>', 0)), @@ -120,6 +123,7 @@ apiDescribe('Composite Index Queries', persistence => { // Test with limits (explicit order by): (a==1) || (b > 0) LIMIT_TO_LAST 2 // Note: The public query API does not allow implicit ordering when limitToLast is used. await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.compositeQuery( coll, or(where('a', '==', 1), where('b', '>', 0)), @@ -132,6 +136,7 @@ apiDescribe('Composite Index Queries', persistence => { // Test with limits (explicit order by ASC): (a==2) || (b == 1) ORDER BY a LIMIT 1 await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.compositeQuery( coll, or(where('a', '==', 2), where('b', '==', 1)), @@ -143,6 +148,7 @@ apiDescribe('Composite Index Queries', persistence => { // Test with limits (explicit order by DESC): (a==2) || (b == 1) ORDER BY a LIMIT_TO_LAST 1 await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.compositeQuery( coll, or(where('a', '==', 2), where('b', '==', 1)), @@ -857,12 +863,14 @@ apiDescribe('Composite Index Queries', persistence => { return testHelper.withTestDocs(persistence, testDocs, async coll => { // implicit AND: a != 1 && b < 2 await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.query(coll, where('a', '!=', 1), where('b', '<', 2)), 'doc2' ); // explicit AND: a != 1 && b < 2 await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.compositeQuery( coll, and(where('a', '!=', 1), where('b', '<', 2)) @@ -873,6 +881,7 @@ apiDescribe('Composite Index Queries', persistence => { // explicit AND: a < 3 && b not-in [2, 3] // Implicitly ordered by: a asc, b asc, __name__ asc await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.compositeQuery( coll, and(where('a', '<', 3), where('b', 'not-in', [2, 3])) @@ -884,6 +893,7 @@ apiDescribe('Composite Index Queries', persistence => { // a <3 && b != 0, implicitly ordered by: a asc, b asc, __name__ asc await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.query( coll, where('b', '!=', 0), @@ -896,6 +906,7 @@ apiDescribe('Composite Index Queries', persistence => { // a <3 && b != 0, ordered by: b desc, a desc, __name__ desc await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.query( coll, where('a', '<', 3), @@ -909,6 +920,7 @@ apiDescribe('Composite Index Queries', persistence => { // explicit OR: multiple inequality: a>2 || b<1. await testHelper.assertOnlineAndOfflineResultsMatch( + coll, testHelper.compositeQuery( coll, or(where('a', '>', 2), where('b', '<', 1)) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 8cbe99b3cd9..9675e02efeb 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -780,7 +780,11 @@ apiDescribe('Database', persistence => { return withTestCollection(persistence, docs, async randomCol => { const orderedQuery = query(randomCol, orderBy('embedding')); - await checkOnlineAndOfflineResultsMatch(orderedQuery, ...documentIds); + await checkOnlineAndOfflineResultsMatch( + randomCol, + orderedQuery, + ...documentIds + ); const orderedQueryLessThan = query( randomCol, @@ -788,6 +792,7 @@ apiDescribe('Database', persistence => { where('embedding', '<', vector([1, 2, 100, 4, 4])) ); await checkOnlineAndOfflineResultsMatch( + randomCol, orderedQueryLessThan, ...documentIds.slice(2, 11) ); @@ -798,6 +803,7 @@ apiDescribe('Database', persistence => { where('embedding', '>', vector([1, 2, 100, 4, 4])) ); await checkOnlineAndOfflineResultsMatch( + randomCol, orderedQueryGreaterThan, ...documentIds.slice(12, 13) ); @@ -2396,6 +2402,7 @@ apiDescribe('Database', persistence => { 'a' ]; await checkOnlineAndOfflineResultsMatch( + collectionRef, orderedQuery, ...expectedDocs ); @@ -2416,6 +2423,7 @@ apiDescribe('Database', persistence => { 'Aa' ]; await checkOnlineAndOfflineResultsMatch( + collectionRef, filteredQuery, ...expectedDocs ); @@ -2467,7 +2475,11 @@ apiDescribe('Database', persistence => { unsubscribe(); - await checkOnlineAndOfflineResultsMatch(orderedQuery, ...expectedDocs); + await checkOnlineAndOfflineResultsMatch( + collectionRef, + orderedQuery, + ...expectedDocs + ); }); }); @@ -2499,7 +2511,11 @@ apiDescribe('Database', persistence => { unsubscribe(); - await checkOnlineAndOfflineResultsMatch(orderedQuery, ...expectedDocs); + await checkOnlineAndOfflineResultsMatch( + collectionRef, + orderedQuery, + ...expectedDocs + ); }); }); @@ -2531,7 +2547,11 @@ apiDescribe('Database', persistence => { unsubscribe(); - await checkOnlineAndOfflineResultsMatch(orderedQuery, ...expectedDocs); + await checkOnlineAndOfflineResultsMatch( + collectionRef, + orderedQuery, + ...expectedDocs + ); }); }); @@ -2563,7 +2583,11 @@ apiDescribe('Database', persistence => { unsubscribe(); - await checkOnlineAndOfflineResultsMatch(orderedQuery, ...expectedDocs); + await checkOnlineAndOfflineResultsMatch( + collectionRef, + orderedQuery, + ...expectedDocs + ); }); }); @@ -2608,7 +2632,11 @@ apiDescribe('Database', persistence => { unsubscribe(); - await checkOnlineAndOfflineResultsMatch(orderedQuery, ...expectedDocs); + await checkOnlineAndOfflineResultsMatch( + collectionRef, + orderedQuery, + ...expectedDocs + ); }); }); diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index 5871607eb03..0f3c1c82a2d 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -1357,6 +1357,7 @@ apiDescribe('Queries', persistence => { return withTestCollection(persistence, testDocs, async coll => { // a == 1 await checkOnlineAndOfflineResultsMatch( + coll, query(coll, where('a', '==', 1)), 'doc1', 'doc4', @@ -1365,18 +1366,21 @@ apiDescribe('Queries', persistence => { // Implicit AND: a == 1 && b == 3 await checkOnlineAndOfflineResultsMatch( + coll, query(coll, where('a', '==', 1), where('b', '==', 3)), 'doc4' ); // explicit AND: a == 1 && b == 3 await checkOnlineAndOfflineResultsMatch( + coll, query(coll, and(where('a', '==', 1), where('b', '==', 3))), 'doc4' ); // a == 1, limit 2 await checkOnlineAndOfflineResultsMatch( + coll, query(coll, where('a', '==', 1), limit(2)), 'doc1', 'doc4' @@ -1384,6 +1388,7 @@ apiDescribe('Queries', persistence => { // explicit OR: a == 1 || b == 1 with limit 2 await checkOnlineAndOfflineResultsMatch( + coll, query(coll, or(where('a', '==', 1), where('b', '==', 1)), limit(2)), 'doc1', 'doc2' @@ -1391,6 +1396,7 @@ apiDescribe('Queries', persistence => { // only limit 2 await checkOnlineAndOfflineResultsMatch( + coll, query(coll, limit(2)), 'doc1', 'doc2' @@ -1398,6 +1404,7 @@ apiDescribe('Queries', persistence => { // limit 2 and order by b desc await checkOnlineAndOfflineResultsMatch( + coll, query(coll, limit(2), orderBy('b', 'desc')), 'doc4', 'doc3' @@ -1417,6 +1424,7 @@ apiDescribe('Queries', persistence => { return withTestCollection(persistence, testDocs, async coll => { // Two equalities: a==1 || b==1. await checkOnlineAndOfflineResultsMatch( + coll, query(coll, or(where('a', '==', 1), where('b', '==', 1))), 'doc1', 'doc2', @@ -1426,6 +1434,7 @@ apiDescribe('Queries', persistence => { // (a==1 && b==0) || (a==3 && b==2) await checkOnlineAndOfflineResultsMatch( + coll, query( coll, or( @@ -1439,6 +1448,7 @@ apiDescribe('Queries', persistence => { // a==1 && (b==0 || b==3). await checkOnlineAndOfflineResultsMatch( + coll, query( coll, and( @@ -1452,6 +1462,7 @@ apiDescribe('Queries', persistence => { // (a==2 || b==2) && (a==3 || b==3) await checkOnlineAndOfflineResultsMatch( + coll, query( coll, and( @@ -1464,6 +1475,7 @@ apiDescribe('Queries', persistence => { // Test with limits without orderBy (the __name__ ordering is the tie breaker). await checkOnlineAndOfflineResultsMatch( + coll, query(coll, or(where('a', '==', 2), where('b', '==', 1)), limit(1)), 'doc2' ); @@ -1483,6 +1495,7 @@ apiDescribe('Queries', persistence => { return withTestCollection(persistence, testDocs, async coll => { // a==2 || b in [2,3] await checkOnlineAndOfflineResultsMatch( + coll, query(coll, or(where('a', '==', 2), where('b', 'in', [2, 3]))), 'doc3', 'doc4', @@ -1504,6 +1517,7 @@ apiDescribe('Queries', persistence => { return withTestCollection(persistence, testDocs, async coll => { // a==2 || b array-contains 7 await checkOnlineAndOfflineResultsMatch( + coll, query(coll, or(where('a', '==', 2), where('b', 'array-contains', 7))), 'doc3', 'doc4', @@ -1512,6 +1526,7 @@ apiDescribe('Queries', persistence => { // a==2 || b array-contains-any [0, 3] await checkOnlineAndOfflineResultsMatch( + coll, query( coll, or(where('a', '==', 2), where('b', 'array-contains-any', [0, 3])) @@ -1535,6 +1550,7 @@ apiDescribe('Queries', persistence => { return withTestCollection(persistence, testDocs, async coll => { await checkOnlineAndOfflineResultsMatch( + coll, query( coll, or( @@ -1549,6 +1565,7 @@ apiDescribe('Queries', persistence => { ); await checkOnlineAndOfflineResultsMatch( + coll, query( coll, and( @@ -1560,6 +1577,7 @@ apiDescribe('Queries', persistence => { ); await checkOnlineAndOfflineResultsMatch( + coll, query( coll, or( @@ -1573,6 +1591,7 @@ apiDescribe('Queries', persistence => { ); await checkOnlineAndOfflineResultsMatch( + coll, query( coll, and( @@ -1598,6 +1617,7 @@ apiDescribe('Queries', persistence => { return withTestCollection(persistence, testDocs, async coll => { await checkOnlineAndOfflineResultsMatch( + coll, query( coll, or(where('a', 'in', [2, 3]), where('b', 'array-contains', 3)) @@ -1608,6 +1628,7 @@ apiDescribe('Queries', persistence => { ); await checkOnlineAndOfflineResultsMatch( + coll, query( coll, and(where('a', 'in', [2, 3]), where('b', 'array-contains', 7)) @@ -1616,6 +1637,7 @@ apiDescribe('Queries', persistence => { ); await checkOnlineAndOfflineResultsMatch( + coll, query( coll, or( @@ -1629,6 +1651,7 @@ apiDescribe('Queries', persistence => { ); await checkOnlineAndOfflineResultsMatch( + coll, query( coll, and( @@ -1653,6 +1676,7 @@ apiDescribe('Queries', persistence => { return withTestCollection(persistence, testDocs, async coll => { await checkOnlineAndOfflineResultsMatch( + coll, query(coll, where('a', '==', 1), orderBy('a')), 'doc1', 'doc4', @@ -1660,6 +1684,7 @@ apiDescribe('Queries', persistence => { ); await checkOnlineAndOfflineResultsMatch( + coll, query(coll, where('a', 'in', [2, 3]), orderBy('a')), 'doc6', 'doc3' @@ -1680,6 +1705,7 @@ apiDescribe('Queries', persistence => { return withTestCollection(persistence, testDocs, async coll => { // Two IN operations on different fields with disjunction. await checkOnlineAndOfflineResultsMatch( + coll, query(coll, or(where('a', 'in', [2, 3]), where('b', 'in', [0, 2]))), 'doc1', 'doc3', @@ -1688,6 +1714,7 @@ apiDescribe('Queries', persistence => { // Two IN operations on different fields with conjunction. await checkOnlineAndOfflineResultsMatch( + coll, query(coll, and(where('a', 'in', [2, 3]), where('b', 'in', [0, 2]))), 'doc3' ); @@ -1695,6 +1722,7 @@ apiDescribe('Queries', persistence => { // Two IN operations on the same field. // a IN [1,2,3] && a IN [0,1,4] should result in "a==1". await checkOnlineAndOfflineResultsMatch( + coll, query( coll, and(where('a', 'in', [1, 2, 3]), where('a', 'in', [0, 1, 4])) @@ -1707,6 +1735,7 @@ apiDescribe('Queries', persistence => { // a IN [2,3] && a IN [0,1,4] is never true and so the result should be an // empty set. await checkOnlineAndOfflineResultsMatch( + coll, query( coll, and(where('a', 'in', [2, 3]), where('a', 'in', [0, 1, 4])) @@ -1715,6 +1744,7 @@ apiDescribe('Queries', persistence => { // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]). await checkOnlineAndOfflineResultsMatch( + coll, query(coll, or(where('a', 'in', [0, 3]), where('a', 'in', [0, 2]))), 'doc3', 'doc6' @@ -1722,6 +1752,7 @@ apiDescribe('Queries', persistence => { // Nested composite filter on the same field. await checkOnlineAndOfflineResultsMatch( + coll, query( coll, and( @@ -1737,6 +1768,7 @@ apiDescribe('Queries', persistence => { // Nested composite filter on the different fields. await checkOnlineAndOfflineResultsMatch( + coll, query( coll, and( @@ -1772,6 +1804,7 @@ apiDescribe('Queries', persistence => { let testQuery = query(coll, where('zip', '!=', 98101)); await checkOnlineAndOfflineResultsMatch( + coll, testQuery, 'a', 'b', @@ -1784,6 +1817,7 @@ apiDescribe('Queries', persistence => { testQuery = query(coll, where('zip', '!=', Number.NaN)); await checkOnlineAndOfflineResultsMatch( + coll, testQuery, 'b', 'c', @@ -1796,6 +1830,7 @@ apiDescribe('Queries', persistence => { testQuery = query(coll, where('zip', '!=', null)); await checkOnlineAndOfflineResultsMatch( + coll, testQuery, 'a', 'b', @@ -1832,6 +1867,7 @@ apiDescribe('Queries', persistence => { where('zip', 'not-in', [98101, 98103, [98101, 98102]]) ); await checkOnlineAndOfflineResultsMatch( + coll, testQuery, 'a', 'b', @@ -1842,7 +1878,7 @@ apiDescribe('Queries', persistence => { ); testQuery = query(coll, where('zip', 'not-in', [null])); - await checkOnlineAndOfflineResultsMatch(testQuery); + await checkOnlineAndOfflineResultsMatch(coll, testQuery); }); }); }); diff --git a/packages/firestore/test/integration/util/composite_index_test_helper.ts b/packages/firestore/test/integration/util/composite_index_test_helper.ts index 5199539768b..a908ed13455 100644 --- a/packages/firestore/test/integration/util/composite_index_test_helper.ts +++ b/packages/firestore/test/integration/util/composite_index_test_helper.ts @@ -162,10 +162,12 @@ export class CompositeIndexTestHelper { // the same as running it while offline. The expected document Ids are hashed to match the // actual document IDs created by the test helper. async assertOnlineAndOfflineResultsMatch( + collection: CollectionReference, query: Query, ...expectedDocs: string[] ): Promise { return checkOnlineAndOfflineResultsMatch( + this.query(collection), query, ...this.toHashedIds(expectedDocs) ); diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index 465bc8edd61..b36ed980295 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -541,19 +541,42 @@ export function partitionedTestDocs(partitions: { * documents as running the query while offline. If `expectedDocs` is provided, it also checks * that both online and offline query result is equal to the expected documents. * + * This function first performs a "get" for the entire COLLECTION from the server. + * It then performs the QUERY from CACHE which, results in `executeFullCollectionScan()` + * It then performs the QUERY from SERVER. + * It then performs the QUERY from CACHE again, which results in `performQueryUsingRemoteKeys()`. + * It then ensure that all the above QUERY results are the same. + * + * @param collection The collection on which the query is performed. * @param query The query to check * @param expectedDocs Ordered list of document keys that are expected to match the query */ export async function checkOnlineAndOfflineResultsMatch( + collection: Query, query: Query, ...expectedDocs: string[] ): Promise { + // Note: Order matters. The following has to be done in the specific order: + + // 1- Pre-populate the cache with the entire collection. + await getDocsFromServer(collection); + + // 2- This performs the query against the cache using full collection scan. + const docsFromCacheFullCollectionScan = await getDocsFromCache(query); + + // 3- This goes to the server (backend/emulator). const docsFromServer = await getDocsFromServer(query); + // 4- This performs the query against the cache using remote keys. + const docsFromCacheUsingRemoteKeys = await getDocsFromCache(query); + + expect(toIds(docsFromServer)).to.deep.equal( + toIds(docsFromCacheFullCollectionScan) + ); + expect(toIds(docsFromServer)).to.deep.equal( + toIds(docsFromCacheUsingRemoteKeys) + ); if (expectedDocs.length !== 0) { expect(expectedDocs).to.deep.equal(toIds(docsFromServer)); } - - const docsFromCache = await getDocsFromCache(query); - expect(toIds(docsFromServer)).to.deep.equal(toIds(docsFromCache)); } From dd6a8f076cb06920a2cff3b9adb78a85624ad64d Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Tue, 29 Apr 2025 09:31:41 -0400 Subject: [PATCH 17/23] fix(vertexai): add missing quote to chat role error message (#8979) --- packages/vertexai/src/methods/chat-session-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vertexai/src/methods/chat-session-helpers.ts b/packages/vertexai/src/methods/chat-session-helpers.ts index 899db4f626a..2106a40b90b 100644 --- a/packages/vertexai/src/methods/chat-session-helpers.ts +++ b/packages/vertexai/src/methods/chat-session-helpers.ts @@ -111,7 +111,7 @@ export function validateChatHistory(history: Content[]): void { if (!validPreviousContentRoles.includes(prevContent.role)) { throw new VertexAIError( VertexAIErrorCode.INVALID_CONTENT, - `Content with role '${role} can't follow '${ + `Content with role '${role}' can't follow '${ prevContent.role }'. Valid previous roles: ${JSON.stringify( VALID_PREVIOUS_CONTENT_ROLES From ea1f9139e6baec0269fbb91233fd3f7f4b0d5875 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Tue, 29 Apr 2025 11:24:16 -0700 Subject: [PATCH 18/23] Auto Enable SSL for Firebase Studio (#8980) --- .changeset/nice-plants-thank.md | 10 ++++++++ common/api-review/util.api.md | 3 +++ .../database-compat/test/database.test.ts | 11 +++++++++ packages/database/src/api/Database.ts | 9 +++++-- packages/firestore/externs.json | 1 + packages/firestore/src/lite-api/database.ts | 6 +++-- .../firestore/test/unit/api/database.test.ts | 14 +++++++++++ packages/functions/src/service.test.ts | 9 +++++++ packages/functions/src/service.ts | 6 ++++- packages/storage/src/service.ts | 9 +++++-- packages/storage/test/unit/service.test.ts | 22 +++++++++++++++++ packages/util/index.node.ts | 1 + packages/util/index.ts | 1 + packages/util/src/url.ts | 24 +++++++++++++++++++ 14 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 .changeset/nice-plants-thank.md create mode 100644 packages/util/src/url.ts diff --git a/.changeset/nice-plants-thank.md b/.changeset/nice-plants-thank.md new file mode 100644 index 00000000000..05fb520760f --- /dev/null +++ b/.changeset/nice-plants-thank.md @@ -0,0 +1,10 @@ +--- +"@firebase/database-compat": patch +"@firebase/database": patch +"@firebase/firestore": patch +"@firebase/functions": patch +"@firebase/storage": patch +"@firebase/util": patch +--- + +Auto Enable SSL for Firebase Studio diff --git a/common/api-review/util.api.md b/common/api-review/util.api.md index 8c62ff229ac..28cc9a160d4 100644 --- a/common/api-review/util.api.md +++ b/common/api-review/util.api.md @@ -269,6 +269,9 @@ export function isBrowserExtension(): boolean; // @public export function isCloudflareWorker(): boolean; +// @public +export function isCloudWorkstation(host: string): boolean; + // Warning: (ae-missing-release-tag) "isElectron" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/packages/database-compat/test/database.test.ts b/packages/database-compat/test/database.test.ts index fa21058591f..19e02943c9c 100644 --- a/packages/database-compat/test/database.test.ts +++ b/packages/database-compat/test/database.test.ts @@ -292,6 +292,17 @@ describe('Database Tests', () => { expect((db as any)._delegate._repo.repoInfo_.isUsingEmulator).to.be.false; }); + it('uses ssl when useEmulator is called with ssl specified', () => { + const db = firebase.database(); + const cloudWorkstation = 'abc.cloudworkstations.dev'; + db.useEmulator(cloudWorkstation, 80); + expect((db as any)._delegate._repo.repoInfo_.isUsingEmulator).to.be.true; + expect((db as any)._delegate._repo.repoInfo_.host).to.equal( + `${cloudWorkstation}:80` + ); + expect((db as any)._delegate._repo.repoInfo_.secure).to.be.true; + }); + it('cannot call useEmulator after use', () => { const db = (firebase as any).database(); diff --git a/packages/database/src/api/Database.ts b/packages/database/src/api/Database.ts index 32fd4674a44..f247fc6288c 100644 --- a/packages/database/src/api/Database.ts +++ b/packages/database/src/api/Database.ts @@ -29,7 +29,8 @@ import { createMockUserToken, deepEqual, EmulatorMockTokenOptions, - getDefaultEmulatorHostnameAndPort + getDefaultEmulatorHostnameAndPort, + isCloudWorkstation } from '@firebase/util'; import { AppCheckTokenProvider } from '../core/AppCheckTokenProvider'; @@ -89,9 +90,12 @@ function repoManagerApplyEmulatorSettings( emulatorOptions: RepoInfoEmulatorOptions, tokenProvider?: AuthTokenProvider ): void { + const portIndex = hostAndPort.lastIndexOf(':'); + const host = hostAndPort.substring(0, portIndex); + const useSsl = isCloudWorkstation(host); repo.repoInfo_ = new RepoInfo( hostAndPort, - /* secure= */ false, + /* secure= */ useSsl, repo.repoInfo_.namespace, repo.repoInfo_.webSocketOnly, repo.repoInfo_.nodeAdmin, @@ -352,6 +356,7 @@ export function connectDatabaseEmulator( ): void { db = getModularInstance(db); db._checkNotDeleted('useEmulator'); + const hostAndPort = `${host}:${port}`; const repo = db._repoInternal; if (db._instanceStarted) { diff --git a/packages/firestore/externs.json b/packages/firestore/externs.json index 03d19ee8e83..d730cfeac0a 100644 --- a/packages/firestore/externs.json +++ b/packages/firestore/externs.json @@ -33,6 +33,7 @@ "packages/util/dist/src/compat.d.ts", "packages/util/dist/src/global.d.ts", "packages/util/dist/src/obj.d.ts", + "packages/util/dist/src/url.d.ts", "packages/firestore/src/protos/firestore_bundle_proto.ts", "packages/firestore/src/protos/firestore_proto_api.ts", "packages/firestore/src/util/error.ts", diff --git a/packages/firestore/src/lite-api/database.ts b/packages/firestore/src/lite-api/database.ts index 9a68e2a86d6..294206e54c2 100644 --- a/packages/firestore/src/lite-api/database.ts +++ b/packages/firestore/src/lite-api/database.ts @@ -26,7 +26,8 @@ import { createMockUserToken, deepEqual, EmulatorMockTokenOptions, - getDefaultEmulatorHostnameAndPort + getDefaultEmulatorHostnameAndPort, + isCloudWorkstation } from '@firebase/util'; import { @@ -325,6 +326,7 @@ export function connectFirestoreEmulator( } = {} ): void { firestore = cast(firestore, Firestore); + const useSsl = isCloudWorkstation(host); const settings = firestore._getSettings(); const existingConfig = { ...settings, @@ -340,7 +342,7 @@ export function connectFirestoreEmulator( const newConfig = { ...settings, host: newHostSetting, - ssl: false, + ssl: useSsl, emulatorOptions: options }; // No-op if the new configuration matches the current configuration. This supports SSR diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index 1cc1df51063..46e4c65f180 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -564,6 +564,20 @@ describe('Settings', () => { expect(db._getEmulatorOptions()).to.equal(emulatorOptions); }); + it('sets ssl to true if cloud workstation host', () => { + // Use a new instance of Firestore in order to configure settings. + const db = newTestFirestore(); + const emulatorOptions = { mockUserToken: 'test' }; + const workstationHost = 'abc.cloudworkstations.dev'; + connectFirestoreEmulator(db, workstationHost, 9000, emulatorOptions); + + expect(db._getSettings().host).to.exist.and.to.equal( + `${workstationHost}:9000` + ); + expect(db._getSettings().ssl).to.exist.and.to.be.true; + expect(db._getEmulatorOptions()).to.equal(emulatorOptions); + }); + it('prefers host from useEmulator to host from settings', () => { // Use a new instance of Firestore in order to configure settings. const db = newTestFirestore(); diff --git a/packages/functions/src/service.test.ts b/packages/functions/src/service.test.ts index bb29f9025fe..8119fda39d5 100644 --- a/packages/functions/src/service.test.ts +++ b/packages/functions/src/service.test.ts @@ -47,6 +47,15 @@ describe('Firebase Functions > Service', () => { 'http://localhost:5005/my-project/us-central1/foo' ); }); + it('can use emulator with SSL', () => { + service = createTestService(app); + const workstationHost = 'abc.cloudworkstations.dev'; + connectFunctionsEmulator(service, workstationHost, 5005); + assert.equal( + service._url('foo'), + `https://${workstationHost}:5005/my-project/us-central1/foo` + ); + }); it('correctly sets region', () => { service = createTestService(app, 'my-region'); diff --git a/packages/functions/src/service.ts b/packages/functions/src/service.ts index 34cb732bf71..dce52e1f19d 100644 --- a/packages/functions/src/service.ts +++ b/packages/functions/src/service.ts @@ -30,6 +30,7 @@ import { Provider } from '@firebase/component'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { MessagingInternalComponentName } from '@firebase/messaging-interop-types'; import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; +import { isCloudWorkstation } from '@firebase/util'; export const DEFAULT_REGION = 'us-central1'; @@ -174,7 +175,10 @@ export function connectFunctionsEmulator( host: string, port: number ): void { - functionsInstance.emulatorOrigin = `http://${host}:${port}`; + const useSsl = isCloudWorkstation(host); + functionsInstance.emulatorOrigin = `http${ + useSsl ? 's' : '' + }://${host}:${port}`; } /** diff --git a/packages/storage/src/service.ts b/packages/storage/src/service.ts index 422e3e1a188..8a942aac62a 100644 --- a/packages/storage/src/service.ts +++ b/packages/storage/src/service.ts @@ -42,7 +42,11 @@ import { } from './implementation/error'; import { validateNumber } from './implementation/type'; import { FirebaseStorage } from './public-types'; -import { createMockUserToken, EmulatorMockTokenOptions } from '@firebase/util'; +import { + createMockUserToken, + EmulatorMockTokenOptions, + isCloudWorkstation +} from '@firebase/util'; import { Connection, ConnectionType } from './implementation/connection'; export function isUrl(path?: string): boolean { @@ -141,7 +145,8 @@ export function connectStorageEmulator( } = {} ): void { storage.host = `${host}:${port}`; - storage._protocol = 'http'; + const useSsl = isCloudWorkstation(host); + storage._protocol = useSsl ? 'https' : 'http'; const { mockUserToken } = options; if (mockUserToken) { storage._overrideAuthToken = diff --git a/packages/storage/test/unit/service.test.ts b/packages/storage/test/unit/service.test.ts index be42bb8dd6e..bc443c60a03 100644 --- a/packages/storage/test/unit/service.test.ts +++ b/packages/storage/test/unit/service.test.ts @@ -248,6 +248,28 @@ GOOG4-RSA-SHA256` expect(service._protocol).to.equal('http'); void getDownloadURL(ref(service, 'test.png')); }); + it('sets emulator host correctly with ssl', done => { + function newSend(connection: TestingConnection, url: string): void { + // Expect emulator host to be in url of storage operations requests, + // in this case getDownloadURL. + expect(url).to.match(/^https:\/\/test\.cloudworkstations\.dev:1234.+/); + connection.abort(); + injectTestConnection(null); + done(); + } + + injectTestConnection(() => newTestConnection(newSend)); + const service = new FirebaseStorageImpl( + testShared.fakeApp, + testShared.fakeAuthProvider, + testShared.fakeAppCheckTokenProvider + ); + const workstationHost = 'test.cloudworkstations.dev'; + connectStorageEmulator(service, workstationHost, 1234); + expect(service.host).to.equal(`${workstationHost}:1234`); + expect(service._protocol).to.equal('https'); + void getDownloadURL(ref(service, 'test.png')); + }); it('sets mock user token string if specified', done => { const mockUserToken = 'my-mock-user-token'; function newSend( diff --git a/packages/util/index.node.ts b/packages/util/index.node.ts index d839460713c..12fcf8a6de5 100644 --- a/packages/util/index.node.ts +++ b/packages/util/index.node.ts @@ -42,3 +42,4 @@ export * from './src/exponential_backoff'; export * from './src/formatters'; export * from './src/compat'; export * from './src/global'; +export * from './src/url'; diff --git a/packages/util/index.ts b/packages/util/index.ts index 51c27c31099..1829c32a420 100644 --- a/packages/util/index.ts +++ b/packages/util/index.ts @@ -37,3 +37,4 @@ export * from './src/exponential_backoff'; export * from './src/formatters'; export * from './src/compat'; export * from './src/global'; +export * from './src/url'; diff --git a/packages/util/src/url.ts b/packages/util/src/url.ts new file mode 100644 index 00000000000..33cec43bea9 --- /dev/null +++ b/packages/util/src/url.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2025 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. + */ + +/** + * Checks whether host is a cloud workstation or not. + * @public + */ +export function isCloudWorkstation(host: string): boolean { + return host.endsWith('.cloudworkstations.dev'); +} From 0e127664946ba324c6566a02b393dafd23fc1ddb Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Wed, 30 Apr 2025 12:19:20 -0700 Subject: [PATCH 19/23] Add support for running the emulators in Cloud Workstation (#8968) --- .changeset/nine-pugs-crash.md | 11 ++++ common/api-review/storage.api.md | 4 +- packages/auth/src/api/index.test.ts | 24 +++++++ packages/auth/src/api/index.ts | 11 +++- packages/auth/test/helpers/mock_fetch.ts | 4 +- packages/data-connect/src/network/fetch.ts | 18 ++++-- .../src/network/transport/rest.ts | 8 ++- packages/data-connect/test/unit/fetch.test.ts | 43 +++++++++++-- packages/firestore/externs.json | 1 + packages/firestore/src/core/database_info.ts | 3 +- packages/firestore/src/lite-api/components.ts | 3 +- packages/firestore/src/lite-api/settings.ts | 3 + .../platform/browser/webchannel_connection.ts | 3 +- .../platform/browser_lite/fetch_connection.ts | 11 +++- .../firestore/src/remote/rest_connection.ts | 15 ++++- .../test/integration/util/internal_helpers.ts | 3 +- .../test/unit/remote/fetch_connection.test.ts | 64 +++++++++++++++++++ .../test/unit/remote/rest_connection.test.ts | 3 +- .../test/unit/specs/spec_test_runner.ts | 3 +- .../storage/src/implementation/connection.ts | 1 + .../storage/src/implementation/request.ts | 17 +++-- .../src/platform/browser/connection.ts | 5 ++ .../storage/src/platform/node/connection.ts | 41 +++++++++--- packages/storage/src/service.ts | 7 +- .../storage/test/browser/connection.test.ts | 18 +++++- packages/storage/test/node/connection.test.ts | 21 +++++- packages/storage/test/unit/connection.ts | 1 + packages/storage/test/unit/service.test.ts | 12 +++- 28 files changed, 314 insertions(+), 44 deletions(-) create mode 100644 .changeset/nine-pugs-crash.md create mode 100644 packages/firestore/test/unit/remote/fetch_connection.test.ts diff --git a/.changeset/nine-pugs-crash.md b/.changeset/nine-pugs-crash.md new file mode 100644 index 00000000000..b4a654a484f --- /dev/null +++ b/.changeset/nine-pugs-crash.md @@ -0,0 +1,11 @@ +--- +"@firebase/auth": patch +"@firebase/data-connect": patch +"@firebase/database-compat": patch +"@firebase/database": patch +"@firebase/firestore": patch +"@firebase/storage": patch +"@firebase/util": patch +--- + +Fix Auth Redirects on Firebase Studio diff --git a/common/api-review/storage.api.md b/common/api-review/storage.api.md index 4964aa40af7..f5302d2d5c5 100644 --- a/common/api-review/storage.api.md +++ b/common/api-review/storage.api.md @@ -58,7 +58,7 @@ export class _FirebaseStorageImpl implements FirebaseStorage { constructor( app: FirebaseApp, _authProvider: Provider, _appCheckProvider: Provider, - _url?: string | undefined, _firebaseVersion?: string | undefined); + _url?: string | undefined, _firebaseVersion?: string | undefined, _isUsingEmulator?: boolean); readonly app: FirebaseApp; // (undocumented) readonly _appCheckProvider: Provider; @@ -77,6 +77,8 @@ export class _FirebaseStorageImpl implements FirebaseStorage { _getAuthToken(): Promise; get host(): string; set host(host: string); + // (undocumented) + _isUsingEmulator: boolean; // Warning: (ae-forgotten-export) The symbol "ConnectionType" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RequestInfo" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Connection" needs to be exported by the entry point index.d.ts diff --git a/packages/auth/src/api/index.test.ts b/packages/auth/src/api/index.test.ts index ea11af59d01..02042fce429 100644 --- a/packages/auth/src/api/index.test.ts +++ b/packages/auth/src/api/index.test.ts @@ -60,6 +60,10 @@ describe('api/_performApiRequest', () => { auth = await testAuth(); }); + afterEach(() => { + sinon.restore(); + }); + context('with regular requests', () => { beforeEach(mockFetch.setUp); afterEach(mockFetch.tearDown); @@ -80,6 +84,26 @@ describe('api/_performApiRequest', () => { expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( 'testSDK/0.0.0' ); + expect(mock.calls[0].fullRequest?.credentials).to.be.undefined; + }); + + it('should set credentials to "include" when using IDX and emulator', async () => { + const mock = mockEndpoint(Endpoint.SIGN_UP, serverResponse); + auth.emulatorConfig = { + host: 'https://something.cloudworkstations.dev', + protocol: '', + port: 8, + options: { + disableWarnings: false + } + }; + await _performApiRequest( + auth, + HttpMethod.POST, + Endpoint.SIGN_UP, + request + ); + expect(mock.calls[0].fullRequest?.credentials).to.eq('include'); }); it('should set the device language if available', async () => { diff --git a/packages/auth/src/api/index.ts b/packages/auth/src/api/index.ts index 769a1b6accc..af9b3c63bf1 100644 --- a/packages/auth/src/api/index.ts +++ b/packages/auth/src/api/index.ts @@ -15,7 +15,12 @@ * limitations under the License. */ -import { FirebaseError, isCloudflareWorker, querystring } from '@firebase/util'; +import { + FirebaseError, + isCloudflareWorker, + isCloudWorkstation, + querystring +} from '@firebase/util'; import { AuthErrorCode, NamedErrorParams } from '../core/errors'; import { @@ -177,6 +182,10 @@ export async function _performApiRequest( fetchArgs.referrerPolicy = 'no-referrer'; } + if (auth.emulatorConfig && isCloudWorkstation(auth.emulatorConfig.host)) { + fetchArgs.credentials = 'include'; + } + return FetchProvider.fetch()( await _getFinalTarget(auth, auth.config.apiHost, path, query), fetchArgs diff --git a/packages/auth/test/helpers/mock_fetch.ts b/packages/auth/test/helpers/mock_fetch.ts index 2d5794b7327..49fa79966f9 100644 --- a/packages/auth/test/helpers/mock_fetch.ts +++ b/packages/auth/test/helpers/mock_fetch.ts @@ -22,6 +22,7 @@ export interface Call { request?: object | string; method?: string; headers: Headers; + fullRequest?: RequestInit; } export interface Route { @@ -59,7 +60,8 @@ const fakeFetch: typeof fetch = ( calls.push({ request: requestBody, method: request?.method, - headers + headers, + fullRequest: request }); return Promise.resolve( diff --git a/packages/data-connect/src/network/fetch.ts b/packages/data-connect/src/network/fetch.ts index 8353c6b99ab..3e8e2cab476 100644 --- a/packages/data-connect/src/network/fetch.ts +++ b/packages/data-connect/src/network/fetch.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { isCloudWorkstation } from '@firebase/util'; + import { Code, DataConnectError, @@ -22,7 +24,7 @@ import { DataConnectOperationFailureResponse } from '../core/error'; import { SDK_VERSION } from '../core/version'; -import { logDebug, logError } from '../logger'; +import { logError } from '../logger'; import { CallerSdkType, CallerSdkTypeEnum } from './transport'; @@ -58,7 +60,8 @@ export function dcFetch( accessToken: string | null, appCheckToken: string | null, _isUsingGen: boolean, - _callerSdkType: CallerSdkType + _callerSdkType: CallerSdkType, + _isUsingEmulator: boolean ): Promise<{ data: T; errors: Error[] }> { if (!connectFetch) { throw new DataConnectError(Code.OTHER, 'No Fetch Implementation detected!'); @@ -77,14 +80,17 @@ export function dcFetch( headers['X-Firebase-AppCheck'] = appCheckToken; } const bodyStr = JSON.stringify(body); - logDebug(`Making request out to ${url} with body: ${bodyStr}`); - - return connectFetch(url, { + const fetchOptions: RequestInit = { body: bodyStr, method: 'POST', headers, signal - }) + }; + if (isCloudWorkstation(url) && _isUsingEmulator) { + fetchOptions.credentials = 'include'; + } + + return connectFetch(url, fetchOptions) .catch(err => { throw new DataConnectError( Code.OTHER, diff --git a/packages/data-connect/src/network/transport/rest.ts b/packages/data-connect/src/network/transport/rest.ts index 0663bc026db..f16154dcb2a 100644 --- a/packages/data-connect/src/network/transport/rest.ts +++ b/packages/data-connect/src/network/transport/rest.ts @@ -36,6 +36,7 @@ export class RESTTransport implements DataConnectTransport { private _accessToken: string | null = null; private _appCheckToken: string | null = null; private _lastToken: string | null = null; + private _isUsingEmulator = false; constructor( options: DataConnectOptions, private apiKey?: string | undefined, @@ -93,6 +94,7 @@ export class RESTTransport implements DataConnectTransport { } useEmulator(host: string, port?: number, isSecure?: boolean): void { this._host = host; + this._isUsingEmulator = true; if (typeof port === 'number') { this._port = port; } @@ -182,7 +184,8 @@ export class RESTTransport implements DataConnectTransport { this._accessToken, this._appCheckToken, this._isUsingGen, - this._callerSdkType + this._callerSdkType, + this._isUsingEmulator ) ); return withAuth; @@ -208,7 +211,8 @@ export class RESTTransport implements DataConnectTransport { this._accessToken, this._appCheckToken, this._isUsingGen, - this._callerSdkType + this._callerSdkType, + this._isUsingEmulator ); }); return taskResult; diff --git a/packages/data-connect/test/unit/fetch.test.ts b/packages/data-connect/test/unit/fetch.test.ts index 6cf2750d50d..be45695190f 100644 --- a/packages/data-connect/test/unit/fetch.test.ts +++ b/packages/data-connect/test/unit/fetch.test.ts @@ -18,10 +18,12 @@ import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import * as sinon from 'sinon'; +import sinonChai from 'sinon-chai'; import { dcFetch, initializeFetch } from '../../src/network/fetch'; import { CallerSdkType, CallerSdkTypeEnum } from '../../src/network/transport'; use(chaiAsPromised); +use(sinonChai); function mockFetch(json: object, reject: boolean): sinon.SinonStub { const fakeFetchImpl = sinon.stub().returns( Promise.resolve({ @@ -57,7 +59,8 @@ describe('fetch', () => { null, null, false, - CallerSdkTypeEnum.Base + CallerSdkTypeEnum.Base, + false ) ).to.eventually.be.rejectedWith(message); }); @@ -81,7 +84,8 @@ describe('fetch', () => { null, null, false, - CallerSdkTypeEnum.Base + CallerSdkTypeEnum.Base, + false ) ).to.eventually.be.rejectedWith(JSON.stringify(json)); }); @@ -112,7 +116,8 @@ describe('fetch', () => { null, null, false, - CallerSdkTypeEnum.Base + CallerSdkTypeEnum.Base, + false ) ).to.eventually.be.rejected.then(error => { expect(error.response.data).to.eq(json.data); @@ -143,7 +148,8 @@ describe('fetch', () => { null, null, false, // _isUsingGen is false - callerSdkType as CallerSdkType + callerSdkType as CallerSdkType, + false ); let expectedHeaderRegex: RegExp; @@ -191,7 +197,8 @@ describe('fetch', () => { null, null, true, // _isUsingGen is true - callerSdkType as CallerSdkType + callerSdkType as CallerSdkType, + false ); let expectedHeaderRegex: RegExp; @@ -215,4 +222,30 @@ describe('fetch', () => { } } }); + it('should call credentials include if using emulator on cloud workstation', async () => { + const json = { + code: 200, + message1: 'success' + }; + const fakeFetchImpl = mockFetch(json, false); + await dcFetch( + 'https://abc.cloudworkstations.dev', + { + name: 'n', + operationName: 'n', + variables: {} + }, + {} as AbortController, + null, + null, + null, + true, // _isUsingGen is true + CallerSdkTypeEnum.Base, + true + ); + expect(fakeFetchImpl).to.have.been.calledWithMatch( + 'https://abc.cloudworkstations.dev', + { credentials: 'include' } + ); + }); }); diff --git a/packages/firestore/externs.json b/packages/firestore/externs.json index d730cfeac0a..c56b078dddf 100644 --- a/packages/firestore/externs.json +++ b/packages/firestore/externs.json @@ -30,6 +30,7 @@ "packages/util/dist/src/defaults.d.ts", "packages/util/dist/src/emulator.d.ts", "packages/util/dist/src/environment.d.ts", + "packages/util/dist/src/url.d.ts", "packages/util/dist/src/compat.d.ts", "packages/util/dist/src/global.d.ts", "packages/util/dist/src/obj.d.ts", diff --git a/packages/firestore/src/core/database_info.ts b/packages/firestore/src/core/database_info.ts index 0325f8166b6..a057516763f 100644 --- a/packages/firestore/src/core/database_info.ts +++ b/packages/firestore/src/core/database_info.ts @@ -48,7 +48,8 @@ export class DatabaseInfo { readonly forceLongPolling: boolean, readonly autoDetectLongPolling: boolean, readonly longPollingOptions: ExperimentalLongPollingOptions, - readonly useFetchStreams: boolean + readonly useFetchStreams: boolean, + readonly isUsingEmulator: boolean ) {} } diff --git a/packages/firestore/src/lite-api/components.ts b/packages/firestore/src/lite-api/components.ts index 436d2b5d4d8..52c3b3729ee 100644 --- a/packages/firestore/src/lite-api/components.ts +++ b/packages/firestore/src/lite-api/components.ts @@ -119,6 +119,7 @@ export function makeDatabaseInfo( settings.experimentalForceLongPolling, settings.experimentalAutoDetectLongPolling, cloneLongPollingOptions(settings.experimentalLongPollingOptions), - settings.useFetchStreams + settings.useFetchStreams, + settings.isUsingEmulator ); } diff --git a/packages/firestore/src/lite-api/settings.ts b/packages/firestore/src/lite-api/settings.ts index a1bba373d13..56c99e7ccea 100644 --- a/packages/firestore/src/lite-api/settings.ts +++ b/packages/firestore/src/lite-api/settings.ts @@ -112,6 +112,8 @@ export class FirestoreSettingsImpl { readonly useFetchStreams: boolean; readonly localCache?: FirestoreLocalCache; + readonly isUsingEmulator: boolean; + // Can be a google-auth-library or gapi client. // eslint-disable-next-line @typescript-eslint/no-explicit-any credentials?: any; @@ -130,6 +132,7 @@ export class FirestoreSettingsImpl { this.host = settings.host; this.ssl = settings.ssl ?? DEFAULT_SSL; } + this.isUsingEmulator = settings.emulatorOptions !== undefined; this.credentials = settings.credentials; this.ignoreUndefinedProperties = !!settings.ignoreUndefinedProperties; diff --git a/packages/firestore/src/platform/browser/webchannel_connection.ts b/packages/firestore/src/platform/browser/webchannel_connection.ts index 5223285c5a4..9a69164457e 100644 --- a/packages/firestore/src/platform/browser/webchannel_connection.ts +++ b/packages/firestore/src/platform/browser/webchannel_connection.ts @@ -71,7 +71,8 @@ export class WebChannelConnection extends RestConnection { rpcName: string, url: string, headers: StringMap, - body: Req + body: Req, + _forwardCredentials: boolean ): Promise { const streamId = generateUniqueDebugId(); return new Promise((resolve: Resolver, reject: Rejecter) => { diff --git a/packages/firestore/src/platform/browser_lite/fetch_connection.ts b/packages/firestore/src/platform/browser_lite/fetch_connection.ts index d11247c8019..227322153e9 100644 --- a/packages/firestore/src/platform/browser_lite/fetch_connection.ts +++ b/packages/firestore/src/platform/browser_lite/fetch_connection.ts @@ -38,17 +38,22 @@ export class FetchConnection extends RestConnection { rpcName: string, url: string, headers: StringMap, - body: Req + body: Req, + forwardCredentials: boolean ): Promise { const requestJson = JSON.stringify(body); let response: Response; try { - response = await fetch(url, { + const fetchArgs: RequestInit = { method: 'POST', headers, body: requestJson - }); + }; + if (forwardCredentials) { + fetchArgs.credentials = 'include'; + } + response = await fetch(url, fetchArgs); } catch (e) { const err = e as { status: number | undefined; statusText: string }; throw new FirestoreError( diff --git a/packages/firestore/src/remote/rest_connection.ts b/packages/firestore/src/remote/rest_connection.ts index 470cb332ce2..2d6889dac3b 100644 --- a/packages/firestore/src/remote/rest_connection.ts +++ b/packages/firestore/src/remote/rest_connection.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { isCloudWorkstation } from '@firebase/util'; + import { SDK_VERSION } from '../../src/core/version'; import { Token } from '../api/credentials'; import { @@ -98,7 +100,15 @@ export abstract class RestConnection implements Connection { }; this.modifyHeadersForRequest(headers, authToken, appCheckToken); - return this.performRPCRequest(rpcName, url, headers, req).then( + const { host } = new URL(url); + const forwardCredentials = isCloudWorkstation(host); + return this.performRPCRequest( + rpcName, + url, + headers, + req, + forwardCredentials + ).then( response => { logDebug(LOG_TAG, `Received RPC '${rpcName}' ${streamId}: `, response); return response; @@ -179,7 +189,8 @@ export abstract class RestConnection implements Connection { rpcName: string, url: string, headers: StringMap, - body: Req + body: Req, + _forwardCredentials: boolean ): Promise; private makeUrl(rpcName: string, path: string): string { diff --git a/packages/firestore/test/integration/util/internal_helpers.ts b/packages/firestore/test/integration/util/internal_helpers.ts index 86ded6af3c1..e5e64b5fbf4 100644 --- a/packages/firestore/test/integration/util/internal_helpers.ts +++ b/packages/firestore/test/integration/util/internal_helpers.ts @@ -61,7 +61,8 @@ export function getDefaultDatabaseInfo(): DatabaseInfo { cloneLongPollingOptions( DEFAULT_SETTINGS.experimentalLongPollingOptions ?? {} ), - /*use FetchStreams= */ false + /*use FetchStreams= */ false, + /*isUsingEmulator=*/ false ); } diff --git a/packages/firestore/test/unit/remote/fetch_connection.test.ts b/packages/firestore/test/unit/remote/fetch_connection.test.ts new file mode 100644 index 00000000000..5a9aa67436f --- /dev/null +++ b/packages/firestore/test/unit/remote/fetch_connection.test.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2025 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. + */ + +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import sinonChai from 'sinon-chai'; + +import { DatabaseId } from '../../../src/core/database_info'; +import { makeDatabaseInfo } from '../../../src/lite-api/components'; +import { FirestoreSettingsImpl } from '../../../src/lite-api/settings'; +import { ResourcePath } from '../../../src/model/path'; +import { FetchConnection } from '../../../src/platform/browser_lite/fetch_connection'; + +use(sinonChai); +use(chaiAsPromised); + +describe('Fetch Connection', () => { + it('should pass in credentials if using emulator and cloud workstation', async () => { + const stub = sinon.stub(globalThis, 'fetch'); + stub.resolves({ + ok: true, + json() { + return Promise.resolve(); + } + } as Response); + const fetchConnection = new FetchConnection( + makeDatabaseInfo( + DatabaseId.empty(), + '', + '', + new FirestoreSettingsImpl({ + host: 'abc.cloudworkstations.dev' + }) + ) + ); + await fetchConnection.invokeRPC( + 'Commit', + new ResourcePath([]), + {}, + null, + null + ); + expect(stub).to.have.been.calledWithMatch( + 'https://abc.cloudworkstations.dev/v1/:commit', + { credentials: 'include' } + ); + stub.restore(); + }); +}); diff --git a/packages/firestore/test/unit/remote/rest_connection.test.ts b/packages/firestore/test/unit/remote/rest_connection.test.ts index d45a75ce67b..100b8b8368e 100644 --- a/packages/firestore/test/unit/remote/rest_connection.test.ts +++ b/packages/firestore/test/unit/remote/rest_connection.test.ts @@ -67,7 +67,8 @@ describe('RestConnection', () => { /*forceLongPolling=*/ false, /*autoDetectLongPolling=*/ false, /*longPollingOptions=*/ {}, - /*useFetchStreams=*/ false + /*useFetchStreams=*/ false, + /*isUsingEmulator=*/ false ); const connection = new TestRestConnection(testDatabaseInfo); diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index ee0af0b8bf8..51d2229b8a1 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -282,7 +282,8 @@ abstract class TestRunner { /*forceLongPolling=*/ false, /*autoDetectLongPolling=*/ false, /*longPollingOptions=*/ {}, - /*useFetchStreams=*/ false + /*useFetchStreams=*/ false, + /*isUsingEmulator=*/ false ); // TODO(mrschmidt): During client startup in `firestore_client`, we block diff --git a/packages/storage/src/implementation/connection.ts b/packages/storage/src/implementation/connection.ts index 80e29c9cd2f..f7630e59708 100644 --- a/packages/storage/src/implementation/connection.ts +++ b/packages/storage/src/implementation/connection.ts @@ -42,6 +42,7 @@ export interface Connection { send( url: string, method: string, + isUsingEmulator: boolean, body?: ArrayBufferView | Blob | string | null, headers?: Headers ): Promise; diff --git a/packages/storage/src/implementation/request.ts b/packages/storage/src/implementation/request.ts index fae46d7a5ab..adfda6e4460 100644 --- a/packages/storage/src/implementation/request.ts +++ b/packages/storage/src/implementation/request.ts @@ -71,7 +71,8 @@ class NetworkRequest implements Request { private timeout_: number, private progressCallback_: ((p1: number, p2: number) => void) | null, private connectionFactory_: () => Connection, - private retry = true + private retry = true, + private isUsingEmulator = false ) { this.promise_ = new Promise((resolve, reject) => { this.resolve_ = resolve as (value?: O | PromiseLike) => void; @@ -111,7 +112,13 @@ class NetworkRequest implements Request { // connection.send() never rejects, so we don't need to have a error handler or use catch on the returned promise. // eslint-disable-next-line @typescript-eslint/no-floating-promises connection - .send(this.url_, this.method_, this.body_, this.headers_) + .send( + this.url_, + this.method_, + this.isUsingEmulator, + this.body_, + this.headers_ + ) .then(() => { if (this.progressCallback_ !== null) { connection.removeUploadProgressListener(progressListener); @@ -261,7 +268,8 @@ export function makeRequest( appCheckToken: string | null, requestFactory: () => Connection, firebaseVersion?: string, - retry = true + retry = true, + isUsingEmulator = false ): Request { const queryPart = makeQueryString(requestInfo.urlParams); const url = requestInfo.url + queryPart; @@ -282,6 +290,7 @@ export function makeRequest( requestInfo.timeout, requestInfo.progressCallback, requestFactory, - retry + retry, + isUsingEmulator ); } diff --git a/packages/storage/src/platform/browser/connection.ts b/packages/storage/src/platform/browser/connection.ts index fdd9b496242..77a2e42809b 100644 --- a/packages/storage/src/platform/browser/connection.ts +++ b/packages/storage/src/platform/browser/connection.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { isCloudWorkstation } from '@firebase/util'; import { Connection, ConnectionType, @@ -62,12 +63,16 @@ abstract class XhrConnection send( url: string, method: string, + isUsingEmulator: boolean, body?: ArrayBufferView | Blob | string, headers?: Headers ): Promise { if (this.sent_) { throw internalError('cannot .send() more than once'); } + if (isCloudWorkstation(url) && isUsingEmulator) { + this.xhr_.withCredentials = true; + } this.sent_ = true; this.xhr_.open(method, url, true); if (headers !== undefined) { diff --git a/packages/storage/src/platform/node/connection.ts b/packages/storage/src/platform/node/connection.ts index c90f664c3b2..2dd869eb2f0 100644 --- a/packages/storage/src/platform/node/connection.ts +++ b/packages/storage/src/platform/node/connection.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { isCloudWorkstation } from '@firebase/util'; import { Connection, ConnectionType, @@ -48,6 +49,7 @@ abstract class FetchConnection async send( url: string, method: string, + isUsingEmulator: boolean, body?: NodeJS.ArrayBufferView | Blob | string, headers?: Record ): Promise { @@ -57,11 +59,13 @@ abstract class FetchConnection this.sent_ = true; try { - const response = await fetch(url, { + const response = await newFetch( + url, method, - headers: headers || {}, - body: body as NodeJS.ArrayBufferView | string - }); + isUsingEmulator, + headers, + body + ); this.headers_ = response.headers; this.statusCode_ = response.status; this.errorCode_ = ErrorCode.NO_ERROR; @@ -152,6 +156,7 @@ export class FetchStreamConnection extends FetchConnection< async send( url: string, method: string, + isUsingEmulator: boolean, body?: NodeJS.ArrayBufferView | Blob | string, headers?: Record ): Promise { @@ -161,11 +166,13 @@ export class FetchStreamConnection extends FetchConnection< this.sent_ = true; try { - const response = await fetch(url, { + const response = await newFetch( + url, method, - headers: headers || {}, - body: body as NodeJS.ArrayBufferView | string - }); + isUsingEmulator, + headers, + body + ); this.headers_ = response.headers; this.statusCode_ = response.status; this.errorCode_ = ErrorCode.NO_ERROR; @@ -186,6 +193,24 @@ export class FetchStreamConnection extends FetchConnection< } } +function newFetch( + url: string, + method: string, + isUsingEmulator: boolean, + headers?: Record, + body?: NodeJS.ArrayBufferView | Blob | string +): Promise { + const fetchArgs: RequestInit = { + method, + headers: headers || {}, + body: body as NodeJS.ArrayBufferView | string + }; + if (isCloudWorkstation(url) && isUsingEmulator) { + fetchArgs.credentials = 'include'; + } + return fetch(url, fetchArgs); +} + export function newStreamConnection(): Connection> { return new FetchStreamConnection(); } diff --git a/packages/storage/src/service.ts b/packages/storage/src/service.ts index 8a942aac62a..8ad31ecb13c 100644 --- a/packages/storage/src/service.ts +++ b/packages/storage/src/service.ts @@ -146,6 +146,7 @@ export function connectStorageEmulator( ): void { storage.host = `${host}:${port}`; const useSsl = isCloudWorkstation(host); + storage._isUsingEmulator = true; storage._protocol = useSsl ? 'https' : 'http'; const { mockUserToken } = options; if (mockUserToken) { @@ -192,7 +193,8 @@ export class FirebaseStorageImpl implements FirebaseStorage { * @internal */ readonly _url?: string, - readonly _firebaseVersion?: string + readonly _firebaseVersion?: string, + public _isUsingEmulator = false ) { this._maxOperationRetryTime = DEFAULT_MAX_OPERATION_RETRY_TIME; this._maxUploadRetryTime = DEFAULT_MAX_UPLOAD_RETRY_TIME; @@ -325,7 +327,8 @@ export class FirebaseStorageImpl implements FirebaseStorage { appCheckToken, requestFactory, this._firebaseVersion, - retry + retry, + this._isUsingEmulator ); this._requests.add(request); // Request removes itself from set when complete. diff --git a/packages/storage/test/browser/connection.test.ts b/packages/storage/test/browser/connection.test.ts index b869c9ee31b..2a0320d0c02 100644 --- a/packages/storage/test/browser/connection.test.ts +++ b/packages/storage/test/browser/connection.test.ts @@ -24,11 +24,27 @@ describe('Connections', () => { it('XhrConnection.send() should not reject on network errors', async () => { const fakeXHR = useFakeXMLHttpRequest(); const connection = new XhrBytesConnection(); - const sendPromise = connection.send('testurl', 'GET'); + const sendPromise = connection.send('testurl', 'GET', false); // simulate a network error ((connection as any).xhr_ as SinonFakeXMLHttpRequest).error(); await sendPromise; expect(connection.getErrorCode()).to.equal(ErrorCode.NETWORK_ERROR); fakeXHR.restore(); }); + it('XhrConnection.send() should send credentials when using cloud workstation', async () => { + const fakeXHR = useFakeXMLHttpRequest(); + const connection = new XhrBytesConnection(); + const sendPromise = connection.send( + 'https://abc.cloudworkstations.dev', + 'GET', + true + ); + // simulate a network error + ((connection as any).xhr_ as SinonFakeXMLHttpRequest).error(); + await sendPromise; + expect( + ((connection as any).xhr_ as SinonFakeXMLHttpRequest).withCredentials + ).to.be.true; + fakeXHR.restore(); + }); }); diff --git a/packages/storage/test/node/connection.test.ts b/packages/storage/test/node/connection.test.ts index 925d1f8f7dc..5c9f2efe41d 100644 --- a/packages/storage/test/node/connection.test.ts +++ b/packages/storage/test/node/connection.test.ts @@ -25,8 +25,27 @@ describe('Connections', () => { const connection = new FetchBytesConnection(); const fetchStub = stub(globalThis, 'fetch').rejects(); - await connection.send('testurl', 'GET'); + await connection.send('testurl', 'GET', false); expect(connection.getErrorCode()).to.equal(ErrorCode.NETWORK_ERROR); + + fetchStub.restore(); + }); + it('FetchConnection.send() should send credentials on cloud workstations', async () => { + const connection = new FetchBytesConnection(); + + const fetchStub = stub(globalThis, 'fetch').rejects(); + await connection.send( + 'http://something.cloudworkstations.dev', + 'GET', + true + ); + expect(connection.getErrorCode()).to.equal(ErrorCode.NETWORK_ERROR); + expect(fetchStub).to.have.been.calledWithMatch( + 'http://something.cloudworkstations.dev', + { + credentials: 'include' + } + ); fetchStub.restore(); }); }); diff --git a/packages/storage/test/unit/connection.ts b/packages/storage/test/unit/connection.ts index 6b800a17f91..a2f0ca58750 100644 --- a/packages/storage/test/unit/connection.ts +++ b/packages/storage/test/unit/connection.ts @@ -60,6 +60,7 @@ export class TestingConnection implements Connection { send( url: string, method: string, + _isUsingEmulator: boolean, body?: ArrayBufferView | Blob | string | null, headers?: Headers ): Promise { diff --git a/packages/storage/test/unit/service.test.ts b/packages/storage/test/unit/service.test.ts index bc443c60a03..b37e624e3d1 100644 --- a/packages/storage/test/unit/service.test.ts +++ b/packages/storage/test/unit/service.test.ts @@ -14,7 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { expect } from 'chai'; +import { expect, use } from 'chai'; +import * as sinon from 'sinon'; import { TaskEvent } from '../../src/implementation/taskenums'; import { Headers } from '../../src/implementation/connection'; import { @@ -34,11 +35,13 @@ import { import { Location } from '../../src/implementation/location'; import { newTestConnection, TestingConnection } from './connection'; import { injectTestConnection } from '../../src/platform/connection'; +import sinonChai from 'sinon-chai'; const fakeAppGs = testShared.makeFakeApp('gs://mybucket'); const fakeAppGsEndingSlash = testShared.makeFakeApp('gs://mybucket/'); const fakeAppInvalidGs = testShared.makeFakeApp('gs://mybucket/hello'); const testLocation = new Location('bucket', 'object'); +use(sinonChai); function makeGsUrl(child: string = ''): string { return 'gs://' + testShared.bucket + '/' + child; @@ -227,6 +230,13 @@ GOOG4-RSA-SHA256` }); }); describe('connectStorageEmulator(service, host, port, options)', () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + afterEach(() => { + sandbox.restore(); + }); it('sets emulator host correctly', done => { function newSend(connection: TestingConnection, url: string): void { // Expect emulator host to be in url of storage operations requests, From 080a90dccbb5a9cc402a3fc8feca4c81fc7e1305 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Wed, 30 Apr 2025 18:49:33 -0700 Subject: [PATCH 20/23] Add Cookie Support For Firebase Studio (#8986) --- common/api-review/util.api.md | 3 +++ packages/auth/src/core/auth/emulator.ts | 7 ++++++- packages/data-connect/src/api/DataConnect.ts | 5 +++++ packages/database/src/api/Database.ts | 8 +++++++- packages/firestore/src/api/database.ts | 12 +++++++++++- packages/firestore/src/lite-api/database.ts | 6 +++++- packages/functions/src/service.ts | 6 +++++- packages/storage/src/service.ts | 7 ++++++- packages/util/src/url.ts | 12 ++++++++++++ 9 files changed, 60 insertions(+), 6 deletions(-) diff --git a/common/api-review/util.api.md b/common/api-review/util.api.md index 28cc9a160d4..0f8fc13cd3a 100644 --- a/common/api-review/util.api.md +++ b/common/api-review/util.api.md @@ -398,6 +398,9 @@ export function ordinal(i: number): string; // @public (undocumented) export type PartialObserver = Partial>; +// @public +export function pingServer(endpoint: string): Promise; + // Warning: (ae-internal-missing-underscore) The name "promiseWithTimeout" should be prefixed with an underscore because the declaration is marked as @internal // // @internal diff --git a/packages/auth/src/core/auth/emulator.ts b/packages/auth/src/core/auth/emulator.ts index 60cc9403d3d..05f2e5e4bd5 100644 --- a/packages/auth/src/core/auth/emulator.ts +++ b/packages/auth/src/core/auth/emulator.ts @@ -18,7 +18,7 @@ import { Auth } from '../../model/public_types'; import { AuthErrorCode } from '../errors'; import { _assert } from '../util/assert'; import { _castAuth } from './auth_impl'; -import { deepEqual } from '@firebase/util'; +import { deepEqual, isCloudWorkstation, pingServer } from '@firebase/util'; /** * Changes the {@link Auth} instance to communicate with the Firebase Auth Emulator, instead of production @@ -100,6 +100,11 @@ export function connectAuthEmulator( if (!disableWarnings) { emitEmulatorWarning(); } + + // Workaround to get cookies in Firebase Studio + if (isCloudWorkstation(host)) { + void pingServer(`${protocol}//${host}:${port}`); + } } function extractProtocol(url: string): string { diff --git a/packages/data-connect/src/api/DataConnect.ts b/packages/data-connect/src/api/DataConnect.ts index dc170809143..c25a09039ac 100644 --- a/packages/data-connect/src/api/DataConnect.ts +++ b/packages/data-connect/src/api/DataConnect.ts @@ -24,6 +24,7 @@ import { import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { Provider } from '@firebase/component'; +import { isCloudWorkstation, pingServer } from '@firebase/util'; import { AppCheckTokenProvider } from '../core/AppCheckTokenProvider'; import { Code, DataConnectError } from '../core/error'; @@ -237,6 +238,10 @@ export function connectDataConnectEmulator( port?: number, sslEnabled = false ): void { + // Workaround to get cookies in Firebase Studio + if (isCloudWorkstation(host)) { + void pingServer(`https://${host}${port ? `:${port}` : ''}`); + } dc.enableEmulator({ host, port, sslEnabled }); } diff --git a/packages/database/src/api/Database.ts b/packages/database/src/api/Database.ts index f247fc6288c..515e278b5c5 100644 --- a/packages/database/src/api/Database.ts +++ b/packages/database/src/api/Database.ts @@ -30,7 +30,8 @@ import { deepEqual, EmulatorMockTokenOptions, getDefaultEmulatorHostnameAndPort, - isCloudWorkstation + isCloudWorkstation, + pingServer } from '@firebase/util'; import { AppCheckTokenProvider } from '../core/AppCheckTokenProvider'; @@ -389,6 +390,11 @@ export function connectDatabaseEmulator( tokenProvider = new EmulatorTokenProvider(token); } + // Workaround to get cookies in Firebase Studio + if (isCloudWorkstation(host)) { + void pingServer(host); + } + // Modify the repo to apply emulator settings repoManagerApplyEmulatorSettings(repo, hostAndPort, options, tokenProvider); } diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 0757378a74c..a2feb19507f 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -21,7 +21,12 @@ import { FirebaseApp, getApp } from '@firebase/app'; -import { deepEqual, getDefaultEmulatorHostnameAndPort } from '@firebase/util'; +import { + deepEqual, + getDefaultEmulatorHostnameAndPort, + isCloudWorkstation, + pingServer +} from '@firebase/util'; import { User } from '../auth/user'; import { @@ -194,6 +199,11 @@ export function initializeFirestore( ); } + // Workaround to get cookies in Firebase Studio + if (settings.host && isCloudWorkstation(settings.host)) { + void pingServer(settings.host); + } + return provider.initialize({ options: settings, instanceIdentifier: databaseId diff --git a/packages/firestore/src/lite-api/database.ts b/packages/firestore/src/lite-api/database.ts index 294206e54c2..8e7fdb27e90 100644 --- a/packages/firestore/src/lite-api/database.ts +++ b/packages/firestore/src/lite-api/database.ts @@ -27,7 +27,8 @@ import { deepEqual, EmulatorMockTokenOptions, getDefaultEmulatorHostnameAndPort, - isCloudWorkstation + isCloudWorkstation, + pingServer } from '@firebase/util'; import { @@ -333,6 +334,9 @@ export function connectFirestoreEmulator( emulatorOptions: firestore._getEmulatorOptions() }; const newHostSetting = `${host}:${port}`; + if (useSsl) { + void pingServer(`https://${newHostSetting}`); + } if (settings.host !== DEFAULT_HOST && settings.host !== newHostSetting) { logWarn( 'Host has been set in both settings() and connectFirestoreEmulator(), emulator host ' + diff --git a/packages/functions/src/service.ts b/packages/functions/src/service.ts index dce52e1f19d..af9d8898d2e 100644 --- a/packages/functions/src/service.ts +++ b/packages/functions/src/service.ts @@ -30,7 +30,7 @@ import { Provider } from '@firebase/component'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { MessagingInternalComponentName } from '@firebase/messaging-interop-types'; import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; -import { isCloudWorkstation } from '@firebase/util'; +import { isCloudWorkstation, pingServer } from '@firebase/util'; export const DEFAULT_REGION = 'us-central1'; @@ -179,6 +179,10 @@ export function connectFunctionsEmulator( functionsInstance.emulatorOrigin = `http${ useSsl ? 's' : '' }://${host}:${port}`; + // Workaround to get cookies in Firebase Studio + if (useSsl) { + void pingServer(functionsInstance.emulatorOrigin); + } } /** diff --git a/packages/storage/src/service.ts b/packages/storage/src/service.ts index 8ad31ecb13c..741dd6eaa1a 100644 --- a/packages/storage/src/service.ts +++ b/packages/storage/src/service.ts @@ -45,7 +45,8 @@ import { FirebaseStorage } from './public-types'; import { createMockUserToken, EmulatorMockTokenOptions, - isCloudWorkstation + isCloudWorkstation, + pingServer } from '@firebase/util'; import { Connection, ConnectionType } from './implementation/connection'; @@ -146,6 +147,10 @@ export function connectStorageEmulator( ): void { storage.host = `${host}:${port}`; const useSsl = isCloudWorkstation(host); + // Workaround to get cookies in Firebase Studio + if (useSsl) { + void pingServer(`https://${storage.host}`); + } storage._isUsingEmulator = true; storage._protocol = useSsl ? 'https' : 'http'; const { mockUserToken } = options; diff --git a/packages/util/src/url.ts b/packages/util/src/url.ts index 33cec43bea9..e41d26594c2 100644 --- a/packages/util/src/url.ts +++ b/packages/util/src/url.ts @@ -22,3 +22,15 @@ export function isCloudWorkstation(host: string): boolean { return host.endsWith('.cloudworkstations.dev'); } + +/** + * Makes a fetch request to the given server. + * Mostly used for forwarding cookies in Firebase Studio. + * @public + */ +export async function pingServer(endpoint: string): Promise { + const result = await fetch(endpoint, { + credentials: 'include' + }); + return result.ok; +} From ac59a1c18d5a5b5fc6e3fb2c0e5647c26f8006c2 Mon Sep 17 00:00:00 2001 From: Mila <107142260+milaGGL@users.noreply.github.com> Date: Thu, 1 May 2025 13:02:18 -0400 Subject: [PATCH 21/23] Remove FieldValue factory methods (#342) --- common/api-review/firestore-lite.api.md | 23 +- common/api-review/firestore.api.md | 23 +- packages/firestore/lite/index.ts | 11 +- packages/firestore/src/api.ts | 11 +- .../firestore/src/api/field_value_impl.ts | 9 +- .../src/lite-api/bson_binary_data.ts | 6 +- ...n_timestamp_value.ts => bson_timestamp.ts} | 14 +- .../src/lite-api/field_value_impl.ts | 90 ------ .../src/lite-api/user_data_reader.ts | 2 +- .../src/lite-api/user_data_writer.ts | 9 +- .../test/integration/api/database.test.ts | 258 +++++++++-------- .../test/integration/api/type.test.ts | 92 +++--- .../firestore/test/lite/integration.test.ts | 48 ++-- .../firestore_index_value_writer.test.ts | 39 ++- .../test/unit/local/index_manager.test.ts | 267 +++++++++++------- .../unit/local/local_store_indexeddb.test.ts | 227 ++++++++------- .../test/unit/model/document.test.ts | 44 +-- .../test/unit/model/object_value.test.ts | 130 +++++---- .../firestore/test/unit/model/values.test.ts | 179 ++++++------ .../test/unit/remote/serializer.helper.ts | 32 +-- 20 files changed, 751 insertions(+), 763 deletions(-) rename packages/firestore/src/lite-api/{bson_timestamp_value.ts => bson_timestamp.ts} (72%) diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index 394fd0b33ce..f0203c034b3 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -68,14 +68,13 @@ export function average(field: string | FieldPath): AggregateField> = UnionToIntersection<{ [K in keyof T & string]: ChildUpdateFields; @@ -426,9 +410,6 @@ export class QueryStartAtConstraint extends QueryConstraint { // @public export function refEqual(left: DocumentReference | CollectionReference, right: DocumentReference | CollectionReference): boolean; -// @public -export function regex(pattern: string, options: string): RegexValue; - // @public export class RegexValue { constructor(pattern: string, options: string); diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 39dcdb6df59..90137f78b00 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -68,14 +68,13 @@ export function average(field: string | FieldPath): AggregateField; @@ -686,9 +670,6 @@ export class QueryStartAtConstraint extends QueryConstraint { // @public export function refEqual(left: DocumentReference | CollectionReference, right: DocumentReference | CollectionReference): boolean; -// @public -export function regex(pattern: string, options: string): RegexValue; - // @public export class RegexValue { constructor(pattern: string, options: string); diff --git a/packages/firestore/lite/index.ts b/packages/firestore/lite/index.ts index 48e0bdae068..7eee71a9893 100644 --- a/packages/firestore/lite/index.ts +++ b/packages/firestore/lite/index.ts @@ -128,14 +128,7 @@ export { arrayUnion, serverTimestamp, deleteField, - vector, - int32, - regex, - bsonBinaryData, - bsonObjectId, - bsonTimestamp, - minKey, - maxKey + vector } from '../src/lite-api/field_value_impl'; export { @@ -156,7 +149,7 @@ export { BsonBinaryData } from '../src/lite-api/bson_binary_data'; export { BsonObjectId } from '../src/lite-api/bson_object_Id'; -export { BsonTimestamp } from '../src/lite-api/bson_timestamp_value'; +export { BsonTimestamp } from '../src/lite-api/bson_timestamp'; export { MinKey } from '../src/lite-api/min_key'; diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index ec6fdd2c4ea..b9d14923bcd 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -173,14 +173,7 @@ export { deleteField, increment, serverTimestamp, - vector, - int32, - regex, - bsonBinaryData, - bsonObjectId, - bsonTimestamp, - minKey, - maxKey + vector } from './api/field_value_impl'; export { VectorValue } from './lite-api/vector_value'; @@ -193,7 +186,7 @@ export { BsonBinaryData } from './lite-api/bson_binary_data'; export { BsonObjectId } from './lite-api/bson_object_Id'; -export { BsonTimestamp } from './lite-api/bson_timestamp_value'; +export { BsonTimestamp } from './lite-api/bson_timestamp'; export { MinKey } from './lite-api/min_key'; diff --git a/packages/firestore/src/api/field_value_impl.ts b/packages/firestore/src/api/field_value_impl.ts index 6e65d273259..1b1283a3543 100644 --- a/packages/firestore/src/api/field_value_impl.ts +++ b/packages/firestore/src/api/field_value_impl.ts @@ -21,12 +21,5 @@ export { arrayUnion, serverTimestamp, deleteField, - vector, - int32, - regex, - bsonBinaryData, - bsonObjectId, - bsonTimestamp, - minKey, - maxKey + vector } from '../lite-api/field_value_impl'; diff --git a/packages/firestore/src/lite-api/bson_binary_data.ts b/packages/firestore/src/lite-api/bson_binary_data.ts index 233dd790aec..8b4b1fe0ef0 100644 --- a/packages/firestore/src/lite-api/bson_binary_data.ts +++ b/packages/firestore/src/lite-api/bson_binary_data.ts @@ -24,13 +24,9 @@ import { Code, FirestoreError } from '../util/error'; * @class BsonBinaryData */ export class BsonBinaryData { - /** The subtype for the data */ - readonly subtype: number; - - /** The binary data as a byte array */ readonly data: Uint8Array; - constructor(subtype: number, data: Uint8Array) { + constructor(readonly subtype: number, data: Uint8Array) { if (subtype < 0 || subtype > 255) { throw new FirestoreError( Code.INVALID_ARGUMENT, diff --git a/packages/firestore/src/lite-api/bson_timestamp_value.ts b/packages/firestore/src/lite-api/bson_timestamp.ts similarity index 72% rename from packages/firestore/src/lite-api/bson_timestamp_value.ts rename to packages/firestore/src/lite-api/bson_timestamp.ts index 0b317f9042c..dc18db02bb1 100644 --- a/packages/firestore/src/lite-api/bson_timestamp_value.ts +++ b/packages/firestore/src/lite-api/bson_timestamp.ts @@ -21,7 +21,19 @@ * @class BsonTimestamp */ export class BsonTimestamp { - constructor(readonly seconds: number, readonly increment: number) {} + constructor(readonly seconds: number, readonly increment: number) { + // Make sure 'seconds' and 'increment' are in the range of a 32-bit unsigned integer. + if (seconds < 0 || seconds > 4294967295) { + throw new Error( + "BsonTimestamp 'seconds' must be in the range of a 32-bit unsigned integer." + ); + } + if (increment < 0 || increment > 4294967295) { + throw new Error( + "BsonTimestamp 'increment' must be in the range of a 32-bit unsigned integer." + ); + } + } /** * Returns true if this `BsonTimestamp` is equal to the provided one. diff --git a/packages/firestore/src/lite-api/field_value_impl.ts b/packages/firestore/src/lite-api/field_value_impl.ts index ade0656e0d3..11db1005235 100644 --- a/packages/firestore/src/lite-api/field_value_impl.ts +++ b/packages/firestore/src/lite-api/field_value_impl.ts @@ -14,15 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { BsonBinaryData } from './bson_binary_data'; -import { BsonObjectId } from './bson_object_Id'; -import { BsonTimestamp } from './bson_timestamp_value'; import { FieldValue } from './field_value'; -import { Int32Value } from './int32_value'; -import { MaxKey } from './max_key'; -import { MinKey } from './min_key'; -import { RegexValue } from './regex_value'; import { ArrayRemoveFieldValueImpl, ArrayUnionFieldValueImpl, @@ -116,85 +108,3 @@ export function increment(n: number): FieldValue { export function vector(values?: number[]): VectorValue { return new VectorValue(values); } - -/** - * Creates a new `Int32Value` constructed with the given number. - * - * @param value - The 32-bit number to be used for constructing the Int32Value - * - * @returns A new `Int32Value` constructed with the given number. - */ -export function int32(value: number): Int32Value { - return new Int32Value(value); -} - -/** - * Creates a new `RegexValue` constructed with the given pattern and options. - * - * @param subtype - The subtype of the BSON binary data. - * @param data - The data to use for the BSON binary data. - * - * @returns A new `RegexValue` constructed with the given pattern and options. - */ -export function regex(pattern: string, options: string): RegexValue { - return new RegexValue(pattern, options); -} - -/** - * Creates a new `BsonBinaryData` constructed with the given subtype and data. - * - * @param subtype - Create a `BsonBinaryData` instance with the given subtype. - * @param data - Create a `BsonBinaryData` instance with a copy of this array of numbers. - * - * @returns A new `BsonBinaryData` constructed with the given subtype and data. - */ -export function bsonBinaryData( - subtype: number, - data: Uint8Array -): BsonBinaryData { - return new BsonBinaryData(subtype, data); -} - -/** - * Creates a new `BsonObjectId` constructed with the given string. - * - * @param value - The 24-character hex string representing the ObjectId. - * - * @returns A new `BsonObjectId` constructed with the given string. - */ -export function bsonObjectId(value: string): BsonObjectId { - return new BsonObjectId(value); -} - -/** - * Creates a new `BsonTimestamp` constructed with the given seconds and increment. - * - * @param seconds - The underlying unsigned 32-bit integer for seconds. - * @param seconds - The underlying unsigned 32-bit integer for increment. - * - * @returns A new `BsonTimestamp` constructed with the given seconds and increment. - */ -export function bsonTimestamp( - seconds: number, - increment: number -): BsonTimestamp { - return new BsonTimestamp(seconds, increment); -} - -/** - * Creates or returns a `MinKey` instance. - * - * @returns A `MinKey` instance. - */ -export function minKey(): MinKey { - return MinKey.instance(); -} - -/** - * Creates or returns a `MaxKey` instance. - * - * @returns A `MaxKey` instance. - */ -export function maxKey(): MaxKey { - return MaxKey.instance(); -} diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index 9d7e6fa79f1..008a5225d9d 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -77,7 +77,7 @@ import { Dict, forEach, isEmpty } from '../util/obj'; import { BsonBinaryData } from './bson_binary_data'; import { BsonObjectId } from './bson_object_Id'; -import { BsonTimestamp } from './bson_timestamp_value'; +import { BsonTimestamp } from './bson_timestamp'; import { Bytes } from './bytes'; import { Firestore } from './database'; import { FieldPath } from './field_path'; diff --git a/packages/firestore/src/lite-api/user_data_writer.ts b/packages/firestore/src/lite-api/user_data_writer.ts index e4719591b4c..94e3f96fe12 100644 --- a/packages/firestore/src/lite-api/user_data_writer.ts +++ b/packages/firestore/src/lite-api/user_data_writer.ts @@ -60,10 +60,11 @@ import { forEach } from '../util/obj'; import { BsonBinaryData } from './bson_binary_data'; import { BsonObjectId } from './bson_object_Id'; -import { BsonTimestamp } from './bson_timestamp_value'; -import { maxKey, minKey } from './field_value_impl'; +import { BsonTimestamp } from './bson_timestamp'; import { GeoPoint } from './geo_point'; import { Int32Value } from './int32_value'; +import { MaxKey } from './max_key'; +import { MinKey } from './min_key'; import { RegexValue } from './regex_value'; import { Timestamp } from './timestamp'; import { VectorValue } from './vector_value'; @@ -118,9 +119,9 @@ export abstract class AbstractUserDataWriter { case TypeOrder.BsonTimestampValue: return this.convertToBsonTimestamp(value.mapValue!); case TypeOrder.MaxKeyValue: - return maxKey(); + return MaxKey.instance(); case TypeOrder.MinKeyValue: - return minKey(); + return MinKey.instance(); default: throw fail('Invalid value type: ' + JSON.stringify(value)); } diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 2071b786f24..6630384bce1 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -67,17 +67,17 @@ import { QuerySnapshot, vector, getDocsFromServer, - bsonBinaryData, - bsonObjectId, - bsonTimestamp, - int32, - maxKey, - minKey, - regex, or, newTestFirestore, GeoPoint, - Bytes + Bytes, + BsonBinaryData, + BsonObjectId, + Int32Value, + MaxKey, + MinKey, + RegexValue, + BsonTimestamp } from '../util/firebase_export'; import { apiDescribe, @@ -2454,20 +2454,20 @@ apiDescribe('Database', persistence => { {}, async coll => { const docRef = await addDoc(coll, { - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - objectId: bsonObjectId('507f191e810c19729de860ea'), - int32: int32(1), - min: minKey(), - max: maxKey(), - regex: regex('^foo', 'i') + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + objectId: new BsonObjectId('507f191e810c19729de860ea'), + int32: new Int32Value(1), + min: MinKey.instance(), + max: MaxKey.instance(), + regex: new RegexValue('^foo', 'i') }); await setDoc( docRef, { - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - timestamp: bsonTimestamp(1, 2), - int32: int32(2) + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: new BsonTimestamp(1, 2), + int32: new Int32Value(2) }, { merge: true } ); @@ -2476,19 +2476,20 @@ apiDescribe('Database', persistence => { expect( snapshot .get('objectId') - .isEqual(bsonObjectId('507f191e810c19729de860ea')) + .isEqual(new BsonObjectId('507f191e810c19729de860ea')) ).to.be.true; - expect(snapshot.get('int32').isEqual(int32(2))).to.be.true; - expect(snapshot.get('min') === minKey()).to.be.true; - expect(snapshot.get('max') === maxKey()).to.be.true; + expect(snapshot.get('int32').isEqual(new Int32Value(2))).to.be.true; + expect(snapshot.get('min') === MinKey.instance()).to.be.true; + expect(snapshot.get('max') === MaxKey.instance()).to.be.true; expect( snapshot .get('binary') - .isEqual(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + .isEqual(new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ).to.be.true; - expect(snapshot.get('timestamp').isEqual(bsonTimestamp(1, 2))).to.be - .true; - expect(snapshot.get('regex').isEqual(regex('^foo', 'i'))).to.be.true; + expect(snapshot.get('timestamp').isEqual(new BsonTimestamp(1, 2))).to + .be.true; + expect(snapshot.get('regex').isEqual(new RegexValue('^foo', 'i'))).to + .be.true; } ); }); @@ -2506,41 +2507,42 @@ apiDescribe('Database', persistence => { // Adding docs to cache, do not wait for promise to resolve. // eslint-disable-next-line @typescript-eslint/no-floating-promises setDoc(docRef, { - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - objectId: bsonObjectId('507f191e810c19729de860ea'), - int32: int32(1), - regex: regex('^foo', 'i'), - timestamp: bsonTimestamp(1, 2), - min: minKey(), - max: maxKey() + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + objectId: new BsonObjectId('507f191e810c19729de860ea'), + int32: new Int32Value(1), + regex: new RegexValue('^foo', 'i'), + timestamp: new BsonTimestamp(1, 2), + min: MinKey.instance(), + max: MaxKey.instance() }); const snapshot = await getDocFromCache(docRef); expect( snapshot .get('binary') - .isEqual(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + .isEqual(new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ).to.be.true; expect( snapshot .get('objectId') - .isEqual(bsonObjectId('507f191e810c19729de860ea')) + .isEqual(new BsonObjectId('507f191e810c19729de860ea')) ).to.be.true; - expect(snapshot.get('int32').isEqual(int32(1))).to.be.true; - expect(snapshot.get('regex').isEqual(regex('^foo', 'i'))).to.be.true; - expect(snapshot.get('timestamp').isEqual(bsonTimestamp(1, 2))).to.be - .true; - expect(snapshot.get('min') === minKey()).to.be.true; - expect(snapshot.get('max') === maxKey()).to.be.true; + expect(snapshot.get('int32').isEqual(new Int32Value(1))).to.be.true; + expect(snapshot.get('regex').isEqual(new RegexValue('^foo', 'i'))).to + .be.true; + expect(snapshot.get('timestamp').isEqual(new BsonTimestamp(1, 2))).to + .be.true; + expect(snapshot.get('min') === MinKey.instance()).to.be.true; + expect(snapshot.get('max') === MaxKey.instance()).to.be.true; } ); }); it('can filter and order objectIds', async () => { const testDocs = { - a: { key: bsonObjectId('507f191e810c19729de860ea') }, - b: { key: bsonObjectId('507f191e810c19729de860eb') }, - c: { key: bsonObjectId('507f191e810c19729de860ec') } + a: { key: new BsonObjectId('507f191e810c19729de860ea') }, + b: { key: new BsonObjectId('507f191e810c19729de860eb') }, + c: { key: new BsonObjectId('507f191e810c19729de860ec') } }; return withTestProjectIdAndCollectionSettings( @@ -2554,7 +2556,7 @@ apiDescribe('Database', persistence => { let orderedQuery = query( coll, - where('key', '>', bsonObjectId('507f191e810c19729de860ea')), + where('key', '>', new BsonObjectId('507f191e810c19729de860ea')), orderBy('key', 'desc') ); @@ -2572,8 +2574,8 @@ apiDescribe('Database', persistence => { orderedQuery = query( coll, where('key', 'in', [ - bsonObjectId('507f191e810c19729de860ea'), - bsonObjectId('507f191e810c19729de860eb') + new BsonObjectId('507f191e810c19729de860ea'), + new BsonObjectId('507f191e810c19729de860eb') ]), orderBy('key', 'desc') ); @@ -2594,9 +2596,9 @@ apiDescribe('Database', persistence => { it('can filter and order Int32 values', async () => { const testDocs = { - a: { key: int32(-1) }, - b: { key: int32(1) }, - c: { key: int32(2) } + a: { key: new Int32Value(-1) }, + b: { key: new Int32Value(1) }, + c: { key: new Int32Value(2) } }; return withTestProjectIdAndCollectionSettings( persistence, @@ -2609,7 +2611,7 @@ apiDescribe('Database', persistence => { let orderedQuery = query( coll, - where('key', '>=', int32(1)), + where('key', '>=', new Int32Value(1)), orderBy('key', 'desc') ); @@ -2626,7 +2628,7 @@ apiDescribe('Database', persistence => { orderedQuery = query( coll, - where('key', 'not-in', [int32(1)]), + where('key', 'not-in', [new Int32Value(1)]), orderBy('key', 'desc') ); @@ -2646,9 +2648,9 @@ apiDescribe('Database', persistence => { it('can filter and order Timestamp values', async () => { const testDocs = { - a: { key: bsonTimestamp(1, 1) }, - b: { key: bsonTimestamp(1, 2) }, - c: { key: bsonTimestamp(2, 1) } + a: { key: new BsonTimestamp(1, 1) }, + b: { key: new BsonTimestamp(1, 2) }, + c: { key: new BsonTimestamp(2, 1) } }; return withTestProjectIdAndCollectionSettings( persistence, @@ -2661,7 +2663,7 @@ apiDescribe('Database', persistence => { let orderedQuery = query( coll, - where('key', '>', bsonTimestamp(1, 1)), + where('key', '>', new BsonTimestamp(1, 1)), orderBy('key', 'desc') ); @@ -2678,7 +2680,7 @@ apiDescribe('Database', persistence => { orderedQuery = query( coll, - where('key', '!=', bsonTimestamp(1, 1)), + where('key', '!=', new BsonTimestamp(1, 1)), orderBy('key', 'desc') ); @@ -2698,9 +2700,9 @@ apiDescribe('Database', persistence => { it('can filter and order Binary values', async () => { const testDocs = { - a: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, - b: { key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) }, - c: { key: bsonBinaryData(2, new Uint8Array([1, 2, 3])) } + a: { key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }, + b: { key: new BsonBinaryData(1, new Uint8Array([1, 2, 4])) }, + c: { key: new BsonBinaryData(2, new Uint8Array([1, 2, 3])) } }; return withTestProjectIdAndCollectionSettings( persistence, @@ -2713,7 +2715,7 @@ apiDescribe('Database', persistence => { let orderedQuery = query( coll, - where('key', '>', bsonBinaryData(1, new Uint8Array([1, 2, 3]))), + where('key', '>', new BsonBinaryData(1, new Uint8Array([1, 2, 3]))), orderBy('key', 'desc') ); @@ -2730,8 +2732,12 @@ apiDescribe('Database', persistence => { orderedQuery = query( coll, - where('key', '>=', bsonBinaryData(1, new Uint8Array([1, 2, 3]))), - where('key', '<', bsonBinaryData(2, new Uint8Array([1, 2, 3]))), + where( + 'key', + '>=', + new BsonBinaryData(1, new Uint8Array([1, 2, 3])) + ), + where('key', '<', new BsonBinaryData(2, new Uint8Array([1, 2, 3]))), orderBy('key', 'desc') ); @@ -2751,9 +2757,9 @@ apiDescribe('Database', persistence => { it('can filter and order Regex values', async () => { const testDocs = { - a: { key: regex('^bar', 'i') }, - b: { key: regex('^bar', 'x') }, - c: { key: regex('^baz', 'i') } + a: { key: new RegexValue('^bar', 'i') }, + b: { key: new RegexValue('^bar', 'x') }, + c: { key: new RegexValue('^baz', 'i') } }; return withTestProjectIdAndCollectionSettings( persistence, @@ -2767,8 +2773,8 @@ apiDescribe('Database', persistence => { const orderedQuery = query( coll, or( - where('key', '>', regex('^bar', 'x')), - where('key', '!=', regex('^bar', 'x')) + where('key', '>', new RegexValue('^bar', 'x')), + where('key', '!=', new RegexValue('^bar', 'x')) ), orderBy('key', 'desc') ); @@ -2789,11 +2795,11 @@ apiDescribe('Database', persistence => { it('can filter and order minKey values', async () => { const testDocs = { - a: { key: minKey() }, - b: { key: minKey() }, + a: { key: MinKey.instance() }, + b: { key: MinKey.instance() }, c: { key: null }, d: { key: 1 }, - e: { key: maxKey() } + e: { key: MaxKey.instance() } }; return withTestProjectIdAndCollectionSettings( persistence, @@ -2804,7 +2810,10 @@ apiDescribe('Database', persistence => { // Populate the cache with all docs first await getDocs(coll); - let filteredQuery = query(coll, where('key', '==', minKey())); + let filteredQuery = query( + coll, + where('key', '==', MinKey.instance()) + ); let snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ testDocs['a'], @@ -2817,7 +2826,7 @@ apiDescribe('Database', persistence => { ); // TODO(Mila/BSON): uncomment after the null inclusion bug - // filteredQuery = query(coll, where('key', '!=', minKey())); + // filteredQuery = query(coll, where('key', '!=', MinKey.instance())); // snapshot = await getDocs(filteredQuery); // expect(toDataArray(snapshot)).to.deep.equal([ // testDocs['d'], @@ -2829,7 +2838,7 @@ apiDescribe('Database', persistence => { // toIds(snapshot) // ); - filteredQuery = query(coll, where('key', '>=', minKey())); + filteredQuery = query(coll, where('key', '>=', MinKey.instance())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ testDocs['a'], @@ -2841,7 +2850,7 @@ apiDescribe('Database', persistence => { toIds(snapshot) ); - filteredQuery = query(coll, where('key', '<=', minKey())); + filteredQuery = query(coll, where('key', '<=', MinKey.instance())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ testDocs['a'], @@ -2853,7 +2862,7 @@ apiDescribe('Database', persistence => { toIds(snapshot) ); - filteredQuery = query(coll, where('key', '>', minKey())); + filteredQuery = query(coll, where('key', '>', MinKey.instance())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); await assertSDKQueryResultsConsistentWithBackend( @@ -2862,7 +2871,7 @@ apiDescribe('Database', persistence => { toIds(snapshot) ); - filteredQuery = query(coll, where('key', '<', minKey())); + filteredQuery = query(coll, where('key', '<', MinKey.instance())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); await assertSDKQueryResultsConsistentWithBackend( @@ -2885,10 +2894,10 @@ apiDescribe('Database', persistence => { it('can filter and order maxKey values', async () => { const testDocs = { - a: { key: minKey() }, + a: { key: MinKey.instance() }, b: { key: 1 }, - c: { key: maxKey() }, - d: { key: maxKey() }, + c: { key: MaxKey.instance() }, + d: { key: MaxKey.instance() }, e: { key: null } }; return withTestProjectIdAndCollectionSettings( @@ -2900,7 +2909,10 @@ apiDescribe('Database', persistence => { // Populate the cache with all docs first await getDocs(coll); - let filteredQuery = query(coll, where('key', '==', maxKey())); + let filteredQuery = query( + coll, + where('key', '==', MaxKey.instance()) + ); let snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ testDocs['c'], @@ -2913,7 +2925,7 @@ apiDescribe('Database', persistence => { ); // TODO(Mila/BSON): uncomment after the null inclusion bug - // filteredQuery = query(coll, where('key', '!=', maxKey())); + // filteredQuery = query(coll, where('key', '!=', MaxKey.instance())); // snapshot = await getDocs(filteredQuery); // expect(toDataArray(snapshot)).to.deep.equal([ // testDocs['a'], @@ -2925,7 +2937,7 @@ apiDescribe('Database', persistence => { // toIds(snapshot) // ); - filteredQuery = query(coll, where('key', '>=', maxKey())); + filteredQuery = query(coll, where('key', '>=', MaxKey.instance())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ testDocs['c'], @@ -2937,7 +2949,7 @@ apiDescribe('Database', persistence => { toIds(snapshot) ); - filteredQuery = query(coll, where('key', '<=', maxKey())); + filteredQuery = query(coll, where('key', '<=', MaxKey.instance())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ testDocs['c'], @@ -2949,7 +2961,7 @@ apiDescribe('Database', persistence => { toIds(snapshot) ); - filteredQuery = query(coll, where('key', '>', maxKey())); + filteredQuery = query(coll, where('key', '>', MaxKey.instance())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); await assertSDKQueryResultsConsistentWithBackend( @@ -2958,7 +2970,7 @@ apiDescribe('Database', persistence => { toIds(snapshot) ); - filteredQuery = query(coll, where('key', '<', maxKey())); + filteredQuery = query(coll, where('key', '<', MaxKey.instance())); snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); await assertSDKQueryResultsConsistentWithBackend( @@ -2981,11 +2993,11 @@ apiDescribe('Database', persistence => { it('can handle null with bson values', async () => { const testDocs = { - a: { key: minKey() }, + a: { key: MinKey.instance() }, b: { key: null }, c: { key: null }, d: { key: 1 }, - e: { key: maxKey() } + e: { key: MaxKey.instance() } }; return withTestProjectIdAndCollectionSettings( @@ -3027,12 +3039,12 @@ apiDescribe('Database', persistence => { it('can listen to documents with bson types', async () => { const testDocs = { - a: { key: maxKey() }, - b: { key: minKey() }, - c: { key: bsonTimestamp(1, 2) }, - d: { key: bsonObjectId('507f191e810c19729de860ea') }, - e: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, - f: { key: regex('^foo', 'i') } + a: { key: MaxKey.instance() }, + b: { key: MinKey.instance() }, + c: { key: new BsonTimestamp(1, 2) }, + d: { key: new BsonObjectId('507f191e810c19729de860ea') }, + e: { key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }, + f: { key: new RegexValue('^foo', 'i') } }; return withTestProjectIdAndCollectionSettings( persistence, @@ -3055,7 +3067,7 @@ apiDescribe('Database', persistence => { testDocs['a'] ]); - const newData = { key: int32(2) }; + const newData = { key: new Int32Value(2) }; await setDoc(doc(coll, 'g'), newData); listenSnapshot = await storeEvent.awaitEvent(); expect(toDataArray(listenSnapshot)).to.deep.equal([ @@ -3077,9 +3089,9 @@ apiDescribe('Database', persistence => { // eslint-disable-next-line no-restricted-properties it.skip('can run transactions on documents with bson types', async () => { const testDocs = { - a: { key: bsonTimestamp(1, 2) }, - b: { key: regex('^foo', 'i') }, - c: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) } + a: { key: new BsonTimestamp(1, 2) }, + b: { key: new RegexValue('^foo', 'i') }, + c: { key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) } }; return withTestProjectIdAndCollectionSettings( persistence, @@ -3114,24 +3126,24 @@ apiDescribe('Database', persistence => { it('SDK orders different value types together the same way online and offline', async () => { const testDocs: { [key: string]: DocumentData } = { a: { key: null }, - b: { key: minKey() }, + b: { key: MinKey.instance() }, c: { key: true }, d: { key: NaN }, - e: { key: int32(1) }, + e: { key: new Int32Value(1) }, f: { key: 2.0 }, g: { key: 3 }, h: { key: new Timestamp(100, 123456000) }, - i: { key: bsonTimestamp(1, 2) }, + i: { key: new BsonTimestamp(1, 2) }, j: { key: 'string' }, k: { key: Bytes.fromUint8Array(new Uint8Array([0, 1, 255])) }, - l: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, - n: { key: bsonObjectId('507f191e810c19729de860ea') }, + l: { key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }, + n: { key: new BsonObjectId('507f191e810c19729de860ea') }, o: { key: new GeoPoint(0, 0) }, - p: { key: regex('^foo', 'i') }, + p: { key: new RegexValue('^foo', 'i') }, q: { key: [1, 2] }, r: { key: vector([1, 2]) }, s: { key: { a: 1 } }, - t: { key: maxKey() } + t: { key: MaxKey.instance() } }; return withTestProjectIdAndCollectionSettings( @@ -3181,25 +3193,25 @@ apiDescribe('Database', persistence => { it('SDK orders bson types the same way online and offline', async () => { const testDocs: { [key: string]: DocumentData } = { - a: { key: maxKey() }, // maxKeys are all equal - b: { key: maxKey() }, - c: { key: int32(1) }, - d: { key: int32(-1) }, - e: { key: int32(0) }, - f: { key: bsonTimestamp(1, 1) }, - g: { key: bsonTimestamp(2, 1) }, - h: { key: bsonTimestamp(1, 2) }, - i: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, - j: { key: bsonBinaryData(1, new Uint8Array([1, 1, 4])) }, - k: { key: bsonBinaryData(2, new Uint8Array([1, 0, 0])) }, - l: { key: bsonObjectId('507f191e810c19729de860eb') }, - m: { key: bsonObjectId('507f191e810c19729de860ea') }, - n: { key: bsonObjectId('407f191e810c19729de860ea') }, - o: { key: regex('^foo', 'i') }, - p: { key: regex('^foo', 'm') }, - q: { key: regex('^bar', 'i') }, - r: { key: minKey() }, // minKeys are all equal - s: { key: minKey() } + a: { key: MaxKey.instance() }, // maxKeys are all equal + b: { key: MaxKey.instance() }, + c: { key: new Int32Value(1) }, + d: { key: new Int32Value(-1) }, + e: { key: new Int32Value(0) }, + f: { key: new BsonTimestamp(1, 1) }, + g: { key: new BsonTimestamp(2, 1) }, + h: { key: new BsonTimestamp(1, 2) }, + i: { key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }, + j: { key: new BsonBinaryData(1, new Uint8Array([1, 1, 4])) }, + k: { key: new BsonBinaryData(2, new Uint8Array([1, 0, 0])) }, + l: { key: new BsonObjectId('507f191e810c19729de860eb') }, + m: { key: new BsonObjectId('507f191e810c19729de860ea') }, + n: { key: new BsonObjectId('407f191e810c19729de860ea') }, + o: { key: new RegexValue('^foo', 'i') }, + p: { key: new RegexValue('^foo', 'm') }, + q: { key: new RegexValue('^bar', 'i') }, + r: { key: MinKey.instance() }, // minKeys are all equal + s: { key: MinKey.instance() } }; return withTestProjectIdAndCollectionSettings( diff --git a/packages/firestore/test/integration/api/type.test.ts b/packages/firestore/test/integration/api/type.test.ts index 156eba426f8..2f7cb7f9295 100644 --- a/packages/firestore/test/integration/api/type.test.ts +++ b/packages/firestore/test/integration/api/type.test.ts @@ -20,9 +20,9 @@ import { expect } from 'chai'; import { addEqualityMatcher } from '../../util/equality_matcher'; import { EventsAccumulator } from '../util/events_accumulator'; import { - bsonBinaryData, - bsonObjectId, - bsonTimestamp, + BsonBinaryData, + BsonObjectId, + BsonTimestamp, Bytes, collection, doc, @@ -34,15 +34,15 @@ import { GeoPoint, getDoc, getDocs, - int32, - maxKey, - minKey, + Int32Value, + MaxKey, + MinKey, onSnapshot, orderBy, query, QuerySnapshot, refEqual, - regex, + RegexValue, runTransaction, setDoc, Timestamp, @@ -256,7 +256,9 @@ apiDescribe('Firestore', persistence => { settings, 1, async dbs => { - await expectRoundtripWithoutTransaction(dbs[0], { min: minKey() }); + await expectRoundtripWithoutTransaction(dbs[0], { + min: MinKey.instance() + }); } ); }); @@ -268,7 +270,9 @@ apiDescribe('Firestore', persistence => { settings, 1, async dbs => { - await expectRoundtripWithoutTransaction(dbs[0], { max: maxKey() }); + await expectRoundtripWithoutTransaction(dbs[0], { + max: MaxKey.instance() + }); } ); }); @@ -281,7 +285,7 @@ apiDescribe('Firestore', persistence => { 1, async dbs => { await expectRoundtripWithoutTransaction(dbs[0], { - regex: regex('^foo', 'i') + regex: new RegexValue('^foo', 'i') }); } ); @@ -294,7 +298,9 @@ apiDescribe('Firestore', persistence => { settings, 1, async dbs => { - await expectRoundtripWithoutTransaction(dbs[0], { int32: int32(1) }); + await expectRoundtripWithoutTransaction(dbs[0], { + int32: new Int32Value(1) + }); } ); }); @@ -307,7 +313,7 @@ apiDescribe('Firestore', persistence => { 1, async dbs => { await expectRoundtripWithoutTransaction(dbs[0], { - bsonTimestamp: bsonTimestamp(1, 2) + bsonTimestamp: new BsonTimestamp(1, 2) }); } ); @@ -321,7 +327,7 @@ apiDescribe('Firestore', persistence => { 1, async dbs => { await expectRoundtripWithoutTransaction(dbs[0], { - objectId: bsonObjectId('507f191e810c19729de860ea') + objectId: new BsonObjectId('507f191e810c19729de860ea') }); } ); @@ -335,7 +341,7 @@ apiDescribe('Firestore', persistence => { 1, async dbs => { await expectRoundtripWithoutTransaction(dbs[0], { - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }); } ); @@ -350,12 +356,12 @@ apiDescribe('Firestore', persistence => { async dbs => { await expectRoundtripWithoutTransaction(dbs[0], { array: [ - bsonBinaryData(1, new Uint8Array([1, 2, 3])), - bsonObjectId('507f191e810c19729de860ea'), - int32(1), - minKey(), - maxKey(), - regex('^foo', 'i') + new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + new BsonObjectId('507f191e810c19729de860ea'), + new Int32Value(1), + MinKey.instance(), + MaxKey.instance(), + new RegexValue('^foo', 'i') ] }); } @@ -371,12 +377,12 @@ apiDescribe('Firestore', persistence => { async dbs => { await expectRoundtripWithoutTransaction(dbs[0], { object: { - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - objectId: bsonObjectId('507f191e810c19729de860ea'), - int32: int32(1), - min: minKey(), - max: maxKey(), - regex: regex('^foo', 'i') + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + objectId: new BsonObjectId('507f191e810c19729de860ea'), + int32: new Int32Value(1), + min: MinKey.instance(), + max: MaxKey.instance(), + regex: new RegexValue('^foo', 'i') } }); } @@ -393,7 +399,7 @@ apiDescribe('Firestore', persistence => { const docRef = doc(coll, 'test-doc'); let errorMessage; try { - await setDoc(docRef, { key: int32(2147483648) }); + await setDoc(docRef, { key: new Int32Value(2147483648) }); } catch (err) { errorMessage = (err as FirestoreError)?.message; } @@ -402,7 +408,7 @@ apiDescribe('Firestore', persistence => { ); try { - await setDoc(docRef, { key: int32(-2147483650) }); + await setDoc(docRef, { key: new Int32Value(-2147483650) }); } catch (err) { errorMessage = (err as FirestoreError)?.message; } @@ -424,22 +430,22 @@ apiDescribe('Firestore', persistence => { let errorMessage; try { // BSON timestamp larger than 32-bit integer gets rejected - await setDoc(docRef, { key: bsonTimestamp(4294967296, 2) }); + await setDoc(docRef, { key: new BsonTimestamp(4294967296, 2) }); } catch (err) { errorMessage = (err as FirestoreError)?.message; } expect(errorMessage).to.contains( - "The field 'seconds' value (4,294,967,296) does not represent an unsigned 32-bit integer." + "BsonTimestamp 'seconds' must be in the range of a 32-bit unsigned integer." ); try { // negative BSON timestamp gets rejected - await setDoc(docRef, { key: bsonTimestamp(-1, 2) }); + await setDoc(docRef, { key: new BsonTimestamp(-1, 2) }); } catch (err) { errorMessage = (err as FirestoreError)?.message; } expect(errorMessage).to.contains( - "The field 'seconds' value (-1) does not represent an unsigned 32-bit integer." + "BsonTimestamp 'seconds' must be in the range of a 32-bit unsigned integer." ); } ); @@ -455,7 +461,7 @@ apiDescribe('Firestore', persistence => { const docRef = doc(coll, 'test-doc'); let errorMessage; try { - await setDoc(docRef, { key: regex('foo', 'a') }); + await setDoc(docRef, { key: new RegexValue('foo', 'a') }); } catch (err) { errorMessage = (err as FirestoreError)?.message; } @@ -478,7 +484,7 @@ apiDescribe('Firestore', persistence => { let errorMessage; try { // bsonObjectId with length not equal to 24 gets rejected - await setDoc(docRef, { key: bsonObjectId('foo') }); + await setDoc(docRef, { key: new BsonObjectId('foo') }); } catch (err) { errorMessage = (err as FirestoreError)?.message; } @@ -500,7 +506,7 @@ apiDescribe('Firestore', persistence => { let errorMessage; try { await setDoc(docRef, { - key: bsonBinaryData(1234, new Uint8Array([1, 2, 3])) + key: new BsonBinaryData(1234, new Uint8Array([1, 2, 3])) }); } catch (err) { errorMessage = (err as FirestoreError)?.message; @@ -515,26 +521,28 @@ apiDescribe('Firestore', persistence => { it('can order values of different TypeOrder together', async () => { const testDocs: { [key: string]: DocumentData } = { nullValue: { key: null }, - minValue: { key: minKey() }, + minValue: { key: MinKey.instance() }, booleanValue: { key: true }, nanValue: { key: NaN }, - int32Value: { key: int32(1) }, + int32Value: { key: new Int32Value(1) }, doubleValue: { key: 2.0 }, integerValue: { key: 3 }, timestampValue: { key: new Timestamp(100, 123456000) }, - bsonTimestampValue: { key: bsonTimestamp(1, 2) }, + bsonTimestampValue: { key: new BsonTimestamp(1, 2) }, stringValue: { key: 'string' }, bytesValue: { key: Bytes.fromUint8Array(new Uint8Array([0, 1, 255])) }, - bsonBinaryValue: { key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) }, + bsonBinaryValue: { + key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) + }, // referenceValue: {key: ref('coll/doc')}, referenceValue: { key: 'placeholder' }, - objectIdValue: { key: bsonObjectId('507f191e810c19729de860ea') }, + objectIdValue: { key: new BsonObjectId('507f191e810c19729de860ea') }, geoPointValue: { key: new GeoPoint(0, 0) }, - regexValue: { key: regex('^foo', 'i') }, + regexValue: { key: new RegexValue('^foo', 'i') }, arrayValue: { key: [1, 2] }, vectorValue: { key: vector([1, 2]) }, objectValue: { key: { a: 1 } }, - maxValue: { key: maxKey() } + maxValue: { key: MaxKey.instance() } }; return withTestProjectIdAndCollectionSettings( diff --git a/packages/firestore/test/lite/integration.test.ts b/packages/firestore/test/lite/integration.test.ts index 9b647587503..25f372bcb95 100644 --- a/packages/firestore/test/lite/integration.test.ts +++ b/packages/firestore/test/lite/integration.test.ts @@ -28,6 +28,9 @@ import { sum, average } from '../../src/lite-api/aggregate'; +import { BsonBinaryData } from '../../src/lite-api/bson_binary_data'; +import { BsonObjectId } from '../../src/lite-api/bson_object_Id'; +import { BsonTimestamp } from '../../src/lite-api/bson_timestamp'; import { Bytes } from '../../src/lite-api/bytes'; import { Firestore, @@ -40,18 +43,14 @@ import { FieldValue } from '../../src/lite-api/field_value'; import { arrayRemove, arrayUnion, - bsonBinaryData, - bsonObjectId, - bsonTimestamp, deleteField, increment, - int32, - maxKey, - minKey, - regex, serverTimestamp, vector } from '../../src/lite-api/field_value_impl'; +import { Int32Value } from '../../src/lite-api/int32_value'; +import { MaxKey } from '../../src/lite-api/max_key'; +import { MinKey } from '../../src/lite-api/min_key'; import { endAt, endBefore, @@ -85,6 +84,7 @@ import { setDoc, updateDoc } from '../../src/lite-api/reference_impl'; +import { RegexValue } from '../../src/lite-api/regex_value'; import { FirestoreDataConverter, snapshotEqual, @@ -2974,37 +2974,41 @@ describe.skip('BSON types', () => { it('can be read and written using the lite SDK', async () => { return withTestCollection(async coll => { const ref = await addDoc(coll, { - objectId: bsonObjectId('507f191e810c19729de860ea'), - int32: int32(1), - min: minKey(), - max: maxKey(), - regex: regex('^foo', 'i') + objectId: new BsonObjectId('507f191e810c19729de860ea'), + int32: new Int32Value(1), + min: MinKey.instance(), + max: MaxKey.instance(), + regex: new RegexValue('^foo', 'i') }); await setDoc( ref, { - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - timestamp: bsonTimestamp(1, 2), - int32: int32(2) + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: new BsonTimestamp(1, 2), + int32: new Int32Value(2) }, { merge: true } ); const snap1 = await getDoc(ref); expect( - snap1.get('objectId').isEqual(bsonObjectId('507f191e810c19729de860ea')) + snap1 + .get('objectId') + .isEqual(new BsonObjectId('507f191e810c19729de860ea')) ).to.be.true; - expect(snap1.get('int32').isEqual(int32(2))).to.be.true; - expect(snap1.get('min') === minKey()).to.be.true; - expect(snap1.get('max') === maxKey()).to.be.true; + expect(snap1.get('int32').isEqual(new Int32Value(2))).to.be.true; + expect(snap1.get('min') === MinKey.instance()).to.be.true; + expect(snap1.get('max') === MaxKey.instance()).to.be.true; expect( snap1 .get('binary') - .isEqual(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + .isEqual(new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ).to.be.true; - expect(snap1.get('timestamp').isEqual(bsonTimestamp(1, 2))).to.be.true; - expect(snap1.get('regex').isEqual(regex('^foo', 'i'))).to.be.true; + expect(snap1.get('timestamp').isEqual(new BsonTimestamp(1, 2))).to.be + .true; + expect(snap1.get('regex').isEqual(new RegexValue('^foo', 'i'))).to.be + .true; }); }); }); diff --git a/packages/firestore/test/unit/index/firestore_index_value_writer.test.ts b/packages/firestore/test/unit/index/firestore_index_value_writer.test.ts index c646726feeb..907881c262c 100644 --- a/packages/firestore/test/unit/index/firestore_index_value_writer.test.ts +++ b/packages/firestore/test/unit/index/firestore_index_value_writer.test.ts @@ -16,15 +16,13 @@ */ import { expect } from 'chai'; -import { - bsonBinaryData, - bsonObjectId, - bsonTimestamp, - int32, - regex -} from '../../../lite'; import { FirestoreIndexValueWriter } from '../../../src/index/firestore_index_value_writer'; import { IndexByteEncoder } from '../../../src/index/index_byte_encoder'; +import { BsonBinaryData } from '../../../src/lite-api/bson_binary_data'; +import { BsonObjectId } from '../../../src/lite-api/bson_object_Id'; +import { BsonTimestamp } from '../../../src/lite-api/bson_timestamp'; +import { Int32Value } from '../../../src/lite-api/int32_value'; +import { RegexValue } from '../../../src/lite-api/regex_value'; import { Timestamp } from '../../../src/lite-api/timestamp'; import { parseBsonBinaryData, @@ -287,7 +285,7 @@ describe('Firestore Index Value Writer', () => { } }; const value3 = parseBsonObjectId( - bsonObjectId('507f191e810c19729de860ea') + new BsonObjectId('507f191e810c19729de860ea') ); expect( @@ -340,8 +338,8 @@ describe('Firestore Index Value Writer', () => { } } }; - const value3 = parseBsonTimestamp(bsonTimestamp(1, 2)); - const value4 = parseBsonTimestamp(bsonTimestamp(2, 1)); + const value3 = parseBsonTimestamp(new BsonTimestamp(1, 2)); + const value4 = parseBsonTimestamp(new BsonTimestamp(2, 1)); expect( compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING) @@ -400,7 +398,7 @@ describe('Firestore Index Value Writer', () => { ); const value3 = parseBsonBinaryData( serializer, - bsonBinaryData(1, new Uint8Array([1, 2, 3])) + new BsonBinaryData(1, new Uint8Array([1, 2, 3])) ); const jsonSerializer = new JsonProtoSerializer( @@ -410,7 +408,7 @@ describe('Firestore Index Value Writer', () => { const value4 = parseBsonBinaryData( jsonSerializer, - bsonBinaryData(1, new Uint8Array([1, 2, 3])) + new BsonBinaryData(1, new Uint8Array([1, 2, 3])) ); expect( @@ -473,8 +471,8 @@ describe('Firestore Index Value Writer', () => { } } }; - const value3 = parseRegexValue(regex('^foo', 'i')); - const value4 = parseRegexValue(regex('^zoo', 'i')); + const value3 = parseRegexValue(new RegexValue('^foo', 'i')); + const value4 = parseRegexValue(new RegexValue('^zoo', 'i')); expect( compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING) @@ -522,7 +520,8 @@ describe('Firestore Index Value Writer', () => { } } }; - const value3 = parseInt32Value(int32(1)); + const value3 = parseInt32Value(new Int32Value(1)); + const value4 = parseInt32Value(new Int32Value(2)); expect( compareIndexEncodedValues(value1, value2, IndexKind.ASCENDING) @@ -543,6 +542,16 @@ describe('Firestore Index Value Writer', () => { expect( compareIndexEncodedValues(value3, value1, IndexKind.ASCENDING) ).to.equal(0); + + expect( + compareIndexEncodedValues(value4, value1, IndexKind.ASCENDING) + ).to.equal(1); + expect( + compareIndexEncodedValues(value4, value2, IndexKind.ASCENDING) + ).to.equal(0); + expect( + compareIndexEncodedValues(value4, value3, IndexKind.ASCENDING) + ).to.equal(1); }); it('can compare BSON MinKey', () => { diff --git a/packages/firestore/test/unit/local/index_manager.test.ts b/packages/firestore/test/unit/local/index_manager.test.ts index 1097f5e682e..b6af448b2db 100644 --- a/packages/firestore/test/unit/local/index_manager.test.ts +++ b/packages/firestore/test/unit/local/index_manager.test.ts @@ -17,7 +17,17 @@ import { expect } from 'chai'; -import { Bytes, GeoPoint } from '../../../src/'; +import { + BsonBinaryData, + BsonObjectId, + BsonTimestamp, + Bytes, + GeoPoint, + Int32Value, + MaxKey, + MinKey, + RegexValue +} from '../../../src/'; import { User } from '../../../src/auth/user'; import { FieldFilter } from '../../../src/core/filter'; import { @@ -31,16 +41,7 @@ import { queryWithLimit, queryWithStartAt } from '../../../src/core/query'; -import { - bsonBinaryData, - bsonObjectId, - bsonTimestamp, - int32, - maxKey, - minKey, - regex, - vector -} from '../../../src/lite-api/field_value_impl'; +import { vector } from '../../../src/lite-api/field_value_impl'; import { Timestamp } from '../../../src/lite-api/timestamp'; import { displayNameForIndexType, @@ -1882,13 +1883,13 @@ describe('IndexedDbIndexManager', async () => { ); await addDoc('coll/doc1', { - key: bsonObjectId('507f191e810c19729de860ea') + key: new BsonObjectId('507f191e810c19729de860ea') }); await addDoc('coll/doc2', { - key: bsonObjectId('507f191e810c19729de860eb') + key: new BsonObjectId('507f191e810c19729de860eb') }); await addDoc('coll/doc3', { - key: bsonObjectId('507f191e810c19729de860ec') + key: new BsonObjectId('507f191e810c19729de860ec') }); const fieldIndexes = await indexManager.getFieldIndexes('coll'); @@ -1899,49 +1900,49 @@ describe('IndexedDbIndexManager', async () => { q = queryWithAddedFilter( query('coll'), - filter('key', '==', bsonObjectId('507f191e810c19729de860ea')) + filter('key', '==', new BsonObjectId('507f191e810c19729de860ea')) ); await verifyResults(q, 'coll/doc1'); q = queryWithAddedFilter( query('coll'), - filter('key', '!=', bsonObjectId('507f191e810c19729de860ea')) + filter('key', '!=', new BsonObjectId('507f191e810c19729de860ea')) ); await verifyResults(q, 'coll/doc2', 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '>=', bsonObjectId('507f191e810c19729de860eb')) + filter('key', '>=', new BsonObjectId('507f191e810c19729de860eb')) ); await verifyResults(q, 'coll/doc2', 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '<=', bsonObjectId('507f191e810c19729de860eb')) + filter('key', '<=', new BsonObjectId('507f191e810c19729de860eb')) ); await verifyResults(q, 'coll/doc1', 'coll/doc2'); q = queryWithAddedFilter( query('coll'), - filter('key', '>', bsonObjectId('507f191e810c19729de860eb')) + filter('key', '>', new BsonObjectId('507f191e810c19729de860eb')) ); await verifyResults(q, 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '<', bsonObjectId('507f191e810c19729de860eb')) + filter('key', '<', new BsonObjectId('507f191e810c19729de860eb')) ); await verifyResults(q, 'coll/doc1'); q = queryWithAddedFilter( query('coll'), - filter('key', '>', bsonObjectId('507f191e810c19729de860ec')) + filter('key', '>', new BsonObjectId('507f191e810c19729de860ec')) ); await verifyResults(q); q = queryWithAddedFilter( query('coll'), - filter('key', '<', bsonObjectId('507f191e810c19729de860ea')) + filter('key', '<', new BsonObjectId('507f191e810c19729de860ea')) ); await verifyResults(q); }); @@ -1951,13 +1952,13 @@ describe('IndexedDbIndexManager', async () => { fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) ); await addDoc('coll/doc1', { - key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }); await addDoc('coll/doc2', { - key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) + key: new BsonBinaryData(1, new Uint8Array([1, 2, 4])) }); await addDoc('coll/doc3', { - key: bsonBinaryData(1, new Uint8Array([2, 1, 2])) + key: new BsonBinaryData(1, new Uint8Array([2, 1, 2])) }); const fieldIndexes = await indexManager.getFieldIndexes('coll'); @@ -1968,49 +1969,49 @@ describe('IndexedDbIndexManager', async () => { q = queryWithAddedFilter( query('coll'), - filter('key', '==', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + filter('key', '==', new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ); await verifyResults(q, 'coll/doc1'); q = queryWithAddedFilter( query('coll'), - filter('key', '!=', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + filter('key', '!=', new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ); await verifyResults(q, 'coll/doc2', 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '>=', bsonBinaryData(1, new Uint8Array([1, 2, 4]))) + filter('key', '>=', new BsonBinaryData(1, new Uint8Array([1, 2, 4]))) ); await verifyResults(q, 'coll/doc2', 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '<=', bsonBinaryData(1, new Uint8Array([1, 2, 4]))) + filter('key', '<=', new BsonBinaryData(1, new Uint8Array([1, 2, 4]))) ); await verifyResults(q, 'coll/doc1', 'coll/doc2'); q = queryWithAddedFilter( query('coll'), - filter('key', '>', bsonBinaryData(1, new Uint8Array([1, 2, 4]))) + filter('key', '>', new BsonBinaryData(1, new Uint8Array([1, 2, 4]))) ); await verifyResults(q, 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '<', bsonBinaryData(1, new Uint8Array([1, 2, 4]))) + filter('key', '<', new BsonBinaryData(1, new Uint8Array([1, 2, 4]))) ); await verifyResults(q, 'coll/doc1'); q = queryWithAddedFilter( query('coll'), - filter('key', '>', bsonBinaryData(1, new Uint8Array([2, 1, 2]))) + filter('key', '>', new BsonBinaryData(1, new Uint8Array([2, 1, 2]))) ); await verifyResults(q); q = queryWithAddedFilter( query('coll'), - filter('key', '<', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + filter('key', '<', new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ); await verifyResults(q); }); @@ -2020,13 +2021,13 @@ describe('IndexedDbIndexManager', async () => { fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) ); await addDoc('coll/doc1', { - key: bsonTimestamp(1, 1) + key: new BsonTimestamp(1, 1) }); await addDoc('coll/doc2', { - key: bsonTimestamp(1, 2) + key: new BsonTimestamp(1, 2) }); await addDoc('coll/doc3', { - key: bsonTimestamp(2, 1) + key: new BsonTimestamp(2, 1) }); const fieldIndexes = await indexManager.getFieldIndexes('coll'); @@ -2037,49 +2038,49 @@ describe('IndexedDbIndexManager', async () => { q = queryWithAddedFilter( query('coll'), - filter('key', '==', bsonTimestamp(1, 1)) + filter('key', '==', new BsonTimestamp(1, 1)) ); await verifyResults(q, 'coll/doc1'); q = queryWithAddedFilter( query('coll'), - filter('key', '!=', bsonTimestamp(1, 1)) + filter('key', '!=', new BsonTimestamp(1, 1)) ); await verifyResults(q, 'coll/doc2', 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '>=', bsonTimestamp(1, 2)) + filter('key', '>=', new BsonTimestamp(1, 2)) ); await verifyResults(q, 'coll/doc2', 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '<=', bsonTimestamp(1, 2)) + filter('key', '<=', new BsonTimestamp(1, 2)) ); await verifyResults(q, 'coll/doc1', 'coll/doc2'); q = queryWithAddedFilter( query('coll'), - filter('key', '>', bsonTimestamp(1, 2)) + filter('key', '>', new BsonTimestamp(1, 2)) ); await verifyResults(q, 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '<', bsonTimestamp(1, 2)) + filter('key', '<', new BsonTimestamp(1, 2)) ); await verifyResults(q, 'coll/doc1'); q = queryWithAddedFilter( query('coll'), - filter('key', '>', bsonTimestamp(2, 1)) + filter('key', '>', new BsonTimestamp(2, 1)) ); await verifyResults(q); q = queryWithAddedFilter( query('coll'), - filter('key', '<', bsonTimestamp(1, 1)) + filter('key', '<', new BsonTimestamp(1, 1)) ); await verifyResults(q); }); @@ -2089,13 +2090,13 @@ describe('IndexedDbIndexManager', async () => { fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) ); await addDoc('coll/doc1', { - key: int32(1) + key: new Int32Value(1) }); await addDoc('coll/doc2', { - key: int32(2) + key: new Int32Value(2) }); await addDoc('coll/doc3', { - key: int32(3) + key: new Int32Value(3) }); const fieldIndexes = await indexManager.getFieldIndexes('coll'); expect(fieldIndexes).to.have.length(1); @@ -2103,28 +2104,52 @@ describe('IndexedDbIndexManager', async () => { let q = queryWithAddedOrderBy(query('coll'), orderBy('key')); await verifyResults(q, 'coll/doc1', 'coll/doc2', 'coll/doc3'); - q = queryWithAddedFilter(query('coll'), filter('key', '==', int32(1))); + q = queryWithAddedFilter( + query('coll'), + filter('key', '==', new Int32Value(1)) + ); await verifyResults(q, 'coll/doc1'); - q = queryWithAddedFilter(query('coll'), filter('key', '!=', int32(1))); + q = queryWithAddedFilter( + query('coll'), + filter('key', '!=', new Int32Value(1)) + ); await verifyResults(q, 'coll/doc2', 'coll/doc3'); - q = queryWithAddedFilter(query('coll'), filter('key', '>=', int32(2))); + q = queryWithAddedFilter( + query('coll'), + filter('key', '>=', new Int32Value(2)) + ); await verifyResults(q, 'coll/doc2', 'coll/doc3'); - q = queryWithAddedFilter(query('coll'), filter('key', '<=', int32(2))); + q = queryWithAddedFilter( + query('coll'), + filter('key', '<=', new Int32Value(2)) + ); await verifyResults(q, 'coll/doc1', 'coll/doc2'); - q = queryWithAddedFilter(query('coll'), filter('key', '>', int32(2))); + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', new Int32Value(2)) + ); await verifyResults(q, 'coll/doc3'); - q = queryWithAddedFilter(query('coll'), filter('key', '<', int32(2))); + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', new Int32Value(2)) + ); await verifyResults(q, 'coll/doc1'); - q = queryWithAddedFilter(query('coll'), filter('key', '>', int32(3))); + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', new Int32Value(3)) + ); await verifyResults(q); - q = queryWithAddedFilter(query('coll'), filter('key', '<', int32(1))); + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', new Int32Value(1)) + ); await verifyResults(q); }); @@ -2133,13 +2158,13 @@ describe('IndexedDbIndexManager', async () => { fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) ); await addDoc('coll/doc1', { - key: regex('a', 'i') + key: new RegexValue('a', 'i') }); await addDoc('coll/doc2', { - key: regex('a', 'm') + key: new RegexValue('a', 'm') }); await addDoc('coll/doc3', { - key: regex('b', 'i') + key: new RegexValue('b', 'i') }); const fieldIndexes = await indexManager.getFieldIndexes('coll'); expect(fieldIndexes).to.have.length(1); @@ -2148,49 +2173,49 @@ describe('IndexedDbIndexManager', async () => { q = queryWithAddedFilter( query('coll'), - filter('key', '==', regex('a', 'i')) + filter('key', '==', new RegexValue('a', 'i')) ); await verifyResults(q, 'coll/doc1'); q = queryWithAddedFilter( query('coll'), - filter('key', '!=', regex('a', 'i')) + filter('key', '!=', new RegexValue('a', 'i')) ); await verifyResults(q, 'coll/doc2', 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '>=', regex('a', 'm')) + filter('key', '>=', new RegexValue('a', 'm')) ); await verifyResults(q, 'coll/doc2', 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '<=', regex('a', 'm')) + filter('key', '<=', new RegexValue('a', 'm')) ); await verifyResults(q, 'coll/doc1', 'coll/doc2'); q = queryWithAddedFilter( query('coll'), - filter('key', '>', regex('a', 'm')) + filter('key', '>', new RegexValue('a', 'm')) ); await verifyResults(q, 'coll/doc3'); q = queryWithAddedFilter( query('coll'), - filter('key', '<', regex('a', 'm')) + filter('key', '<', new RegexValue('a', 'm')) ); await verifyResults(q, 'coll/doc1'); q = queryWithAddedFilter( query('coll'), - filter('key', '>', regex('b', 'i')) + filter('key', '>', new RegexValue('b', 'i')) ); await verifyResults(q); q = queryWithAddedFilter( query('coll'), - filter('key', '<', regex('a', 'i')) + filter('key', '<', new RegexValue('a', 'i')) ); await verifyResults(q); }); @@ -2200,10 +2225,10 @@ describe('IndexedDbIndexManager', async () => { fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) ); await addDoc('coll/doc1', { - key: minKey() + key: MinKey.instance() }); await addDoc('coll/doc2', { - key: minKey() + key: MinKey.instance() }); await addDoc('coll/doc3', { key: null @@ -2212,7 +2237,7 @@ describe('IndexedDbIndexManager', async () => { key: 1 }); await addDoc('coll/doc5', { - key: maxKey() + key: MaxKey.instance() }); const fieldIndexes = await indexManager.getFieldIndexes('coll'); @@ -2228,22 +2253,40 @@ describe('IndexedDbIndexManager', async () => { 'coll/doc5' ); - q = queryWithAddedFilter(query('coll'), filter('key', '==', minKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '==', MinKey.instance()) + ); await verifyResults(q, 'coll/doc1', 'coll/doc2'); - q = queryWithAddedFilter(query('coll'), filter('key', '!=', minKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '!=', MinKey.instance()) + ); await verifyResults(q, 'coll/doc4', 'coll/doc5'); - q = queryWithAddedFilter(query('coll'), filter('key', '>=', minKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '>=', MinKey.instance()) + ); await verifyResults(q, 'coll/doc1', 'coll/doc2'); - q = queryWithAddedFilter(query('coll'), filter('key', '<=', minKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '<=', MinKey.instance()) + ); await verifyResults(q, 'coll/doc1', 'coll/doc2'); - q = queryWithAddedFilter(query('coll'), filter('key', '>', minKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', MinKey.instance()) + ); await verifyResults(q); - q = queryWithAddedFilter(query('coll'), filter('key', '<', minKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', MinKey.instance()) + ); await verifyResults(q); }); @@ -2252,16 +2295,16 @@ describe('IndexedDbIndexManager', async () => { fieldIndex('coll', { fields: [['key', IndexKind.ASCENDING]] }) ); await addDoc('coll/doc1', { - key: minKey() + key: MinKey.instance() }); await addDoc('coll/doc2', { key: 1 }); await addDoc('coll/doc3', { - key: maxKey() + key: MaxKey.instance() }); await addDoc('coll/doc4', { - key: maxKey() + key: MaxKey.instance() }); await addDoc('coll/doc5', { key: null @@ -2280,22 +2323,40 @@ describe('IndexedDbIndexManager', async () => { 'coll/doc4' ); - q = queryWithAddedFilter(query('coll'), filter('key', '==', maxKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '==', MaxKey.instance()) + ); await verifyResults(q, 'coll/doc3', 'coll/doc4'); - q = queryWithAddedFilter(query('coll'), filter('key', '!=', maxKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '!=', MaxKey.instance()) + ); await verifyResults(q, 'coll/doc1', 'coll/doc2'); - q = queryWithAddedFilter(query('coll'), filter('key', '>=', maxKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '>=', MaxKey.instance()) + ); await verifyResults(q, 'coll/doc3', 'coll/doc4'); - q = queryWithAddedFilter(query('coll'), filter('key', '<=', maxKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '<=', MaxKey.instance()) + ); await verifyResults(q, 'coll/doc3', 'coll/doc4'); - q = queryWithAddedFilter(query('coll'), filter('key', '>', maxKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '>', MaxKey.instance()) + ); await verifyResults(q); - q = queryWithAddedFilter(query('coll'), filter('key', '<', maxKey())); + q = queryWithAddedFilter( + query('coll'), + filter('key', '<', MaxKey.instance()) + ); await verifyResults(q); }); @@ -2304,45 +2365,45 @@ describe('IndexedDbIndexManager', async () => { fieldIndex('coll', { fields: [['key', IndexKind.DESCENDING]] }) ); await addDoc('coll/doc1', { - key: minKey() + key: MinKey.instance() }); await addDoc('coll/doc2', { - key: int32(2) + key: new Int32Value(2) }); await addDoc('coll/doc3', { - key: int32(1) + key: new Int32Value(1) }); await addDoc('coll/doc4', { - key: bsonTimestamp(1, 2) + key: new BsonTimestamp(1, 2) }); await addDoc('coll/doc5', { - key: bsonTimestamp(1, 1) + key: new BsonTimestamp(1, 1) }); await addDoc('coll/doc6', { - key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) + key: new BsonBinaryData(1, new Uint8Array([1, 2, 4])) }); await addDoc('coll/doc7', { - key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }); await addDoc('coll/doc8', { - key: bsonObjectId('507f191e810c19729de860eb') + key: new BsonObjectId('507f191e810c19729de860eb') }); await addDoc('coll/doc9', { - key: bsonObjectId('507f191e810c19729de860ea') + key: new BsonObjectId('507f191e810c19729de860ea') }); await addDoc('coll/doc10', { - key: regex('a', 'm') + key: new RegexValue('a', 'm') }); await addDoc('coll/doc11', { - key: regex('a', 'i') + key: new RegexValue('a', 'i') }); await addDoc('coll/doc12', { - key: maxKey() + key: MaxKey.instance() }); const fieldIndexes = await indexManager.getFieldIndexes('coll'); @@ -2375,7 +2436,7 @@ describe('IndexedDbIndexManager', async () => { key: null }); await addDoc('coll/doc2', { - key: minKey() + key: MinKey.instance() }); await addDoc('coll/doc3', { key: true @@ -2384,7 +2445,7 @@ describe('IndexedDbIndexManager', async () => { key: NaN }); await addDoc('coll/doc5', { - key: int32(1) + key: new Int32Value(1) }); await addDoc('coll/doc6', { key: 2.0 @@ -2396,7 +2457,7 @@ describe('IndexedDbIndexManager', async () => { key: new Timestamp(100, 123456000) }); await addDoc('coll/doc9', { - key: bsonTimestamp(1, 2) + key: new BsonTimestamp(1, 2) }); await addDoc('coll/doc10', { key: 'string' @@ -2405,19 +2466,19 @@ describe('IndexedDbIndexManager', async () => { key: Bytes.fromUint8Array(new Uint8Array([0, 1, 255])) as Bytes }); await addDoc('coll/doc12', { - key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }); await addDoc('coll/doc13', { key: ref('coll/doc') }); await addDoc('coll/doc14', { - key: bsonObjectId('507f191e810c19729de860ea') + key: new BsonObjectId('507f191e810c19729de860ea') }); await addDoc('coll/doc15', { key: new GeoPoint(0, 1) }); await addDoc('coll/doc16', { - key: regex('^foo', 'i') + key: new RegexValue('^foo', 'i') }); await addDoc('coll/doc17', { key: [1, 2] @@ -2429,7 +2490,7 @@ describe('IndexedDbIndexManager', async () => { key: { a: 1 } }); await addDoc('coll/doc20', { - key: maxKey() + key: MaxKey.instance() }); const fieldIndexes = await indexManager.getFieldIndexes('coll'); diff --git a/packages/firestore/test/unit/local/local_store_indexeddb.test.ts b/packages/firestore/test/unit/local/local_store_indexeddb.test.ts index 0e5afbc2914..5f68684d193 100644 --- a/packages/firestore/test/unit/local/local_store_indexeddb.test.ts +++ b/packages/firestore/test/unit/local/local_store_indexeddb.test.ts @@ -18,7 +18,18 @@ import { isIndexedDBAvailable } from '@firebase/util'; import { expect } from 'chai'; -import { serverTimestamp, Timestamp, GeoPoint } from '../../../src'; +import { + serverTimestamp, + Timestamp, + GeoPoint, + BsonObjectId, + BsonBinaryData, + BsonTimestamp, + Int32Value, + RegexValue, + MaxKey, + MinKey +} from '../../../src'; import { User } from '../../../src/auth/user'; import { BundleConverterImpl } from '../../../src/core/bundle_impl'; import { @@ -30,16 +41,7 @@ import { } from '../../../src/core/query'; import { Target } from '../../../src/core/target'; import { TargetId } from '../../../src/core/types'; -import { - bsonBinaryData, - bsonObjectId, - bsonTimestamp, - int32, - maxKey, - minKey, - regex, - vector -} from '../../../src/lite-api/field_value_impl'; +import { vector } from '../../../src/lite-api/field_value_impl'; import { IndexBackfiller } from '../../../src/local/index_backfiller'; import { LocalStore } from '../../../src/local/local_store'; import { @@ -972,12 +974,14 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.writeMutations( setMutation('coll/a', { - key: bsonObjectId('507f191e810c19729de860ea') + key: new BsonObjectId('507f191e810c19729de860ea') }), setMutation('coll/b', { - key: bsonObjectId('507f191e810c19729de860eb') + key: new BsonObjectId('507f191e810c19729de860eb') }), - setMutation('coll/c', { key: bsonObjectId('507f191e810c19729de860ec') }) + setMutation('coll/c', { + key: new BsonObjectId('507f191e810c19729de860ec') + }) ); await test.backfillIndexes(); @@ -992,7 +996,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', - filter('key', '==', bsonObjectId('507f191e810c19729de860ea')) + filter('key', '==', new BsonObjectId('507f191e810c19729de860ea')) ); await test.executeQuery(query_); test.assertOverlaysRead(1, 0, { @@ -1002,7 +1006,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', - filter('key', '!=', bsonObjectId('507f191e810c19729de860ea')) + filter('key', '!=', new BsonObjectId('507f191e810c19729de860ea')) ); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { @@ -1013,7 +1017,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', - filter('key', '>=', bsonObjectId('507f191e810c19729de860eb')) + filter('key', '>=', new BsonObjectId('507f191e810c19729de860eb')) ); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { @@ -1024,7 +1028,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', - filter('key', '<', bsonObjectId('507f191e810c19729de860ea')) + filter('key', '<', new BsonObjectId('507f191e810c19729de860ea')) ); await test.executeQuery(query_); test.assertOverlaysRead(0, 0); @@ -1033,8 +1037,8 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', filter('key', 'in', [ - bsonObjectId('507f191e810c19729de860ea'), - bsonObjectId('507f191e810c19729de860eb') + new BsonObjectId('507f191e810c19729de860ea'), + new BsonObjectId('507f191e810c19729de860eb') ]) ); await test.executeQuery(query_); @@ -1047,8 +1051,8 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', filter('key', 'not-in', [ - bsonObjectId('507f191e810c19729de860ea'), - bsonObjectId('507f191e810c19729de860eb') + new BsonObjectId('507f191e810c19729de860ea'), + new BsonObjectId('507f191e810c19729de860eb') ]) ); await test.executeQuery(query_); @@ -1065,9 +1069,9 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); await test.configureFieldsIndexes(index); await test.writeMutations( - setMutation('coll/a', { key: bsonTimestamp(1000, 1000) }), - setMutation('coll/b', { key: bsonTimestamp(1001, 1000) }), - setMutation('coll/c', { key: bsonTimestamp(1000, 1001) }) + setMutation('coll/a', { key: new BsonTimestamp(1000, 1000) }), + setMutation('coll/b', { key: new BsonTimestamp(1001, 1000) }), + setMutation('coll/c', { key: new BsonTimestamp(1000, 1001) }) ); await test.backfillIndexes(); @@ -1080,14 +1084,20 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/a', 'coll/c', 'coll/b'); - query_ = query('coll', filter('key', '==', bsonTimestamp(1000, 1000))); + query_ = query( + 'coll', + filter('key', '==', new BsonTimestamp(1000, 1000)) + ); await test.executeQuery(query_); test.assertOverlaysRead(1, 0, { [key('coll/a').toString()]: MutationType.Set }); test.assertQueryReturned(query_, 'coll/a'); - query_ = query('coll', filter('key', '!=', bsonTimestamp(1000, 1000))); + query_ = query( + 'coll', + filter('key', '!=', new BsonTimestamp(1000, 1000)) + ); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/b').toString()]: MutationType.Set, @@ -1095,7 +1105,10 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/c', 'coll/b'); - query_ = query('coll', filter('key', '>=', bsonTimestamp(1000, 1001))); + query_ = query( + 'coll', + filter('key', '>=', new BsonTimestamp(1000, 1001)) + ); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/b').toString()]: MutationType.Set, @@ -1103,7 +1116,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/c', 'coll/b'); - query_ = query('coll', filter('key', '<', bsonTimestamp(1000, 1000))); + query_ = query('coll', filter('key', '<', new BsonTimestamp(1000, 1000))); await test.executeQuery(query_); test.assertOverlaysRead(0, 0); test.assertQueryReturned(query_); @@ -1111,8 +1124,8 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', filter('key', 'in', [ - bsonTimestamp(1000, 1000), - bsonTimestamp(1001, 1000) + new BsonTimestamp(1000, 1000), + new BsonTimestamp(1001, 1000) ]) ); await test.executeQuery(query_); @@ -1125,8 +1138,8 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', filter('key', 'not-in', [ - bsonTimestamp(1000, 1000), - bsonTimestamp(1001, 1000) + new BsonTimestamp(1000, 1000), + new BsonTimestamp(1001, 1000) ]) ); await test.executeQuery(query_); @@ -1144,16 +1157,16 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.configureFieldsIndexes(index); await test.writeMutations( setMutation('coll/a', { - key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }), setMutation('coll/b', { - key: bsonBinaryData(1, new Uint8Array([1, 2])) + key: new BsonBinaryData(1, new Uint8Array([1, 2])) }), setMutation('coll/c', { - key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) + key: new BsonBinaryData(1, new Uint8Array([1, 2, 4])) }), setMutation('coll/d', { - key: bsonBinaryData(2, new Uint8Array([1, 2])) + key: new BsonBinaryData(2, new Uint8Array([1, 2])) }) ); await test.backfillIndexes(); @@ -1170,7 +1183,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', - filter('key', '==', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + filter('key', '==', new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ); await test.executeQuery(query_); test.assertOverlaysRead(1, 0, { @@ -1180,7 +1193,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', - filter('key', '!=', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + filter('key', '!=', new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ); await test.executeQuery(query_); test.assertOverlaysRead(3, 0, { @@ -1192,7 +1205,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', - filter('key', '>=', bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + filter('key', '>=', new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ); await test.executeQuery(query_); test.assertOverlaysRead(3, 0, { @@ -1204,7 +1217,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', - filter('key', '<', bsonBinaryData(1, new Uint8Array([1, 2]))) + filter('key', '<', new BsonBinaryData(1, new Uint8Array([1, 2]))) ); await test.executeQuery(query_); test.assertOverlaysRead(0, 0); @@ -1213,8 +1226,8 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', filter('key', 'in', [ - bsonBinaryData(1, new Uint8Array([1, 2, 3])), - bsonBinaryData(1, new Uint8Array([1, 2])) + new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + new BsonBinaryData(1, new Uint8Array([1, 2])) ]) ); await test.executeQuery(query_); @@ -1228,8 +1241,8 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', filter('key', 'not-in', [ - bsonBinaryData(1, new Uint8Array([1, 2, 3])), - bsonBinaryData(1, new Uint8Array([1, 2])) + new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + new BsonBinaryData(1, new Uint8Array([1, 2])) ]) ); await test.executeQuery(query_); @@ -1247,9 +1260,9 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); await test.configureFieldsIndexes(index); await test.writeMutations( - setMutation('coll/a', { key: int32(-1) }), - setMutation('coll/b', { key: int32(0) }), - setMutation('coll/c', { key: int32(1) }) + setMutation('coll/a', { key: new Int32Value(-1) }), + setMutation('coll/b', { key: new Int32Value(0) }), + setMutation('coll/c', { key: new Int32Value(1) }) ); await test.backfillIndexes(); @@ -1262,14 +1275,14 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/a', 'coll/b', 'coll/c'); - query_ = query('coll', filter('key', '==', int32(0))); + query_ = query('coll', filter('key', '==', new Int32Value(0))); await test.executeQuery(query_); test.assertOverlaysRead(1, 0, { [key('coll/b').toString()]: MutationType.Set }); test.assertQueryReturned(query_, 'coll/b'); - query_ = query('coll', filter('key', '!=', int32(0))); + query_ = query('coll', filter('key', '!=', new Int32Value(0))); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/a').toString()]: MutationType.Set, @@ -1277,7 +1290,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/a', 'coll/c'); - query_ = query('coll', filter('key', '>=', int32(0))); + query_ = query('coll', filter('key', '>=', new Int32Value(0))); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/b').toString()]: MutationType.Set, @@ -1285,12 +1298,15 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/b', 'coll/c'); - query_ = query('coll', filter('key', '<', int32(-1))); + query_ = query('coll', filter('key', '<', new Int32Value(-1))); await test.executeQuery(query_); test.assertOverlaysRead(0, 0); test.assertQueryReturned(query_); - query_ = query('coll', filter('key', 'in', [int32(0), int32(1)])); + query_ = query( + 'coll', + filter('key', 'in', [new Int32Value(0), new Int32Value(1)]) + ); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/b').toString()]: MutationType.Set, @@ -1298,7 +1314,10 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/b', 'coll/c'); - query_ = query('coll', filter('key', 'not-in', [int32(0), int32(1)])); + query_ = query( + 'coll', + filter('key', 'not-in', [new Int32Value(0), new Int32Value(1)]) + ); await test.executeQuery(query_); test.assertOverlaysRead(1, 0, { [key('coll/a').toString()]: MutationType.Set @@ -1313,9 +1332,9 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); await test.configureFieldsIndexes(index); await test.writeMutations( - setMutation('coll/a', { key: regex('a', 'i') }), - setMutation('coll/b', { key: regex('a', 'm') }), - setMutation('coll/c', { key: regex('b', 'i') }) + setMutation('coll/a', { key: new RegexValue('a', 'i') }), + setMutation('coll/b', { key: new RegexValue('a', 'm') }), + setMutation('coll/c', { key: new RegexValue('b', 'i') }) ); await test.backfillIndexes(); @@ -1328,14 +1347,14 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/a', 'coll/b', 'coll/c'); - query_ = query('coll', filter('key', '==', regex('a', 'i'))); + query_ = query('coll', filter('key', '==', new RegexValue('a', 'i'))); await test.executeQuery(query_); test.assertOverlaysRead(1, 0, { [key('coll/a').toString()]: MutationType.Set }); test.assertQueryReturned(query_, 'coll/a'); - query_ = query('coll', filter('key', '!=', regex('a', 'i'))); + query_ = query('coll', filter('key', '!=', new RegexValue('a', 'i'))); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/b').toString()]: MutationType.Set, @@ -1343,7 +1362,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/b', 'coll/c'); - query_ = query('coll', filter('key', '>=', regex('a', 'm'))); + query_ = query('coll', filter('key', '>=', new RegexValue('a', 'm'))); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/b').toString()]: MutationType.Set, @@ -1351,14 +1370,17 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/b', 'coll/c'); - query_ = query('coll', filter('key', '<', regex('a', 'i'))); + query_ = query('coll', filter('key', '<', new RegexValue('a', 'i'))); await test.executeQuery(query_); test.assertOverlaysRead(0, 0); test.assertQueryReturned(query_); query_ = query( 'coll', - filter('key', 'in', [regex('a', 'i'), regex('a', 'm')]) + filter('key', 'in', [ + new RegexValue('a', 'i'), + new RegexValue('a', 'm') + ]) ); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { @@ -1369,7 +1391,10 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { query_ = query( 'coll', - filter('key', 'not-in', [regex('a', 'i'), regex('a', 'm')]) + filter('key', 'not-in', [ + new RegexValue('a', 'i'), + new RegexValue('a', 'm') + ]) ); await test.executeQuery(query_); test.assertOverlaysRead(1, 0, { @@ -1385,11 +1410,11 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); await test.configureFieldsIndexes(index); await test.writeMutations( - setMutation('coll/a', { key: minKey() }), - setMutation('coll/b', { key: minKey() }), + setMutation('coll/a', { key: MinKey.instance() }), + setMutation('coll/b', { key: MinKey.instance() }), setMutation('coll/c', { key: null }), setMutation('coll/d', { key: 1 }), - setMutation('coll/e', { key: maxKey() }) + setMutation('coll/e', { key: MaxKey.instance() }) ); await test.backfillIndexes(); @@ -1411,7 +1436,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { 'coll/e' ); - query_ = query('coll', filter('key', '==', minKey())); + query_ = query('coll', filter('key', '==', MinKey.instance())); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/a').toString()]: MutationType.Set, @@ -1419,7 +1444,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/a', 'coll/b'); - query_ = query('coll', filter('key', '!=', minKey())); + query_ = query('coll', filter('key', '!=', MinKey.instance())); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/d').toString()]: MutationType.Set, @@ -1427,7 +1452,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/d', 'coll/e'); - query_ = query('coll', filter('key', '>=', minKey())); + query_ = query('coll', filter('key', '>=', MinKey.instance())); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/a').toString()]: MutationType.Set, @@ -1435,12 +1460,12 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/a', 'coll/b'); - query_ = query('coll', filter('key', '<', minKey())); + query_ = query('coll', filter('key', '<', MinKey.instance())); await test.executeQuery(query_); test.assertOverlaysRead(0, 0, {}); test.assertQueryReturned(query_); - query_ = query('coll', filter('key', 'in', [minKey()])); + query_ = query('coll', filter('key', 'in', [MinKey.instance()])); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/a').toString()]: MutationType.Set, @@ -1448,7 +1473,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/a', 'coll/b'); - query_ = query('coll', filter('key', 'not-in', [minKey()])); + query_ = query('coll', filter('key', 'not-in', [MinKey.instance()])); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/d').toString()]: MutationType.Set, @@ -1464,11 +1489,11 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); await test.configureFieldsIndexes(index); await test.writeMutations( - setMutation('coll/a', { key: maxKey() }), - setMutation('coll/b', { key: maxKey() }), + setMutation('coll/a', { key: MaxKey.instance() }), + setMutation('coll/b', { key: MaxKey.instance() }), setMutation('coll/c', { key: null }), setMutation('coll/d', { key: 1 }), - setMutation('coll/e', { key: minKey() }) + setMutation('coll/e', { key: MinKey.instance() }) ); await test.backfillIndexes(); @@ -1490,7 +1515,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { 'coll/b' ); - query_ = query('coll', filter('key', '==', maxKey())); + query_ = query('coll', filter('key', '==', MaxKey.instance())); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/a').toString()]: MutationType.Set, @@ -1498,7 +1523,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/a', 'coll/b'); - query_ = query('coll', filter('key', '!=', maxKey())); + query_ = query('coll', filter('key', '!=', MaxKey.instance())); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/d').toString()]: MutationType.Set, @@ -1506,7 +1531,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/e', 'coll/d'); - query_ = query('coll', filter('key', '<=', maxKey())); + query_ = query('coll', filter('key', '<=', MaxKey.instance())); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/a').toString()]: MutationType.Set, @@ -1514,17 +1539,17 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/a', 'coll/b'); - query_ = query('coll', filter('key', '>', maxKey())); + query_ = query('coll', filter('key', '>', MaxKey.instance())); await test.executeQuery(query_); test.assertOverlaysRead(0, 0, {}); test.assertQueryReturned(query_); - query_ = query('coll', filter('key', '<', maxKey())); + query_ = query('coll', filter('key', '<', MaxKey.instance())); await test.executeQuery(query_); test.assertOverlaysRead(0, 0, {}); test.assertQueryReturned(query_); - query_ = query('coll', filter('key', 'in', [maxKey()])); + query_ = query('coll', filter('key', 'in', [MaxKey.instance()])); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/a').toString()]: MutationType.Set, @@ -1532,7 +1557,7 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { }); test.assertQueryReturned(query_, 'coll/a', 'coll/b'); - query_ = query('coll', filter('key', 'not-in', [maxKey()])); + query_ = query('coll', filter('key', 'not-in', [MaxKey.instance()])); await test.executeQuery(query_); test.assertOverlaysRead(2, 0, { [key('coll/d').toString()]: MutationType.Set, @@ -1549,26 +1574,26 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.configureFieldsIndexes(index); await test.writeMutations( - setMutation('coll/a', { key: minKey() }), - setMutation('coll/b', { key: int32(2) }), - setMutation('coll/c', { key: int32(1) }), - setMutation('coll/d', { key: bsonTimestamp(1000, 1001) }), - setMutation('coll/e', { key: bsonTimestamp(1000, 1000) }), + setMutation('coll/a', { key: MinKey.instance() }), + setMutation('coll/b', { key: new Int32Value(2) }), + setMutation('coll/c', { key: new Int32Value(1) }), + setMutation('coll/d', { key: new BsonTimestamp(1000, 1001) }), + setMutation('coll/e', { key: new BsonTimestamp(1000, 1000) }), setMutation('coll/f', { - key: bsonBinaryData(1, new Uint8Array([1, 2, 4])) + key: new BsonBinaryData(1, new Uint8Array([1, 2, 4])) }), setMutation('coll/g', { - key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }), setMutation('coll/h', { - key: bsonObjectId('507f191e810c19729de860eb') + key: new BsonObjectId('507f191e810c19729de860eb') }), setMutation('coll/i', { - key: bsonObjectId('507f191e810c19729de860ea') + key: new BsonObjectId('507f191e810c19729de860ea') }), - setMutation('coll/j', { key: regex('^bar', 'm') }), - setMutation('coll/k', { key: regex('^bar', 'i') }), - setMutation('coll/l', { key: maxKey() }) + setMutation('coll/j', { key: new RegexValue('^bar', 'm') }), + setMutation('coll/k', { key: new RegexValue('^bar', 'i') }), + setMutation('coll/l', { key: MaxKey.instance() }) ); await test.backfillIndexes(); @@ -1614,29 +1639,29 @@ describe('LocalStore w/ IndexedDB Persistence (Non generic)', () => { await test.writeMutations( setMutation('coll/a', { key: null }), - setMutation('coll/b', { key: minKey() }), + setMutation('coll/b', { key: MinKey.instance() }), setMutation('coll/c', { key: true }), setMutation('coll/d', { key: NaN }), - setMutation('coll/e', { key: int32(1) }), + setMutation('coll/e', { key: new Int32Value(1) }), setMutation('coll/f', { key: 2.0 }), setMutation('coll/g', { key: 3 }), setMutation('coll/h', { key: new Timestamp(100, 123456000) }), - setMutation('coll/i', { key: bsonTimestamp(1, 2) }), + setMutation('coll/i', { key: new BsonTimestamp(1, 2) }), setMutation('coll/j', { key: 'string' }), setMutation('coll/k', { key: blob(1, 2, 3) }), setMutation('coll/l', { - key: bsonBinaryData(1, new Uint8Array([1, 2, 3])) + key: new BsonBinaryData(1, new Uint8Array([1, 2, 3])) }), setMutation('coll/m', { key: ref('foo/bar') }), setMutation('coll/n', { - key: bsonObjectId('507f191e810c19729de860ea') + key: new BsonObjectId('507f191e810c19729de860ea') }), setMutation('coll/o', { key: new GeoPoint(1, 2) }), - setMutation('coll/p', { key: regex('^bar', 'm') }), + setMutation('coll/p', { key: new RegexValue('^bar', 'm') }), setMutation('coll/q', { key: [2, 'foo'] }), setMutation('coll/r', { key: vector([1, 2, 3]) }), setMutation('coll/s', { key: { bar: 1, foo: 2 } }), - setMutation('coll/t', { key: maxKey() }) + setMutation('coll/t', { key: MaxKey.instance() }) ); await test.backfillIndexes(); diff --git a/packages/firestore/test/unit/model/document.test.ts b/packages/firestore/test/unit/model/document.test.ts index 2c2387cca63..f67e9d971a0 100644 --- a/packages/firestore/test/unit/model/document.test.ts +++ b/packages/firestore/test/unit/model/document.test.ts @@ -18,14 +18,14 @@ import { expect } from 'chai'; import { - bsonBinaryData, - bsonObjectId, - bsonTimestamp, - int32, - maxKey, - minKey, - regex -} from '../../../src/lite-api/field_value_impl'; + BsonBinaryData, + BsonObjectId, + BsonTimestamp, + Int32Value, + MaxKey, + MinKey, + RegexValue +} from '../../../src'; import { doc, expectEqual, @@ -55,26 +55,26 @@ describe('Document', () => { it('can be constructed with bson types', () => { const data = { - objectId: bsonObjectId('foo'), - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - timestamp: bsonTimestamp(1, 2), - min: minKey(), - max: maxKey(), - regex: regex('a', 'b'), - int32: int32(1) + objectId: new BsonObjectId('foo'), + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: new BsonTimestamp(1, 2), + min: MinKey.instance(), + max: MaxKey.instance(), + regex: new RegexValue('a', 'b'), + int32: new Int32Value(1) }; const document = doc('rooms/Eros', 1, data); const value = document.data; expect(value.value).to.deep.equal( wrap({ - objectId: bsonObjectId('foo'), - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - timestamp: bsonTimestamp(1, 2), - min: minKey(), - max: maxKey(), - regex: regex('a', 'b'), - int32: int32(1) + objectId: new BsonObjectId('foo'), + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: new BsonTimestamp(1, 2), + min: MinKey.instance(), + max: MaxKey.instance(), + regex: new RegexValue('a', 'b'), + int32: new Int32Value(1) }) ); expect(value).not.to.equal(data); diff --git a/packages/firestore/test/unit/model/object_value.test.ts b/packages/firestore/test/unit/model/object_value.test.ts index 13cfa02131b..40b18893e68 100644 --- a/packages/firestore/test/unit/model/object_value.test.ts +++ b/packages/firestore/test/unit/model/object_value.test.ts @@ -18,15 +18,15 @@ import { expect } from 'chai'; import { - vector, - bsonObjectId, - bsonBinaryData, - bsonTimestamp, - int32, - regex, - minKey, - maxKey -} from '../../../src/lite-api/field_value_impl'; + BsonObjectId, + BsonBinaryData, + BsonTimestamp, + RegexValue, + Int32Value, + MaxKey, + MinKey +} from '../../../src'; +import { vector } from '../../../src/lite-api/field_value_impl'; import { extractFieldMask, ObjectValue } from '../../../src/model/object_value'; import { TypeOrder } from '../../../src/model/type_order'; import { typeOrder } from '../../../src/model/values'; @@ -38,13 +38,13 @@ describe('ObjectValue', () => { foo: { a: 1, b: true, c: 'string' }, embedding: vector([1]), bson: { - objectId: bsonObjectId('foo'), - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - timestamp: bsonTimestamp(1, 2), - min: minKey(), - max: maxKey(), - regex: regex('a', 'b'), - int32: int32(1) + objectId: new BsonObjectId('foo'), + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: new BsonTimestamp(1, 2), + min: MinKey.instance(), + max: MaxKey.instance(), + regex: new RegexValue('a', 'b'), + int32: new Int32Value(1) } }); @@ -102,30 +102,36 @@ describe('ObjectValue', () => { expect(objValue.field(field('bson'))!).to.deep.equal( wrap({ - objectId: bsonObjectId('foo'), - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - timestamp: bsonTimestamp(1, 2), - min: minKey(), - max: maxKey(), - regex: regex('a', 'b'), - int32: int32(1) + objectId: new BsonObjectId('foo'), + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: new BsonTimestamp(1, 2), + min: MinKey.instance(), + max: MaxKey.instance(), + regex: new RegexValue('a', 'b'), + int32: new Int32Value(1) }) ); expect(objValue.field(field('bson.objectId'))!).to.deep.equal( - wrap(bsonObjectId('foo')) + wrap(new BsonObjectId('foo')) ); expect(objValue.field(field('bson.binary'))!).to.deep.equal( - wrap(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + wrap(new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ); expect(objValue.field(field('bson.timestamp'))!).to.deep.equal( - wrap(bsonTimestamp(1, 2)) + wrap(new BsonTimestamp(1, 2)) + ); + expect(objValue.field(field('bson.min'))!).to.deep.equal( + wrap(MinKey.instance()) + ); + expect(objValue.field(field('bson.max'))!).to.deep.equal( + wrap(MaxKey.instance()) ); - expect(objValue.field(field('bson.min'))!).to.deep.equal(wrap(minKey())); - expect(objValue.field(field('bson.max'))!).to.deep.equal(wrap(maxKey())); expect(objValue.field(field('bson.regex'))!).to.deep.equal( - wrap(regex('a', 'b')) + wrap(new RegexValue('a', 'b')) + ); + expect(objValue.field(field('bson.int32'))!).to.deep.equal( + wrap(new Int32Value(1)) ); - expect(objValue.field(field('bson.int32'))!).to.deep.equal(wrap(int32(1))); }); it('can overwrite existing fields', () => { @@ -234,56 +240,56 @@ describe('ObjectValue', () => { it('can handle bson types in ObjectValue', () => { const objValue = ObjectValue.empty(); // Add new fields - objValue.set(field('objectId'), wrap(bsonObjectId('foo-value'))); + objValue.set(field('objectId'), wrap(new BsonObjectId('foo-value'))); objValue.set( field('binary'), - wrap(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + wrap(new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ); - objValue.set(field('timestamp'), wrap(bsonTimestamp(1, 2))); - objValue.set(field('regex'), wrap(regex('a', 'b'))); - objValue.set(field('int32'), wrap(int32(1))); - objValue.set(field('min'), wrap(minKey())); - objValue.set(field('max'), wrap(maxKey())); + objValue.set(field('timestamp'), wrap(new BsonTimestamp(1, 2))); + objValue.set(field('regex'), wrap(new RegexValue('a', 'b'))); + objValue.set(field('int32'), wrap(new Int32Value(1))); + objValue.set(field('min'), wrap(MinKey.instance())); + objValue.set(field('max'), wrap(MaxKey.instance())); assertObjectEquals(objValue, { - objectId: bsonObjectId('foo-value'), - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - timestamp: bsonTimestamp(1, 2), - regex: regex('a', 'b'), - int32: int32(1), - min: minKey(), - max: maxKey() + objectId: new BsonObjectId('foo-value'), + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: new BsonTimestamp(1, 2), + regex: new RegexValue('a', 'b'), + int32: new Int32Value(1), + min: MinKey.instance(), + max: MaxKey.instance() }); // Overwrite existing fields - objValue.set(field('objectId'), wrap(bsonObjectId('new-foo-value'))); + objValue.set(field('objectId'), wrap(new BsonObjectId('new-foo-value'))); // Create nested objects objValue.set( field('foo.binary'), - wrap(bsonBinaryData(2, new Uint8Array([1, 2, 3]))) + wrap(new BsonBinaryData(2, new Uint8Array([1, 2, 3]))) ); - objValue.set(field('foo.timestamp'), wrap(bsonTimestamp(1, 2))); + objValue.set(field('foo.timestamp'), wrap(new BsonTimestamp(1, 2))); // Delete fields objValue.delete(field('binary')); // overwrite nested objects - objValue.set(field('foo.timestamp'), wrap(bsonTimestamp(2, 1))); + objValue.set(field('foo.timestamp'), wrap(new BsonTimestamp(2, 1))); // Overwrite primitive values to create objects objValue.set(field('min'), wrap(null)); assertObjectEquals(objValue, { - objectId: bsonObjectId('new-foo-value'), - timestamp: bsonTimestamp(1, 2), - regex: regex('a', 'b'), - int32: int32(1), + objectId: new BsonObjectId('new-foo-value'), + timestamp: new BsonTimestamp(1, 2), + regex: new RegexValue('a', 'b'), + int32: new Int32Value(1), min: null, - max: maxKey(), + max: MaxKey.instance(), foo: { - binary: bsonBinaryData(2, new Uint8Array([1, 2, 3])), - timestamp: bsonTimestamp(2, 1) + binary: new BsonBinaryData(2, new Uint8Array([1, 2, 3])), + timestamp: new BsonTimestamp(2, 1) } }); }); @@ -294,13 +300,13 @@ describe('ObjectValue', () => { map: { a: 1, b: true, c: 'string', nested: { d: 'e' } }, emptymap: {}, bar: { - objectId: bsonObjectId('foo'), - binary: bsonBinaryData(1, new Uint8Array([1, 2, 3])), - timestamp: bsonTimestamp(1, 2), - min: minKey(), - max: maxKey(), - regex: regex('a', 'b'), - int32: int32(1) + objectId: new BsonObjectId('foo'), + binary: new BsonBinaryData(1, new Uint8Array([1, 2, 3])), + timestamp: new BsonTimestamp(1, 2), + min: MinKey.instance(), + max: MaxKey.instance(), + regex: new RegexValue('a', 'b'), + int32: new Int32Value(1) } }); const expectedMask = mask( diff --git a/packages/firestore/test/unit/model/values.test.ts b/packages/firestore/test/unit/model/values.test.ts index 0d93d335ded..048ae762a98 100644 --- a/packages/firestore/test/unit/model/values.test.ts +++ b/packages/firestore/test/unit/model/values.test.ts @@ -17,25 +17,19 @@ import { expect } from 'chai'; -import { GeoPoint, Timestamp } from '../../../src'; -import { DatabaseId } from '../../../src/core/database_info'; -import { BsonBinaryData } from '../../../src/lite-api/bson_binary_data'; -import { BsonObjectId } from '../../../src/lite-api/bson_object_Id'; -import { BsonTimestamp } from '../../../src/lite-api/bson_timestamp_value'; import { - vector, - regex, - bsonTimestamp, - int32, - bsonBinaryData, - bsonObjectId, - minKey, - maxKey -} from '../../../src/lite-api/field_value_impl'; -import { Int32Value } from '../../../src/lite-api/int32_value'; -import { MaxKey } from '../../../src/lite-api/max_key'; -import { MinKey } from '../../../src/lite-api/min_key'; -import { RegexValue } from '../../../src/lite-api/regex_value'; + GeoPoint, + Timestamp, + BsonBinaryData, + BsonTimestamp, + BsonObjectId, + RegexValue, + Int32Value, + MaxKey, + MinKey +} from '../../../src'; +import { DatabaseId } from '../../../src/core/database_info'; +import { vector } from '../../../src/lite-api/field_value_impl'; import { serverTimestamp } from '../../../src/model/server_timestamps'; import { canonicalId, @@ -79,7 +73,7 @@ describe('Values', () => { [wrap(true), wrap(true)], [wrap(false), wrap(false)], [wrap(null), wrap(null)], - [wrap(minKey()), wrap(minKey()), wrap(MinKey.instance())], + [wrap(MinKey.instance()), wrap(MinKey.instance())], [wrap(0 / 0), wrap(Number.NaN), wrap(NaN)], // -0.0 and 0.0 order the same but are not considered equal. [wrap(-0.0)], @@ -117,20 +111,20 @@ describe('Values', () => { [wrap({ foo: 1 })], [wrap(vector([]))], [wrap(vector([1, 2.3, -4.0]))], - [wrap(regex('^foo', 'i')), wrap(new RegexValue('^foo', 'i'))], - [wrap(bsonTimestamp(57, 4)), wrap(new BsonTimestamp(57, 4))], + [wrap(new RegexValue('^foo', 'i')), wrap(new RegexValue('^foo', 'i'))], + [wrap(new BsonTimestamp(57, 4)), wrap(new BsonTimestamp(57, 4))], [ - wrap(bsonBinaryData(128, Uint8Array.from([7, 8, 9]))), wrap(new BsonBinaryData(128, Uint8Array.from([7, 8, 9]))), - wrap(bsonBinaryData(128, Buffer.from([7, 8, 9]))), + wrap(new BsonBinaryData(128, Uint8Array.from([7, 8, 9]))), + wrap(new BsonBinaryData(128, Buffer.from([7, 8, 9]))), wrap(new BsonBinaryData(128, Buffer.from([7, 8, 9]))) ], [ - wrap(bsonObjectId('123456789012')), + wrap(new BsonObjectId('123456789012')), wrap(new BsonObjectId('123456789012')) ], - [wrap(int32(255)), wrap(new Int32Value(255))], - [wrap(maxKey()), wrap(maxKey()), wrap(MaxKey.instance())] + [wrap(new Int32Value(255)), wrap(new Int32Value(255))], + [wrap(MaxKey.instance()), wrap(MaxKey.instance())] ]; expectEqualitySets(values, (v1, v2) => valueEquals(v1, v2)); }); @@ -170,7 +164,7 @@ describe('Values', () => { [wrap(null)], // MinKey is after null - [wrap(minKey())], + [wrap(MinKey.instance())], // booleans [wrap(false)], @@ -183,23 +177,23 @@ describe('Values', () => { [wrap(Number.MIN_SAFE_INTEGER - 1)], [wrap(Number.MIN_SAFE_INTEGER)], // 64-bit and 32-bit integers order together numerically. - [{ integerValue: -2147483648 }, wrap(int32(-2147483648))], + [{ integerValue: -2147483648 }, wrap(new Int32Value(-2147483648))], [wrap(-1.1)], // Integers, Int32Values and Doubles order the same. - [{ integerValue: -1 }, { doubleValue: -1 }, wrap(int32(-1))], + [{ integerValue: -1 }, { doubleValue: -1 }, wrap(new Int32Value(-1))], [wrap(-Number.MIN_VALUE)], // zeros all compare the same. [ { integerValue: 0 }, { doubleValue: 0 }, { doubleValue: -0 }, - wrap(int32(0)) + wrap(new Int32Value(0)) ], [wrap(Number.MIN_VALUE)], - [{ integerValue: 1 }, { doubleValue: 1.0 }, wrap(int32(1))], + [{ integerValue: 1 }, { doubleValue: 1.0 }, wrap(new Int32Value(1))], [wrap(1.1)], - [wrap(int32(2))], - [wrap(int32(2147483647))], + [wrap(new Int32Value(2))], + [wrap(new Int32Value(2147483647))], [wrap(Number.MAX_SAFE_INTEGER)], [wrap(Number.MAX_SAFE_INTEGER + 1)], [wrap(Infinity)], @@ -215,9 +209,9 @@ describe('Values', () => { ], // request timestamp - [wrap(bsonTimestamp(123, 4))], - [wrap(bsonTimestamp(123, 5))], - [wrap(bsonTimestamp(124, 0))], + [wrap(new BsonTimestamp(123, 4))], + [wrap(new BsonTimestamp(123, 5))], + [wrap(new BsonTimestamp(124, 0))], // server timestamps come after all concrete timestamps. [serverTimestamp(Timestamp.fromDate(date1), null)], @@ -243,11 +237,11 @@ describe('Values', () => { [wrap(blob(255))], [ - wrap(bsonBinaryData(5, Buffer.from([1, 2, 3]))), - wrap(bsonBinaryData(5, new Uint8Array([1, 2, 3]))) + wrap(new BsonBinaryData(5, Buffer.from([1, 2, 3]))), + wrap(new BsonBinaryData(5, new Uint8Array([1, 2, 3]))) ], - [wrap(bsonBinaryData(7, Buffer.from([1])))], - [wrap(bsonBinaryData(7, new Uint8Array([2])))], + [wrap(new BsonBinaryData(7, Buffer.from([1])))], + [wrap(new BsonBinaryData(7, new Uint8Array([2])))], // reference values [refValue(dbId('p1', 'd1'), key('c1/doc1'))], @@ -258,11 +252,11 @@ describe('Values', () => { [refValue(dbId('p2', 'd1'), key('c1/doc1'))], // ObjectId - [wrap(bsonObjectId('foo')), wrap(bsonObjectId('foo'))], + [wrap(new BsonObjectId('foo')), wrap(new BsonObjectId('foo'))], // TODO(Mila/BSON): uncomment after string sort bug is fixed - // [wrap(bsonObjectId('Ḟoo'))], // with latin capital letter f with dot above - // [wrap(bsonObjectId('foo\u0301'))], // with combining acute accent - [wrap(bsonObjectId('xyz'))], + // [wrap(new BsonObjectId('Ḟoo'))], // with latin capital letter f with dot above + // [wrap(new BsonObjectId('foo\u0301'))], // with combining acute accent + [wrap(new BsonObjectId('xyz'))], // geo points [wrap(new GeoPoint(-90, -180))], @@ -279,10 +273,10 @@ describe('Values', () => { [wrap(new GeoPoint(90, 180))], // regular expressions - [wrap(regex('a', 'bar1'))], - [wrap(regex('foo', 'bar1'))], - [wrap(regex('foo', 'bar2'))], - [wrap(regex('go', 'bar1'))], + [wrap(new RegexValue('a', 'bar1'))], + [wrap(new RegexValue('foo', 'bar1'))], + [wrap(new RegexValue('foo', 'bar2'))], + [wrap(new RegexValue('go', 'bar1'))], // arrays [wrap([])], @@ -305,7 +299,7 @@ describe('Values', () => { [wrap({ foo: '0' })], // MaxKey - [wrap(maxKey())] + [wrap(MaxKey.instance())] ]; expectCorrectComparisonGroups( @@ -412,28 +406,34 @@ describe('Values', () => { }, { expectedByteSize: 27, - elements: [wrap(regex('a', 'b')), wrap(regex('c', 'd'))] + elements: [ + wrap(new RegexValue('a', 'b')), + wrap(new RegexValue('c', 'd')) + ] }, { expectedByteSize: 13, - elements: [wrap(bsonObjectId('foo')), wrap(bsonObjectId('bar'))] + elements: [wrap(new BsonObjectId('foo')), wrap(new BsonObjectId('bar'))] }, { expectedByteSize: 53, - elements: [wrap(bsonTimestamp(1, 2)), wrap(bsonTimestamp(3, 4))] + elements: [wrap(new BsonTimestamp(1, 2)), wrap(new BsonTimestamp(3, 4))] }, { expectedByteSize: 8, - elements: [wrap(int32(1)), wrap(int32(2147483647))] + elements: [wrap(new Int32Value(1)), wrap(new Int32Value(2147483647))] }, { expectedByteSize: 16, elements: [ - wrap(bsonBinaryData(1, new Uint8Array([127, 128]))), - wrap(bsonBinaryData(128, new Uint8Array([1, 2]))) + wrap(new BsonBinaryData(1, new Uint8Array([127, 128]))), + wrap(new BsonBinaryData(128, new Uint8Array([1, 2]))) ] }, - { expectedByteSize: 11, elements: [wrap(minKey()), wrap(maxKey())] } + { + expectedByteSize: 11, + elements: [wrap(MinKey.instance()), wrap(MaxKey.instance())] + } ]; for (const group of equalityGroups) { @@ -464,11 +464,11 @@ describe('Values', () => { [wrap({ a: 'a', b: 'b' }), wrap({ a: 'a', b: 'b', c: 'c' })], [wrap({ a: 'a', b: 'b' }), wrap({ a: 'a', b: 'b', c: 'c' })], [wrap(vector([2, 3])), wrap(vector([1, 2, 3]))], - [wrap(regex('a', 'b')), wrap(regex('cc', 'dd'))], - [wrap(bsonObjectId('foo')), wrap(bsonObjectId('foobar'))], + [wrap(new RegexValue('a', 'b')), wrap(new RegexValue('cc', 'dd'))], + [wrap(new BsonObjectId('foo')), wrap(new BsonObjectId('foobar'))], [ - wrap(bsonBinaryData(128, new Uint8Array([127, 128]))), - wrap(bsonBinaryData(1, new Uint8Array([1, 2, 3]))) + wrap(new BsonBinaryData(128, new Uint8Array([127, 128]))), + wrap(new BsonBinaryData(1, new Uint8Array([1, 2, 3]))) ] ]; @@ -489,7 +489,7 @@ describe('Values', () => { [valuesGetLowerBound({ nullValue: 'NULL_VALUE' }), wrap(null)], // lower bound of MinKey is MinKey - [valuesGetLowerBound(MIN_KEY_VALUE), wrap(minKey())], + [valuesGetLowerBound(MIN_KEY_VALUE), wrap(MinKey.instance())], // booleans [valuesGetLowerBound({ booleanValue: true }), wrap(false)], @@ -512,11 +512,11 @@ describe('Values', () => { // bson timestamps [ - valuesGetLowerBound(wrap(bsonTimestamp(4294967295, 4294967295))), + valuesGetLowerBound(wrap(new BsonTimestamp(4294967295, 4294967295))), MIN_BSON_TIMESTAMP_VALUE, - wrap(bsonTimestamp(0, 0)) + wrap(new BsonTimestamp(0, 0)) ], - [wrap(bsonTimestamp(1, 1))], + [wrap(new BsonTimestamp(1, 1))], // strings [valuesGetLowerBound({ stringValue: 'Z' }), wrap('')], @@ -529,11 +529,11 @@ describe('Values', () => { // bson binary data [ valuesGetLowerBound( - wrap(bsonBinaryData(128, new Uint8Array([128, 128]))) + wrap(new BsonBinaryData(128, new Uint8Array([128, 128]))) ), MIN_BSON_BINARY_VALUE ], - [wrap(bsonBinaryData(0, new Uint8Array([0])))], + [wrap(new BsonBinaryData(0, new Uint8Array([0])))], // resource names [ @@ -544,11 +544,11 @@ describe('Values', () => { // bson object ids [ - valuesGetLowerBound(wrap(bsonObjectId('ZZZ'))), - wrap(bsonObjectId('')), + valuesGetLowerBound(wrap(new BsonObjectId('ZZZ'))), + wrap(new BsonObjectId('')), MIN_BSON_OBJECT_ID_VALUE ], - [wrap(bsonObjectId('a'))], + [wrap(new BsonObjectId('a'))], // geo points [ @@ -559,11 +559,11 @@ describe('Values', () => { // regular expressions [ - valuesGetLowerBound(wrap(regex('ZZZ', 'i'))), - wrap(regex('', '')), + valuesGetLowerBound(wrap(new RegexValue('ZZZ', 'i'))), + wrap(new RegexValue('', '')), MIN_REGEX_VALUE ], - [wrap(regex('a', 'i'))], + [wrap(new RegexValue('a', 'i'))], // arrays [valuesGetLowerBound({ arrayValue: {} }), wrap([])], @@ -590,7 +590,7 @@ describe('Values', () => { [valuesGetLowerBound({ mapValue: {} }), wrap({})], // MaxKey - [wrap(maxKey())] + [wrap(MaxKey.instance())] ]; expectCorrectComparisonGroups( @@ -607,7 +607,10 @@ describe('Values', () => { [wrap(null)], // upper value of null is MinKey - [valuesGetUpperBound({ nullValue: 'NULL_VALUE' }), wrap(minKey())], + [ + valuesGetUpperBound({ nullValue: 'NULL_VALUE' }), + wrap(MinKey.instance()) + ], // upper value of MinKey is boolean `false` [valuesGetUpperBound(MIN_KEY_VALUE), wrap(false)], @@ -617,7 +620,7 @@ describe('Values', () => { [valuesGetUpperBound({ booleanValue: false })], // numbers - [wrap(int32(2147483647))], //largest int32 value + [wrap(new Int32Value(2147483647))], //largest int32 value [wrap(Number.MAX_SAFE_INTEGER)], [wrap(Number.POSITIVE_INFINITY)], [valuesGetUpperBound({ doubleValue: NaN })], @@ -627,7 +630,7 @@ describe('Values', () => { [valuesGetUpperBound({ timestampValue: {} })], // bson timestamps - [wrap(bsonTimestamp(4294967295, 4294967295))], // largest bson timestamp value + [wrap(new BsonTimestamp(4294967295, 4294967295))], // largest bson timestamp value [valuesGetUpperBound(MIN_BSON_TIMESTAMP_VALUE)], // strings @@ -639,7 +642,7 @@ describe('Values', () => { [valuesGetUpperBound({ bytesValue: '' })], // bson binary data - [wrap(bsonBinaryData(128, new Uint8Array([255, 255, 255])))], + [wrap(new BsonBinaryData(128, new Uint8Array([255, 255, 255])))], [valuesGetUpperBound(MIN_BSON_BINARY_VALUE)], // resource names @@ -647,7 +650,7 @@ describe('Values', () => { [valuesGetUpperBound({ referenceValue: '' })], // bson object ids - [wrap(bsonObjectId('foo'))], + [wrap(new BsonObjectId('foo'))], [valuesGetUpperBound(MIN_BSON_OBJECT_ID_VALUE)], // geo points @@ -655,7 +658,7 @@ describe('Values', () => { [valuesGetUpperBound({ geoPointValue: {} })], // regular expressions - [wrap(regex('a', 'i'))], + [wrap(new RegexValue('a', 'i'))], [valuesGetUpperBound(MIN_REGEX_VALUE)], // arrays @@ -670,7 +673,7 @@ describe('Values', () => { [wrap({ 'a': 'b' })], // MaxKey - [wrap(maxKey())] + [wrap(MaxKey.instance())] ]; expectCorrectComparisonGroups( @@ -712,19 +715,21 @@ describe('Values', () => { expect( canonicalId(wrap({ 'a': ['b', { 'c': new GeoPoint(30, 60) }] })) ).to.equal('{a:[b,{c:geo(30,60)}]}'); - expect(canonicalId(wrap(regex('a', 'b')))).to.equal( + expect(canonicalId(wrap(new RegexValue('a', 'b')))).to.equal( '{__regex__:{options:b,pattern:a}}' ); - expect(canonicalId(wrap(bsonObjectId('foo')))).to.equal('{__oid__:foo}'); - expect(canonicalId(wrap(bsonTimestamp(1, 2)))).to.equal( + expect(canonicalId(wrap(new BsonObjectId('foo')))).to.equal( + '{__oid__:foo}' + ); + expect(canonicalId(wrap(new BsonTimestamp(1, 2)))).to.equal( '{__request_timestamp__:{increment:2,seconds:1}}' ); - expect(canonicalId(wrap(int32(1)))).to.equal('{__int__:1}'); + expect(canonicalId(wrap(new Int32Value(1)))).to.equal('{__int__:1}'); expect( - canonicalId(wrap(bsonBinaryData(1, new Uint8Array([1, 2, 3])))) + canonicalId(wrap(new BsonBinaryData(1, new Uint8Array([1, 2, 3])))) ).to.equal('{__binary__:AQECAw==}'); - expect(canonicalId(wrap(minKey()))).to.equal('{__min__:null}'); - expect(canonicalId(wrap(maxKey()))).to.equal('{__max__:null}'); + expect(canonicalId(wrap(MinKey.instance()))).to.equal('{__min__:null}'); + expect(canonicalId(wrap(MaxKey.instance()))).to.equal('{__max__:null}'); }); it('canonical IDs ignore sort order', () => { diff --git a/packages/firestore/test/unit/remote/serializer.helper.ts b/packages/firestore/test/unit/remote/serializer.helper.ts index 9c116549928..24d7b039d0c 100644 --- a/packages/firestore/test/unit/remote/serializer.helper.ts +++ b/packages/firestore/test/unit/remote/serializer.helper.ts @@ -20,11 +20,18 @@ import { expect } from 'chai'; import { arrayRemove, arrayUnion, + BsonBinaryData, + BsonObjectId, + BsonTimestamp, Bytes, DocumentReference, GeoPoint, increment, + Int32Value, + MaxKey, + MinKey, refEqual, + RegexValue, serverTimestamp, Timestamp } from '../../../src'; @@ -52,16 +59,7 @@ import { } from '../../../src/core/query'; import { SnapshotVersion } from '../../../src/core/snapshot_version'; import { Target, targetEquals, TargetImpl } from '../../../src/core/target'; -import { - bsonBinaryData, - bsonObjectId, - bsonTimestamp, - int32, - maxKey, - minKey, - regex, - vector -} from '../../../src/lite-api/field_value_impl'; +import { vector } from '../../../src/lite-api/field_value_impl'; import { parseQueryValue } from '../../../src/lite-api/user_data_reader'; import { TargetData, TargetPurpose } from '../../../src/local/target_data'; import { FieldMask } from '../../../src/model/field_mask'; @@ -577,12 +575,12 @@ export function serializerTest( it('converts BSON types in mapValue', () => { const examples = [ - bsonObjectId('foo'), - bsonTimestamp(1, 2), - minKey(), - maxKey(), - regex('a', 'b'), - int32(1) + new BsonObjectId('foo'), + new BsonTimestamp(1, 2), + MinKey.instance(), + MaxKey.instance(), + new RegexValue('a', 'b'), + new Int32Value(1) ]; for (const example of examples) { @@ -598,7 +596,7 @@ export function serializerTest( } // BsonBinaryData will be serialized differently Proto3Json VS. regular Protobuf format - const bsonBinary = bsonBinaryData(1, new Uint8Array([1, 2, 3])); + const bsonBinary = new BsonBinaryData(1, new Uint8Array([1, 2, 3])); const expectedJson: api.Value = { mapValue: { fields: { From 4600a34360e0edff345068ad7a44403d9cd1b5fd Mon Sep 17 00:00:00 2001 From: Ehsan Date: Fri, 2 May 2025 15:07:37 -0700 Subject: [PATCH 22/23] Create seven-actors-kneel.md --- .changeset/seven-actors-kneel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/seven-actors-kneel.md diff --git a/.changeset/seven-actors-kneel.md b/.changeset/seven-actors-kneel.md new file mode 100644 index 00000000000..6bc97d0e3ed --- /dev/null +++ b/.changeset/seven-actors-kneel.md @@ -0,0 +1,5 @@ +--- +"@firebase/firestore": minor +--- + +feat: Adds support for MinKey, MaxKey, RegexValue, Int32Value, BsonObjectId, BsonTimestamp, and BsonBinaryData. From cbba951db7ced310ff052d88241fa682ad2e971a Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Mon, 5 May 2025 12:40:20 -0400 Subject: [PATCH 23/23] resolve TODOs --- packages/firestore/src/model/values.ts | 5 +- .../test/integration/api/database.test.ts | 103 +++++++++--------- .../test/integration/util/helpers.ts | 3 +- .../firestore/test/unit/model/values.test.ts | 12 +- 4 files changed, 60 insertions(+), 63 deletions(-) diff --git a/packages/firestore/src/model/values.ts b/packages/firestore/src/model/values.ts index 848325c3521..fca9c34b9ea 100644 --- a/packages/firestore/src/model/values.ts +++ b/packages/firestore/src/model/values.ts @@ -644,7 +644,7 @@ function compareRegex(left: Value, right: Value): number { ]?.stringValue ?? ''; // First order by patterns, and then options. - const patternDiff = primitiveComparator(leftPattern, rightPattern); + const patternDiff = compareUtf8Strings(leftPattern, rightPattern); return patternDiff !== 0 ? patternDiff : primitiveComparator(leftOptions, rightOptions); @@ -656,8 +656,7 @@ function compareBsonObjectIds(left: Value, right: Value): number { const rightOid = right.mapValue!.fields?.[RESERVED_BSON_OBJECT_ID_KEY]?.stringValue ?? ''; - // TODO(Mila/BSON): use compareUtf8Strings once the bug fix is merged. - return primitiveComparator(leftOid, rightOid); + return compareUtf8Strings(leftOid, rightOid); } /** diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 8d9167cee9c..ce5a3d34eae 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -2818,9 +2818,6 @@ apiDescribe('Database', persistence => { settings, testDocs, async coll => { - // Populate the cache with all docs first - await getDocs(coll); - let orderedQuery = query( coll, where('key', '>', new BsonObjectId('507f191e810c19729de860ea')), @@ -2833,6 +2830,7 @@ apiDescribe('Database', persistence => { testDocs['b'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, toIds(snapshot) @@ -2853,6 +2851,7 @@ apiDescribe('Database', persistence => { testDocs['a'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, toIds(snapshot) @@ -2873,9 +2872,6 @@ apiDescribe('Database', persistence => { settings, testDocs, async coll => { - // Populate the cache with all docs first - await getDocs(coll); - let orderedQuery = query( coll, where('key', '>=', new Int32Value(1)), @@ -2888,6 +2884,7 @@ apiDescribe('Database', persistence => { testDocs['b'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, toIds(snapshot) @@ -2905,6 +2902,7 @@ apiDescribe('Database', persistence => { testDocs['a'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, toIds(snapshot) @@ -2925,9 +2923,6 @@ apiDescribe('Database', persistence => { settings, testDocs, async coll => { - // Populate the cache with all docs first - await getDocs(coll); - let orderedQuery = query( coll, where('key', '>', new BsonTimestamp(1, 1)), @@ -2940,6 +2935,7 @@ apiDescribe('Database', persistence => { testDocs['b'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, toIds(snapshot) @@ -2957,6 +2953,7 @@ apiDescribe('Database', persistence => { testDocs['b'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, toIds(snapshot) @@ -2977,9 +2974,6 @@ apiDescribe('Database', persistence => { settings, testDocs, async coll => { - // Populate the cache with all docs first - await getDocs(coll); - let orderedQuery = query( coll, where('key', '>', new BsonBinaryData(1, new Uint8Array([1, 2, 3]))), @@ -2992,6 +2986,7 @@ apiDescribe('Database', persistence => { testDocs['b'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, toIds(snapshot) @@ -3014,6 +3009,7 @@ apiDescribe('Database', persistence => { testDocs['a'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, toIds(snapshot) @@ -3034,9 +3030,6 @@ apiDescribe('Database', persistence => { settings, testDocs, async coll => { - // Populate the cache with all docs first - await getDocs(coll); - const orderedQuery = query( coll, or( @@ -3052,6 +3045,7 @@ apiDescribe('Database', persistence => { testDocs['a'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, toIds(snapshot) @@ -3074,9 +3068,6 @@ apiDescribe('Database', persistence => { settings, testDocs, async coll => { - // Populate the cache with all docs first - await getDocs(coll); - let filteredQuery = query( coll, where('key', '==', MinKey.instance()) @@ -3087,23 +3078,24 @@ apiDescribe('Database', persistence => { testDocs['b'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) ); - // TODO(Mila/BSON): uncomment after the null inclusion bug - // filteredQuery = query(coll, where('key', '!=', MinKey.instance())); - // snapshot = await getDocs(filteredQuery); - // expect(toDataArray(snapshot)).to.deep.equal([ - // testDocs['d'], - // testDocs['e'] - // ]); - // await assertSDKQueryResultsConsistentWithBackend( - // filteredQuery, - // testDocs, - // toIds(snapshot) - // ); + filteredQuery = query(coll, where('key', '!=', MinKey.instance())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['d'], + testDocs['e'] + ]); + await assertSDKQueryResultsConsistentWithBackend( + coll, + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '>=', MinKey.instance())); snapshot = await getDocs(filteredQuery); @@ -3112,6 +3104,7 @@ apiDescribe('Database', persistence => { testDocs['b'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3124,6 +3117,7 @@ apiDescribe('Database', persistence => { testDocs['b'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3133,6 +3127,7 @@ apiDescribe('Database', persistence => { snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3142,6 +3137,7 @@ apiDescribe('Database', persistence => { snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3151,6 +3147,7 @@ apiDescribe('Database', persistence => { snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3173,9 +3170,6 @@ apiDescribe('Database', persistence => { settings, testDocs, async coll => { - // Populate the cache with all docs first - await getDocs(coll); - let filteredQuery = query( coll, where('key', '==', MaxKey.instance()) @@ -3186,23 +3180,24 @@ apiDescribe('Database', persistence => { testDocs['d'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) ); - // TODO(Mila/BSON): uncomment after the null inclusion bug - // filteredQuery = query(coll, where('key', '!=', MaxKey.instance())); - // snapshot = await getDocs(filteredQuery); - // expect(toDataArray(snapshot)).to.deep.equal([ - // testDocs['a'], - // testDocs['b'] - // ]); - // await assertSDKQueryResultsConsistentWithBackend( - // filteredQuery, - // testDocs, - // toIds(snapshot) - // ); + filteredQuery = query(coll, where('key', '!=', MaxKey.instance())); + snapshot = await getDocs(filteredQuery); + expect(toDataArray(snapshot)).to.deep.equal([ + testDocs['a'], + testDocs['b'] + ]); + await assertSDKQueryResultsConsistentWithBackend( + coll, + filteredQuery, + testDocs, + toIds(snapshot) + ); filteredQuery = query(coll, where('key', '>=', MaxKey.instance())); snapshot = await getDocs(filteredQuery); @@ -3211,6 +3206,7 @@ apiDescribe('Database', persistence => { testDocs['d'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3223,6 +3219,7 @@ apiDescribe('Database', persistence => { testDocs['d'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3232,6 +3229,7 @@ apiDescribe('Database', persistence => { snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3241,6 +3239,7 @@ apiDescribe('Database', persistence => { snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3250,6 +3249,7 @@ apiDescribe('Database', persistence => { snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3273,9 +3273,6 @@ apiDescribe('Database', persistence => { settings, testDocs, async coll => { - // Populate the cache with all docs first - await getDocs(coll); - let filteredQuery = query(coll, where('key', '==', null)); let snapshot = await getDocs(filteredQuery); expect(toDataArray(snapshot)).to.deep.equal([ @@ -3283,6 +3280,7 @@ apiDescribe('Database', persistence => { testDocs['c'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3296,6 +3294,7 @@ apiDescribe('Database', persistence => { testDocs['e'] ]); await assertSDKQueryResultsConsistentWithBackend( + coll, filteredQuery, testDocs, toIds(snapshot) @@ -3424,11 +3423,9 @@ apiDescribe('Database', persistence => { await setDoc(doc(coll, 'm'), { key: docRef }); testDocs['m'] = { key: docRef }; - // Populate the cache with all docs first - await getDocs(coll); - const orderedQuery = query(coll, orderBy('key', 'desc')); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, [ @@ -3487,11 +3484,9 @@ apiDescribe('Database', persistence => { settings, testDocs, async coll => { - // Populate the cache with all docs first - await getDocs(coll); - const orderedQuery = query(coll, orderBy('key')); await assertSDKQueryResultsConsistentWithBackend( + coll, orderedQuery, testDocs, [ diff --git a/packages/firestore/test/integration/util/helpers.ts b/packages/firestore/test/integration/util/helpers.ts index cdedf2a3e02..7cfe7d3a7a4 100644 --- a/packages/firestore/test/integration/util/helpers.ts +++ b/packages/firestore/test/integration/util/helpers.ts @@ -633,6 +633,7 @@ export async function checkOnlineAndOfflineResultsMatch( * @returns {Promise} A Promise that resolves when the assertions are complete. */ export async function assertSDKQueryResultsConsistentWithBackend( + collection: CollectionReference, query: Query, allData: { [key: string]: DocumentData }, expectedDocIds: string[] @@ -640,7 +641,7 @@ export async function assertSDKQueryResultsConsistentWithBackend( // Check the cache round trip first to make sure cache is properly populated, otherwise the // snapshot listener below will return partial results from previous // "assertSDKQueryResultsConsistentWithBackend" calls if it is called multiple times in one test - await checkOnlineAndOfflineResultsMatch(query, ...expectedDocIds); + await checkOnlineAndOfflineResultsMatch(collection, query, ...expectedDocIds); const eventAccumulator = new EventsAccumulator(); const unsubscribe = onSnapshot( diff --git a/packages/firestore/test/unit/model/values.test.ts b/packages/firestore/test/unit/model/values.test.ts index 048ae762a98..dce8f1e123c 100644 --- a/packages/firestore/test/unit/model/values.test.ts +++ b/packages/firestore/test/unit/model/values.test.ts @@ -252,11 +252,13 @@ describe('Values', () => { [refValue(dbId('p2', 'd1'), key('c1/doc1'))], // ObjectId - [wrap(new BsonObjectId('foo')), wrap(new BsonObjectId('foo'))], - // TODO(Mila/BSON): uncomment after string sort bug is fixed - // [wrap(new BsonObjectId('Ḟoo'))], // with latin capital letter f with dot above - // [wrap(new BsonObjectId('foo\u0301'))], // with combining acute accent - [wrap(new BsonObjectId('xyz'))], + [wrap(new BsonObjectId('507f191e810c19729de860ea'))], + [wrap(new BsonObjectId('507f191e810c19729de860eb'))], + // latin small letter e + combining acute accent + latin small letter b + [wrap(new BsonObjectId('e\u0301b'))], + [wrap(new BsonObjectId('æ'))], + // latin small letter e with acute accent + latin small letter a + [wrap(new BsonObjectId('\u00e9a'))], // geo points [wrap(new GeoPoint(-90, -180))],