| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "CloudFirestorePlugin.h" |
| |
| #import <Firebase/Firebase.h> |
| |
| @interface NSError (FlutterError) |
| @property(readonly, nonatomic) FlutterError *flutterError; |
| @end |
| |
| @implementation NSError (FlutterError) |
| - (FlutterError *)flutterError { |
| return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", self.code] |
| message:self.domain |
| details:self.localizedDescription]; |
| } |
| @end |
| |
| FIRQuery *getQuery(NSDictionary *arguments) { |
| FIRQuery *query = [[FIRFirestore firestore] collectionWithPath:arguments[@"path"]]; |
| NSDictionary *parameters = arguments[@"parameters"]; |
| NSArray *whereConditions = parameters[@"where"]; |
| for (id item in whereConditions) { |
| NSArray *condition = item; |
| NSString *fieldName = condition[0]; |
| NSString *op = condition[1]; |
| id value = condition[2]; |
| if ([op isEqualToString:@"=="]) { |
| query = [query queryWhereField:fieldName isEqualTo:value]; |
| } else if ([op isEqualToString:@"<"]) { |
| query = [query queryWhereField:fieldName isLessThan:value]; |
| } else if ([op isEqualToString:@"<="]) { |
| query = [query queryWhereField:fieldName isLessThanOrEqualTo:value]; |
| } else if ([op isEqualToString:@">"]) { |
| query = [query queryWhereField:fieldName isGreaterThan:value]; |
| } else if ([op isEqualToString:@">="]) { |
| query = [query queryWhereField:fieldName isGreaterThanOrEqualTo:value]; |
| } else { |
| // Unsupported operator |
| } |
| } |
| id limit = parameters[@"limit"]; |
| if (limit) { |
| NSNumber *length = limit; |
| query = [query queryLimitedTo:[length intValue]]; |
| } |
| NSArray *orderBy = parameters[@"orderBy"]; |
| if (orderBy) { |
| for (id item in orderBy) { |
| NSArray *orderByParameters = item; |
| NSString *fieldName = orderByParameters[0]; |
| NSNumber *descending = orderByParameters[1]; |
| query = [query queryOrderedByField:fieldName descending:[descending boolValue]]; |
| } |
| } |
| id startAt = parameters[@"startAt"]; |
| if (startAt) { |
| NSArray *startAtValues = startAt; |
| query = [query queryStartingAtValues:startAtValues]; |
| } |
| id startAfter = parameters[@"startAfter"]; |
| if (startAfter) { |
| NSArray *startAfterValues = startAfter; |
| query = [query queryStartingAfterValues:startAfterValues]; |
| } |
| id endAt = parameters[@"endAt"]; |
| if (endAt) { |
| NSArray *endAtValues = endAt; |
| query = [query queryEndingAtValues:endAtValues]; |
| } |
| id endBefore = parameters[@"endBefore"]; |
| if (endBefore) { |
| NSArray *endBeforeValues = endBefore; |
| query = [query queryEndingBeforeValues:endBeforeValues]; |
| } |
| return query; |
| } |
| |
| NSDictionary *parseQuerySnapshot(FIRQuerySnapshot *snapshot) { |
| NSMutableArray *paths = [NSMutableArray array]; |
| NSMutableArray *documents = [NSMutableArray array]; |
| for (FIRDocumentSnapshot *document in snapshot.documents) { |
| [paths addObject:document.reference.path]; |
| [documents addObject:document.data]; |
| } |
| NSMutableArray *documentChanges = [NSMutableArray array]; |
| for (FIRDocumentChange *documentChange in snapshot.documentChanges) { |
| NSString *type; |
| switch (documentChange.type) { |
| case FIRDocumentChangeTypeAdded: |
| type = @"DocumentChangeType.added"; |
| break; |
| case FIRDocumentChangeTypeModified: |
| type = @"DocumentChangeType.modified"; |
| break; |
| case FIRDocumentChangeTypeRemoved: |
| type = @"DocumentChangeType.removed"; |
| break; |
| } |
| [documentChanges addObject:@{ |
| @"type" : type, |
| @"document" : documentChange.document.data, |
| @"path" : documentChange.document.reference.path, |
| @"oldIndex" : [NSNumber numberWithInt:documentChange.oldIndex], |
| @"newIndex" : [NSNumber numberWithInt:documentChange.newIndex], |
| }]; |
| } |
| return @{ |
| @"paths" : paths, |
| @"documentChanges" : documentChanges, |
| @"documents" : documents, |
| }; |
| } |
| |
| @interface FLTCloudFirestorePlugin () |
| @property(nonatomic, retain) FlutterMethodChannel *channel; |
| @end |
| |
| @implementation FLTCloudFirestorePlugin { |
| NSMutableDictionary<NSNumber *, id<FIRListenerRegistration>> *_listeners; |
| int _nextListenerHandle; |
| NSMutableDictionary *transactions; |
| NSMutableDictionary *transactionResults; |
| } |
| |
| + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar { |
| FlutterMethodChannel *channel = |
| [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/cloud_firestore" |
| binaryMessenger:[registrar messenger]]; |
| FLTCloudFirestorePlugin *instance = [[FLTCloudFirestorePlugin alloc] init]; |
| instance.channel = channel; |
| [registrar addMethodCallDelegate:instance channel:channel]; |
| } |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| if (![FIRApp defaultApp]) { |
| [FIRApp configure]; |
| } |
| _listeners = [NSMutableDictionary<NSNumber *, id<FIRListenerRegistration>> dictionary]; |
| _nextListenerHandle = 0; |
| transactions = [NSMutableDictionary<NSNumber *, FIRTransaction *> dictionary]; |
| transactionResults = [NSMutableDictionary<NSNumber *, id> dictionary]; |
| } |
| return self; |
| } |
| |
| - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { |
| void (^defaultCompletionBlock)(NSError *) = ^(NSError *error) { |
| result(error.flutterError); |
| }; |
| if ([@"Firestore#runTransaction" isEqualToString:call.method]) { |
| [[FIRFirestore firestore] runTransactionWithBlock:^id(FIRTransaction *transaction, |
| NSError **pError) { |
| NSNumber *transactionId = call.arguments[@"transactionId"]; |
| NSNumber *transactionTimeout = call.arguments[@"transactionTimeout"]; |
| |
| transactions[transactionId] = transaction; |
| |
| dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); |
| |
| [self.channel invokeMethod:@"DoTransaction" |
| arguments:call.arguments |
| result:^(id doTransactionResult) { |
| transactionResults[transactionId] = doTransactionResult; |
| dispatch_semaphore_signal(semaphore); |
| }]; |
| |
| dispatch_semaphore_wait( |
| semaphore, dispatch_time(DISPATCH_TIME_NOW, [transactionTimeout integerValue] * 1000000)); |
| |
| return transactionResults[transactionId]; |
| } |
| completion:^(id transactionResult, NSError *error) { |
| if (error != nil) { |
| result([FlutterError errorWithCode:[NSString stringWithFormat:@"%ld", error.code] |
| message:error.localizedDescription |
| details:nil]); |
| } |
| result(transactionResult); |
| }]; |
| } else if ([@"Transaction#get" isEqualToString:call.method]) { |
| dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
| NSNumber *transactionId = call.arguments[@"transactionId"]; |
| NSString *path = call.arguments[@"path"]; |
| FIRDocumentReference *documentReference = [[FIRFirestore firestore] documentWithPath:path]; |
| FIRTransaction *transaction = transactions[transactionId]; |
| NSError *error = [[NSError alloc] init]; |
| |
| FIRDocumentSnapshot *snapshot = [transaction getDocument:documentReference error:&error]; |
| |
| if (error != nil) { |
| result([FlutterError errorWithCode:[NSString stringWithFormat:@"%tu", [error code]] |
| message:[error localizedDescription] |
| details:nil]); |
| } else if (snapshot != nil) { |
| result(@{ |
| @"path" : snapshot.reference.path, |
| @"data" : snapshot.exists ? snapshot.data : [NSNull null] |
| }); |
| } else { |
| result([FlutterError errorWithCode:@"DOCUMENT_NOT_FOUND" |
| message:@"Document not found." |
| details:nil]); |
| } |
| }); |
| } else if ([@"Transaction#update" isEqualToString:call.method]) { |
| dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
| NSNumber *transactionId = call.arguments[@"transactionId"]; |
| NSString *path = call.arguments[@"path"]; |
| FIRDocumentReference *documentReference = [[FIRFirestore firestore] documentWithPath:path]; |
| FIRTransaction *transaction = transactions[transactionId]; |
| |
| [transaction updateData:call.arguments[@"data"] forDocument:documentReference]; |
| result(nil); |
| }); |
| } else if ([@"Transaction#set" isEqualToString:call.method]) { |
| dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
| NSNumber *transactionId = call.arguments[@"transactionId"]; |
| NSString *path = call.arguments[@"path"]; |
| FIRDocumentReference *documentReference = [[FIRFirestore firestore] documentWithPath:path]; |
| FIRTransaction *transaction = transactions[transactionId]; |
| |
| [transaction setData:call.arguments[@"data"] forDocument:documentReference]; |
| result(nil); |
| }); |
| } else if ([@"Transaction#delete" isEqualToString:call.method]) { |
| dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
| NSNumber *transactionId = call.arguments[@"transactionId"]; |
| NSString *path = call.arguments[@"path"]; |
| FIRDocumentReference *documentReference = [[FIRFirestore firestore] documentWithPath:path]; |
| FIRTransaction *transaction = transactions[transactionId]; |
| |
| [transaction deleteDocument:documentReference]; |
| result(nil); |
| }); |
| } else if ([@"DocumentReference#setData" isEqualToString:call.method]) { |
| NSString *path = call.arguments[@"path"]; |
| NSDictionary *options = call.arguments[@"options"]; |
| FIRDocumentReference *reference = [[FIRFirestore firestore] documentWithPath:path]; |
| if (![options isEqual:[NSNull null]] && |
| [options[@"merge"] isEqual:[NSNumber numberWithBool:YES]]) { |
| [reference setData:call.arguments[@"data"] |
| options:[FIRSetOptions merge] |
| completion:defaultCompletionBlock]; |
| } else { |
| [reference setData:call.arguments[@"data"] completion:defaultCompletionBlock]; |
| } |
| } else if ([@"DocumentReference#updateData" isEqualToString:call.method]) { |
| NSString *path = call.arguments[@"path"]; |
| FIRDocumentReference *reference = [[FIRFirestore firestore] documentWithPath:path]; |
| [reference updateData:call.arguments[@"data"] completion:defaultCompletionBlock]; |
| } else if ([@"DocumentReference#delete" isEqualToString:call.method]) { |
| NSString *path = call.arguments[@"path"]; |
| FIRDocumentReference *reference = [[FIRFirestore firestore] documentWithPath:path]; |
| [reference deleteDocumentWithCompletion:defaultCompletionBlock]; |
| } else if ([@"DocumentReference#get" isEqualToString:call.method]) { |
| NSString *path = call.arguments[@"path"]; |
| FIRDocumentReference *reference = [[FIRFirestore firestore] documentWithPath:path]; |
| [reference getDocumentWithCompletion:^(FIRDocumentSnapshot *_Nullable snapshot, |
| NSError *_Nullable error) { |
| if (error) { |
| result(error.flutterError); |
| } else { |
| result(@{ |
| @"path" : snapshot.reference.path, |
| @"data" : snapshot.exists ? snapshot.data : [NSNull null] |
| }); |
| } |
| }]; |
| } else if ([@"Query#addSnapshotListener" isEqualToString:call.method]) { |
| __block NSNumber *handle = [NSNumber numberWithInt:_nextListenerHandle++]; |
| FIRQuery *query; |
| @try { |
| query = getQuery(call.arguments); |
| } @catch (NSException *exception) { |
| result([FlutterError errorWithCode:@"invalid_query" |
| message:[exception name] |
| details:[exception reason]]); |
| } |
| id<FIRListenerRegistration> listener = [query |
| addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) { |
| if (error) result(error.flutterError); |
| NSMutableDictionary *arguments = [parseQuerySnapshot(snapshot) mutableCopy]; |
| [arguments setObject:handle forKey:@"handle"]; |
| [self.channel invokeMethod:@"QuerySnapshot" arguments:arguments]; |
| }]; |
| _listeners[handle] = listener; |
| result(handle); |
| } else if ([@"Query#addDocumentListener" isEqualToString:call.method]) { |
| __block NSNumber *handle = [NSNumber numberWithInt:_nextListenerHandle++]; |
| FIRDocumentReference *reference = |
| [[FIRFirestore firestore] documentWithPath:call.arguments[@"path"]]; |
| id<FIRListenerRegistration> listener = |
| [reference addSnapshotListener:^(FIRDocumentSnapshot *snapshot, NSError *_Nullable error) { |
| if (error) result(error.flutterError); |
| [self.channel invokeMethod:@"DocumentSnapshot" |
| arguments:@{ |
| @"handle" : handle, |
| @"path" : snapshot.reference.path, |
| @"data" : snapshot.exists ? snapshot.data : [NSNull null], |
| }]; |
| }]; |
| _listeners[handle] = listener; |
| result(handle); |
| } else if ([@"Query#getDocuments" isEqualToString:call.method]) { |
| FIRQuery *query; |
| @try { |
| query = getQuery(call.arguments); |
| } @catch (NSException *exception) { |
| result([FlutterError errorWithCode:@"invalid_query" |
| message:[exception name] |
| details:[exception reason]]); |
| } |
| [query getDocumentsWithCompletion:^(FIRQuerySnapshot *_Nullable snapshot, |
| NSError *_Nullable error) { |
| if (error) result(error.flutterError); |
| result(parseQuerySnapshot(snapshot)); |
| }]; |
| } else if ([@"Query#removeListener" isEqualToString:call.method]) { |
| NSNumber *handle = call.arguments[@"handle"]; |
| [[_listeners objectForKey:handle] remove]; |
| [_listeners removeObjectForKey:handle]; |
| result(nil); |
| } else { |
| result(FlutterMethodNotImplemented); |
| } |
| } |
| |
| @end |