diff options
author | Gil <mcg@google.com> | 2017-10-03 08:55:22 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-03 08:55:22 -0700 |
commit | bde743ed25166a0b320ae157bfb1d68064f531c9 (patch) | |
tree | 4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Source/API | |
parent | bf550507ffa8beee149383a5bf1e2363bccefbb4 (diff) |
Release 4.3.0 (#327)
Initial release of Firestore at 0.8.0
Bump FirebaseCommunity to 0.1.3
Diffstat (limited to 'Firestore/Source/API')
36 files changed, 3725 insertions, 0 deletions
diff --git a/Firestore/Source/API/FIRCollectionReference+Internal.h b/Firestore/Source/API/FIRCollectionReference+Internal.h new file mode 100644 index 0000000..1d00cbb --- /dev/null +++ b/Firestore/Source/API/FIRCollectionReference+Internal.h @@ -0,0 +1,28 @@ +/* + * 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 "FIRCollectionReference.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTResourcePath; + +/** Internal FIRCollectionReference API we don't want exposed in our public header files. */ +@interface FIRCollectionReference (Internal) ++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRCollectionReference.m b/Firestore/Source/API/FIRCollectionReference.m new file mode 100644 index 0000000..1ded4d2 --- /dev/null +++ b/Firestore/Source/API/FIRCollectionReference.m @@ -0,0 +1,113 @@ +/* + * 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 "FIRCollectionReference.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRQuery+Internal.h" +#import "FIRQuery_Init.h" +#import "FSTAssert.h" +#import "FSTDocumentKey.h" +#import "FSTPath.h" +#import "FSTQuery.h" +#import "FSTUsageValidation.h" +#import "FSTUtil.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRCollectionReference () +- (instancetype)initWithPath:(FSTResourcePath *)path + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; + +// Mark the super class designated initializer unavailable. +- (instancetype)initWithQuery:(FSTQuery *)query + firestore:(FIRFirestore *)firestore + __attribute__((unavailable("Use the initWithPath constructor of FIRCollectionReference."))); +@end + +@implementation FIRCollectionReference (Internal) ++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore { + return [[FIRCollectionReference alloc] initWithPath:path firestore:firestore]; +} +@end + +@implementation FIRCollectionReference + +- (instancetype)initWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore { + if (path.length % 2 != 1) { + FSTThrowInvalidArgument( + @"Invalid collection reference. Collection references must have an odd " + "number of segments, but %@ has %d", + path.canonicalString, path.length); + } + self = [super initWithQuery:[FSTQuery queryWithPath:path] firestore:firestore]; + return self; +} + +// Override the designated initializer from the super class. +- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { + FSTFail(@"Use FIRCollectionReference initWithPath: initializer."); +} + +- (NSString *)collectionID { + return [self.query.path lastSegment]; +} + +- (FIRDocumentReference *_Nullable)parent { + FSTResourcePath *parentPath = [self.query.path pathByRemovingLastSegment]; + if (parentPath.isEmpty) { + return nil; + } else { + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:parentPath]; + return [FIRDocumentReference referenceWithKey:key firestore:self.firestore]; + } +} + +- (NSString *)path { + return [self.query.path canonicalString]; +} + +- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { + if (!documentPath) { + FSTThrowInvalidArgument(@"Document path cannot be nil."); + } + FSTResourcePath *subPath = [FSTResourcePath pathWithString:documentPath]; + FSTResourcePath *path = [self.query.path pathByAppendingPath:subPath]; + return [FIRDocumentReference referenceWithPath:path firestore:self.firestore]; +} + +- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data { + return [self addDocumentWithData:data completion:nil]; +} + +- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data + completion: + (nullable void (^)(NSError *_Nullable error))completion { + FIRDocumentReference *docRef = [self documentWithAutoID]; + [docRef setData:data completion:completion]; + return docRef; +} + +- (FIRDocumentReference *)documentWithAutoID { + NSString *autoID = [FSTUtil autoID]; + FSTDocumentKey *key = + [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:autoID]]; + return [FIRDocumentReference referenceWithKey:key firestore:self.firestore]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentChange+Internal.h b/Firestore/Source/API/FIRDocumentChange+Internal.h new file mode 100644 index 0000000..7e2e5c6 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentChange+Internal.h @@ -0,0 +1,32 @@ +/* + * 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 "FIRDocumentChange.h" + +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +/** Internal FIRDocumentChange API we don't want exposed in our public header files. */ +@interface FIRDocumentChange (Internal) + +/** Calculates the array of FIRDocumentChange's based on the given FSTViewSnapshot. */ ++ (NSArray<FIRDocumentChange *> *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot + firestore:(FIRFirestore *)firestore; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentChange.m b/Firestore/Source/API/FIRDocumentChange.m new file mode 100644 index 0000000..f284bfe --- /dev/null +++ b/Firestore/Source/API/FIRDocumentChange.m @@ -0,0 +1,129 @@ +/* + * 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 "FIRDocumentChange.h" + +#import "FIRDocumentSnapshot+Internal.h" +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentSet.h" +#import "FSTQuery.h" +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentChange () + +- (instancetype)initWithType:(FIRDocumentChangeType)type + document:(FIRDocumentSnapshot *)document + oldIndex:(NSUInteger)oldIndex + newIndex:(NSUInteger)newIndex NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRDocumentChange (Internal) + ++ (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)change { + if (change.type == FSTDocumentViewChangeTypeAdded) { + return FIRDocumentChangeTypeAdded; + } else if (change.type == FSTDocumentViewChangeTypeModified || + change.type == FSTDocumentViewChangeTypeMetadata) { + return FIRDocumentChangeTypeModified; + } else if (change.type == FSTDocumentViewChangeTypeRemoved) { + return FIRDocumentChangeTypeRemoved; + } else { + FSTFail(@"Unknown FSTDocumentViewChange: %ld", (long)change.type); + } +} + ++ (NSArray<FIRDocumentChange *> *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot + firestore:(FIRFirestore *)firestore { + if (snapshot.oldDocuments.isEmpty) { + // Special case the first snapshot because index calculation is easy and fast + FSTDocument *_Nullable lastDocument = nil; + NSUInteger index = 0; + NSMutableArray<FIRDocumentChange *> *changes = [NSMutableArray array]; + for (FSTDocumentViewChange *change in snapshot.documentChanges) { + FIRDocumentSnapshot *document = + [FIRDocumentSnapshot snapshotWithFirestore:firestore + documentKey:change.document.key + document:change.document + fromCache:snapshot.isFromCache]; + FSTAssert(change.type == FSTDocumentViewChangeTypeAdded, + @"Invalid event type for first snapshot"); + FSTAssert(!lastDocument || + snapshot.query.comparator(lastDocument, change.document) == NSOrderedAscending, + @"Got added events in wrong order"); + [changes addObject:[[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeAdded + document:document + oldIndex:NSNotFound + newIndex:index++]]; + } + return changes; + } else { + // A DocumentSet that is updated incrementally as changes are applied to use to lookup the index + // of a document. + FSTDocumentSet *indexTracker = snapshot.oldDocuments; + NSMutableArray<FIRDocumentChange *> *changes = [NSMutableArray array]; + for (FSTDocumentViewChange *change in snapshot.documentChanges) { + FIRDocumentSnapshot *document = + [FIRDocumentSnapshot snapshotWithFirestore:firestore + documentKey:change.document.key + document:change.document + fromCache:snapshot.isFromCache]; + + NSUInteger oldIndex = NSNotFound; + NSUInteger newIndex = NSNotFound; + if (change.type != FSTDocumentViewChangeTypeAdded) { + oldIndex = [indexTracker indexOfKey:change.document.key]; + FSTAssert(oldIndex != NSNotFound, @"Index for document not found"); + indexTracker = [indexTracker documentSetByRemovingKey:change.document.key]; + } + if (change.type != FSTDocumentViewChangeTypeRemoved) { + indexTracker = [indexTracker documentSetByAddingDocument:change.document]; + newIndex = [indexTracker indexOfKey:change.document.key]; + } + [FIRDocumentChange documentChangeTypeForChange:change]; + FIRDocumentChangeType type = [FIRDocumentChange documentChangeTypeForChange:change]; + [changes addObject:[[FIRDocumentChange alloc] initWithType:type + document:document + oldIndex:oldIndex + newIndex:newIndex]]; + } + return changes; + } +} + +@end + +@implementation FIRDocumentChange + +- (instancetype)initWithType:(FIRDocumentChangeType)type + document:(FIRDocumentSnapshot *)document + oldIndex:(NSUInteger)oldIndex + newIndex:(NSUInteger)newIndex { + if (self = [super init]) { + _type = type; + _document = document; + _oldIndex = oldIndex; + _newIndex = newIndex; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentReference+Internal.h b/Firestore/Source/API/FIRDocumentReference+Internal.h new file mode 100644 index 0000000..5e12ddc --- /dev/null +++ b/Firestore/Source/API/FIRDocumentReference+Internal.h @@ -0,0 +1,34 @@ +/* + * 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" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTDocumentKey; +@class FSTResourcePath; + +/** Internal FIRDocumentReference API we don't want exposed in our public header files. */ +@interface FIRDocumentReference (Internal) + ++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore; ++ (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore; + +@property(nonatomic, strong, readonly) FSTDocumentKey *key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentReference.m b/Firestore/Source/API/FIRDocumentReference.m new file mode 100644 index 0000000..5515bd6 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentReference.m @@ -0,0 +1,285 @@ +/* + * 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 "FIRCollectionReference+Internal.h" +#import "FIRDocumentReference+Internal.h" +#import "FIRDocumentSnapshot+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRFirestoreErrors.h" +#import "FIRListenerRegistration+Internal.h" +#import "FIRSetOptions+Internal.h" +#import "FIRSnapshotMetadata.h" +#import "FSTAssert.h" +#import "FSTAsyncQueryListener.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentSet.h" +#import "FSTEventManager.h" +#import "FSTFieldValue.h" +#import "FSTFirestoreClient.h" +#import "FSTMutation.h" +#import "FSTPath.h" +#import "FSTQuery.h" +#import "FSTUsageValidation.h" +#import "FSTUserDataConverter.h" + +#import <GRPCClient/GRPCCall.h> + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRDocumentListenOptions + +@interface FIRDocumentListenOptions () + +- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges + NS_DESIGNATED_INITIALIZER; + +@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] 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; +} + +- (NSString *)documentID { + return [self.key.path lastSegment]; +} + +- (FIRCollectionReference *)parent { + FSTResourcePath *parentPath = [self.key.path pathByRemovingLastSegment]; + return [FIRCollectionReference referenceWithPath:parentPath firestore:self.firestore]; +} + +- (NSString *)path { + return [self.key.path canonicalString]; +} + +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { + if (!collectionPath) { + FSTThrowInvalidArgument(@"Collection path cannot be nil."); + } + FSTResourcePath *subPath = [FSTResourcePath pathWithString:collectionPath]; + FSTResourcePath *path = [self.key.path pathByAppendingPath:subPath]; + return [FIRCollectionReference referenceWithPath:path firestore:self.firestore]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData { + return [self setData:documentData options:[FIRSetOptions overwrite] completion:nil]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData options:(FIRSetOptions *)options { + return [self setData:documentData options:options completion:nil]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData + completion:(nullable void (^)(NSError *_Nullable error))completion { + return [self setData:documentData options:[FIRSetOptions overwrite] completion:completion]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData + options:(FIRSetOptions *)options + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedSetData:documentData options:options]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]] + completion:completion]; +} + +- (void)updateData:(NSDictionary<id, id> *)fields { + return [self updateData:fields completion:nil]; +} + +- (void)updateData:(NSDictionary<id, id> *)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<FIRListenerRegistration> 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<FIRListenerRegistration>)addSnapshotListener:(FIRDocumentSnapshotBlock)listener { + return [self addSnapshotListenerWithOptions:nil listener:listener]; +} + +- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions: + (nullable FIRDocumentListenOptions *)options + listener:(FIRDocumentSnapshotBlock)listener { + return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] + listener:listener]; +} + +- (id<FIRListenerRegistration>) +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 diff --git a/Firestore/Source/API/FIRDocumentSnapshot+Internal.h b/Firestore/Source/API/FIRDocumentSnapshot+Internal.h new file mode 100644 index 0000000..f2776f0 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentSnapshot+Internal.h @@ -0,0 +1,37 @@ +/* + * 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 "FIRDocumentSnapshot.h" + +@class FIRFirestore; +@class FSTDocument; +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** Internal FIRDocumentSnapshot API we don't want exposed in our public header files. */ +@interface FIRDocumentSnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache; + +@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentSnapshot.m b/Firestore/Source/API/FIRDocumentSnapshot.m new file mode 100644 index 0000000..b5f61ba --- /dev/null +++ b/Firestore/Source/API/FIRDocumentSnapshot.m @@ -0,0 +1,175 @@ +/* + * 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 "FIRDocumentSnapshot.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRFieldPath+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRSnapshotMetadata+Internal.h" +#import "FSTDatabaseID.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTFieldValue.h" +#import "FSTPath.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentSnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) FSTDocumentKey *internalKey; +@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument; +@property(nonatomic, assign, readonly) BOOL fromCache; + +@end + +@implementation FIRDocumentSnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache { + return [[FIRDocumentSnapshot alloc] initWithFirestore:firestore + documentKey:documentKey + document:document + fromCache:fromCache]; +} + +@end + +@implementation FIRDocumentSnapshot { + FIRSnapshotMetadata *_cachedMetadata; +} + +@dynamic metadata; + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache { + if (self = [super init]) { + _firestore = firestore; + _internalKey = documentKey; + _internalDocument = document; + _fromCache = fromCache; + } + return self; +} + +@dynamic exists; + +- (BOOL)exists { + return _internalDocument != nil; +} + +- (FIRDocumentReference *)reference { + return [FIRDocumentReference referenceWithKey:self.internalKey firestore:self.firestore]; +} + +- (NSString *)documentID { + return [self.internalKey.path lastSegment]; +} + +- (FIRSnapshotMetadata *)metadata { + if (!_cachedMetadata) { + _cachedMetadata = [FIRSnapshotMetadata + snapshotMetadataWithPendingWrites:self.internalDocument.hasLocalMutations + fromCache:self.fromCache]; + } + return _cachedMetadata; +} + +- (NSDictionary<NSString *, id> *)data { + FSTDocument *document = self.internalDocument; + + if (!document) { + FSTThrowInvalidUsage( + @"NonExistentDocumentException", + @"Document '%@' doesn't exist. " + @"Check document.exists to make sure the document exists before calling document.data.", + self.internalKey); + } + + return [self convertedObject:[self.internalDocument data]]; +} + +- (nullable id)objectForKeyedSubscript:(id)key { + FIRFieldPath *fieldPath; + + if ([key isKindOfClass:[NSString class]]) { + fieldPath = [FIRFieldPath pathWithDotSeparatedString:key]; + } else if ([key isKindOfClass:[FIRFieldPath class]]) { + fieldPath = key; + } else { + FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath."); + } + + FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; + return [self convertedValue:fieldValue]; +} + +- (id)convertedValue:(FSTFieldValue *)value { + if ([value isKindOfClass:[FSTObjectValue class]]) { + return [self convertedObject:(FSTObjectValue *)value]; + } else if ([value isKindOfClass:[FSTArrayValue class]]) { + return [self convertedArray:(FSTArrayValue *)value]; + } else if ([value isKindOfClass:[FSTReferenceValue class]]) { + FSTReferenceValue *ref = (FSTReferenceValue *)value; + FSTDatabaseID *refDatabase = ref.databaseID; + FSTDatabaseID *database = self.firestore.databaseID; + if (![refDatabase isEqualToDatabaseId:database]) { + // TODO(b/32073923): Log this as a proper warning. + NSLog( + @"WARNING: Document %@ contains a document reference within a different database " + "(%@/%@) which is not supported. It will be treated as a reference within the " + "current database (%@/%@) instead.", + self.reference.path, refDatabase.projectID, refDatabase.databaseID, database.projectID, + database.databaseID); + } + return [FIRDocumentReference referenceWithKey:ref.value firestore:self.firestore]; + } else { + return value.value; + } +} + +- (NSDictionary<NSString *, id> *)convertedObject:(FSTObjectValue *)objectValue { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + [objectValue.internalValue + enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) { + result[key] = [self convertedValue:value]; + }]; + return result; +} + +- (NSArray<id> *)convertedArray:(FSTArrayValue *)arrayValue { + NSArray<FSTFieldValue *> *internalValue = arrayValue.internalValue; + NSMutableArray *result = [NSMutableArray arrayWithCapacity:internalValue.count]; + [internalValue enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) { + [result addObject:[self convertedValue:value]]; + }]; + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldPath+Internal.h b/Firestore/Source/API/FIRFieldPath+Internal.h new file mode 100644 index 0000000..227cdad --- /dev/null +++ b/Firestore/Source/API/FIRFieldPath+Internal.h @@ -0,0 +1,39 @@ +/* + * 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 "FIRFieldPath.h" + +@class FSTFieldPath; + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRFieldPath () + +- (instancetype)initPrivate:(FSTFieldPath *)path NS_DESIGNATED_INITIALIZER; + +/** Internal field path representation */ +@property(nonatomic, strong, readonly) FSTFieldPath *internalValue; + +@end + +/** Internal FIRFieldPath API we don't want exposed in our public header files. */ +@interface FIRFieldPath (Internal) + ++ (instancetype)pathWithDotSeparatedString:(NSString *)path; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldPath.m b/Firestore/Source/API/FIRFieldPath.m new file mode 100644 index 0000000..b3c919c --- /dev/null +++ b/Firestore/Source/API/FIRFieldPath.m @@ -0,0 +1,101 @@ +/* + * 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 "FIRFieldPath+Internal.h" + +#import "FSTPath.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRFieldPath + +- (instancetype)initWithFields:(NSArray<NSString *> *)fieldNames { + if (fieldNames.count == 0) { + FSTThrowInvalidArgument(@"Invalid field path. Provided names must not be empty."); + } + + for (int i = 0; i < fieldNames.count; ++i) { + if (fieldNames[i].length == 0) { + FSTThrowInvalidArgument(@"Invalid field name at index %d. Field names must not be empty.", i); + } + } + + return [self initPrivate:[FSTFieldPath pathWithSegments:fieldNames]]; +} + ++ (instancetype)documentID { + return [[FIRFieldPath alloc] initPrivate:FSTFieldPath.keyFieldPath]; +} + +- (instancetype)initPrivate:(FSTFieldPath *)fieldPath { + if (self = [super init]) { + _internalValue = fieldPath; + } + return self; +} + ++ (instancetype)pathWithDotSeparatedString:(NSString *)path { + if ([[FIRFieldPath reservedCharactersRegex] + numberOfMatchesInString:path + options:0 + range:NSMakeRange(0, path.length)] > 0) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", path); + } + @try { + return [[FIRFieldPath alloc] initWithFields:[path componentsSeparatedByString:@"."]]; + } @catch (NSException *exception) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not be empty, begin with '.', end with '.', or " + @"contain '..'", + path); + } +} + +/** Matches any characters in a field path string that are reserved. */ ++ (NSRegularExpression *)reservedCharactersRegex { + static NSRegularExpression *regex = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + regex = [NSRegularExpression regularExpressionWithPattern:@"[~*/\\[\\]]" options:0 error:nil]; + }); + return regex; +} + +- (id)copyWithZone:(NSZone *__nullable)zone { + return [[[self class] alloc] initPrivate:self.internalValue]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[FIRFieldPath class]]) { + return NO; + } + + return [self.internalValue isEqual:((FIRFieldPath *)object).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldValue+Internal.h b/Firestore/Source/API/FIRFieldValue+Internal.h new file mode 100644 index 0000000..1b4a99c --- /dev/null +++ b/Firestore/Source/API/FIRFieldValue+Internal.h @@ -0,0 +1,37 @@ +/* + * 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 "FIRFieldValue.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * FIRFieldValue class for field deletes. Exposed internally so code can do isKindOfClass checks on + * it. + */ +@interface FSTDeleteFieldValue : FIRFieldValue +- (instancetype)init NS_UNAVAILABLE; +@end + +/** + * FIRFieldValue class for server timestamps. Exposed internally so code can do isKindOfClass checks + * on it. + */ +@interface FSTServerTimestampFieldValue : FIRFieldValue +- (instancetype)init NS_UNAVAILABLE; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldValue.m b/Firestore/Source/API/FIRFieldValue.m new file mode 100644 index 0000000..a44d8fa --- /dev/null +++ b/Firestore/Source/API/FIRFieldValue.m @@ -0,0 +1,96 @@ +/* + * 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 "FIRFieldValue+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRFieldValue () +- (instancetype)initPrivate NS_DESIGNATED_INITIALIZER; +@end + +#pragma mark - FSTDeleteFieldValue + +@interface FSTDeleteFieldValue () +/** Returns a single shared instance of the class. */ ++ (instancetype)deleteFieldValue; +@end + +@implementation FSTDeleteFieldValue + +- (instancetype)initPrivate { + self = [super initPrivate]; + return self; +} + ++ (instancetype)deleteFieldValue { + static FSTDeleteFieldValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTDeleteFieldValue alloc] initPrivate]; + }); + return sharedInstance; +} + +@end + +#pragma mark - FSTServerTimestampFieldValue + +@interface FSTServerTimestampFieldValue () +/** Returns a single shared instance of the class. */ ++ (instancetype)serverTimestampFieldValue; +@end + +@implementation FSTServerTimestampFieldValue + +- (instancetype)initPrivate { + self = [super initPrivate]; + return self; +} + ++ (instancetype)serverTimestampFieldValue { + static FSTServerTimestampFieldValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTServerTimestampFieldValue alloc] initPrivate]; + }); + return sharedInstance; +} + +@end + +#pragma mark - FIRFieldValue + +@implementation FIRFieldValue + +- (instancetype)initPrivate { + self = [super init]; + return self; +} + ++ (instancetype)fieldValueForDelete { + return [FSTDeleteFieldValue deleteFieldValue]; +} + ++ (instancetype)fieldValueForServerTimestamp { + return [FSTServerTimestampFieldValue serverTimestampFieldValue]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestore+Internal.h b/Firestore/Source/API/FIRFirestore+Internal.h new file mode 100644 index 0000000..08f5266 --- /dev/null +++ b/Firestore/Source/API/FIRFirestore+Internal.h @@ -0,0 +1,64 @@ +/* + * 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 "FIRFirestore.h" +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTDatabaseID; +@class FSTDispatchQueue; +@class FSTFirestoreClient; +@class FSTUserDataConverter; +@protocol FSTCredentialsProvider; + +@interface FIRFirestore (/* Init */) + +/** + * Initializes a Firestore object with all the required parameters directly. This exists so that + * tests can create FIRFirestore objects without needing FIRApp. + */ +- (instancetype)initWithProjectID:(NSString *)projectID + database:(NSString *)database + persistenceKey:(NSString *)persistenceKey + credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + firebaseApp:(FIRApp *)app; + +@end + +/** Internal FIRFirestore API we don't want exposed in our public header files. */ +@interface FIRFirestore (Internal) + +/** Checks to see if logging is is globally enabled for the Firestore client. */ ++ (BOOL)isLoggingEnabled; + +/** + * Shutdown this `FIRFirestore`, releasing all resources (abandoning any outstanding writes, + * removing all listens, closing all network connections, etc.). + * + * @param completion A block to execute once everything has shut down. + */ +- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion + FIR_SWIFT_NAME(shutdown(completion:)); + +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; +@property(nonatomic, strong, readonly) FSTFirestoreClient *client; +@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestore.m b/Firestore/Source/API/FIRFirestore.m new file mode 100644 index 0000000..e8c4fa6 --- /dev/null +++ b/Firestore/Source/API/FIRFirestore.m @@ -0,0 +1,284 @@ +/* + * 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 "FIRFirestore.h" + +#import <FirebaseCommunity/FIRApp.h> +#import <FirebaseCommunity/FIRLogger.h> +#import <FirebaseCommunity/FIROptions.h> + +#import "FIRCollectionReference+Internal.h" +#import "FIRDocumentReference+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRFirestoreSettings.h" +#import "FIRTransaction+Internal.h" +#import "FIRWriteBatch+Internal.h" +#import "FSTUserDataConverter.h" + +#import "FSTAssert.h" +#import "FSTCredentialsProvider.h" +#import "FSTDatabaseID.h" +#import "FSTDatabaseInfo.h" +#import "FSTDispatchQueue.h" +#import "FSTDocumentKey.h" +#import "FSTFirestoreClient.h" +#import "FSTLogger.h" +#import "FSTPath.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; + +@interface FIRFirestore () + +@property(nonatomic, strong) FSTDatabaseID *databaseID; +@property(nonatomic, strong) NSString *persistenceKey; +@property(nonatomic, strong) id<FSTCredentialsProvider> credentialsProvider; +@property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue; + +@property(nonatomic, strong) FSTFirestoreClient *client; +@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; + +@end + +@implementation FIRFirestore { + FIRFirestoreSettings *_settings; +} + ++ (NSMutableDictionary<NSString *, FIRFirestore *> *)instances { + static dispatch_once_t token = 0; + static NSMutableDictionary<NSString *, FIRFirestore *> *instances; + dispatch_once(&token, ^{ + instances = [NSMutableDictionary dictionary]; + }); + return instances; +} + ++ (instancetype)firestore { + FIRApp *app = [FIRApp defaultApp]; + if (!app) { + FSTThrowInvalidUsage( + @"FIRAppNotConfiguredException", + @"Failed to get FIRApp instance. Please call FIRApp.configure() before using FIRFirestore"); + } + return [self firestoreForApp:app database:kDefaultDatabaseID]; +} + ++ (instancetype)firestoreForApp:(FIRApp *)app { + return [self firestoreForApp:app database:kDefaultDatabaseID]; +} + +// TODO(b/62410906): make this public ++ (instancetype)firestoreForApp:(FIRApp *)app database:(NSString *)database { + if (!app) { + FSTThrowInvalidArgument( + @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd " + "like to use the default FirebaseApp instance."); + } + if (!database) { + FSTThrowInvalidArgument( + @"database identifier may not be nil. Use '%@' if you want the default " + "database", + kDefaultDatabaseID); + } + NSString *key = [NSString stringWithFormat:@"%@|%@", app.name, database]; + + NSMutableDictionary<NSString *, FIRFirestore *> *instances = self.instances; + @synchronized(instances) { + FIRFirestore *firestore = instances[key]; + if (!firestore) { + NSString *projectID = app.options.projectID; + FSTAssert(projectID, @"FIROptions.projectID cannot be nil."); + + FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)]; + + id<FSTCredentialsProvider> credentialsProvider; + credentialsProvider = [[FSTFirebaseCredentialsProvider alloc] initWithApp:app]; + + NSString *persistenceKey = app.name; + + firestore = [[FIRFirestore alloc] initWithProjectID:projectID + database:database + persistenceKey:persistenceKey + credentialsProvider:credentialsProvider + workerDispatchQueue:workerDispatchQueue + firebaseApp:app]; + instances[key] = firestore; + } + + return firestore; + } +} + +- (instancetype)initWithProjectID:(NSString *)projectID + database:(NSString *)database + persistenceKey:(NSString *)persistenceKey + credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + firebaseApp:(FIRApp *)app { + if (self = [super init]) { + _databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; + FSTPreConverterBlock block = ^id _Nullable(id _Nullable input) { + if ([input isKindOfClass:[FIRDocumentReference class]]) { + FIRDocumentReference *documentReference = (FIRDocumentReference *)input; + return [[FSTDocumentKeyReference alloc] initWithKey:documentReference.key + databaseID:documentReference.firestore.databaseID]; + } else { + return input; + } + }; + _dataConverter = + [[FSTUserDataConverter alloc] initWithDatabaseID:_databaseID preConverter:block]; + _persistenceKey = persistenceKey; + _credentialsProvider = credentialsProvider; + _workerDispatchQueue = workerDispatchQueue; + _app = app; + _settings = [[FIRFirestoreSettings alloc] init]; + } + return self; +} + +- (FIRFirestoreSettings *)settings { + // Disallow mutation of our internal settings + return [_settings copy]; +} + +- (void)setSettings:(FIRFirestoreSettings *)settings { + // As a special exception, don't throw if the same settings are passed repeatedly. This should + // make it more friendly to create a Firestore instance. + if (_client && ![_settings isEqual:settings]) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"Firestore instance has already been started and its settings can no " + "longer be changed. You can only set settings before calling any " + "other methods on a Firestore instance."); + } + _settings = [settings copy]; +} + +/** + * Ensures that the FirestoreClient is configured. + * @return self + */ +- (instancetype)firestoreWithConfiguredClient { + if (!_client) { + // These values are validated elsewhere; this is just double-checking: + FSTAssert(_settings.host, @"FIRFirestoreSettings.host cannot be nil."); + FSTAssert(_settings.dispatchQueue, @"FIRFirestoreSettings.dispatchQueue cannot be nil."); + + FSTDatabaseInfo *databaseInfo = + [FSTDatabaseInfo databaseInfoWithDatabaseID:_databaseID + persistenceKey:_persistenceKey + host:_settings.host + sslEnabled:_settings.sslEnabled]; + + FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue]; + + _client = [FSTFirestoreClient clientWithDatabaseInfo:databaseInfo + usePersistence:_settings.persistenceEnabled + credentialsProvider:_credentialsProvider + userDispatchQueue:userDispatchQueue + workerDispatchQueue:_workerDispatchQueue]; + } + return self; +} + +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { + if (!collectionPath) { + FSTThrowInvalidArgument(@"Collection path cannot be nil."); + } + FSTResourcePath *path = [FSTResourcePath pathWithString:collectionPath]; + return + [FIRCollectionReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient]; +} + +- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { + if (!documentPath) { + FSTThrowInvalidArgument(@"Document path cannot be nil."); + } + FSTResourcePath *path = [FSTResourcePath pathWithString:documentPath]; + return [FIRDocumentReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient]; +} + +- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock + dispatchQueue:(dispatch_queue_t)queue + completion: + (void (^)(id _Nullable result, NSError *_Nullable error))completion { + // We wrap the function they provide in order to use internal implementation classes for + // FSTTransaction, and to run the user callback block on the proper queue. + if (!updateBlock) { + FSTThrowInvalidArgument(@"Transaction block cannot be nil."); + } else if (!completion) { + FSTThrowInvalidArgument(@"Transaction completion block cannot be nil."); + } + + FSTTransactionBlock wrappedUpdate = + ^(FSTTransaction *internalTransaction, + void (^internalCompletion)(id _Nullable, NSError *_Nullable)) { + FIRTransaction *transaction = + [FIRTransaction transactionWithFSTTransaction:internalTransaction firestore:self]; + dispatch_async(queue, ^{ + NSError *_Nullable error = nil; + id _Nullable result = updateBlock(transaction, &error); + if (error) { + // Force the result to be nil in the case of an error, in case the user set both. + result = nil; + } + internalCompletion(result, error); + }); + }; + [self firestoreWithConfiguredClient]; + [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion]; +} + +- (FIRWriteBatch *)batch { + return [FIRWriteBatch writeBatchWithFirestore:[self firestoreWithConfiguredClient]]; +} + +- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **error))updateBlock + completion: + (void (^)(id _Nullable result, NSError *_Nullable error))completion { + static dispatch_queue_t transactionDispatchQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + transactionDispatchQueue = dispatch_queue_create("com.google.firebase.firestore.transaction", + DISPATCH_QUEUE_CONCURRENT); + }); + [self runTransactionWithBlock:updateBlock + dispatchQueue:transactionDispatchQueue + completion:completion]; +} + +- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + if (!self.client) { + completion(nil); + return; + } + return [self.client shutdownWithCompletion:completion]; +} + ++ (BOOL)isLoggingEnabled { + return FIRIsLoggableLevel(FIRLoggerLevelDebug, NO); +} + ++ (void)enableLogging:(BOOL)logging { + FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestoreSettings.m b/Firestore/Source/API/FIRFirestoreSettings.m new file mode 100644 index 0000000..106a0b5 --- /dev/null +++ b/Firestore/Source/API/FIRFirestoreSettings.m @@ -0,0 +1,92 @@ +/* + * 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 "FIRFirestoreSettings.h" + +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kDefaultHost = @"firestore.googleapis.com"; +static const BOOL kDefaultSSLEnabled = YES; +static const BOOL kDefaultPersistenceEnabled = YES; + +@implementation FIRFirestoreSettings + +- (instancetype)init { + if (self = [super init]) { + _host = kDefaultHost; + _sslEnabled = kDefaultSSLEnabled; + _dispatchQueue = dispatch_get_main_queue(); + _persistenceEnabled = kDefaultPersistenceEnabled; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FIRFirestoreSettings class]]) { + return NO; + } + + FIRFirestoreSettings *otherSettings = (FIRFirestoreSettings *)other; + return [self.host isEqual:otherSettings.host] && + self.isSSLEnabled == otherSettings.isSSLEnabled && + self.dispatchQueue == otherSettings.dispatchQueue && + self.isPersistenceEnabled == otherSettings.isPersistenceEnabled; +} + +- (NSUInteger)hash { + NSUInteger result = [self.host hash]; + result = 31 * result + (self.isSSLEnabled ? 1231 : 1237); + // Ignore the dispatchQueue to avoid having to deal with sizeof(dispatch_queue_t). + result = 31 * result + (self.isPersistenceEnabled ? 1231 : 1237); + return result; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + FIRFirestoreSettings *copy = [[FIRFirestoreSettings alloc] init]; + copy.host = _host; + copy.sslEnabled = _sslEnabled; + copy.dispatchQueue = _dispatchQueue; + copy.persistenceEnabled = _persistenceEnabled; + return copy; +} + +- (void)setHost:(NSString *)host { + if (!host) { + FSTThrowInvalidArgument( + @"host setting may not be nil. You should generally just use the default value " + "(which is %@)", + kDefaultHost); + } + _host = [host mutableCopy]; +} + +- (void)setDispatchQueue:(dispatch_queue_t)dispatchQueue { + if (!dispatchQueue) { + FSTThrowInvalidArgument( + @"dispatch queue setting may not be nil. Create a new dispatch queue with " + "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default " + "(which is the main queue, returned from dispatch_get_main_queue())"); + } + _dispatchQueue = dispatchQueue; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestoreVersion.h b/Firestore/Source/API/FIRFirestoreVersion.h new file mode 100644 index 0000000..6fb21eb --- /dev/null +++ b/Firestore/Source/API/FIRFirestoreVersion.h @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** Version for Firestore. */ + +#import <Foundation/Foundation.h> + +/** Version string for the Firebase Firestore SDK. */ +FOUNDATION_EXPORT const unsigned char *const FirebaseFirestoreVersionString; diff --git a/Firestore/Source/API/FIRFirestoreVersion.m b/Firestore/Source/API/FIRFirestoreVersion.m new file mode 100644 index 0000000..4f8bb28 --- /dev/null +++ b/Firestore/Source/API/FIRFirestoreVersion.m @@ -0,0 +1,29 @@ +/* + * 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. + */ + +#ifndef FIRFirestore_VERSION +#error "FIRFirestore_VERSION is not defined: add -DFIRFirestore_VERSION=... to the build invocation" +#endif + +// The following two macros supply the incantation so that the C +// preprocessor does not try to parse the version as a floating +// point number. See +// https://www.guyrutenberg.com/2008/12/20/expanding-macros-into-string-constants-in-c/ +#define STR(x) STR_EXPAND(x) +#define STR_EXPAND(x) #x + +const unsigned char *const FirebaseFirestoreVersionString = + (const unsigned char *const)STR(FIRFirestore_VERSION); diff --git a/Firestore/Source/API/FIRGeoPoint+Internal.h b/Firestore/Source/API/FIRGeoPoint+Internal.h new file mode 100644 index 0000000..6eb8548 --- /dev/null +++ b/Firestore/Source/API/FIRGeoPoint+Internal.h @@ -0,0 +1,26 @@ +/* + * 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 "FIRGeoPoint.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Internal FIRGeoPoint API we don't want exposed in our public header files. */ +@interface FIRGeoPoint (Internal) +- (NSComparisonResult)compare:(FIRGeoPoint *)other; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRGeoPoint.m b/Firestore/Source/API/FIRGeoPoint.m new file mode 100644 index 0000000..a50cf37 --- /dev/null +++ b/Firestore/Source/API/FIRGeoPoint.m @@ -0,0 +1,85 @@ +/* + * 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 "FIRGeoPoint+Internal.h" + +#import "FSTComparison.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRGeoPoint + +- (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude { + if (self = [super init]) { + if (latitude < -90 || latitude > 90 || !isfinite(latitude)) { + FSTThrowInvalidArgument( + @"GeoPoint requires a latitude value in the range of [-90, 90], " + "but was %f", + latitude); + } + if (longitude < -180 || longitude > 180 || !isfinite(longitude)) { + FSTThrowInvalidArgument( + @"GeoPoint requires a longitude value in the range of [-180, 180], " + "but was %f", + longitude); + } + + _latitude = latitude; + _longitude = longitude; + } + return self; +} + +- (NSComparisonResult)compare:(FIRGeoPoint *)other { + NSComparisonResult result = FSTCompareDoubles(self.latitude, other.latitude); + if (result != NSOrderedSame) { + return result; + } else { + return FSTCompareDoubles(self.longitude, other.longitude); + } +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"<FIRGeoPoint: (%f, %f)>", self.latitude, self.longitude]; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FIRGeoPoint class]]) { + return NO; + } + FIRGeoPoint *otherGeoPoint = (FIRGeoPoint *)other; + return FSTDoubleBitwiseEquals(self.latitude, otherGeoPoint.latitude) && + FSTDoubleBitwiseEquals(self.longitude, otherGeoPoint.longitude); +} + +- (NSUInteger)hash { + return 31 * FSTDoubleBitwiseHash(self.latitude) + FSTDoubleBitwiseHash(self.longitude); +} + +/** Implements NSCopying without actually copying because geopoints are immutable. */ +- (id)copyWithZone:(NSZone *_Nullable)zone { + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRListenerRegistration+Internal.h b/Firestore/Source/API/FIRListenerRegistration+Internal.h new file mode 100644 index 0000000..4cd2d57 --- /dev/null +++ b/Firestore/Source/API/FIRListenerRegistration+Internal.h @@ -0,0 +1,34 @@ +/* + * 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 "FIRListenerRegistration.h" + +@class FSTAsyncQueryListener; +@class FSTFirestoreClient; +@class FSTQueryListener; + +NS_ASSUME_NONNULL_BEGIN + +/** Private implementation of the FIRListenerRegistration protocol. */ +@interface FSTListenerRegistration : NSObject <FIRListenerRegistration> + +- (instancetype)initWithClient:(FSTFirestoreClient *)client + asyncListener:(FSTAsyncQueryListener *)asyncListener + internalListener:(FSTQueryListener *)internalListener; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRListenerRegistration.m b/Firestore/Source/API/FIRListenerRegistration.m new file mode 100644 index 0000000..9ce0127 --- /dev/null +++ b/Firestore/Source/API/FIRListenerRegistration.m @@ -0,0 +1,57 @@ +/* + * 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 "FIRListenerRegistration+Internal.h" + +#import "FSTAsyncQueryListener.h" +#import "FSTFirestoreClient.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTListenerRegistration () + +/** The client that was used to register this listen. */ +@property(nonatomic, strong, readonly) FSTFirestoreClient *client; + +/** The async listener that is used to mute events synchronously. */ +@property(nonatomic, strong, readonly) FSTAsyncQueryListener *asyncListener; + +/** The internal FSTQueryListener that can be used to unlisten the query. */ +@property(nonatomic, strong, readwrite) FSTQueryListener *internalListener; + +@end + +@implementation FSTListenerRegistration + +- (instancetype)initWithClient:(FSTFirestoreClient *)client + asyncListener:(FSTAsyncQueryListener *)asyncListener + internalListener:(FSTQueryListener *)internalListener { + if (self = [super init]) { + _client = client; + _asyncListener = asyncListener; + _internalListener = internalListener; + } + return self; +} + +- (void)remove { + [self.asyncListener mute]; + [self.client removeListener:self.internalListener]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery+Internal.h b/Firestore/Source/API/FIRQuery+Internal.h new file mode 100644 index 0000000..3c2b2a7 --- /dev/null +++ b/Firestore/Source/API/FIRQuery+Internal.h @@ -0,0 +1,29 @@ +/* + * 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 "FIRQuery.h" + +@class FSTQuery; + +NS_ASSUME_NONNULL_BEGIN + +/** Internal FIRQuery API we don't want exposed in our public header files. */ +@interface FIRQuery (Internal) ++ (FIRQuery *)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore; +@property(nonatomic, strong, readonly) FSTQuery *query; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery.m b/Firestore/Source/API/FIRQuery.m new file mode 100644 index 0000000..63244fd --- /dev/null +++ b/Firestore/Source/API/FIRQuery.m @@ -0,0 +1,520 @@ +/* + * 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 "FIRQuery.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRDocumentReference.h" +#import "FIRDocumentSnapshot+Internal.h" +#import "FIRFieldPath+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRListenerRegistration+Internal.h" +#import "FIRQuery+Internal.h" +#import "FIRQuerySnapshot+Internal.h" +#import "FIRQuery_Init.h" +#import "FIRSnapshotMetadata+Internal.h" +#import "FSTAssert.h" +#import "FSTAsyncQueryListener.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTEventManager.h" +#import "FSTFieldValue.h" +#import "FSTFirestoreClient.h" +#import "FSTPath.h" +#import "FSTQuery.h" +#import "FSTUsageValidation.h" +#import "FSTUserDataConverter.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQueryListenOptions () + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRQueryListenOptions + ++ (instancetype)options { + return [[FIRQueryListenOptions alloc] init]; +} + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { + if (self = [super init]) { + _includeQueryMetadataChanges = includeQueryMetadataChanges; + _includeDocumentMetadataChanges = includeDocumentMetadataChanges; + } + return self; +} + +- (instancetype)init { + return [self initWithIncludeQueryMetadataChanges:NO includeDocumentMetadataChanges:NO]; +} + +- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges { + return [[FIRQueryListenOptions alloc] + initWithIncludeQueryMetadataChanges:includeQueryMetadataChanges + includeDocumentMetadataChanges:_includeDocumentMetadataChanges]; +} + +- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { + return [[FIRQueryListenOptions alloc] + initWithIncludeQueryMetadataChanges:_includeQueryMetadataChanges + includeDocumentMetadataChanges:includeDocumentMetadataChanges]; +} + +@end + +@interface FIRQuery () +@property(nonatomic, strong, readonly) FSTQuery *query; +@end + +@implementation FIRQuery (Internal) ++ (instancetype)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { + return [[FIRQuery alloc] initWithQuery:query firestore:firestore]; +} +@end + +@implementation FIRQuery + +#pragma mark - Public Methods + +- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { + if (self = [super init]) { + _query = query; + _firestore = firestore; + } + return self; +} + +- (void)getDocumentsWithCompletion:(void (^)(FIRQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error))completion { + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:YES]; + + dispatch_semaphore_t registered = dispatch_semaphore_create(0); + __block id<FIRListenerRegistration> listenerRegistration; + FIRQuerySnapshotBlock listener = ^(FIRQuerySnapshot *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]; + + completion(snapshot, nil); + }; + + listenerRegistration = [self addSnapshotListenerInternalWithOptions:options listener:listener]; + dispatch_semaphore_signal(registered); +} + +- (id<FIRListenerRegistration>)addSnapshotListener:(FIRQuerySnapshotBlock)listener { + return [self addSnapshotListenerWithOptions:nil listener:listener]; +} + +- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions: + (nullable FIRQueryListenOptions *)options + listener:(FIRQuerySnapshotBlock)listener { + return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] + listener:listener]; +} + +- (id<FIRListenerRegistration>) +addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions + listener:(FIRQuerySnapshotBlock)listener { + FIRFirestore *firestore = self.firestore; + FSTQuery *query = self.query; + + FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { + if (error) { + listener(nil, error); + return; + } + + FIRSnapshotMetadata *metadata = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites + fromCache:snapshot.fromCache]; + + listener([FIRQuerySnapshot snapshotWithFirestore:firestore + originalQuery:query + snapshot:snapshot + metadata:metadata], + 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]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isLessThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isLessThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual + field:field + value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThan:(id)value { + return + [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual + field:field + value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryOrderedByField:(NSString *)field { + return + [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] descending:NO]; +} + +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath { + return [self queryOrderedByFieldPath:fieldPath descending:NO]; +} + +- (FIRQuery *)queryOrderedByField:(NSString *)field descending:(BOOL)descending { + return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] + descending:descending]; +} + +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath descending:(BOOL)descending { + [self validateNewOrderByPath:fieldPath.internalValue]; + if (self.query.startAt) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You must not specify a starting point before specifying the order by."); + } + if (self.query.endAt) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You must not specify an ending point before specifying the order by."); + } + FSTSortOrder *sortOrder = + [FSTSortOrder sortOrderWithFieldPath:fieldPath.internalValue ascending:!descending]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingSortOrder:sortOrder] + firestore:self.firestore]; +} + +- (FIRQuery *)queryLimitedTo:(NSInteger)limit { + if (limit <= 0) { + FSTThrowInvalidArgument(@"Invalid Query. Query limit (%ld) is invalid. Limit must be positive.", + (long)limit); + } + return [FIRQuery referenceWithQuery:[self.query queryBySettingLimit:limit] firestore:_firestore]; +} + +- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +#pragma mark - Private Methods + +/** Private helper for all of the queryWhereField: methods. */ +- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator + field:(NSString *)field + value:(id)value { + return [self queryWithFilterOperator:filterOperator + path:[FIRFieldPath pathWithDotSeparatedString:field].internalValue + value:value]; +} + +- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator + path:(FSTFieldPath *)fieldPath + value:(id)value { + FSTFieldValue *fieldValue; + if ([fieldPath isKeyFieldPath]) { + if ([value isKindOfClass:[NSString class]]) { + NSString *documentKey = (NSString *)value; + if ([documentKey containsString:@"/"]) { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide " + "a valid document ID, but '%@' contains a '/' character.", + documentKey); + } else if (documentKey.length == 0) { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide " + "a valid document ID, but it was an empty string."); + } + FSTResourcePath *path = [self.query.path pathByAppendingSegment:documentKey]; + fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithPath:path] + databaseID:self.firestore.databaseID]; + } else if ([value isKindOfClass:[FIRDocumentReference class]]) { + FIRDocumentReference *ref = (FIRDocumentReference *)value; + fieldValue = [FSTReferenceValue referenceValue:ref.key databaseID:self.firestore.databaseID]; + } else { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide a " + "valid string or DocumentReference, but it was of type: %@", + NSStringFromClass([value class])); + } + } else { + fieldValue = [self.firestore.dataConverter parsedQueryValue:value]; + } + + id<FSTFilter> filter; + if ([fieldValue isEqual:[FSTNullValue nullValue]]) { + if (filterOperator != FSTRelationFilterOperatorEqual) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid Query. You can only perform equality comparisons on nil / " + "NSNull."); + } + filter = [[FSTNullFilter alloc] initWithField:fieldPath]; + } else if ([fieldValue isEqual:[FSTDoubleValue nanValue]]) { + if (filterOperator != FSTRelationFilterOperatorEqual) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid Query. You can only perform equality comparisons on NaN."); + } + filter = [[FSTNanFilter alloc] initWithField:fieldPath]; + } else { + filter = [FSTRelationFilter filterWithField:fieldPath + filterOperator:filterOperator + value:fieldValue]; + [self validateNewRelationFilter:filter]; + } + return [FIRQuery referenceWithQuery:[self.query queryByAddingFilter:filter] + firestore:self.firestore]; +} + +- (void)validateNewRelationFilter:(FSTRelationFilter *)filter { + if ([filter isInequality]) { + FSTFieldPath *existingField = [self.query inequalityFilterField]; + if (existingField && ![existingField isEqual:filter.field]) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid Query. All where filters with an inequality " + "(lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same " + "field. But you have inequality filters on '%@' and '%@'", + existingField, filter.field); + } + + FSTFieldPath *firstOrderByField = [self.query firstSortOrderField]; + if (firstOrderByField) { + [self validateOrderByField:firstOrderByField matchesInequalityField:filter.field]; + } + } +} + +- (void)validateNewOrderByPath:(FSTFieldPath *)fieldPath { + if (![self.query firstSortOrderField]) { + // This is the first order by. It must match any inequality. + FSTFieldPath *inequalityField = [self.query inequalityFilterField]; + if (inequalityField) { + [self validateOrderByField:fieldPath matchesInequalityField:inequalityField]; + } + } +} + +- (void)validateOrderByField:(FSTFieldPath *)orderByField + matchesInequalityField:(FSTFieldPath *)inequalityField { + if (!([orderByField isEqual:inequalityField])) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You have a where filter with an " + "inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) on field '%@' " + "and so you must also use '%@' as your first queryOrderedBy field, but your first " + "queryOrderedBy is currently on field '%@' instead.", + inequalityField, inequalityField, orderByField); + } +} + +/** + * Create a FSTBound from a query given the document. + * + * Note that the FSTBound will always include the key of the document and the position will be + * unambiguous. + * + * Will throw if the document does not contain all fields of the order by of the query. + */ +- (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)isBefore { + if (![snapshot exists]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a document " + @"that doesn't exist."); + } + FSTDocument *document = snapshot.internalDocument; + NSMutableArray<FSTFieldValue *> *components = [NSMutableArray array]; + + // Because people expect to continue/end a query at the exact document provided, we need to + // use the implicit sort order rather than the explicit sort order, because it's guaranteed to + // contain the document key. That way the position becomes unambiguous and the query + // continues/ends exactly at the provided document. Without the key (by using the explicit sort + // orders), multiple documents could match the position, yielding duplicate results. + for (FSTSortOrder *sortOrder in self.query.sortOrders) { + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + [components addObject:[FSTReferenceValue referenceValue:document.key + databaseID:self.firestore.databaseID]]; + } else { + FSTFieldValue *value = [document fieldForPath:sortOrder.field]; + if (value != nil) { + [components addObject:value]; + } else { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a " + "document for which the field '%@' (used as the order by) " + "does not exist.", + sortOrder.field.canonicalString); + } + } + } + return [FSTBound boundWithPosition:components isBefore:isBefore]; +} + +/** Converts a list of field values to an FSTBound. */ +- (FSTBound *)boundFromFieldValues:(NSArray<id> *)fieldValues isBefore:(BOOL)isBefore { + // Use explicit sort order because it has to match the query the user made + NSArray<FSTSortOrder *> *explicitSortOrders = self.query.explicitSortOrders; + if (fieldValues.count > explicitSortOrders.count) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using more values " + @"than were specified in the order by."); + } + + NSMutableArray<FSTFieldValue *> *components = [NSMutableArray array]; + [fieldValues enumerateObjectsUsingBlock:^(id rawValue, NSUInteger idx, BOOL *stop) { + FSTSortOrder *sortOrder = explicitSortOrders[idx]; + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + if (![rawValue isKindOfClass:[NSString class]]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. Expected a string for the document ID."); + } + NSString *documentID = (NSString *)rawValue; + if ([documentID containsString:@"/"]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. Document ID '%@' contains a slash.", documentID); + } + FSTDocumentKey *key = + [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:documentID]]; + [components + addObject:[FSTReferenceValue referenceValue:key databaseID:self.firestore.databaseID]]; + } else { + FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue]; + [components addObject:fieldValue]; + } + }]; + + return [FSTBound boundWithPosition:components isBefore:isBefore]; +} + +/** Converts the public API options object to the internal options object. */ +- (FSTListenOptions *)internalOptions:(nullable FIRQueryListenOptions *)options { + return [[FSTListenOptions alloc] + initWithIncludeQueryMetadataChanges:options.includeQueryMetadataChanges + includeDocumentMetadataChanges:options.includeDocumentMetadataChanges + waitForSyncWhenOnline:NO]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot+Internal.h b/Firestore/Source/API/FIRQuerySnapshot+Internal.h new file mode 100644 index 0000000..3a1e9db --- /dev/null +++ b/Firestore/Source/API/FIRQuerySnapshot+Internal.h @@ -0,0 +1,37 @@ +/* + * 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 "FIRQuerySnapshot.h" + +@class FIRFirestore; +@class FIRSnapshotMetadata; +@class FSTDocumentSet; +@class FSTQuery; +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +/** Internal FIRQuerySnapshot API we don't want exposed in our public header files. */ +@interface FIRQuerySnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot.m b/Firestore/Source/API/FIRQuerySnapshot.m new file mode 100644 index 0000000..4bf4edf --- /dev/null +++ b/Firestore/Source/API/FIRQuerySnapshot.m @@ -0,0 +1,125 @@ +/* + * 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 "FIRQuerySnapshot+Internal.h" + +#import "FIRDocumentChange+Internal.h" +#import "FIRDocumentSnapshot+Internal.h" +#import "FIRQuery+Internal.h" +#import "FIRSnapshotMetadata.h" +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentSet.h" +#import "FSTQuery.h" +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQuerySnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) FSTQuery *originalQuery; +@property(nonatomic, strong, readonly) FSTViewSnapshot *snapshot; + +@end + +@implementation FIRQuerySnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata { + return [[FIRQuerySnapshot alloc] initWithFirestore:firestore + originalQuery:query + snapshot:snapshot + metadata:metadata]; +} + +@end + +@implementation FIRQuerySnapshot { + // Cached value of the documents property. + NSArray<FIRDocumentSnapshot *> *_documents; + + // Cached value of the documentChanges property. + NSArray<FIRDocumentChange *> *_documentChanges; +} + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata { + if (self = [super init]) { + _firestore = firestore; + _originalQuery = query; + _snapshot = snapshot; + _metadata = metadata; + } + return self; +} + +@dynamic empty; + +- (FIRQuery *)query { + return [FIRQuery referenceWithQuery:self.originalQuery firestore:self.firestore]; +} + +- (BOOL)isEmpty { + return self.snapshot.documents.isEmpty; +} + +// This property is exposed as an NSInteger instead of an NSUInteger since (as of Xcode 8.1) +// Swift bridges NSUInteger as UInt, and we want to avoid forcing Swift users to cast their ints +// where we can. See cr/146959032 for additional context. +- (NSInteger)count { + return self.snapshot.documents.count; +} + +- (NSArray<FIRDocumentSnapshot *> *)documents { + if (!_documents) { + FSTDocumentSet *documentSet = self.snapshot.documents; + FIRFirestore *firestore = self.firestore; + BOOL fromCache = self.metadata.fromCache; + + NSMutableArray<FIRDocumentSnapshot *> *result = [NSMutableArray array]; + for (FSTDocument *document in documentSet.documentEnumerator) { + [result addObject:[FIRDocumentSnapshot snapshotWithFirestore:firestore + documentKey:document.key + document:document + fromCache:fromCache]]; + } + + _documents = result; + } + return _documents; +} + +- (NSArray<FIRDocumentChange *> *)documentChanges { + if (!_documentChanges) { + _documentChanges = + [FIRDocumentChange documentChangesForSnapshot:self.snapshot firestore:self.firestore]; + } + return _documentChanges; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery_Init.h b/Firestore/Source/API/FIRQuery_Init.h new file mode 100644 index 0000000..d6b0f37 --- /dev/null +++ b/Firestore/Source/API/FIRQuery_Init.h @@ -0,0 +1,32 @@ +/* + * 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 "FIRQuery.h" + +@class FSTQuery; + +NS_ASSUME_NONNULL_BEGIN + +/** + * An Internal class extension for `FIRQuery` that exposes the init method to classes + * that need to derive from it. + */ +@interface FIRQuery (/*Init*/) +- (instancetype)initWithQuery:(FSTQuery *)query + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSetOptions+Internal.h b/Firestore/Source/API/FIRSetOptions+Internal.h new file mode 100644 index 0000000..9118096 --- /dev/null +++ b/Firestore/Source/API/FIRSetOptions+Internal.h @@ -0,0 +1,33 @@ +/* + * 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 "FIRSetOptions.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSetOptions () + +- (instancetype)initWithMerge:(BOOL)merge NS_DESIGNATED_INITIALIZER; + +@end + +@interface FIRSetOptions (Internal) + ++ (instancetype)overwrite; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSetOptions.m b/Firestore/Source/API/FIRSetOptions.m new file mode 100644 index 0000000..ea68c63 --- /dev/null +++ b/Firestore/Source/API/FIRSetOptions.m @@ -0,0 +1,65 @@ +/* + * 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 "FIRSetOptions+Internal.h" +#import "FSTMutation.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRSetOptions + +- (instancetype)initWithMerge:(BOOL)merge { + if (self = [super init]) { + _merge = merge; + } + return self; +} + ++ (instancetype)merge { + return [[FIRSetOptions alloc] initWithMerge:YES]; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FIRSetOptions class]]) { + return NO; + } + + FIRSetOptions *otherOptions = (FIRSetOptions *)other; + + return otherOptions.merge != self.merge; +} + +- (NSUInteger)hash { + return self.merge ? 1231 : 1237; +} +@end + +@implementation FIRSetOptions (Internal) + ++ (instancetype)overwrite { + static FIRSetOptions *overwriteInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + overwriteInstance = [[FIRSetOptions alloc] initWithMerge:NO]; + }); + return overwriteInstance; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotMetadata+Internal.h b/Firestore/Source/API/FIRSnapshotMetadata+Internal.h new file mode 100644 index 0000000..d3265cd --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotMetadata+Internal.h @@ -0,0 +1,29 @@ +/* + * 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 "FIRSnapshotMetadata.h" + +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotMetadata (Internal) + ++ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotMetadata.m b/Firestore/Source/API/FIRSnapshotMetadata.m new file mode 100644 index 0000000..ff49d8f --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotMetadata.m @@ -0,0 +1,49 @@ +/* + * 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 "FIRSnapshotMetadata.h" + +#import "FIRSnapshotMetadata+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotMetadata () + +- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache; + +@end + +@implementation FIRSnapshotMetadata (Internal) + ++ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { + return [[FIRSnapshotMetadata alloc] initWithPendingWrites:pendingWrites fromCache:fromCache]; +} + +@end + +@implementation FIRSnapshotMetadata + +- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { + if (self = [super init]) { + _pendingWrites = pendingWrites; + _fromCache = fromCache; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRTransaction+Internal.h b/Firestore/Source/API/FIRTransaction+Internal.h new file mode 100644 index 0000000..8fd3f65 --- /dev/null +++ b/Firestore/Source/API/FIRTransaction+Internal.h @@ -0,0 +1,27 @@ +/* + * 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 "FIRTransaction.h" + +@class FIRFirestore; +@class FSTTransaction; + +@interface FIRTransaction (Internal) + ++ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore; + +@end diff --git a/Firestore/Source/API/FIRTransaction.m b/Firestore/Source/API/FIRTransaction.m new file mode 100644 index 0000000..02006bb --- /dev/null +++ b/Firestore/Source/API/FIRTransaction.m @@ -0,0 +1,147 @@ +/* + * 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 "FIRTransaction+Internal.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRDocumentSnapshot+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRSetOptions+Internal.h" +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTTransaction.h" +#import "FSTUsageValidation.h" +#import "FSTUserDataConverter.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRTransaction + +@interface FIRTransaction () + +- (instancetype)initWithTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTTransaction *internalTransaction; +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@end + +@implementation FIRTransaction (Internal) + ++ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore { + return [[FIRTransaction alloc] initWithTransaction:transaction firestore:firestore]; +} + +@end + +@implementation FIRTransaction + +- (instancetype)initWithTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore { + self = [super init]; + if (self) { + _internalTransaction = transaction; + _firestore = firestore; + } + return self; +} + +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document { + return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; +} + +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options { + [self validateReference:document]; + FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options]; + [self.internalTransaction setData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)updateData:(NSDictionary<id, id> *)fields + forDocument:(FIRDocumentReference *)document { + [self validateReference:document]; + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + [self.internalTransaction updateData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document { + [self validateReference:document]; + [self.internalTransaction deleteDocument:document.key]; + return self; +} + +- (void)getDocument:(FIRDocumentReference *)document + completion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + [self validateReference:document]; + [self.internalTransaction + lookupDocumentsForKeys:@[ document.key ] + completion:^(NSArray<FSTMaybeDocument *> *_Nullable documents, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + FSTAssert(documents.count == 1, + @"Mismatch in docs returned from document lookup."); + FSTMaybeDocument *internalDoc = documents.firstObject; + if ([internalDoc isKindOfClass:[FSTDeletedDocument class]]) { + completion(nil, nil); + return; + } + FIRDocumentSnapshot *doc = + [FIRDocumentSnapshot snapshotWithFirestore:self.firestore + documentKey:internalDoc.key + document:(FSTDocument *)internalDoc + fromCache:NO]; + completion(doc, nil); + }]; +} + +- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document + error:(NSError *__autoreleasing *)error { + [self validateReference:document]; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block FIRDocumentSnapshot *result; + // We have to explicitly assign the innerError into a local to cause it to retain correctly. + __block NSError *outerError = nil; + [self getDocument:document + completion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable innerError) { + result = snapshot; + outerError = innerError; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (error) { + *error = outerError; + } + return result; +} + +- (void)validateReference:(FIRDocumentReference *)reference { + if (reference.firestore != self.firestore) { + FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRWriteBatch+Internal.h b/Firestore/Source/API/FIRWriteBatch+Internal.h new file mode 100644 index 0000000..a434e02 --- /dev/null +++ b/Firestore/Source/API/FIRWriteBatch+Internal.h @@ -0,0 +1,25 @@ +/* + * 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 "FIRWriteBatch.h" + +@class FIRFirestore; + +@interface FIRWriteBatch (Internal) + ++ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore; + +@end diff --git a/Firestore/Source/API/FIRWriteBatch.m b/Firestore/Source/API/FIRWriteBatch.m new file mode 100644 index 0000000..32b6ce8 --- /dev/null +++ b/Firestore/Source/API/FIRWriteBatch.m @@ -0,0 +1,116 @@ +/* + * 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 "FIRWriteBatch+Internal.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRSetOptions+Internal.h" +#import "FSTFirestoreClient.h" +#import "FSTMutation.h" +#import "FSTUsageValidation.h" +#import "FSTUserDataConverter.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRWriteBatch + +@interface FIRWriteBatch () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) NSMutableArray<FSTMutation *> *mutations; +@property(nonatomic, assign) BOOL committed; + +@end + +@implementation FIRWriteBatch (Internal) + ++ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore { + return [[FIRWriteBatch alloc] initWithFirestore:firestore]; +} + +@end + +@implementation FIRWriteBatch + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore { + self = [super init]; + if (self) { + _firestore = firestore; + _mutations = [NSMutableArray array]; + } + return self; +} + +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document { + return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; +} + +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options]; + [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key + precondition:[FSTPrecondition none]]]; + return self; +} + +- (FIRWriteBatch *)updateData:(NSDictionary<id, id> *)fields + forDocument:(FIRDocumentReference *)document { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + [self.mutations + addObjectsFromArray:[parsed mutationsWithKey:document.key + precondition:[FSTPrecondition preconditionWithExists:YES]]]; + return self; +} + +- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document { + [self verifyNotCommitted]; + [self validateReference:document]; + [self.mutations addObject:[[FSTDeleteMutation alloc] initWithKey:document.key + precondition:[FSTPrecondition none]]]; + return self; +} + +- (void)commitWithCompletion:(void (^)(NSError *_Nullable error))completion { + [self verifyNotCommitted]; + self.committed = TRUE; + [self.firestore.client writeMutations:self.mutations completion:completion]; +} + +- (void)verifyNotCommitted { + if (self.committed) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"A write batch can no longer be used after commit has been called."); + } +} + +- (void)validateReference:(FIRDocumentReference *)reference { + if (reference.firestore != self.firestore) { + FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FSTUserDataConverter.h b/Firestore/Source/API/FSTUserDataConverter.h new file mode 100644 index 0000000..69d1fa9 --- /dev/null +++ b/Firestore/Source/API/FSTUserDataConverter.h @@ -0,0 +1,124 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FIRSetOptions; +@class FSTDatabaseID; +@class FSTDocumentKey; +@class FSTObjectValue; +@class FSTFieldMask; +@class FSTFieldValue; +@class FSTFieldTransform; +@class FSTMutation; +@class FSTPrecondition; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** The result of parsing document data (e.g. for a setData call). */ +@interface FSTParsedSetData : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(nullable FSTFieldMask *)fieldMask + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms + NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTObjectValue *data; +@property(nonatomic, strong, readonly, nullable) FSTFieldMask *fieldMask; +@property(nonatomic, strong, readonly) NSArray<FSTFieldTransform *> *fieldTransforms; + +/** + * Converts the parsed document data into 1 or 2 mutations (depending on whether there are any + * field transforms) using the specified document key and precondition. + */ +- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition; + +@end + +/** The result of parsing "update" data (i.e. for an updateData call). */ +@interface FSTParsedUpdateData : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(FSTFieldMask *)fieldMask + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms + NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTObjectValue *data; +@property(nonatomic, strong, readonly) FSTFieldMask *fieldMask; +@property(nonatomic, strong, readonly) NSArray<FSTFieldTransform *> *fieldTransforms; + +/** + * Converts the parsed update data into 1 or 2 mutations (depending on whether there are any + * field transforms) using the specified document key and precondition. + */ +- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition; + +@end + +/** + * An internal representation of FIRDocumentReference, representing a key in a specific database. + * This is necessary because keys assume a database from context (usually the current one). + * FSTDocumentKeyReference binds a key to a specific databaseID. + * + * TODO(b/64160088): Make FSTDocumentKey aware of the specific databaseID it is tied to. + */ +@interface FSTDocumentKeyReference : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithKey:(FSTDocumentKey *)key + databaseID:(FSTDatabaseID *)databaseID NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; + +@end + +/** + * An interface that allows arbitrary pre-converting of user data. + * + * Returns the converted value (can return back the input to act as a no-op). + */ +typedef id _Nullable (^FSTPreConverterBlock)(id _Nullable); + +/** + * Helper for parsing raw user input (provided via the API) into internal model classes. + */ +@interface FSTUserDataConverter : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID + preConverter:(FSTPreConverterBlock)preConverter NS_DESIGNATED_INITIALIZER; + +/** Parse document data (e.g. from a setData call). */ +- (FSTParsedSetData *)parsedSetData:(id)input options:(FIRSetOptions *)options; + +/** Parse "update" data (i.e. from an updateData call). */ +- (FSTParsedUpdateData *)parsedUpdateData:(id)input; + +/** Parse a "query value" (e.g. value in a where filter or a value in a cursor bound). */ +- (FSTFieldValue *)parsedQueryValue:(id)input; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FSTUserDataConverter.m b/Firestore/Source/API/FSTUserDataConverter.m new file mode 100644 index 0000000..7a6c950 --- /dev/null +++ b/Firestore/Source/API/FSTUserDataConverter.m @@ -0,0 +1,568 @@ +/* + * 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 "FSTUserDataConverter.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRFieldPath+Internal.h" +#import "FIRFieldValue+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRGeoPoint.h" +#import "FIRSetOptions+Internal.h" +#import "FSTAssert.h" +#import "FSTDatabaseID.h" +#import "FSTDocumentKey.h" +#import "FSTFieldValue.h" +#import "FSTMutation.h" +#import "FSTPath.h" +#import "FSTTimestamp.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const RESERVED_FIELD_DESIGNATOR = @"__"; + +#pragma mark - FSTParsedSetData + +@implementation FSTParsedSetData +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(nullable FSTFieldMask *)fieldMask + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms { + self = [super init]; + if (self) { + _data = data; + _fieldMask = fieldMask; + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition { + NSMutableArray<FSTMutation *> *mutations = [NSMutableArray array]; + if (self.fieldMask) { + [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key + fieldMask:self.fieldMask + value:self.data + precondition:precondition]]; + } else { + [mutations addObject:[[FSTSetMutation alloc] initWithKey:key + value:self.data + precondition:precondition]]; + } + if (self.fieldTransforms.count > 0) { + [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:self.fieldTransforms]]; + } + return mutations; +} + +@end + +#pragma mark - FSTParsedUpdateData + +@implementation FSTParsedUpdateData +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(FSTFieldMask *)fieldMask + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms { + self = [super init]; + if (self) { + _data = data; + _fieldMask = fieldMask; + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition { + NSMutableArray<FSTMutation *> *mutations = [NSMutableArray array]; + [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key + fieldMask:self.fieldMask + value:self.data + precondition:precondition]]; + if (self.fieldTransforms.count > 0) { + [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:self.fieldTransforms]]; + } + return mutations; +} + +@end + +/** + * Represents what type of API method provided the data being parsed; useful for determining which + * error conditions apply during parsing and providing better error messages. + */ +typedef NS_ENUM(NSInteger, FSTUserDataSource) { + FSTUserDataSourceSet, + FSTUserDataSourceUpdate, + FSTUserDataSourceQueryValue, // from a where clause or cursor bound. +}; + +#pragma mark - FSTParseContext + +/** + * A "context" object passed around while parsing user data. + */ +@interface FSTParseContext : NSObject +/** The current path being parsed. */ +// TODO(b/34871131): path should never be nil, but we don't support array paths right now. +@property(strong, nonatomic, readonly, nullable) FSTFieldPath *path; + +/** + * What type of API method provided the data being parsed; useful for determining which error + * conditions apply during parsing and providing better error messages. + */ +@property(nonatomic, assign) FSTUserDataSource dataSource; +@property(nonatomic, strong, readonly) NSMutableArray<FSTFieldTransform *> *fieldTransforms; +@property(nonatomic, strong, readonly) NSMutableArray<FSTFieldPath *> *fieldMask; + +- (instancetype)init NS_UNAVAILABLE; +/** + * Initializes a FSTParseContext with the given source and path. + * + * @param dataSource Indicates what kind of API method this data came from. + * @param path A path within the object being parsed. This could be an empty path (in which case + * the context represents the root of the data being parsed), or a nonempty path (indicating the + * context represents a nested location within the data). + * + * TODO(b/34871131): We don't support array paths right now, so path can be nil to indicate + * the context represents any location within an array (in which case certain features will not work + * and errors will be somewhat compromised). + */ +- (instancetype)initWithSource:(FSTUserDataSource)dataSource + path:(nullable FSTFieldPath *)path + fieldTransforms:(NSMutableArray<FSTFieldTransform *> *)fieldTransforms + fieldMask:(NSMutableArray<FSTFieldPath *> *)fieldMask + NS_DESIGNATED_INITIALIZER; + +// Helpers to get a FSTParseContext for a child field. +- (instancetype)contextForField:(NSString *)fieldName; +- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath; +- (instancetype)contextForArrayIndex:(NSUInteger)index; +@end + +@implementation FSTParseContext + ++ (instancetype)contextWithSource:(FSTUserDataSource)dataSource path:(nullable FSTFieldPath *)path { + FSTParseContext *context = [[FSTParseContext alloc] initWithSource:dataSource + path:path + fieldTransforms:[NSMutableArray array] + fieldMask:[NSMutableArray array]]; + [context validatePath]; + return context; +} + +- (instancetype)initWithSource:(FSTUserDataSource)dataSource + path:(nullable FSTFieldPath *)path + fieldTransforms:(NSMutableArray<FSTFieldTransform *> *)fieldTransforms + fieldMask:(NSMutableArray<FSTFieldPath *> *)fieldMask { + if (self = [super init]) { + _dataSource = dataSource; + _path = path; + _fieldTransforms = fieldTransforms; + _fieldMask = fieldMask; + } + return self; +} + +- (instancetype)contextForField:(NSString *)fieldName { + FSTParseContext *context = + [[FSTParseContext alloc] initWithSource:self.dataSource + path:[self.path pathByAppendingSegment:fieldName] + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; + [context validatePathSegment:fieldName]; + return context; +} + +- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath { + FSTParseContext *context = + [[FSTParseContext alloc] initWithSource:self.dataSource + path:[self.path pathByAppendingPath:fieldPath] + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; + [context validatePath]; + return context; +} + +- (instancetype)contextForArrayIndex:(NSUInteger)index { + // TODO(b/34871131): We don't support array paths right now; so make path nil. + return [[FSTParseContext alloc] initWithSource:self.dataSource + path:nil + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; +} + +/** + * Returns a string that can be appended to error messages indicating what field caused the error. + */ +- (NSString *)fieldDescription { + // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. + if (!self.path || self.path.empty) { + return @""; + } else { + return [NSString stringWithFormat:@" (found in field %@)", self.path]; + } +} + +- (BOOL)isWrite { + return _dataSource == FSTUserDataSourceSet || _dataSource == FSTUserDataSourceUpdate; +} + +- (void)validatePath { + // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. + if (self.path == nil) { + return; + } + for (int i = 0; i < self.path.length; i++) { + [self validatePathSegment:[self.path segmentAtIndex:i]]; + } +} + +- (void)validatePathSegment:(NSString *)segment { + if ([self isWrite] && [segment hasPrefix:RESERVED_FIELD_DESIGNATOR] && + [segment hasSuffix:RESERVED_FIELD_DESIGNATOR]) { + FSTThrowInvalidArgument(@"Document fields cannot begin and end with %@%@", + RESERVED_FIELD_DESIGNATOR, [self fieldDescription]); + } +} + +@end + +#pragma mark - FSTDocumentKeyReference + +@implementation FSTDocumentKeyReference + +- (instancetype)initWithKey:(FSTDocumentKey *)key databaseID:(FSTDatabaseID *)databaseID { + self = [super init]; + if (self) { + _key = key; + _databaseID = databaseID; + } + return self; +} + +@end + +#pragma mark - FSTUserDataConverter + +@interface FSTUserDataConverter () +@property(strong, nonatomic, readonly) FSTDatabaseID *databaseID; +@property(strong, nonatomic, readonly) FSTPreConverterBlock preConverter; +@end + +@implementation FSTUserDataConverter + +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID + preConverter:(FSTPreConverterBlock)preConverter { + self = [super init]; + if (self) { + _databaseID = databaseID; + _preConverter = preConverter; + } + return self; +} + +- (FSTParsedSetData *)parsedSetData:(id)input options:(FIRSetOptions *)options { + // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust + // Obj-C to verify the type for us. + if (![input isKindOfClass:[NSDictionary class]]) { + FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); + } + + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceSet path:[FSTFieldPath emptyPath]]; + + __block FSTObjectValue *updateData = [FSTObjectValue objectValue]; + + [input enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + // Treat key as a complete field name (don't split on dots, etc.) + FSTFieldPath *path = [[FIRFieldPath alloc] initWithFields:@[ key ]].internalValue; + + value = self.preConverter(value); + + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForFieldPath:path]]; + if (parsedValue) { + updateData = [updateData objectBySettingValue:parsedValue forPath:path]; + } + }]; + + return [[FSTParsedSetData alloc] + initWithData:updateData + fieldMask:options.merge ? [[FSTFieldMask alloc] initWithFields:context.fieldMask] : nil + fieldTransforms:context.fieldTransforms]; +} + +- (FSTParsedUpdateData *)parsedUpdateData:(id)input { + // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust + // Obj-C to verify the type for us. + if (![input isKindOfClass:[NSDictionary class]]) { + FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); + } + + NSDictionary *dict = input; + + NSMutableArray<FSTFieldPath *> *fieldMaskPaths = [NSMutableArray array]; + __block FSTObjectValue *updateData = [FSTObjectValue objectValue]; + + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceUpdate path:[FSTFieldPath emptyPath]]; + [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + FSTFieldPath *path; + + if ([key isKindOfClass:[NSString class]]) { + path = [FIRFieldPath pathWithDotSeparatedString:key].internalValue; + } else if ([key isKindOfClass:[FIRFieldPath class]]) { + path = ((FIRFieldPath *)key).internalValue; + } else { + FSTThrowInvalidArgument( + @"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."); + } + + value = self.preConverter(value); + if ([value isKindOfClass:[FSTDeleteFieldValue class]]) { + // Add it to the field mask, but don't add anything to updateData. + [fieldMaskPaths addObject:path]; + } else { + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForFieldPath:path]]; + if (parsedValue) { + [fieldMaskPaths addObject:path]; + updateData = [updateData objectBySettingValue:parsedValue forPath:path]; + } + } + }]; + + FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:fieldMaskPaths]; + return [[FSTParsedUpdateData alloc] initWithData:updateData + fieldMask:mask + fieldTransforms:context.fieldTransforms]; +} + +- (FSTFieldValue *)parsedQueryValue:(id)input { + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceQueryValue path:[FSTFieldPath emptyPath]]; + FSTFieldValue *_Nullable parsed = [self parseData:input context:context]; + FSTAssert(parsed, @"Parsed data should not be nil."); + FSTAssert(context.fieldTransforms.count == 0, @"Field transforms should have been disallowed."); + return parsed; +} + +/** + * Internal helper for parsing user data. + * + * @param input Data to be parsed. + * @param context A context object representing the current path being parsed, the source of the + * data being parsed, etc. + * + * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be + * included in the resulting parsed data. + */ +- (nullable FSTFieldValue *)parseData:(id)input context:(FSTParseContext *)context { + input = self.preConverter(input); + if ([input isKindOfClass:[NSArray class]]) { + // TODO(b/34871131): We may need a different way to detect nested arrays once we support array + // paths (at which point we should include the path containing the array in the error message). + if (!context.path) { + FSTThrowInvalidArgument(@"Nested arrays are not supported"); + } + NSArray *array = input; + NSMutableArray<FSTFieldValue *> *result = [NSMutableArray arrayWithCapacity:array.count]; + [array enumerateObjectsUsingBlock:^(id entry, NSUInteger idx, BOOL *stop) { + FSTFieldValue *_Nullable parsedEntry = + [self parseData:entry context:[context contextForArrayIndex:idx]]; + if (!parsedEntry) { + // Just include nulls in the array for fields being replaced with a sentinel. + parsedEntry = [FSTNullValue nullValue]; + } + [result addObject:parsedEntry]; + }]; + // We don't support field mask paths more granular than the top-level array. + [context.fieldMask addObject:context.path]; + return [[FSTArrayValue alloc] initWithValueNoCopy:result]; + + } else if ([input isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = input; + NSMutableDictionary<NSString *, FSTFieldValue *> *result = + [NSMutableDictionary dictionaryWithCapacity:dict.count]; + [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForField:key]]; + if (parsedValue) { + result[key] = parsedValue; + } + }]; + return [[FSTObjectValue alloc] initWithDictionary:result]; + + } else { + // If context.path is null, we are inside an array and we should have already added the root of + // the array to the field mask. + if (context.path) { + [context.fieldMask addObject:context.path]; + } + return [self parseScalarValue:input context:context]; + } +} + +/** + * Helper to parse a scalar value (i.e. not an NSDictionary or NSArray). + * + * Note that it handles all NSNumber values that are encodable as int64_t or doubles + * (depending on the underlying type of the NSNumber). Unsigned integer values are handled though + * any value outside what is representable by int64_t (a signed 64-bit value) will throw an + * exception. + * + * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be + * included in the resulting parsed data. + */ +- (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTParseContext *)context { + if (!input || [input isMemberOfClass:[NSNull class]]) { + return [FSTNullValue nullValue]; + + } else if ([input isKindOfClass:[NSNumber class]]) { + // Recover the underlying type of the number, using the method described here: + // http://stackoverflow.com/questions/2518761/get-type-of-nsnumber + const char *cType = [input objCType]; + + // Type Encoding values taken from + // https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/ + // Articles/ocrtTypeEncodings.html + switch (cType[0]) { + case 'q': + return [FSTIntegerValue integerValue:[input longLongValue]]; + + case 'i': // Falls through. + case 's': // Falls through. + case 'l': // Falls through. + case 'I': // Falls through. + case 'S': + // Coerce integer values that aren't long long. Allow unsigned integer types that are + // guaranteed small enough to skip a length check. + return [FSTIntegerValue integerValue:[input longLongValue]]; + + case 'L': // Falls through. + case 'Q': + // Unsigned integers that could be too large. Note that the 'L' (long) case is handled here + // because when compiled for LP64, unsigned long is 64 bits and could overflow int64_t. + { + unsigned long long extended = [input unsignedLongLongValue]; + + if (extended > LLONG_MAX) { + FSTThrowInvalidArgument(@"NSNumber (%llu) is too large%@", + [input unsignedLongLongValue], [context fieldDescription]); + + } else { + return [FSTIntegerValue integerValue:(int64_t)extended]; + } + } + + case 'f': + return [FSTDoubleValue doubleValue:[input doubleValue]]; + + case 'd': + // Double values are already the right type, so just reuse the existing boxed double. + // + // Note that NSNumber already performs NaN normalization to a single shared instance + // so there's no need to treat NaN specially here. + return [FSTDoubleValue doubleValue:[input doubleValue]]; + + case 'B': // Falls through. + case 'c': // Falls through. + case 'C': + // Boolean values are weird. + // + // On arm64, objCType of a BOOL-valued NSNumber will be "c", even though @encode(BOOL) + // returns "B". "c" is the same as @encode(signed char). Unfortunately this means that + // legitimate usage of signed chars is impossible, but this should be rare. + // + // Additionally, for consistency, map unsigned chars to bools in the same way. + return [FSTBooleanValue booleanValue:[input boolValue]]; + + default: + // All documented codes should be handled above, so this shouldn't happen. + FSTCFail(@"Unknown NSNumber objCType %s on %@", cType, input); + } + + } else if ([input isKindOfClass:[NSString class]]) { + return [FSTStringValue stringValue:input]; + + } else if ([input isKindOfClass:[NSDate class]]) { + return [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:input]]; + + } else if ([input isKindOfClass:[FIRGeoPoint class]]) { + return [FSTGeoPointValue geoPointValue:input]; + + } else if ([input isKindOfClass:[NSData class]]) { + return [FSTBlobValue blobValue:input]; + + } else if ([input isKindOfClass:[FSTDocumentKeyReference class]]) { + FSTDocumentKeyReference *reference = input; + if (![reference.databaseID isEqual:self.databaseID]) { + FSTDatabaseID *other = reference.databaseID; + FSTThrowInvalidArgument( + @"Document Reference is for database %@/%@ but should be for database %@/%@%@", + other.projectID, other.databaseID, self.databaseID.projectID, self.databaseID.databaseID, + [context fieldDescription]); + } + return [FSTReferenceValue referenceValue:reference.key databaseID:self.databaseID]; + + } else if ([input isKindOfClass:[FIRFieldValue class]]) { + if ([input isKindOfClass:[FSTDeleteFieldValue class]]) { + // We shouldn't encounter delete sentinels here. Provide a good error. + if (context.dataSource != FSTUserDataSourceUpdate) { + FSTThrowInvalidArgument(@"FieldValue.delete() can only be used with updateData()."); + } else { + FSTAssert(context.path.length > 0, + @"FieldValue.delete() at the top level should have already been handled."); + FSTThrowInvalidArgument( + @"FieldValue.delete() can only appear at the top level of your " + "update data%@", + [context fieldDescription]); + } + } else if ([input isKindOfClass:[FSTServerTimestampFieldValue class]]) { + if (context.dataSource != FSTUserDataSourceSet && + context.dataSource != FSTUserDataSourceUpdate) { + FSTThrowInvalidArgument( + @"FieldValue.serverTimestamp() can only be used with setData() and updateData()."); + } + if (!context.path) { + FSTThrowInvalidArgument( + @"FieldValue.serverTimestamp() is not currently supported inside arrays%@", + [context fieldDescription]); + } + [context.fieldTransforms + addObject:[[FSTFieldTransform alloc] + initWithPath:context.path + transform:[FSTServerTimestampTransform serverTimestampTransform]]]; + + // Return nil so this value is omitted from the parsed result. + return nil; + } else { + FSTFail(@"Unknown FIRFieldValue type: %@", NSStringFromClass([input class])); + } + + } else { + FSTThrowInvalidArgument(@"Unsupported type: %@%@", NSStringFromClass([input class]), + [context fieldDescription]); + } +} + +@end + +NS_ASSUME_NONNULL_END |