2017-10-03 08:55:22 -07:00
|
|
|
/*
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
|
|
|
|
|
2017-11-16 20:52:01 -08:00
|
|
|
#import "Firestore/Source/Remote/FSTSerializerBeta.h"
|
2017-10-03 08:55:22 -07:00
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
#include <inttypes.h>
|
|
|
|
|
|
2017-10-03 08:55:22 -07:00
|
|
|
#import <GRPCClient/GRPCCall.h>
|
|
|
|
|
|
2017-11-16 20:52:01 -08:00
|
|
|
#import "Firestore/Protos/objc/google/firestore/v1beta1/Common.pbobjc.h"
|
|
|
|
|
#import "Firestore/Protos/objc/google/firestore/v1beta1/Document.pbobjc.h"
|
|
|
|
|
#import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h"
|
|
|
|
|
#import "Firestore/Protos/objc/google/firestore/v1beta1/Query.pbobjc.h"
|
|
|
|
|
#import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h"
|
|
|
|
|
#import "Firestore/Protos/objc/google/rpc/Status.pbobjc.h"
|
|
|
|
|
#import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h"
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
#import "FIRFirestoreErrors.h"
|
|
|
|
|
#import "FIRGeoPoint.h"
|
2017-11-16 20:52:01 -08:00
|
|
|
#import "Firestore/Source/Core/FSTQuery.h"
|
|
|
|
|
#import "Firestore/Source/Core/FSTSnapshotVersion.h"
|
|
|
|
|
#import "Firestore/Source/Core/FSTTimestamp.h"
|
|
|
|
|
#import "Firestore/Source/Local/FSTQueryData.h"
|
|
|
|
|
#import "Firestore/Source/Model/FSTDocument.h"
|
|
|
|
|
#import "Firestore/Source/Model/FSTDocumentKey.h"
|
|
|
|
|
#import "Firestore/Source/Model/FSTFieldValue.h"
|
|
|
|
|
#import "Firestore/Source/Model/FSTMutation.h"
|
|
|
|
|
#import "Firestore/Source/Model/FSTMutationBatch.h"
|
|
|
|
|
#import "Firestore/Source/Model/FSTPath.h"
|
|
|
|
|
#import "Firestore/Source/Remote/FSTExistenceFilter.h"
|
|
|
|
|
#import "Firestore/Source/Remote/FSTWatchChange.h"
|
|
|
|
|
#import "Firestore/Source/Util/FSTAssert.h"
|
2017-10-03 08:55:22 -07:00
|
|
|
|
2018-02-06 13:51:11 -05:00
|
|
|
#include "Firestore/core/src/firebase/firestore/model/database_id.h"
|
|
|
|
|
#include "Firestore/core/src/firebase/firestore/util/string_apple.h"
|
|
|
|
|
|
|
|
|
|
namespace util = firebase::firestore::util;
|
|
|
|
|
using firebase::firestore::model::DatabaseId;
|
|
|
|
|
|
2017-10-03 08:55:22 -07:00
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
|
|
|
|
@interface FSTSerializerBeta ()
|
2018-02-06 13:51:11 -05:00
|
|
|
// Does not own this DatabaseId.
|
|
|
|
|
@property(nonatomic, assign, readonly) const DatabaseId *databaseID;
|
2017-10-03 08:55:22 -07:00
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
@implementation FSTSerializerBeta
|
|
|
|
|
|
2018-02-06 13:51:11 -05:00
|
|
|
- (instancetype)initWithDatabaseID:(const DatabaseId *)databaseID {
|
2017-10-03 08:55:22 -07:00
|
|
|
self = [super init];
|
|
|
|
|
if (self) {
|
|
|
|
|
_databaseID = databaseID;
|
|
|
|
|
}
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - FSTSnapshotVersion <=> GPBTimestamp
|
|
|
|
|
|
|
|
|
|
- (GPBTimestamp *)encodedTimestamp:(FSTTimestamp *)timestamp {
|
|
|
|
|
GPBTimestamp *result = [GPBTimestamp message];
|
|
|
|
|
result.seconds = timestamp.seconds;
|
|
|
|
|
result.nanos = timestamp.nanos;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp {
|
|
|
|
|
return [[FSTTimestamp alloc] initWithSeconds:timestamp.seconds nanos:timestamp.nanos];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version {
|
|
|
|
|
return [self encodedTimestamp:version.timestamp];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version {
|
|
|
|
|
return [FSTSnapshotVersion versionWithTimestamp:[self decodedTimestamp:version]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - FIRGeoPoint <=> GTPLatLng
|
|
|
|
|
|
|
|
|
|
- (GTPLatLng *)encodedGeoPoint:(FIRGeoPoint *)geoPoint {
|
|
|
|
|
GTPLatLng *latLng = [GTPLatLng message];
|
|
|
|
|
latLng.latitude = geoPoint.latitude;
|
|
|
|
|
latLng.longitude = geoPoint.longitude;
|
|
|
|
|
return latLng;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FIRGeoPoint *)decodedGeoPoint:(GTPLatLng *)latLng {
|
|
|
|
|
return [[FIRGeoPoint alloc] initWithLatitude:latLng.latitude longitude:latLng.longitude];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - FSTDocumentKey <=> Key proto
|
|
|
|
|
|
|
|
|
|
- (NSString *)encodedDocumentKey:(FSTDocumentKey *)key {
|
|
|
|
|
return [self encodedResourcePathForDatabaseID:self.databaseID path:key.path];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTDocumentKey *)decodedDocumentKey:(NSString *)name {
|
|
|
|
|
FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:name];
|
2018-02-06 13:51:11 -05:00
|
|
|
FSTAssert([[path segmentAtIndex:1]
|
|
|
|
|
isEqualToString:util::WrapNSStringNoCopy(self.databaseID->project_id())],
|
2017-10-03 08:55:22 -07:00
|
|
|
@"Tried to deserialize key from different project.");
|
2018-02-06 13:51:11 -05:00
|
|
|
FSTAssert([[path segmentAtIndex:3]
|
|
|
|
|
isEqualToString:util::WrapNSStringNoCopy(self.databaseID->database_id())],
|
2017-10-03 08:55:22 -07:00
|
|
|
@"Tried to deserialize key from different datbase.");
|
|
|
|
|
return [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]];
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-06 13:51:11 -05:00
|
|
|
- (NSString *)encodedResourcePathForDatabaseID:(const DatabaseId *)databaseID
|
2017-10-03 08:55:22 -07:00
|
|
|
path:(FSTResourcePath *)path {
|
|
|
|
|
return [[[[self encodedResourcePathForDatabaseID:databaseID] pathByAppendingSegment:@"documents"]
|
|
|
|
|
pathByAppendingPath:path] canonicalString];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTResourcePath *)decodedResourcePathWithDatabaseID:(NSString *)name {
|
|
|
|
|
FSTResourcePath *path = [FSTResourcePath pathWithString:name];
|
|
|
|
|
FSTAssert([self validQualifiedResourcePath:path], @"Tried to deserialize invalid key %@", path);
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSString *)encodedQueryPath:(FSTResourcePath *)path {
|
|
|
|
|
if (path.length == 0) {
|
|
|
|
|
// If the path is empty, the backend requires we leave off the /documents at the end.
|
|
|
|
|
return [self encodedDatabaseID];
|
|
|
|
|
}
|
|
|
|
|
return [self encodedResourcePathForDatabaseID:self.databaseID path:path];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTResourcePath *)decodedQueryPath:(NSString *)name {
|
|
|
|
|
FSTResourcePath *resource = [self decodedResourcePathWithDatabaseID:name];
|
|
|
|
|
if (resource.length == 4) {
|
|
|
|
|
return [FSTResourcePath pathWithSegments:@[]];
|
|
|
|
|
} else {
|
|
|
|
|
return [self localResourcePathForQualifiedResourcePath:resource];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-06 13:51:11 -05:00
|
|
|
- (FSTResourcePath *)encodedResourcePathForDatabaseID:(const DatabaseId *)databaseID {
|
|
|
|
|
return [FSTResourcePath pathWithSegments:@[
|
|
|
|
|
@"projects", util::WrapNSStringNoCopy(databaseID->project_id()), @"databases",
|
|
|
|
|
util::WrapNSStringNoCopy(databaseID->database_id())
|
|
|
|
|
]];
|
2017-10-03 08:55:22 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTResourcePath *)localResourcePathForQualifiedResourcePath:(FSTResourcePath *)resourceName {
|
|
|
|
|
FSTAssert(
|
|
|
|
|
resourceName.length > 4 && [[resourceName segmentAtIndex:4] isEqualToString:@"documents"],
|
|
|
|
|
@"Tried to deserialize invalid key %@", resourceName);
|
|
|
|
|
return [resourceName pathByRemovingFirstSegments:5];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (BOOL)validQualifiedResourcePath:(FSTResourcePath *)path {
|
|
|
|
|
return path.length >= 4 && [[path segmentAtIndex:0] isEqualToString:@"projects"] &&
|
|
|
|
|
[[path segmentAtIndex:2] isEqualToString:@"databases"];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSString *)encodedDatabaseID {
|
|
|
|
|
return [[self encodedResourcePathForDatabaseID:self.databaseID] canonicalString];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - FSTFieldValue <=> Value proto
|
|
|
|
|
|
|
|
|
|
- (GCFSValue *)encodedFieldValue:(FSTFieldValue *)fieldValue {
|
2018-01-31 11:23:55 -08:00
|
|
|
Class fieldClass = [fieldValue class];
|
|
|
|
|
if (fieldClass == [FSTNullValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
return [self encodedNull];
|
|
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
} else if (fieldClass == [FSTBooleanValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
return [self encodedBool:[[fieldValue value] boolValue]];
|
|
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
} else if (fieldClass == [FSTIntegerValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
return [self encodedInteger:[[fieldValue value] longLongValue]];
|
|
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
} else if (fieldClass == [FSTDoubleValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
return [self encodedDouble:[[fieldValue value] doubleValue]];
|
|
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
} else if (fieldClass == [FSTStringValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
return [self encodedString:[fieldValue value]];
|
|
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
} else if (fieldClass == [FSTTimestampValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
return [self encodedTimestampValue:((FSTTimestampValue *)fieldValue).internalValue];
|
|
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
} else if (fieldClass == [FSTGeoPointValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
return [self encodedGeoPointValue:[fieldValue value]];
|
|
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
} else if (fieldClass == [FSTBlobValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
return [self encodedBlobValue:[fieldValue value]];
|
|
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
} else if (fieldClass == [FSTReferenceValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
FSTReferenceValue *ref = (FSTReferenceValue *)fieldValue;
|
|
|
|
|
return [self encodedReferenceValueForDatabaseID:[ref databaseID] key:[ref value]];
|
|
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
} else if (fieldClass == [FSTObjectValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.mapValue = [self encodedMapValue:(FSTObjectValue *)fieldValue];
|
|
|
|
|
return result;
|
|
|
|
|
|
2018-01-31 11:23:55 -08:00
|
|
|
} else if (fieldClass == [FSTArrayValue class]) {
|
2017-10-03 08:55:22 -07:00
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.arrayValue = [self encodedArrayValue:(FSTArrayValue *)fieldValue];
|
|
|
|
|
return result;
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
FSTFail(@"Unhandled type %@ on %@", NSStringFromClass([fieldValue class]), fieldValue);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTFieldValue *)decodedFieldValue:(GCFSValue *)valueProto {
|
|
|
|
|
switch (valueProto.valueTypeOneOfCase) {
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_NullValue:
|
|
|
|
|
return [FSTNullValue nullValue];
|
|
|
|
|
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_BooleanValue:
|
|
|
|
|
return [FSTBooleanValue booleanValue:valueProto.booleanValue];
|
|
|
|
|
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_IntegerValue:
|
|
|
|
|
return [FSTIntegerValue integerValue:valueProto.integerValue];
|
|
|
|
|
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_DoubleValue:
|
|
|
|
|
return [FSTDoubleValue doubleValue:valueProto.doubleValue];
|
|
|
|
|
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_StringValue:
|
|
|
|
|
return [FSTStringValue stringValue:valueProto.stringValue];
|
|
|
|
|
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_TimestampValue:
|
|
|
|
|
return [FSTTimestampValue timestampValue:[self decodedTimestamp:valueProto.timestampValue]];
|
|
|
|
|
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_GeoPointValue:
|
|
|
|
|
return [FSTGeoPointValue geoPointValue:[self decodedGeoPoint:valueProto.geoPointValue]];
|
|
|
|
|
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_BytesValue:
|
|
|
|
|
return [FSTBlobValue blobValue:valueProto.bytesValue];
|
|
|
|
|
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_ReferenceValue:
|
|
|
|
|
return [self decodedReferenceValue:valueProto.referenceValue];
|
|
|
|
|
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_ArrayValue:
|
|
|
|
|
return [self decodedArrayValue:valueProto.arrayValue];
|
|
|
|
|
|
|
|
|
|
case GCFSValue_ValueType_OneOfCase_MapValue:
|
|
|
|
|
return [self decodedMapValue:valueProto.mapValue];
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unhandled type %d on %@", valueProto.valueTypeOneOfCase, valueProto);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSValue *)encodedNull {
|
|
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.nullValue = GPBNullValue_NullValue;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSValue *)encodedBool:(BOOL)value {
|
|
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.booleanValue = value;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSValue *)encodedDouble:(double)value {
|
|
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.doubleValue = value;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSValue *)encodedInteger:(int64_t)value {
|
|
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.integerValue = value;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSValue *)encodedString:(NSString *)value {
|
|
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.stringValue = value;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSValue *)encodedTimestampValue:(FSTTimestamp *)value {
|
|
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.timestampValue = [self encodedTimestamp:value];
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSValue *)encodedGeoPointValue:(FIRGeoPoint *)value {
|
|
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.geoPointValue = [self encodedGeoPoint:value];
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSValue *)encodedBlobValue:(NSData *)value {
|
|
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.bytesValue = value;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-06 13:51:11 -05:00
|
|
|
- (GCFSValue *)encodedReferenceValueForDatabaseID:(const DatabaseId *)databaseID
|
2017-10-03 08:55:22 -07:00
|
|
|
key:(FSTDocumentKey *)key {
|
2018-02-06 13:51:11 -05:00
|
|
|
FSTAssert(*databaseID == *self.databaseID, @"Database %@:%@ cannot encode reference from %@:%@",
|
|
|
|
|
util::WrapNSStringNoCopy(self.databaseID->project_id()),
|
|
|
|
|
util::WrapNSStringNoCopy(self.databaseID->database_id()),
|
|
|
|
|
util::WrapNSStringNoCopy(databaseID->project_id()),
|
|
|
|
|
util::WrapNSStringNoCopy(databaseID->database_id()));
|
2017-10-03 08:55:22 -07:00
|
|
|
GCFSValue *result = [GCFSValue message];
|
|
|
|
|
result.referenceValue = [self encodedResourcePathForDatabaseID:databaseID path:key.path];
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTReferenceValue *)decodedReferenceValue:(NSString *)resourceName {
|
|
|
|
|
FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:resourceName];
|
|
|
|
|
NSString *project = [path segmentAtIndex:1];
|
|
|
|
|
NSString *database = [path segmentAtIndex:3];
|
|
|
|
|
FSTDocumentKey *key =
|
|
|
|
|
[FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]];
|
2018-02-06 13:51:11 -05:00
|
|
|
|
|
|
|
|
const DatabaseId database_id(util::MakeStringView(project), util::MakeStringView(database));
|
|
|
|
|
FSTAssert(database_id == *self.databaseID, @"Database %@:%@ cannot encode reference from %@:%@",
|
|
|
|
|
util::WrapNSStringNoCopy(self.databaseID->project_id()),
|
|
|
|
|
util::WrapNSStringNoCopy(self.databaseID->database_id()),
|
|
|
|
|
util::WrapNSStringNoCopy(database_id.project_id()),
|
|
|
|
|
util::WrapNSStringNoCopy(database_id.database_id()));
|
|
|
|
|
return [FSTReferenceValue referenceValue:key databaseID:self.databaseID];
|
2017-10-03 08:55:22 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSArrayValue *)encodedArrayValue:(FSTArrayValue *)arrayValue {
|
|
|
|
|
GCFSArrayValue *proto = [GCFSArrayValue message];
|
|
|
|
|
NSMutableArray<GCFSValue *> *protoContents = [proto valuesArray];
|
|
|
|
|
|
|
|
|
|
[[arrayValue internalValue]
|
|
|
|
|
enumerateObjectsUsingBlock:^(FSTFieldValue *value, NSUInteger idx, BOOL *stop) {
|
|
|
|
|
GCFSValue *converted = [self encodedFieldValue:value];
|
|
|
|
|
[protoContents addObject:converted];
|
|
|
|
|
}];
|
|
|
|
|
return proto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTArrayValue *)decodedArrayValue:(GCFSArrayValue *)arrayValue {
|
|
|
|
|
NSMutableArray<FSTFieldValue *> *contents =
|
|
|
|
|
[NSMutableArray arrayWithCapacity:arrayValue.valuesArray_Count];
|
|
|
|
|
|
|
|
|
|
[arrayValue.valuesArray
|
|
|
|
|
enumerateObjectsUsingBlock:^(GCFSValue *value, NSUInteger idx, BOOL *stop) {
|
|
|
|
|
[contents addObject:[self decodedFieldValue:value]];
|
|
|
|
|
}];
|
|
|
|
|
return [[FSTArrayValue alloc] initWithValueNoCopy:contents];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSMapValue *)encodedMapValue:(FSTObjectValue *)value {
|
|
|
|
|
GCFSMapValue *result = [GCFSMapValue message];
|
|
|
|
|
result.fields = [self encodedFields:value];
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTObjectValue *)decodedMapValue:(GCFSMapValue *)map {
|
|
|
|
|
return [self decodedFields:map.fields];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Encodes an FSTObjectValue into a dictionary.
|
|
|
|
|
* @return a new dictionary that can be assigned to a field in another proto.
|
|
|
|
|
*/
|
|
|
|
|
- (NSMutableDictionary<NSString *, GCFSValue *> *)encodedFields:(FSTObjectValue *)value {
|
|
|
|
|
FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *fields = value.internalValue;
|
|
|
|
|
NSMutableDictionary<NSString *, GCFSValue *> *result = [NSMutableDictionary dictionary];
|
|
|
|
|
[fields enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) {
|
|
|
|
|
GCFSValue *converted = [self encodedFieldValue:obj];
|
|
|
|
|
result[key] = converted;
|
|
|
|
|
}];
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTObjectValue *)decodedFields:(NSDictionary<NSString *, GCFSValue *> *)fields {
|
|
|
|
|
__block FSTObjectValue *result = [FSTObjectValue objectValue];
|
|
|
|
|
[fields enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, GCFSValue *_Nonnull obj,
|
|
|
|
|
BOOL *_Nonnull stop) {
|
|
|
|
|
FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ key ]];
|
|
|
|
|
FSTFieldValue *value = [self decodedFieldValue:obj];
|
|
|
|
|
result = [result objectBySettingValue:value forPath:path];
|
|
|
|
|
}];
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - FSTObjectValue <=> Document proto
|
|
|
|
|
|
|
|
|
|
- (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue
|
|
|
|
|
key:(FSTDocumentKey *)key {
|
|
|
|
|
GCFSDocument *proto = [GCFSDocument message];
|
|
|
|
|
proto.name = [self encodedDocumentKey:key];
|
|
|
|
|
proto.fields = [self encodedFields:objectValue];
|
|
|
|
|
return proto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - FSTMaybeDocument <= BatchGetDocumentsResponse proto
|
|
|
|
|
|
|
|
|
|
- (FSTMaybeDocument *)decodedMaybeDocumentFromBatch:(GCFSBatchGetDocumentsResponse *)response {
|
|
|
|
|
switch (response.resultOneOfCase) {
|
|
|
|
|
case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Found:
|
|
|
|
|
return [self decodedFoundDocument:response];
|
|
|
|
|
case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Missing:
|
|
|
|
|
return [self decodedDeletedDocument:response];
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unknown document type: %@", response);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTDocument *)decodedFoundDocument:(GCFSBatchGetDocumentsResponse *)response {
|
|
|
|
|
FSTAssert(!!response.found, @"Tried to deserialize a found document from a deleted document.");
|
|
|
|
|
FSTDocumentKey *key = [self decodedDocumentKey:response.found.name];
|
|
|
|
|
FSTObjectValue *value = [self decodedFields:response.found.fields];
|
|
|
|
|
FSTSnapshotVersion *version = [self decodedVersion:response.found.updateTime];
|
|
|
|
|
FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]],
|
|
|
|
|
@"Got a document response with no snapshot version");
|
|
|
|
|
|
|
|
|
|
return [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response {
|
|
|
|
|
FSTAssert(!!response.missing, @"Tried to deserialize a deleted document from a found document.");
|
|
|
|
|
FSTDocumentKey *key = [self decodedDocumentKey:response.missing];
|
|
|
|
|
FSTSnapshotVersion *version = [self decodedVersion:response.readTime];
|
|
|
|
|
FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]],
|
|
|
|
|
@"Got a no document response with no snapshot version");
|
|
|
|
|
return [FSTDeletedDocument documentWithKey:key version:version];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - FSTMutation => GCFSWrite proto
|
|
|
|
|
|
|
|
|
|
- (GCFSWrite *)encodedMutation:(FSTMutation *)mutation {
|
|
|
|
|
GCFSWrite *proto = [GCFSWrite message];
|
|
|
|
|
|
|
|
|
|
Class mutationClass = [mutation class];
|
|
|
|
|
if (mutationClass == [FSTSetMutation class]) {
|
|
|
|
|
FSTSetMutation *set = (FSTSetMutation *)mutation;
|
|
|
|
|
proto.update = [self encodedDocumentWithFields:set.value key:set.key];
|
|
|
|
|
|
|
|
|
|
} else if (mutationClass == [FSTPatchMutation class]) {
|
|
|
|
|
FSTPatchMutation *patch = (FSTPatchMutation *)mutation;
|
|
|
|
|
proto.update = [self encodedDocumentWithFields:patch.value key:patch.key];
|
|
|
|
|
proto.updateMask = [self encodedFieldMask:patch.fieldMask];
|
|
|
|
|
|
|
|
|
|
} else if (mutationClass == [FSTTransformMutation class]) {
|
|
|
|
|
FSTTransformMutation *transform = (FSTTransformMutation *)mutation;
|
|
|
|
|
|
|
|
|
|
proto.transform = [GCFSDocumentTransform message];
|
|
|
|
|
proto.transform.document = [self encodedDocumentKey:transform.key];
|
|
|
|
|
proto.transform.fieldTransformsArray = [self encodedFieldTransforms:transform.fieldTransforms];
|
|
|
|
|
// NOTE: We set a precondition of exists: true as a safety-check, since we always combine
|
|
|
|
|
// FSTTransformMutations with an FSTSetMutation or FSTPatchMutation which (if successful) should
|
|
|
|
|
// end up with an existing document.
|
|
|
|
|
proto.currentDocument.exists = YES;
|
|
|
|
|
|
|
|
|
|
} else if (mutationClass == [FSTDeleteMutation class]) {
|
2018-01-31 11:23:55 -08:00
|
|
|
FSTDeleteMutation *deleteMutation = (FSTDeleteMutation *)mutation;
|
|
|
|
|
proto.delete_p = [self encodedDocumentKey:deleteMutation.key];
|
2017-10-03 08:55:22 -07:00
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
FSTFail(@"Unknown mutation type %@", NSStringFromClass(mutationClass));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!mutation.precondition.isNone) {
|
|
|
|
|
proto.currentDocument = [self encodedPrecondition:mutation.precondition];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return proto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTMutation *)decodedMutation:(GCFSWrite *)mutation {
|
|
|
|
|
FSTPrecondition *precondition = [mutation hasCurrentDocument]
|
|
|
|
|
? [self decodedPrecondition:mutation.currentDocument]
|
|
|
|
|
: [FSTPrecondition none];
|
|
|
|
|
|
|
|
|
|
switch (mutation.operationOneOfCase) {
|
|
|
|
|
case GCFSWrite_Operation_OneOfCase_Update:
|
|
|
|
|
if (mutation.hasUpdateMask) {
|
|
|
|
|
return [[FSTPatchMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name]
|
|
|
|
|
fieldMask:[self decodedFieldMask:mutation.updateMask]
|
|
|
|
|
value:[self decodedFields:mutation.update.fields]
|
|
|
|
|
precondition:precondition];
|
|
|
|
|
} else {
|
|
|
|
|
return [[FSTSetMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name]
|
|
|
|
|
value:[self decodedFields:mutation.update.fields]
|
|
|
|
|
precondition:precondition];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case GCFSWrite_Operation_OneOfCase_Delete_p:
|
|
|
|
|
return [[FSTDeleteMutation alloc] initWithKey:[self decodedDocumentKey:mutation.delete_p]
|
|
|
|
|
precondition:precondition];
|
|
|
|
|
|
|
|
|
|
case GCFSWrite_Operation_OneOfCase_Transform: {
|
|
|
|
|
FSTPreconditionExists exists = precondition.exists;
|
|
|
|
|
FSTAssert(exists == FSTPreconditionExistsYes,
|
|
|
|
|
@"Transforms must have precondition \"exists == true\"");
|
|
|
|
|
|
|
|
|
|
return [[FSTTransformMutation alloc]
|
|
|
|
|
initWithKey:[self decodedDocumentKey:mutation.transform.document]
|
|
|
|
|
fieldTransforms:[self decodedFieldTransforms:mutation.transform.fieldTransformsArray]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Note that insert is intentionally unhandled, since we don't ever deal in them.
|
|
|
|
|
FSTFail(@"Unknown mutation operation: %d", mutation.operationOneOfCase);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSPrecondition *)encodedPrecondition:(FSTPrecondition *)precondition {
|
|
|
|
|
FSTAssert(!precondition.isNone, @"Can't serialize an empty precondition");
|
|
|
|
|
GCFSPrecondition *message = [GCFSPrecondition message];
|
|
|
|
|
if (precondition.updateTime) {
|
|
|
|
|
message.updateTime = [self encodedVersion:precondition.updateTime];
|
|
|
|
|
} else if (precondition.exists != FSTPreconditionExistsNotSet) {
|
|
|
|
|
message.exists = precondition.exists == FSTPreconditionExistsYes;
|
|
|
|
|
} else {
|
|
|
|
|
FSTFail(@"Unknown precondition: %@", precondition);
|
|
|
|
|
}
|
|
|
|
|
return message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTPrecondition *)decodedPrecondition:(GCFSPrecondition *)precondition {
|
|
|
|
|
switch (precondition.conditionTypeOneOfCase) {
|
|
|
|
|
case GCFSPrecondition_ConditionType_OneOfCase_GPBUnsetOneOfCase:
|
|
|
|
|
return [FSTPrecondition none];
|
|
|
|
|
|
|
|
|
|
case GCFSPrecondition_ConditionType_OneOfCase_Exists:
|
|
|
|
|
return [FSTPrecondition preconditionWithExists:precondition.exists];
|
|
|
|
|
|
|
|
|
|
case GCFSPrecondition_ConditionType_OneOfCase_UpdateTime:
|
|
|
|
|
return [FSTPrecondition
|
|
|
|
|
preconditionWithUpdateTime:[self decodedVersion:precondition.updateTime]];
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unrecognized Precondition one-of case %@", precondition);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask {
|
|
|
|
|
GCFSDocumentMask *mask = [GCFSDocumentMask message];
|
|
|
|
|
for (FSTFieldPath *field in fieldMask.fields) {
|
|
|
|
|
[mask.fieldPathsArray addObject:field.canonicalString];
|
|
|
|
|
}
|
|
|
|
|
return mask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTFieldMask *)decodedFieldMask:(GCFSDocumentMask *)fieldMask {
|
|
|
|
|
NSMutableArray<FSTFieldPath *> *fields =
|
|
|
|
|
[NSMutableArray arrayWithCapacity:fieldMask.fieldPathsArray_Count];
|
|
|
|
|
for (NSString *path in fieldMask.fieldPathsArray) {
|
|
|
|
|
[fields addObject:[FSTFieldPath pathWithServerFormat:path]];
|
|
|
|
|
}
|
|
|
|
|
return [[FSTFieldMask alloc] initWithFields:fields];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSMutableArray<GCFSDocumentTransform_FieldTransform *> *)encodedFieldTransforms:
|
|
|
|
|
(NSArray<FSTFieldTransform *> *)fieldTransforms {
|
|
|
|
|
NSMutableArray *protos = [NSMutableArray array];
|
|
|
|
|
for (FSTFieldTransform *fieldTransform in fieldTransforms) {
|
|
|
|
|
FSTAssert([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]],
|
|
|
|
|
@"Unknown transform: %@", fieldTransform.transform);
|
|
|
|
|
GCFSDocumentTransform_FieldTransform *proto = [GCFSDocumentTransform_FieldTransform message];
|
|
|
|
|
proto.fieldPath = fieldTransform.path.canonicalString;
|
|
|
|
|
proto.setToServerValue = GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime;
|
|
|
|
|
[protos addObject:proto];
|
|
|
|
|
}
|
|
|
|
|
return protos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSArray<FSTFieldTransform *> *)decodedFieldTransforms:
|
|
|
|
|
(NSArray<GCFSDocumentTransform_FieldTransform *> *)protos {
|
|
|
|
|
NSMutableArray<FSTFieldTransform *> *fieldTransforms = [NSMutableArray array];
|
|
|
|
|
for (GCFSDocumentTransform_FieldTransform *proto in protos) {
|
|
|
|
|
FSTAssert(
|
|
|
|
|
proto.setToServerValue == GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime,
|
|
|
|
|
@"Unknown transform setToServerValue: %d", proto.setToServerValue);
|
|
|
|
|
[fieldTransforms
|
|
|
|
|
addObject:[[FSTFieldTransform alloc]
|
|
|
|
|
initWithPath:[FSTFieldPath pathWithServerFormat:proto.fieldPath]
|
|
|
|
|
transform:[FSTServerTimestampTransform serverTimestampTransform]]];
|
|
|
|
|
}
|
|
|
|
|
return fieldTransforms;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - FSTMutationResult <= GCFSWriteResult proto
|
|
|
|
|
|
|
|
|
|
- (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation {
|
|
|
|
|
// NOTE: Deletes don't have an updateTime.
|
|
|
|
|
FSTSnapshotVersion *_Nullable version =
|
|
|
|
|
mutation.updateTime ? [self decodedVersion:mutation.updateTime] : nil;
|
|
|
|
|
NSMutableArray *_Nullable transformResults = nil;
|
|
|
|
|
if (mutation.transformResultsArray.count > 0) {
|
|
|
|
|
transformResults = [NSMutableArray array];
|
|
|
|
|
for (GCFSValue *result in mutation.transformResultsArray) {
|
|
|
|
|
[transformResults addObject:[self decodedFieldValue:result]];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return [[FSTMutationResult alloc] initWithVersion:version transformResults:transformResults];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - FSTQueryData => GCFSTarget proto
|
|
|
|
|
|
|
|
|
|
- (nullable NSMutableDictionary<NSString *, NSString *> *)encodedListenRequestLabelsForQueryData:
|
|
|
|
|
(FSTQueryData *)queryData {
|
|
|
|
|
NSString *value = [self encodedLabelForPurpose:queryData.purpose];
|
|
|
|
|
if (!value) {
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSMutableDictionary<NSString *, NSString *> *result =
|
|
|
|
|
[NSMutableDictionary dictionaryWithCapacity:1];
|
|
|
|
|
[result setObject:value forKey:@"goog-listen-tags"];
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (nullable NSString *)encodedLabelForPurpose:(FSTQueryPurpose)purpose {
|
|
|
|
|
switch (purpose) {
|
|
|
|
|
case FSTQueryPurposeListen:
|
|
|
|
|
return nil;
|
|
|
|
|
case FSTQueryPurposeExistenceFilterMismatch:
|
|
|
|
|
return @"existence-filter-mismatch";
|
|
|
|
|
case FSTQueryPurposeLimboResolution:
|
|
|
|
|
return @"limbo-document";
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unrecognized query purpose: %lu", (unsigned long)purpose);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData {
|
|
|
|
|
GCFSTarget *result = [GCFSTarget message];
|
|
|
|
|
FSTQuery *query = queryData.query;
|
|
|
|
|
|
|
|
|
|
if ([query isDocumentQuery]) {
|
|
|
|
|
result.documents = [self encodedDocumentsTarget:query];
|
|
|
|
|
} else {
|
|
|
|
|
result.query = [self encodedQueryTarget:query];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.targetId = queryData.targetID;
|
|
|
|
|
if (queryData.resumeToken.length > 0) {
|
|
|
|
|
result.resumeToken = queryData.resumeToken;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(FSTQuery *)query {
|
|
|
|
|
GCFSTarget_DocumentsTarget *result = [GCFSTarget_DocumentsTarget message];
|
|
|
|
|
NSMutableArray<NSString *> *docs = result.documentsArray;
|
|
|
|
|
[docs addObject:[self encodedQueryPath:query.path]];
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target {
|
|
|
|
|
NSArray<NSString *> *documents = target.documentsArray;
|
|
|
|
|
FSTAssert(documents.count == 1, @"DocumentsTarget contained other than 1 document %lu",
|
|
|
|
|
(unsigned long)documents.count);
|
|
|
|
|
|
|
|
|
|
NSString *name = documents[0];
|
|
|
|
|
return [FSTQuery queryWithPath:[self decodedQueryPath:name]];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query {
|
|
|
|
|
// Dissect the path into parent, collectionId, and optional key filter.
|
|
|
|
|
GCFSTarget_QueryTarget *queryTarget = [GCFSTarget_QueryTarget message];
|
|
|
|
|
if (query.path.length == 0) {
|
|
|
|
|
queryTarget.parent = [self encodedQueryPath:query.path];
|
|
|
|
|
} else {
|
|
|
|
|
FSTResourcePath *path = query.path;
|
|
|
|
|
FSTAssert(path.length % 2 != 0, @"Document queries with filters are not supported.");
|
|
|
|
|
queryTarget.parent = [self encodedQueryPath:[path pathByRemovingLastSegment]];
|
|
|
|
|
GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
|
|
|
|
|
from.collectionId = path.lastSegment;
|
|
|
|
|
[queryTarget.structuredQuery.fromArray addObject:from];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Encode the filters.
|
|
|
|
|
GCFSStructuredQuery_Filter *_Nullable where = [self encodedFilters:query.filters];
|
|
|
|
|
if (where) {
|
|
|
|
|
queryTarget.structuredQuery.where = where;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSArray<GCFSStructuredQuery_Order *> *orders = [self encodedSortOrders:query.sortOrders];
|
|
|
|
|
if (orders.count) {
|
|
|
|
|
[queryTarget.structuredQuery.orderByArray addObjectsFromArray:orders];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (query.limit != NSNotFound) {
|
|
|
|
|
queryTarget.structuredQuery.limit.value = (int32_t)query.limit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (query.startAt) {
|
|
|
|
|
queryTarget.structuredQuery.startAt = [self encodedBound:query.startAt];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (query.endAt) {
|
|
|
|
|
queryTarget.structuredQuery.endAt = [self encodedBound:query.endAt];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return queryTarget;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target {
|
|
|
|
|
FSTResourcePath *path = [self decodedQueryPath:target.parent];
|
|
|
|
|
|
|
|
|
|
GCFSStructuredQuery *query = target.structuredQuery;
|
|
|
|
|
NSUInteger fromCount = query.fromArray_Count;
|
|
|
|
|
if (fromCount > 0) {
|
|
|
|
|
FSTAssert(fromCount == 1,
|
|
|
|
|
@"StructuredQuery.from with more than one collection is not supported.");
|
|
|
|
|
|
|
|
|
|
GCFSStructuredQuery_CollectionSelector *from = query.fromArray[0];
|
|
|
|
|
path = [path pathByAppendingSegment:from.collectionId];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSArray<id<FSTFilter>> *filterBy;
|
|
|
|
|
if (query.hasWhere) {
|
|
|
|
|
filterBy = [self decodedFilters:query.where];
|
|
|
|
|
} else {
|
|
|
|
|
filterBy = @[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSArray<FSTSortOrder *> *orderBy;
|
|
|
|
|
if (query.orderByArray_Count > 0) {
|
|
|
|
|
orderBy = [self decodedSortOrders:query.orderByArray];
|
|
|
|
|
} else {
|
|
|
|
|
orderBy = @[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSInteger limit = NSNotFound;
|
|
|
|
|
if (query.hasLimit) {
|
|
|
|
|
limit = query.limit.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FSTBound *_Nullable startAt;
|
|
|
|
|
if (query.hasStartAt) {
|
|
|
|
|
startAt = [self decodedBound:query.startAt];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FSTBound *_Nullable endAt;
|
|
|
|
|
if (query.hasEndAt) {
|
|
|
|
|
endAt = [self decodedBound:query.endAt];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [[FSTQuery alloc] initWithPath:path
|
|
|
|
|
filterBy:filterBy
|
|
|
|
|
orderBy:orderBy
|
|
|
|
|
limit:limit
|
|
|
|
|
startAt:startAt
|
|
|
|
|
endAt:endAt];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark Filters
|
|
|
|
|
|
|
|
|
|
- (GCFSStructuredQuery_Filter *_Nullable)encodedFilters:(NSArray<id<FSTFilter>> *)filters {
|
|
|
|
|
if (filters.count == 0) {
|
|
|
|
|
return nil;
|
|
|
|
|
}
|
|
|
|
|
NSMutableArray<GCFSStructuredQuery_Filter *> *protos = [NSMutableArray array];
|
|
|
|
|
for (id<FSTFilter> filter in filters) {
|
|
|
|
|
if ([filter isKindOfClass:[FSTRelationFilter class]]) {
|
|
|
|
|
[protos addObject:[self encodedRelationFilter:filter]];
|
|
|
|
|
} else {
|
|
|
|
|
[protos addObject:[self encodedUnaryFilter:filter]];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (protos.count == 1) {
|
|
|
|
|
// Special case: no existing filters and we only need to add one filter. This can be made the
|
|
|
|
|
// single root filter without a composite filter.
|
|
|
|
|
return protos[0];
|
|
|
|
|
}
|
|
|
|
|
GCFSStructuredQuery_Filter *composite = [GCFSStructuredQuery_Filter message];
|
|
|
|
|
composite.compositeFilter.op = GCFSStructuredQuery_CompositeFilter_Operator_And;
|
|
|
|
|
composite.compositeFilter.filtersArray = protos;
|
|
|
|
|
return composite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSArray<id<FSTFilter>> *)decodedFilters:(GCFSStructuredQuery_Filter *)proto {
|
|
|
|
|
NSMutableArray<id<FSTFilter>> *result = [NSMutableArray array];
|
|
|
|
|
|
|
|
|
|
NSArray<GCFSStructuredQuery_Filter *> *filters;
|
|
|
|
|
if (proto.filterTypeOneOfCase ==
|
|
|
|
|
GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter) {
|
|
|
|
|
FSTAssert(proto.compositeFilter.op == GCFSStructuredQuery_CompositeFilter_Operator_And,
|
|
|
|
|
@"Only AND-type composite filters are supported, got %d", proto.compositeFilter.op);
|
|
|
|
|
filters = proto.compositeFilter.filtersArray;
|
|
|
|
|
} else {
|
|
|
|
|
filters = @[ proto ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (GCFSStructuredQuery_Filter *filter in filters) {
|
|
|
|
|
switch (filter.filterTypeOneOfCase) {
|
|
|
|
|
case GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter:
|
|
|
|
|
FSTFail(@"Nested composite filters are not supported");
|
|
|
|
|
|
|
|
|
|
case GCFSStructuredQuery_Filter_FilterType_OneOfCase_FieldFilter:
|
|
|
|
|
[result addObject:[self decodedRelationFilter:filter.fieldFilter]];
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case GCFSStructuredQuery_Filter_FilterType_OneOfCase_UnaryFilter:
|
|
|
|
|
[result addObject:[self decodedUnaryFilter:filter.unaryFilter]];
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unrecognized Filter.filterType %d", filter.filterTypeOneOfCase);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter {
|
|
|
|
|
GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message];
|
|
|
|
|
GCFSStructuredQuery_FieldFilter *fieldFilter = proto.fieldFilter;
|
|
|
|
|
fieldFilter.field = [self encodedFieldPath:filter.field];
|
|
|
|
|
fieldFilter.op = [self encodedRelationFilterOperator:filter.filterOperator];
|
|
|
|
|
fieldFilter.value = [self encodedFieldValue:filter.value];
|
|
|
|
|
return proto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTRelationFilter *)decodedRelationFilter:(GCFSStructuredQuery_FieldFilter *)proto {
|
|
|
|
|
FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath];
|
|
|
|
|
FSTRelationFilterOperator filterOperator = [self decodedRelationFilterOperator:proto.op];
|
|
|
|
|
FSTFieldValue *value = [self decodedFieldValue:proto.value];
|
|
|
|
|
return [FSTRelationFilter filterWithField:fieldPath filterOperator:filterOperator value:value];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSStructuredQuery_Filter *)encodedUnaryFilter:(id<FSTFilter>)filter {
|
|
|
|
|
GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message];
|
|
|
|
|
proto.unaryFilter.field = [self encodedFieldPath:filter.field];
|
|
|
|
|
if ([filter isKindOfClass:[FSTNanFilter class]]) {
|
|
|
|
|
proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNan;
|
|
|
|
|
} else if ([filter isKindOfClass:[FSTNullFilter class]]) {
|
|
|
|
|
proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNull;
|
|
|
|
|
} else {
|
|
|
|
|
FSTFail(@"Unrecognized filter: %@", filter);
|
|
|
|
|
}
|
|
|
|
|
return proto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (id<FSTFilter>)decodedUnaryFilter:(GCFSStructuredQuery_UnaryFilter *)proto {
|
|
|
|
|
FSTFieldPath *field = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath];
|
|
|
|
|
switch (proto.op) {
|
|
|
|
|
case GCFSStructuredQuery_UnaryFilter_Operator_IsNan:
|
|
|
|
|
return [[FSTNanFilter alloc] initWithField:field];
|
|
|
|
|
|
|
|
|
|
case GCFSStructuredQuery_UnaryFilter_Operator_IsNull:
|
|
|
|
|
return [[FSTNullFilter alloc] initWithField:field];
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unrecognized UnaryFilter.operator %d", proto.op);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSStructuredQuery_FieldReference *)encodedFieldPath:(FSTFieldPath *)fieldPath {
|
|
|
|
|
GCFSStructuredQuery_FieldReference *ref = [GCFSStructuredQuery_FieldReference message];
|
|
|
|
|
ref.fieldPath = fieldPath.canonicalString;
|
|
|
|
|
return ref;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSStructuredQuery_FieldFilter_Operator)encodedRelationFilterOperator:
|
|
|
|
|
(FSTRelationFilterOperator)filterOperator {
|
|
|
|
|
switch (filterOperator) {
|
|
|
|
|
case FSTRelationFilterOperatorLessThan:
|
|
|
|
|
return GCFSStructuredQuery_FieldFilter_Operator_LessThan;
|
|
|
|
|
case FSTRelationFilterOperatorLessThanOrEqual:
|
|
|
|
|
return GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual;
|
|
|
|
|
case FSTRelationFilterOperatorEqual:
|
|
|
|
|
return GCFSStructuredQuery_FieldFilter_Operator_Equal;
|
|
|
|
|
case FSTRelationFilterOperatorGreaterThanOrEqual:
|
|
|
|
|
return GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual;
|
|
|
|
|
case FSTRelationFilterOperatorGreaterThan:
|
|
|
|
|
return GCFSStructuredQuery_FieldFilter_Operator_GreaterThan;
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unhandled FSTRelationFilterOperator: %ld", (long)filterOperator);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTRelationFilterOperator)decodedRelationFilterOperator:
|
|
|
|
|
(GCFSStructuredQuery_FieldFilter_Operator)filterOperator {
|
|
|
|
|
switch (filterOperator) {
|
|
|
|
|
case GCFSStructuredQuery_FieldFilter_Operator_LessThan:
|
|
|
|
|
return FSTRelationFilterOperatorLessThan;
|
|
|
|
|
case GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual:
|
|
|
|
|
return FSTRelationFilterOperatorLessThanOrEqual;
|
|
|
|
|
case GCFSStructuredQuery_FieldFilter_Operator_Equal:
|
|
|
|
|
return FSTRelationFilterOperatorEqual;
|
|
|
|
|
case GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual:
|
|
|
|
|
return FSTRelationFilterOperatorGreaterThanOrEqual;
|
|
|
|
|
case GCFSStructuredQuery_FieldFilter_Operator_GreaterThan:
|
|
|
|
|
return FSTRelationFilterOperatorGreaterThan;
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unhandled FieldFilter.operator: %d", filterOperator);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark Property Orders
|
|
|
|
|
|
|
|
|
|
- (NSArray<GCFSStructuredQuery_Order *> *)encodedSortOrders:(NSArray<FSTSortOrder *> *)orders {
|
|
|
|
|
NSMutableArray<GCFSStructuredQuery_Order *> *protos = [NSMutableArray array];
|
|
|
|
|
for (FSTSortOrder *order in orders) {
|
|
|
|
|
[protos addObject:[self encodedSortOrder:order]];
|
|
|
|
|
}
|
|
|
|
|
return protos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSArray<FSTSortOrder *> *)decodedSortOrders:(NSArray<GCFSStructuredQuery_Order *> *)protos {
|
|
|
|
|
NSMutableArray<FSTSortOrder *> *result = [NSMutableArray arrayWithCapacity:protos.count];
|
|
|
|
|
for (GCFSStructuredQuery_Order *orderProto in protos) {
|
|
|
|
|
[result addObject:[self decodedSortOrder:orderProto]];
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (GCFSStructuredQuery_Order *)encodedSortOrder:(FSTSortOrder *)sortOrder {
|
|
|
|
|
GCFSStructuredQuery_Order *proto = [GCFSStructuredQuery_Order message];
|
|
|
|
|
proto.field = [self encodedFieldPath:sortOrder.field];
|
|
|
|
|
if (sortOrder.ascending) {
|
|
|
|
|
proto.direction = GCFSStructuredQuery_Direction_Ascending;
|
|
|
|
|
} else {
|
|
|
|
|
proto.direction = GCFSStructuredQuery_Direction_Descending;
|
|
|
|
|
}
|
|
|
|
|
return proto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTSortOrder *)decodedSortOrder:(GCFSStructuredQuery_Order *)proto {
|
|
|
|
|
FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath];
|
|
|
|
|
BOOL ascending;
|
|
|
|
|
switch (proto.direction) {
|
|
|
|
|
case GCFSStructuredQuery_Direction_Ascending:
|
|
|
|
|
ascending = YES;
|
|
|
|
|
break;
|
|
|
|
|
case GCFSStructuredQuery_Direction_Descending:
|
|
|
|
|
ascending = NO;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unrecognized GCFSStructuredQuery_Direction %d", proto.direction);
|
|
|
|
|
}
|
|
|
|
|
return [FSTSortOrder sortOrderWithFieldPath:fieldPath ascending:ascending];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - Bounds/Cursors
|
|
|
|
|
|
|
|
|
|
- (GCFSCursor *)encodedBound:(FSTBound *)bound {
|
|
|
|
|
GCFSCursor *proto = [GCFSCursor message];
|
|
|
|
|
proto.before = bound.isBefore;
|
|
|
|
|
for (FSTFieldValue *fieldValue in bound.position) {
|
|
|
|
|
GCFSValue *value = [self encodedFieldValue:fieldValue];
|
|
|
|
|
[proto.valuesArray addObject:value];
|
|
|
|
|
}
|
|
|
|
|
return proto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTBound *)decodedBound:(GCFSCursor *)proto {
|
|
|
|
|
NSMutableArray<FSTFieldValue *> *indexComponents = [NSMutableArray array];
|
|
|
|
|
|
|
|
|
|
for (GCFSValue *valueProto in proto.valuesArray) {
|
|
|
|
|
FSTFieldValue *value = [self decodedFieldValue:valueProto];
|
|
|
|
|
[indexComponents addObject:value];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [FSTBound boundWithPosition:indexComponents isBefore:proto.before];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#pragma mark - FSTWatchChange <= GCFSListenResponse proto
|
|
|
|
|
|
|
|
|
|
- (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange {
|
|
|
|
|
switch (watchChange.responseTypeOneOfCase) {
|
|
|
|
|
case GCFSListenResponse_ResponseType_OneOfCase_TargetChange:
|
|
|
|
|
return [self decodedTargetChangeFromWatchChange:watchChange.targetChange];
|
|
|
|
|
|
|
|
|
|
case GCFSListenResponse_ResponseType_OneOfCase_DocumentChange:
|
|
|
|
|
return [self decodedDocumentChange:watchChange.documentChange];
|
|
|
|
|
|
|
|
|
|
case GCFSListenResponse_ResponseType_OneOfCase_DocumentDelete:
|
|
|
|
|
return [self decodedDocumentDelete:watchChange.documentDelete];
|
|
|
|
|
|
|
|
|
|
case GCFSListenResponse_ResponseType_OneOfCase_DocumentRemove:
|
|
|
|
|
return [self decodedDocumentRemove:watchChange.documentRemove];
|
|
|
|
|
|
|
|
|
|
case GCFSListenResponse_ResponseType_OneOfCase_Filter:
|
|
|
|
|
return [self decodedExistenceFilterWatchChange:watchChange.filter];
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unknown WatchChange.changeType %" PRId32, watchChange.responseTypeOneOfCase);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange {
|
|
|
|
|
// We have only reached a consistent snapshot for the entire stream if there is a read_time set
|
|
|
|
|
// and it applies to all targets (i.e. the list of targets is empty). The backend is guaranteed to
|
|
|
|
|
// send such responses.
|
|
|
|
|
if (watchChange.responseTypeOneOfCase != GCFSListenResponse_ResponseType_OneOfCase_TargetChange) {
|
|
|
|
|
return [FSTSnapshotVersion noVersion];
|
|
|
|
|
}
|
|
|
|
|
if (watchChange.targetChange.targetIdsArray.count != 0) {
|
|
|
|
|
return [FSTSnapshotVersion noVersion];
|
|
|
|
|
}
|
|
|
|
|
return [self decodedVersion:watchChange.targetChange.readTime];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTWatchTargetChange *)decodedTargetChangeFromWatchChange:(GCFSTargetChange *)change {
|
|
|
|
|
FSTWatchTargetChangeState state = [self decodedWatchTargetChangeState:change.targetChangeType];
|
|
|
|
|
NSMutableArray<NSNumber *> *targetIDs =
|
|
|
|
|
[NSMutableArray arrayWithCapacity:change.targetIdsArray_Count];
|
|
|
|
|
|
|
|
|
|
[change.targetIdsArray enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) {
|
|
|
|
|
[targetIDs addObject:@(value)];
|
|
|
|
|
}];
|
|
|
|
|
|
|
|
|
|
NSError *cause = nil;
|
|
|
|
|
if (change.hasCause) {
|
|
|
|
|
cause = [NSError errorWithDomain:FIRFirestoreErrorDomain
|
|
|
|
|
code:change.cause.code
|
|
|
|
|
userInfo:@{NSLocalizedDescriptionKey : change.cause.message}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [[FSTWatchTargetChange alloc] initWithState:state
|
|
|
|
|
targetIDs:targetIDs
|
|
|
|
|
resumeToken:change.resumeToken
|
|
|
|
|
cause:cause];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTWatchTargetChangeState)decodedWatchTargetChangeState:
|
|
|
|
|
(GCFSTargetChange_TargetChangeType)state {
|
|
|
|
|
switch (state) {
|
|
|
|
|
case GCFSTargetChange_TargetChangeType_NoChange:
|
|
|
|
|
return FSTWatchTargetChangeStateNoChange;
|
|
|
|
|
case GCFSTargetChange_TargetChangeType_Add:
|
|
|
|
|
return FSTWatchTargetChangeStateAdded;
|
|
|
|
|
case GCFSTargetChange_TargetChangeType_Remove:
|
|
|
|
|
return FSTWatchTargetChangeStateRemoved;
|
|
|
|
|
case GCFSTargetChange_TargetChangeType_Current:
|
|
|
|
|
return FSTWatchTargetChangeStateCurrent;
|
|
|
|
|
case GCFSTargetChange_TargetChangeType_Reset:
|
|
|
|
|
return FSTWatchTargetChangeStateReset;
|
|
|
|
|
default:
|
|
|
|
|
FSTFail(@"Unexpected TargetChange.state: %" PRId32, state);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSArray<NSNumber *> *)decodedIntegerArray:(GPBInt32Array *)values {
|
|
|
|
|
NSMutableArray<NSNumber *> *result = [NSMutableArray arrayWithCapacity:values.count];
|
|
|
|
|
[values enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) {
|
|
|
|
|
[result addObject:@(value)];
|
|
|
|
|
}];
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTDocumentWatchChange *)decodedDocumentChange:(GCFSDocumentChange *)change {
|
|
|
|
|
FSTObjectValue *value = [self decodedFields:change.document.fields];
|
|
|
|
|
FSTDocumentKey *key = [self decodedDocumentKey:change.document.name];
|
|
|
|
|
FSTSnapshotVersion *version = [self decodedVersion:change.document.updateTime];
|
|
|
|
|
FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]],
|
|
|
|
|
@"Got a document change with no snapshot version");
|
|
|
|
|
FSTMaybeDocument *document =
|
|
|
|
|
[FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO];
|
|
|
|
|
|
|
|
|
|
NSArray<NSNumber *> *updatedTargetIds = [self decodedIntegerArray:change.targetIdsArray];
|
|
|
|
|
NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray];
|
|
|
|
|
|
|
|
|
|
return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedTargetIds
|
|
|
|
|
removedTargetIDs:removedTargetIds
|
|
|
|
|
documentKey:document.key
|
|
|
|
|
document:document];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTDocumentWatchChange *)decodedDocumentDelete:(GCFSDocumentDelete *)change {
|
|
|
|
|
FSTDocumentKey *key = [self decodedDocumentKey:change.document];
|
|
|
|
|
// Note that version might be unset in which case we use [FSTSnapshotVersion noVersion]
|
|
|
|
|
FSTSnapshotVersion *version = [self decodedVersion:change.readTime];
|
|
|
|
|
FSTMaybeDocument *document = [FSTDeletedDocument documentWithKey:key version:version];
|
|
|
|
|
|
|
|
|
|
NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray];
|
|
|
|
|
|
|
|
|
|
return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
|
|
|
|
|
removedTargetIDs:removedTargetIds
|
|
|
|
|
documentKey:document.key
|
|
|
|
|
document:document];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTDocumentWatchChange *)decodedDocumentRemove:(GCFSDocumentRemove *)change {
|
|
|
|
|
FSTDocumentKey *key = [self decodedDocumentKey:change.document];
|
|
|
|
|
NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray];
|
|
|
|
|
|
|
|
|
|
return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
|
|
|
|
|
removedTargetIDs:removedTargetIds
|
|
|
|
|
documentKey:key
|
|
|
|
|
document:nil];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FSTExistenceFilterWatchChange *)decodedExistenceFilterWatchChange:(GCFSExistenceFilter *)filter {
|
|
|
|
|
// TODO(dimond): implement existence filter parsing
|
|
|
|
|
FSTExistenceFilter *existenceFilter = [FSTExistenceFilter filterWithCount:filter.count];
|
|
|
|
|
FSTTargetID targetID = filter.targetId;
|
|
|
|
|
return [FSTExistenceFilterWatchChange changeWithFilter:existenceFilter targetID:targetID];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|