/* * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this library 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 { isString } from '@react-native-firebase/app/dist/module/common'; import DocumentReference, { provideDocumentSnapshotClass } from './FirestoreDocumentReference'; import { FieldPath, fromDotSeparatedString } from './FieldPath'; import FirestorePath from './FirestorePath'; import SnapshotMetadata from './FirestoreSnapshotMetadata'; import type { SnapshotOptions } from './types/firestore'; import { extractFieldPathData } from './utils'; import { parseNativeMap } from './utils/serialize'; import type { FirestoreInternal, ConverterWithFromFirestoreInternal, DocumentFieldValueInternal, } from './types/internal'; import type { DocumentData, DocumentSnapshot as DocumentSnapshotDeclare, FirestoreDataConverter, QueryDocumentSnapshot, } from './types/firestore'; export interface DocumentSnapshotNativeData { path: string; data?: unknown; dataEstimate?: unknown; dataPrevious?: unknown; dataNone?: unknown; metadata?: [boolean, boolean]; exists?: boolean; } export default class DocumentSnapshot< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, > { _firestore: FirestoreInternal; _nativeData: DocumentSnapshotNativeData; _data: Record | undefined; _dataEstimate: Record | undefined; _dataPrevious: Record | undefined; _dataNone: Record | undefined; _metadata: SnapshotMetadata; _ref: DocumentReference; _exists: boolean; _converter: FirestoreDataConverter | null; constructor( firestore: FirestoreInternal, nativeData: DocumentSnapshotNativeData, converter: FirestoreDataConverter | null, ) { this._firestore = firestore; this._nativeData = nativeData; this._converter = converter; this._data = parseNativeMap(firestore, nativeData.data as Record | undefined); // The native side only sends `dataEstimate`/`dataPrevious`/`dataNone` when the // document has pending writes (these variants are otherwise identical to `data`). // Avoid the extra parse work entirely for the common settled-document case rather // than relying on `parseNativeMap`'s own `undefined` short-circuit. this._dataEstimate = nativeData.dataEstimate === undefined ? undefined : parseNativeMap(firestore, nativeData.dataEstimate as Record); this._dataPrevious = nativeData.dataPrevious === undefined ? undefined : parseNativeMap(firestore, nativeData.dataPrevious as Record); this._dataNone = nativeData.dataNone === undefined ? undefined : parseNativeMap(firestore, nativeData.dataNone as Record); this._metadata = new SnapshotMetadata(nativeData.metadata ?? [false, false]); this._ref = new DocumentReference( firestore, FirestorePath.fromName(nativeData.path), this._converter, ); this._exists = nativeData.exists ?? false; } _dataForOptions(options?: SnapshotOptions): Record | undefined { // Older native payloads only include `data`; fall back to it if an option-specific map is absent. switch (options?.serverTimestamps) { case 'estimate': return this._dataEstimate ?? this._data; case 'previous': return this._dataPrevious ?? this._data; case 'none': return this._dataNone ?? this._data; default: return this._data; } } get id(): string { return this._ref.id; } get metadata(): SnapshotMetadata { return this._metadata; } get ref(): DocumentReference { return this._ref; } exists(): this is QueryDocumentSnapshot { return this._exists; } data(options?: SnapshotOptions): AppModelType | undefined { if (this._converter) { try { return (this._converter as ConverterWithFromFirestoreInternal).fromFirestore( new DocumentSnapshot( this._firestore, this._nativeData, null, ) as unknown as DocumentSnapshotDeclare, options, ) as AppModelType; } catch (e) { throw new Error( `firebase.firestore() DocumentSnapshot.data(*) 'withConverter.fromFirestore' threw an error: ${(e as Error).message}.`, ); } } return this._dataForOptions(options) as AppModelType | undefined; } get(fieldPath: string | FieldPath, options?: SnapshotOptions): DocumentFieldValueInternal { if (!isString(fieldPath) && !(fieldPath instanceof FieldPath)) { throw new Error( "firebase.firestore() DocumentSnapshot.get(*) 'fieldPath' expected type string or FieldPath.", ); } let path: FieldPath; if (isString(fieldPath)) { try { path = fromDotSeparatedString(fieldPath); } catch (e) { throw new Error( `firebase.firestore() DocumentSnapshot.get(*) 'fieldPath' ${(e as Error).message}.`, ); } } else { path = fieldPath; } return extractFieldPathData( this._dataForOptions(options), path._segments, ) as DocumentFieldValueInternal; } isEqual(other: DocumentSnapshot): boolean { if (!(other instanceof DocumentSnapshot)) { throw new Error( "firebase.firestore() DocumentSnapshot.isEqual(*) 'other' expected a DocumentSnapshot instance.", ); } if ( this.exists() !== other.exists() || !this.metadata.isEqual(other.metadata) || !this.ref.isEqual(other.ref) ) { return false; } const thisData = JSON.stringify(this.data()); const otherData = JSON.stringify(other.data()); return thisData === otherData; } } provideDocumentSnapshotClass(DocumentSnapshot);