/** * @flow * DocumentReference representation wrapper */ import CollectionReference from './CollectionReference'; import DocumentSnapshot from './DocumentSnapshot'; import { buildNativeMap } from './utils/serialize'; import { getAppEventName, SharedEventEmitter } from '../../utils/events'; import { getLogger } from '../../utils/log'; import { firestoreAutoId, isFunction, isObject, isString } from '../../utils'; import { getNativeModule } from '../../utils/native'; import type Firestore from './'; import type { FirestoreNativeDocumentSnapshot, FirestoreWriteOptions } from '../../types'; import type Path from './Path'; type DocumentListenOptions = { includeMetadataChanges: boolean, } type ObserverOnError = (Object) => void; type ObserverOnNext = (DocumentSnapshot) => void; type Observer = { error?: ObserverOnError, next: ObserverOnNext, } /** * @class DocumentReference */ export default class DocumentReference { _documentPath: Path; _firestore: Firestore; constructor(firestore: Firestore, documentPath: Path) { this._documentPath = documentPath; this._firestore = firestore; } get firestore(): Firestore { return this._firestore; } get id(): string | null { return this._documentPath.id; } get parent(): CollectionReference { const parentPath = this._documentPath.parent(); if (!parentPath) { throw new Error('Invalid document path'); } return new CollectionReference(this._firestore, parentPath); } get path(): string { return this._documentPath.relativeName; } collection(collectionPath: string): CollectionReference { const path = this._documentPath.child(collectionPath); if (!path.isCollection) { throw new Error('Argument "collectionPath" must point to a collection.'); } return new CollectionReference(this._firestore, path); } delete(): Promise { return getNativeModule(this._firestore) .documentDelete(this.path); } get(): Promise { return getNativeModule(this._firestore) .documentGet(this.path) .then(result => new DocumentSnapshot(this._firestore, result)); } onSnapshot( optionsOrObserverOrOnNext: DocumentListenOptions | Observer | ObserverOnNext, observerOrOnNextOrOnError?: Observer | ObserverOnNext | ObserverOnError, onError?: ObserverOnError, ) { let observer: Observer; let docListenOptions = {}; // Called with: onNext, ?onError if (isFunction(optionsOrObserverOrOnNext)) { if (observerOrOnNextOrOnError && !isFunction(observerOrOnNextOrOnError)) { throw new Error('DocumentReference.onSnapshot failed: Second argument must be a valid function.'); } // $FlowBug: Not coping with the overloaded method signature observer = { next: optionsOrObserverOrOnNext, error: observerOrOnNextOrOnError, }; } else if (optionsOrObserverOrOnNext && isObject(optionsOrObserverOrOnNext)) { // Called with: Observer if (optionsOrObserverOrOnNext.next) { if (isFunction(optionsOrObserverOrOnNext.next)) { if (optionsOrObserverOrOnNext.error && !isFunction(optionsOrObserverOrOnNext.error)) { throw new Error('DocumentReference.onSnapshot failed: Observer.error must be a valid function.'); } // $FlowBug: Not coping with the overloaded method signature observer = { next: optionsOrObserverOrOnNext.next, error: optionsOrObserverOrOnNext.error, }; } else { throw new Error('DocumentReference.onSnapshot failed: Observer.next must be a valid function.'); } } else if (optionsOrObserverOrOnNext.includeMetadataChanges) { docListenOptions = optionsOrObserverOrOnNext; // Called with: Options, onNext, ?onError if (isFunction(observerOrOnNextOrOnError)) { if (onError && !isFunction(onError)) { throw new Error('DocumentReference.onSnapshot failed: Third argument must be a valid function.'); } // $FlowBug: Not coping with the overloaded method signature observer = { next: observerOrOnNextOrOnError, error: onError, }; // Called with Options, Observer } else if (observerOrOnNextOrOnError && isObject(observerOrOnNextOrOnError) && observerOrOnNextOrOnError.next) { if (isFunction(observerOrOnNextOrOnError.next)) { if (observerOrOnNextOrOnError.error && !isFunction(observerOrOnNextOrOnError.error)) { throw new Error('DocumentReference.onSnapshot failed: Observer.error must be a valid function.'); } observer = { next: observerOrOnNextOrOnError.next, error: observerOrOnNextOrOnError.error, }; } else { throw new Error('DocumentReference.onSnapshot failed: Observer.next must be a valid function.'); } } else { throw new Error('DocumentReference.onSnapshot failed: Second argument must be a function or observer.'); } } else { throw new Error('DocumentReference.onSnapshot failed: First argument must be a function, observer or options.'); } } else { throw new Error('DocumentReference.onSnapshot failed: Called with invalid arguments.'); } const listenerId = firestoreAutoId(); const listener = (nativeDocumentSnapshot: FirestoreNativeDocumentSnapshot) => { const documentSnapshot = new DocumentSnapshot(this.firestore, nativeDocumentSnapshot); observer.next(documentSnapshot); }; // Listen to snapshot events SharedEventEmitter.addListener( getAppEventName(this._firestore, `onDocumentSnapshot:${listenerId}`), listener, ); // Listen for snapshot error events if (observer.error) { SharedEventEmitter.addListener( getAppEventName(this._firestore, `onDocumentSnapshotError:${listenerId}`), observer.error, ); } // Add the native listener getNativeModule(this._firestore) .documentOnSnapshot(this.path, listenerId, docListenOptions); // Return an unsubscribe method return this._offDocumentSnapshot.bind(this, listenerId, listener); } set(data: Object, writeOptions?: FirestoreWriteOptions): Promise { const nativeData = buildNativeMap(data); return getNativeModule(this._firestore) .documentSet(this.path, nativeData, writeOptions); } update(...args: any[]): Promise { let data = {}; if (args.length === 1) { if (!isObject(args[0])) { throw new Error('DocumentReference.update failed: If using a single argument, it must be an object.'); } data = args[0]; } else if (args.length % 2 === 1) { throw new Error('DocumentReference.update failed: Must have either a single object argument, or equal numbers of key/value pairs.'); } else { for (let i = 0; i < args.length; i += 2) { const key = args[i]; const value = args[i + 1]; if (!isString(key)) { throw new Error(`DocumentReference.update failed: Argument at index ${i} must be a string`); } data[key] = value; } } const nativeData = buildNativeMap(data); return getNativeModule(this._firestore) .documentUpdate(this.path, nativeData); } /** * INTERNALS */ /** * Remove document snapshot listener * @param listener */ _offDocumentSnapshot(listenerId: string, listener: Function) { getLogger(this._firestore).info('Removing onDocumentSnapshot listener'); SharedEventEmitter.removeListener(getAppEventName(this._firestore, `onDocumentSnapshot:${listenerId}`), listener); SharedEventEmitter.removeListener(getAppEventName(this._firestore, `onDocumentSnapshotError:${listenerId}`), listener); getNativeModule(this._firestore) .documentOffSnapshot(this.path, listenerId); } }