/* * Copyright 2017 Google * * 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 "FIRDocumentReference.h" #import #import "FIRFirestoreErrors.h" #import "FIRSnapshotMetadata.h" #import "Firestore/Source/API/FIRCollectionReference+Internal.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRListenerRegistration+Internal.h" #import "Firestore/Source/API/FIRSetOptions+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTFirestoreClient.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Model/FSTDocumentKey.h" #import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTPath.h" #import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Source/Util/FSTAsyncQueryListener.h" #import "Firestore/Source/Util/FSTUsageValidation.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; using firebase::firestore::model::ResourcePath; NS_ASSUME_NONNULL_BEGIN #pragma mark - FIRDocumentListenOptions @interface FIRDocumentListenOptions () - (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges NS_DESIGNATED_INITIALIZER; @property(nonatomic, assign, readonly) BOOL includeMetadataChanges; @end @implementation FIRDocumentListenOptions + (instancetype)options { return [[FIRDocumentListenOptions alloc] init]; } - (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges { if (self = [super init]) { _includeMetadataChanges = includeMetadataChanges; } return self; } - (instancetype)init { return [self initWithIncludeMetadataChanges:NO]; } - (instancetype)includeMetadataChanges:(BOOL)includeMetadataChanges { return [[FIRDocumentListenOptions alloc] initWithIncludeMetadataChanges:includeMetadataChanges]; } @end #pragma mark - FIRDocumentReference @interface FIRDocumentReference () - (instancetype)initWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; @property(nonatomic, strong, readonly) FSTDocumentKey *key; @end @implementation FIRDocumentReference (Internal) + (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore { if (path.length % 2 != 0) { FSTThrowInvalidArgument( @"Invalid document reference. Document references must have an even " "number of segments, but %@ has %d", path.canonicalString, path.length); } return [FIRDocumentReference referenceWithKey:[FSTDocumentKey keyWithPath:[path toCPPResourcePath]] firestore:firestore]; } + (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { return [[FIRDocumentReference alloc] initWithKey:key firestore:firestore]; } @end @implementation FIRDocumentReference - (instancetype)initWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { if (self = [super init]) { _key = key; _firestore = firestore; } return self; } #pragma mark - NSObject Methods - (BOOL)isEqual:(nullable id)other { if (other == self) return YES; if (![[other class] isEqual:[self class]]) return NO; return [self isEqualToReference:other]; } - (BOOL)isEqualToReference:(nullable FIRDocumentReference *)reference { if (self == reference) return YES; if (reference == nil) return NO; return [self.firestore isEqual:reference.firestore] && [self.key isEqualToKey:reference.key]; } - (NSUInteger)hash { NSUInteger hash = [self.firestore hash]; hash = hash * 31u + [self.key hash]; return hash; } #pragma mark - Public Methods - (NSString *)documentID { return util::WrapNSString(self.key.path.last_segment()); } - (FIRCollectionReference *)parent { return [FIRCollectionReference referenceWithPath:[FSTResourcePath resourcePathWithCPPResourcePath:self.key.path.PopLast()] firestore:self.firestore]; } - (NSString *)path { return util::WrapNSString(self.key.path.CanonicalString()); } - (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { if (!collectionPath) { FSTThrowInvalidArgument(@"Collection path cannot be nil."); } FSTResourcePath *subPath = [FSTResourcePath pathWithString:collectionPath]; FSTResourcePath *path = [FSTResourcePath resourcePathWithCPPResourcePath:self.key.path.Append([subPath toCPPResourcePath])]; return [FIRCollectionReference referenceWithPath:path firestore:self.firestore]; } - (void)setData:(NSDictionary *)documentData { return [self setData:documentData options:[FIRSetOptions overwrite] completion:nil]; } - (void)setData:(NSDictionary *)documentData options:(FIRSetOptions *)options { return [self setData:documentData options:options completion:nil]; } - (void)setData:(NSDictionary *)documentData completion:(nullable void (^)(NSError *_Nullable error))completion { return [self setData:documentData options:[FIRSetOptions overwrite] completion:completion]; } - (void)setData:(NSDictionary *)documentData options:(FIRSetOptions *)options completion:(nullable void (^)(NSError *_Nullable error))completion { FSTParsedSetData *parsed = options.isMerge ? [self.firestore.dataConverter parsedMergeData:documentData] : [self.firestore.dataConverter parsedSetData:documentData]; return [self.firestore.client writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]] completion:completion]; } - (void)updateData:(NSDictionary *)fields { return [self updateData:fields completion:nil]; } - (void)updateData:(NSDictionary *)fields completion:(nullable void (^)(NSError *_Nullable error))completion { FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; return [self.firestore.client writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition preconditionWithExists:YES]] completion:completion]; } - (void)deleteDocument { return [self deleteDocumentWithCompletion:nil]; } - (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { FSTDeleteMutation *mutation = [[FSTDeleteMutation alloc] initWithKey:self.key precondition:[FSTPrecondition none]]; return [self.firestore.client writeMutations:@[ mutation ] completion:completion]; } - (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document, NSError *_Nullable error))completion { FSTListenOptions *listenOptions = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES includeDocumentMetadataChanges:YES waitForSyncWhenOnline:YES]; dispatch_semaphore_t registered = dispatch_semaphore_create(0); __block id listenerRegistration; FIRDocumentSnapshotBlock listener = ^(FIRDocumentSnapshot *snapshot, NSError *error) { if (error) { completion(nil, error); return; } // Remove query first before passing event to user to avoid user actions affecting the // now stale query. dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); [listenerRegistration remove]; if (!snapshot.exists && snapshot.metadata.fromCache) { // TODO(dimond): Reconsider how to raise missing documents when offline. // If we're online and the document doesn't exist then we call the completion with // a document with document.exists set to false. If we're offline however, we call the // completion handler with an error. Two options: // 1) Cache the negative response from the server so we can deliver that even when you're // offline. // 2) Actually call the completion handler with an error if the document doesn't exist when // you are offline. // TODO(dimond): Use proper error domain completion(nil, [NSError errorWithDomain:FIRFirestoreErrorDomain code:FIRFirestoreErrorCodeUnavailable userInfo:@{ NSLocalizedDescriptionKey : @"Failed to get document because the client is offline.", }]); } else { completion(snapshot, nil); } }; listenerRegistration = [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; dispatch_semaphore_signal(registered); } - (id)addSnapshotListener:(FIRDocumentSnapshotBlock)listener { return [self addSnapshotListenerWithOptions:nil listener:listener]; } - (id)addSnapshotListenerWithOptions: (nullable FIRDocumentListenOptions *)options listener:(FIRDocumentSnapshotBlock)listener { return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] listener:listener]; } - (id) addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions listener:(FIRDocumentSnapshotBlock)listener { FIRFirestore *firestore = self.firestore; FSTQuery *query = [FSTQuery queryWithPath:self.key.path]; FSTDocumentKey *key = self.key; FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { if (error) { listener(nil, error); return; } FSTAssert(snapshot.documents.count <= 1, @"Too many document returned on a document query"); FSTDocument *document = [snapshot.documents documentForKey:key]; FIRDocumentSnapshot *result = [FIRDocumentSnapshot snapshotWithFirestore:firestore documentKey:key document:document fromCache:snapshot.fromCache]; listener(result, nil); }; FSTAsyncQueryListener *asyncListener = [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue snapshotHandler:snapshotHandler]; FSTQueryListener *internalListener = [firestore.client listenToQuery:query options:internalOptions viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client asyncListener:asyncListener internalListener:internalListener]; } /** Converts the public API options object to the internal options object. */ - (FSTListenOptions *)internalOptions:(nullable FIRDocumentListenOptions *)options { return [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:options.includeMetadataChanges includeDocumentMetadataChanges:options.includeMetadataChanges waitForSyncWhenOnline:NO]; } @end NS_ASSUME_NONNULL_END