diff --git a/packages/firestore/exp/test/shim.ts b/packages/firestore/exp/test/shim.ts index 72fd0f0f80d..22777f3253a 100644 --- a/packages/firestore/exp/test/shim.ts +++ b/packages/firestore/exp/test/shim.ts @@ -23,13 +23,10 @@ import * as exp from '../index'; import { addDoc, - arrayRemove, - arrayUnion, clearIndexedDbPersistence, collection, collectionGroup, deleteDoc, - deleteField, disableNetwork, doc, DocumentReference as DocumentReferenceExp, @@ -43,7 +40,6 @@ import { getDocs, getDocsFromCache, getDocsFromServer, - increment, initializeFirestore, onSnapshot, onSnapshotsInSync, @@ -51,7 +47,6 @@ import { queryEqual, refEqual, runTransaction, - serverTimestamp, setDoc, snapshotEqual, terminate, @@ -71,8 +66,10 @@ import { import { UntypedFirestoreDataConverter } from '../../src/api/user_data_reader'; import { isPartialObserver, PartialObserver } from '../../src/api/observer'; import { isPlainObject } from '../../src/util/input_validation'; +import { Compat } from '../../src/compat/compat'; export { GeoPoint, Timestamp } from '../index'; +export { FieldValue } from '../../src/compat/field_value'; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -80,9 +77,9 @@ export { GeoPoint, Timestamp } from '../index'; // of the experimental SDK. This shim is used to run integration tests against // both SDK versions. -export class FirebaseApp implements FirebaseAppLegacy { - constructor(readonly _delegate: FirebaseAppExp) {} - +export class FirebaseApp + extends Compat + implements FirebaseAppLegacy { name = this._delegate.name; options = this._delegate.options; automaticDataCollectionEnabled = this._delegate @@ -93,9 +90,9 @@ export class FirebaseApp implements FirebaseAppLegacy { } } -export class FirebaseFirestore implements legacy.FirebaseFirestore { - constructor(private readonly _delegate: exp.FirebaseFirestore) {} - +export class FirebaseFirestore + extends Compat + implements legacy.FirebaseFirestore { app = new FirebaseApp(this._delegate.app); settings(settings: legacy.Settings): void { @@ -170,11 +167,15 @@ export class FirebaseFirestore implements legacy.FirebaseFirestore { }; } -export class Transaction implements legacy.Transaction { +export class Transaction + extends Compat + implements legacy.Transaction { constructor( private readonly _firestore: FirebaseFirestore, - private readonly _delegate: exp.Transaction - ) {} + private readonly delegate: exp.Transaction + ) { + super(delegate); + } get(documentRef: DocumentReference): Promise> { return this._delegate @@ -231,9 +232,9 @@ export class Transaction implements legacy.Transaction { } } -export class WriteBatch implements legacy.WriteBatch { - constructor(private readonly _delegate: exp.WriteBatch) {} - +export class WriteBatch + extends Compat + implements legacy.WriteBatch { set( documentRef: DocumentReference, data: T, @@ -288,11 +289,14 @@ export class WriteBatch implements legacy.WriteBatch { } export class DocumentReference + extends Compat> implements legacy.DocumentReference { constructor( readonly firestore: FirebaseFirestore, - readonly _delegate: exp.DocumentReference - ) {} + delegate: exp.DocumentReference + ) { + super(delegate); + } readonly id = this._delegate.id; readonly path = this._delegate.path; @@ -407,11 +411,14 @@ export class DocumentReference } export class DocumentSnapshot + extends Compat> implements legacy.DocumentSnapshot { constructor( private readonly _firestore: FirebaseFirestore, - readonly _delegate: exp.DocumentSnapshot - ) {} + delegate: exp.DocumentSnapshot + ) { + super(delegate); + } readonly ref = new DocumentReference(this._firestore, this._delegate.ref); readonly id = this._delegate.id; @@ -449,11 +456,12 @@ export class QueryDocumentSnapshot } } -export class Query implements legacy.Query { - constructor( - readonly firestore: FirebaseFirestore, - readonly _delegate: exp.Query - ) {} +export class Query + extends Compat> + implements legacy.Query { + constructor(readonly firestore: FirebaseFirestore, delegate: exp.Query) { + super(delegate); + } where( fieldPath: string | FieldPath, @@ -680,34 +688,6 @@ export class CollectionReference } } -export class FieldValue implements legacy.FieldValue { - constructor(readonly _delegate: exp.FieldValue) {} - - static serverTimestamp(): FieldValue { - return new FieldValue(serverTimestamp()); - } - - static delete(): FieldValue { - return new FieldValue(deleteField()); - } - - static arrayUnion(...elements: any[]): FieldValue { - return new FieldValue(arrayUnion(...unwrap(elements))); - } - - static arrayRemove(...elements: any[]): FieldValue { - return new FieldValue(arrayRemove(...unwrap(elements))); - } - - static increment(n: number): FieldValue { - return new FieldValue(increment(n)); - } - - isEqual(other: FieldValue): boolean { - return this._delegate.isEqual(other._delegate); - } -} - export class FieldPath implements legacy.FieldPath { private readonly fieldNames: string[]; @@ -728,9 +708,7 @@ export class FieldPath implements legacy.FieldPath { } } -export class Blob implements legacy.Blob { - constructor(readonly _delegate: BytesExp) {} - +export class Blob extends Compat implements legacy.Blob { static fromBase64String(base64: string): Blob { return new Blob(BytesExp.fromBase64String(base64)); } @@ -790,17 +768,7 @@ function wrap(value: any): any { function unwrap(value: any): any { if (Array.isArray(value)) { return value.map(v => unwrap(v)); - } else if (value instanceof FieldPath) { - return value._delegate; - } else if (value instanceof FieldValue) { - return value._delegate; - } else if (value instanceof Blob) { - return value._delegate; - } else if (value instanceof DocumentReference) { - return value._delegate; - } else if (value instanceof DocumentSnapshot) { - return value._delegate; - } else if (value instanceof QueryDocumentSnapshot) { + } else if (value instanceof Compat) { return value._delegate; } else if (isPlainObject(value)) { const obj: any = {}; diff --git a/packages/firestore/src/api/field_value.ts b/packages/firestore/src/api/field_value.ts index 29cf62f5b4f..884f299e3da 100644 --- a/packages/firestore/src/api/field_value.ts +++ b/packages/firestore/src/api/field_value.ts @@ -15,13 +15,6 @@ * limitations under the License. */ -import { FieldValue as PublicFieldValue } from '@firebase/firestore-types'; -import { - validateArgType, - validateAtLeastNumberOfArgs, - validateExactNumberOfArgs, - validateNoArgs -} from '../util/input_validation'; import { FieldTransform } from '../model/mutation'; import { ArrayRemoveTransformOperation, @@ -81,7 +74,7 @@ export class DeleteFieldValueImpl extends _SerializableFieldValue { return null; } - isEqual(other: FieldValue): boolean { + isEqual(other: DeleteFieldValueImpl): boolean { return other instanceof DeleteFieldValueImpl; } } @@ -129,7 +122,7 @@ export class ServerTimestampFieldValueImpl extends _SerializableFieldValue { return new FieldTransform(context.path!, new ServerTimestampTransform()); } - isEqual(other: FieldValue): boolean { + isEqual(other: ServerTimestampFieldValueImpl): boolean { return other instanceof ServerTimestampFieldValueImpl; } } @@ -155,7 +148,7 @@ export class ArrayUnionFieldValueImpl extends _SerializableFieldValue { return new FieldTransform(context.path!, arrayUnion); } - isEqual(other: FieldValue): boolean { + isEqual(other: ArrayUnionFieldValueImpl): boolean { // TODO(mrschmidt): Implement isEquals return this === other; } @@ -179,7 +172,7 @@ export class ArrayRemoveFieldValueImpl extends _SerializableFieldValue { return new FieldTransform(context.path!, arrayUnion); } - isEqual(other: FieldValue): boolean { + isEqual(other: ArrayRemoveFieldValueImpl): boolean { // TODO(mrschmidt): Implement isEquals return this === other; } @@ -198,86 +191,8 @@ export class NumericIncrementFieldValueImpl extends _SerializableFieldValue { return new FieldTransform(context.path!, numericIncrement); } - isEqual(other: FieldValue): boolean { + isEqual(other: NumericIncrementFieldValueImpl): boolean { // TODO(mrschmidt): Implement isEquals return this === other; } } - -/** The public FieldValue class of the lite API. */ -export abstract class FieldValue - extends _SerializableFieldValue - implements PublicFieldValue { - protected constructor() { - super(); - } - - static delete(): PublicFieldValue { - validateNoArgs('FieldValue.delete', arguments); - return new FieldValueDelegate( - new DeleteFieldValueImpl('FieldValue.delete') - ); - } - - static serverTimestamp(): PublicFieldValue { - validateNoArgs('FieldValue.serverTimestamp', arguments); - return new FieldValueDelegate( - new ServerTimestampFieldValueImpl('FieldValue.serverTimestamp') - ); - } - - static arrayUnion(...elements: unknown[]): PublicFieldValue { - validateAtLeastNumberOfArgs('FieldValue.arrayUnion', arguments, 1); - // NOTE: We don't actually parse the data until it's used in set() or - // update() since we'd need the Firestore instance to do this. - return new FieldValueDelegate( - new ArrayUnionFieldValueImpl('FieldValue.arrayUnion', elements) - ); - } - - static arrayRemove(...elements: unknown[]): PublicFieldValue { - validateAtLeastNumberOfArgs('FieldValue.arrayRemove', arguments, 1); - // NOTE: We don't actually parse the data until it's used in set() or - // update() since we'd need the Firestore instance to do this. - return new FieldValueDelegate( - new ArrayRemoveFieldValueImpl('FieldValue.arrayRemove', elements) - ); - } - - static increment(n: number): PublicFieldValue { - validateArgType('FieldValue.increment', 'number', 1, n); - validateExactNumberOfArgs('FieldValue.increment', arguments, 1); - return new FieldValueDelegate( - new NumericIncrementFieldValueImpl('FieldValue.increment', n) - ); - } -} - -/** - * A delegate class that allows the FieldValue implementations returned by - * deleteField(), serverTimestamp(), arrayUnion(), arrayRemove() and - * increment() to be an instance of the legacy FieldValue class declared above. - * - * We don't directly subclass `FieldValue` in the various field value - * implementations as the base FieldValue class differs between the lite, full - * and legacy SDK. - */ -class FieldValueDelegate extends FieldValue implements PublicFieldValue { - readonly _methodName: string; - - constructor(readonly _delegate: _SerializableFieldValue) { - super(); - this._methodName = _delegate._methodName; - } - - _toFieldTransform(context: ParseContext): FieldTransform | null { - return this._delegate._toFieldTransform(context); - } - - isEqual(other: PublicFieldValue): boolean { - if (!(other instanceof FieldValueDelegate)) { - return false; - } - return this._delegate.isEqual(other._delegate); - } -} diff --git a/packages/firestore/src/api/user_data_reader.ts b/packages/firestore/src/api/user_data_reader.ts index 175a50f0107..56d273f4a88 100644 --- a/packages/firestore/src/api/user_data_reader.ts +++ b/packages/firestore/src/api/user_data_reader.ts @@ -51,6 +51,7 @@ import { DeleteFieldValueImpl, _SerializableFieldValue } from './field_value'; import { GeoPoint } from './geo_point'; import { newSerializer } from '../platform/serializer'; import { Bytes } from '../../lite/src/api/bytes'; +import { Compat } from '../compat/compat'; const RESERVED_FIELD_REGEX = /^__.*__$/; @@ -566,6 +567,12 @@ export function parseData( input: unknown, context: ParseContext ): ProtoValue | null { + // Unwrap the API type from the Compat SDK. This will return the API type + // from firestore-exp. + if (input instanceof Compat) { + input = input._delegate; + } + if (looksLikeJsonObject(input)) { validatePlainObject('Unsupported field value:', context, input); return parseObject(input, context); diff --git a/packages/firestore/src/compat/compat.ts b/packages/firestore/src/compat/compat.ts new file mode 100644 index 00000000000..56a6abf9d77 --- /dev/null +++ b/packages/firestore/src/compat/compat.ts @@ -0,0 +1,26 @@ +/** + * @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. + */ + +/** + * A class implemented by all API types of the legacy Firestore API which + * contains a reference to the API type in the firestore-exp API. All internal + * code unwraps these references, which allows us to only use firestore-exp + * types in the SDK. + */ +export abstract class Compat { + protected constructor(readonly _delegate: T) {} +} diff --git a/packages/firestore/src/compat/field_value.ts b/packages/firestore/src/compat/field_value.ts new file mode 100644 index 00000000000..fd94d688a0d --- /dev/null +++ b/packages/firestore/src/compat/field_value.ts @@ -0,0 +1,67 @@ +/** + * @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. + */ + +import * as exp from '../../exp/index'; +import { + deleteField, + serverTimestamp, + arrayUnion, + arrayRemove, + increment +} from '../../exp/index'; +import * as legacy from '@firebase/firestore-types'; +import { + validateArgType, + validateAtLeastNumberOfArgs, + validateExactNumberOfArgs, + validateNoArgs +} from '../util/input_validation'; +import { Compat } from './compat'; + +export class FieldValue + extends Compat + implements legacy.FieldValue { + static serverTimestamp(): FieldValue { + validateNoArgs('FieldValue.serverTimestamp', arguments); + return new FieldValue(serverTimestamp()); + } + + static delete(): FieldValue { + validateNoArgs('FieldValue.delete', arguments); + return new FieldValue(deleteField()); + } + + static arrayUnion(...elements: unknown[]): FieldValue { + validateAtLeastNumberOfArgs('FieldValue.arrayUnion', arguments, 1); + return new FieldValue(arrayUnion(...elements)); + } + + static arrayRemove(...elements: unknown[]): FieldValue { + validateAtLeastNumberOfArgs('FieldValue.arrayRemove', arguments, 1); + return new FieldValue(arrayRemove(...elements)); + } + + static increment(n: number): FieldValue { + validateArgType('FieldValue.increment', 'number', 1, n); + validateExactNumberOfArgs('FieldValue.increment', arguments, 1); + return new FieldValue(increment(n)); + } + + isEqual(other: FieldValue): boolean { + return this._delegate.isEqual(other._delegate); + } +} diff --git a/packages/firestore/src/config.ts b/packages/firestore/src/config.ts index c73ce45a165..42d5d48128a 100644 --- a/packages/firestore/src/config.ts +++ b/packages/firestore/src/config.ts @@ -35,7 +35,7 @@ import { Blob } from './api/blob'; import { FieldPath } from './api/field_path'; import { GeoPoint } from './api/geo_point'; import { Timestamp } from './api/timestamp'; -import { FieldValue } from './api/field_value'; +import { FieldValue } from './compat/field_value'; const firestoreNamespace = { Firestore, diff --git a/packages/firestore/test/integration/api/array_transforms.test.ts b/packages/firestore/test/integration/api/array_transforms.test.ts index 0d2718d844e..3e9951d0e1d 100644 --- a/packages/firestore/test/integration/api/array_transforms.test.ts +++ b/packages/firestore/test/integration/api/array_transforms.test.ts @@ -21,6 +21,9 @@ import { expect } from 'chai'; import { EventsAccumulator } from '../util/events_accumulator'; import * as firebaseExport from '../util/firebase_export'; import { apiDescribe, withTestDb, withTestDoc } from '../util/helpers'; +import { addEqualityMatcher } from '../../util/equality_matcher'; + +addEqualityMatcher(); const FieldValue = firebaseExport.FieldValue; @@ -144,6 +147,13 @@ apiDescribe('Array Transforms:', (persistence: boolean) => { }); }); + it('arrayUnion() supports DocumentReference', async () => { + await withTestSetup(async () => { + await docRef.set({ array: FieldValue.arrayUnion(docRef) }); + await expectLocalAndRemoteEvent({ array: [docRef] }); + }); + }); + /** * Unlike the withTestSetup() tests above, these tests intentionally avoid * having any ongoing listeners so that we can test what gets stored in the