diff --git a/.changeset/wild-otters-fly.md b/.changeset/wild-otters-fly.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/wild-otters-fly.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/firestore/lite/test/shim.ts b/packages/firestore/lite/test/shim.ts new file mode 100644 index 00000000000..96dbe2c8509 --- /dev/null +++ b/packages/firestore/lite/test/shim.ts @@ -0,0 +1,649 @@ +/** + * @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 legacy from '@firebase/firestore-types'; +import * as lite from '../'; + +import { + addDoc, + arrayRemove, + arrayUnion, + collection, + collectionGroup, + deleteDoc, + deleteField, + doc, + DocumentReference as DocumentReferenceLite, + FieldPath as FieldPathLite, + getDoc, + getQuery, + increment, + parent, + queryEqual, + refEqual, + runTransaction, + serverTimestamp, + setDoc, + snapshotEqual, + terminate, + updateDoc, + writeBatch, + initializeFirestore +} from '../../lite/index.node'; +import { UntypedFirestoreDataConverter } from '../../src/api/user_data_reader'; +import { isPlainObject } from '../../src/util/input_validation'; + +export { GeoPoint, Blob, Timestamp } from '../index.node'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +// This module defines a shim layer that implements the legacy API on top +// of the lite SDK. This shim is used to run integration tests against +// both SDK versions. + +const NOT_SUPPORTED_MSG = 'Not supported in Lite SDK'; + +export class FirebaseFirestore implements legacy.FirebaseFirestore { + constructor(private readonly _delegate: lite.FirebaseFirestore) {} + + app = this._delegate.app; + + settings(settings: legacy.Settings): void { + initializeFirestore(this.app, settings); + } + + enablePersistence(settings?: legacy.PersistenceSettings): Promise { + throw new Error(NOT_SUPPORTED_MSG); + } + + collection(collectionPath: string): CollectionReference { + return new CollectionReference(collection(this._delegate, collectionPath)); + } + + doc(documentPath: string): DocumentReference { + return new DocumentReference(doc(this._delegate, documentPath)); + } + + collectionGroup(collectionId: string): Query { + return new Query(collectionGroup(this._delegate, collectionId)); + } + + runTransaction( + updateFunction: (transaction: legacy.Transaction) => Promise + ): Promise { + return runTransaction(this._delegate, t => + updateFunction(new Transaction(t)) + ); + } + + batch(): legacy.WriteBatch { + return new WriteBatch(writeBatch(this._delegate)); + } + + clearPersistence(): Promise { + throw new Error(NOT_SUPPORTED_MSG); + } + + enableNetwork(): Promise { + throw new Error(NOT_SUPPORTED_MSG); + } + + disableNetwork(): Promise { + throw new Error(NOT_SUPPORTED_MSG); + } + + waitForPendingWrites(): Promise { + throw new Error(NOT_SUPPORTED_MSG); + } + + onSnapshotsInSync(_: any): () => void { + throw new Error(NOT_SUPPORTED_MSG); + } + + terminate(): Promise { + return terminate(this._delegate); + } + + INTERNAL = { + delete: () => terminate(this._delegate) + }; +} + +export class Transaction implements legacy.Transaction { + constructor(private readonly _delegate: lite.Transaction) {} + + get(documentRef: DocumentReference): Promise> { + return this._delegate + .get(documentRef._delegate) + .then(result => new DocumentSnapshot(result)); + } + + set( + documentRef: DocumentReference, + data: T, + options?: legacy.SetOptions + ): Transaction { + if (options) { + this._delegate.set(documentRef._delegate, unwrap(data), options); + } else { + this._delegate.set(documentRef._delegate, unwrap(data)); + } + return this; + } + + update( + documentRef: DocumentReference, + data: legacy.UpdateData + ): Transaction; + update( + documentRef: DocumentReference, + field: string | FieldPath, + value: any, + ...moreFieldsAndValues: any[] + ): Transaction; + update( + documentRef: DocumentReference, + dataOrField: any, + value?: any, + ...moreFieldsAndValues: any[] + ): Transaction { + if (arguments.length === 2) { + this._delegate.update(documentRef._delegate, unwrap(dataOrField)); + } else { + this._delegate.update( + documentRef._delegate, + unwrap(dataOrField), + unwrap(value), + ...unwrap(moreFieldsAndValues) + ); + } + + return this; + } + + delete(documentRef: DocumentReference): Transaction { + this._delegate.delete(documentRef._delegate); + return this; + } +} + +export class WriteBatch implements legacy.WriteBatch { + constructor(private readonly _delegate: lite.WriteBatch) {} + + set( + documentRef: DocumentReference, + data: T, + options?: legacy.SetOptions + ): WriteBatch { + if (options) { + this._delegate.set(documentRef._delegate, unwrap(data), options); + } else { + this._delegate.set(documentRef._delegate, unwrap(data)); + } + return this; + } + + update( + documentRef: DocumentReference, + data: legacy.UpdateData + ): WriteBatch; + update( + documentRef: DocumentReference, + field: string | FieldPath, + value: any, + ...moreFieldsAndValues: any[] + ): WriteBatch; + update( + documentRef: DocumentReference, + dataOrField: any, + value?: any, + ...moreFieldsAndValues: any[] + ): WriteBatch { + if (arguments.length === 2) { + this._delegate.update(documentRef._delegate, unwrap(dataOrField)); + } else { + this._delegate.update( + documentRef._delegate, + unwrap(dataOrField), + unwrap(value), + ...unwrap(moreFieldsAndValues) + ); + } + + return this; + } + + delete(documentRef: DocumentReference): WriteBatch { + this._delegate.delete(documentRef._delegate); + return this; + } + + commit(): Promise { + return this._delegate.commit(); + } +} + +export class DocumentReference + implements legacy.DocumentReference { + constructor(readonly _delegate: lite.DocumentReference) {} + + readonly id = this._delegate.id; + readonly firestore = new FirebaseFirestore(this._delegate.firestore); + readonly path = this._delegate.path; + + get parent(): legacy.CollectionReference { + return new CollectionReference(parent(this._delegate)); + } + + collection( + collectionPath: string + ): legacy.CollectionReference { + return new CollectionReference(collection(this._delegate, collectionPath)); + } + + isEqual(other: DocumentReference): boolean { + return refEqual(this._delegate, other._delegate); + } + + set(data: Partial, options?: legacy.SetOptions): Promise { + if (options) { + return setDoc(this._delegate, unwrap(data), options); + } else { + return setDoc(this._delegate, unwrap(data)); + } + } + + update(data: legacy.UpdateData): Promise; + update( + field: string | FieldPath, + value: any, + ...moreFieldsAndValues: any[] + ): Promise; + update( + dataOrField: any, + value?: any, + ...moreFieldsAndValues: any[] + ): Promise { + if (arguments.length === 1) { + return updateDoc(this._delegate, unwrap(dataOrField)); + } else { + return updateDoc( + this._delegate, + unwrap(dataOrField), + unwrap(value), + ...unwrap(moreFieldsAndValues) + ); + } + } + + delete(): Promise { + return deleteDoc(this._delegate); + } + + get(options?: legacy.GetOptions): Promise> { + if (options) { + throw new Error(NOT_SUPPORTED_MSG); + } + + return getDoc(this._delegate).then(result => new DocumentSnapshot(result)); + } + + onSnapshot(...args: any): () => void { + throw new Error(NOT_SUPPORTED_MSG); + } + + withConverter( + converter: legacy.FirestoreDataConverter + ): DocumentReference { + return new DocumentReference( + this._delegate.withConverter( + converter as UntypedFirestoreDataConverter + ) + ); + } +} + +export class DocumentSnapshot + implements legacy.DocumentSnapshot { + constructor(readonly _delegate: lite.DocumentSnapshot) {} + + readonly ref = new DocumentReference(this._delegate.ref); + readonly id = this._delegate.id; + + get metadata(): legacy.SnapshotMetadata { + throw new Error(NOT_SUPPORTED_MSG); + } + + get exists(): boolean { + return this._delegate.exists(); + } + + data(options?: legacy.SnapshotOptions): T | undefined { + if (options) { + throw new Error(NOT_SUPPORTED_MSG); + } + return wrap(this._delegate.data()); + } + + get(fieldPath: string | FieldPath, options?: legacy.SnapshotOptions): any { + if (options) { + throw new Error(NOT_SUPPORTED_MSG); + } + return wrap(this._delegate.get(unwrap(fieldPath))); + } + + isEqual(other: DocumentSnapshot): boolean { + return snapshotEqual(this._delegate, other._delegate); + } +} + +export class QueryDocumentSnapshot + extends DocumentSnapshot + implements legacy.QueryDocumentSnapshot { + constructor(readonly _delegate: lite.QueryDocumentSnapshot) { + super(_delegate); + } + + data(options?: legacy.SnapshotOptions): T { + if (options) { + throw new Error(NOT_SUPPORTED_MSG); + } + return this._delegate.data(); + } +} + +export class Query implements legacy.Query { + constructor(readonly _delegate: lite.Query) {} + + readonly firestore = new FirebaseFirestore(this._delegate.firestore); + + where( + fieldPath: string | FieldPath, + opStr: legacy.WhereFilterOp, + value: any + ): Query { + return new Query( + this._delegate.where(unwrap(fieldPath), opStr, unwrap(value)) + ); + } + + orderBy( + fieldPath: string | FieldPath, + directionStr?: legacy.OrderByDirection + ): Query { + return new Query( + this._delegate.orderBy(unwrap(fieldPath), directionStr) + ); + } + + limit(limit: number): Query { + return new Query(this._delegate.limit(limit)); + } + + limitToLast(limit: number): Query { + return new Query(this._delegate.limitToLast(limit)); + } + + startAt(...args: any[]): Query { + if (args[0] instanceof DocumentSnapshot) { + return new Query(this._delegate.startAt(args[0]._delegate)); + } else { + return new Query(this._delegate.startAt(...unwrap(args))); + } + } + + startAfter(...args: any[]): Query { + if (args[0] instanceof DocumentSnapshot) { + return new Query(this._delegate.startAfter(args[0]._delegate)); + } else { + return new Query(this._delegate.startAfter(...unwrap(args))); + } + } + + endBefore(...args: any[]): Query { + if (args[0] instanceof DocumentSnapshot) { + return new Query(this._delegate.endBefore(args[0]._delegate)); + } else { + return new Query(this._delegate.endBefore(...unwrap(args))); + } + } + + endAt(...args: any[]): Query { + if (args[0] instanceof DocumentSnapshot) { + return new Query(this._delegate.endAt(args[0]._delegate)); + } else { + return new Query(this._delegate.endAt(...unwrap(args))); + } + } + + isEqual(other: legacy.Query): boolean { + return queryEqual(this._delegate, (other as Query)._delegate); + } + + get(options?: legacy.GetOptions): Promise> { + if (options) { + throw new Error(NOT_SUPPORTED_MSG); + } + return getQuery(this._delegate).then(result => new QuerySnapshot(result)); + } + + onSnapshot(...args: any): () => void { + throw new Error(NOT_SUPPORTED_MSG); + } + + withConverter(converter: legacy.FirestoreDataConverter): Query { + return new Query( + this._delegate.withConverter( + converter as UntypedFirestoreDataConverter + ) + ); + } +} + +export class QuerySnapshot + implements legacy.QuerySnapshot { + constructor(readonly _delegate: lite.QuerySnapshot) {} + + readonly query = new Query(this._delegate.query); + readonly size = this._delegate.size; + readonly empty = this._delegate.empty; + + get metadata(): legacy.SnapshotMetadata { + throw new Error(NOT_SUPPORTED_MSG); + } + + get docs(): Array> { + return this._delegate.docs.map(doc => new QueryDocumentSnapshot(doc)); + } + + docChanges(options?: legacy.SnapshotListenOptions): Array> { + throw new Error(NOT_SUPPORTED_MSG); + } + + forEach( + callback: (result: QueryDocumentSnapshot) => void, + thisArg?: any + ): void { + this._delegate.forEach(snapshot => { + callback.call(thisArg, new QueryDocumentSnapshot(snapshot)); + }); + } + + isEqual(other: QuerySnapshot): boolean { + return snapshotEqual(this._delegate, other._delegate); + } +} + +export class DocumentChange + implements legacy.DocumentChange { + constructor() { + throw new Error(NOT_SUPPORTED_MSG); + } + get type(): legacy.DocumentChangeType { + throw new Error(NOT_SUPPORTED_MSG); + } + get doc(): legacy.QueryDocumentSnapshot { + throw new Error(NOT_SUPPORTED_MSG); + } + get oldIndex(): number { + throw new Error(NOT_SUPPORTED_MSG); + } + get newIndex(): number { + throw new Error(NOT_SUPPORTED_MSG); + } +} + +export class CollectionReference extends Query + implements legacy.CollectionReference { + constructor(readonly _delegate: lite.CollectionReference) { + super(_delegate); + } + + readonly id = this._delegate.id; + readonly path = this._delegate.path; + + get parent(): DocumentReference | null { + const docRef = parent(this._delegate); + return docRef ? new DocumentReference(docRef) : null; + } + + doc(documentPath?: string): DocumentReference { + if (documentPath !== undefined) { + return new DocumentReference(doc(this._delegate, documentPath)); + } else { + return new DocumentReference(doc(this._delegate)); + } + } + + add(data: T): Promise> { + return addDoc(this._delegate, unwrap(data)).then( + docRef => new DocumentReference(docRef) + ); + } + + isEqual(other: CollectionReference): boolean { + return refEqual(this._delegate, other._delegate); + } + + withConverter( + converter: legacy.FirestoreDataConverter + ): CollectionReference { + return new CollectionReference( + this._delegate.withConverter( + converter as UntypedFirestoreDataConverter + ) + ); + } +} + +export class FieldValue implements legacy.FieldValue { + constructor(readonly _delegate: lite.FieldValue) {} + + static serverTimestamp(): FieldValue { + return new FieldValue(serverTimestamp()); + } + + static delete(): FieldValue { + return new FieldValue(deleteField()); + } + + static arrayUnion(...elements: any[]): FieldValue { + return new FieldValue(arrayUnion(...elements)); + } + + static arrayRemove(...elements: any[]): FieldValue { + return new FieldValue(arrayRemove(...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[]; + + constructor(...fieldNames: string[]) { + this.fieldNames = fieldNames; + } + + get _delegate(): FieldPathLite { + return new FieldPathLite(...this.fieldNames); + } + + static documentId(): FieldPath { + return new FieldPath('__name__'); + } + + isEqual(other: FieldPath): boolean { + throw new Error('isEqual() is not supported in shim'); + } +} + +/** + * Takes document data that uses the lite API types and replaces them with the + * API types defined in this shim. + */ +function wrap(value: any): any { + if (Array.isArray(value)) { + return value.map(v => wrap(v)); + } else if (value instanceof FieldPathLite) { + return new FieldPath(...value._internalPath.toArray()); + } else if (value instanceof DocumentReferenceLite) { + return new DocumentReference(value); + } else if (isPlainObject(value)) { + const obj: any = {}; + for (const key in value) { + if (value.hasOwnProperty(key)) { + obj[key] = wrap(value[key]); + } + } + return obj; + } else { + return value; + } +} + +/** + * Takes user data that uses API types from this shim and replaces them + * with the the lite API types. + */ +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 DocumentReference) { + return value._delegate; + } else if (isPlainObject(value)) { + const obj: any = {}; + for (const key in value) { + if (value.hasOwnProperty(key)) { + obj[key] = unwrap(value[key]); + } + } + return obj; + } else { + return value; + } +}