From 7085f3d4a984d468ff07e4ba8387b9e8135e34ea Mon Sep 17 00:00:00 2001 From: Aashish Patil Date: Wed, 24 May 2023 13:29:10 -0700 Subject: [PATCH 1/4] Referenceable Object Swift PropertyWrapper - @FirestoreObjRef --- .../FirestoreObjRefWrapper.swift | 150 ++++++++++ .../ReferenceableObject.swift | 138 ++++++++++ .../ReferenceableObjectManager.swift | 257 ++++++++++++++++++ 3 files changed, 545 insertions(+) create mode 100644 Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift create mode 100644 Firestore/Swift/Source/ReferenceableObject/ReferenceableObject.swift create mode 100644 Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift diff --git a/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift b/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift new file mode 100644 index 00000000000..2f33f8b633e --- /dev/null +++ b/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift @@ -0,0 +1,150 @@ +/* + * Copyright 2023 Google LLC + * + * 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 FirebaseFirestore + +/// Property wrapper @FirestoreObjectReference, +/// Indicates that the specified property value should be stored by reference instead of by value, inline +/// with the parent object. +/// +/// When loading a parent object, any references are not loaded by default and can be loaded on demand +/// using the projected value of the wrapper. +/// +/// structs that can be stored as a reference must implement the `ReferenceableObject` protocol +/// +/// variables that are annotated with the propertyWrapper should be marked as `Optional` since they can be nil when not loaded or set +/// +/// Example: +/// We have three structs representing a simplified UserProfile model - +/// `UserProfile` +/// `Employer` - an employer who the `UserProfile` works for +/// `WorkLocation` - representing a generic location. This can be used to represent a generic location +/// +/// Since multiple Users can work for an Employer, it makes sense to have only one instance of an Employer that is referred to +/// by multiple Users. Similarly multiple users can be in a WorkLocation. Additionally, an Employer can also be located +/// in a WorkLocation (e.g. headquarters of an Employer). We can mark WorkLocation and Employer as a ReferenceableObjects. +/// +/// +/// ``` +/// struct UserProfile: ReferenceableObject { +/// var username: String +/// +/// @FirestoreObjRef +/// var employer: Employer? +/// +/// @FirestoreObjRef +/// var workLocation: WorkLocation? +/// +/// } +/// +/// struct Employer: ReferenceableObject { +/// var name: String +/// +/// @FirestoreObjRef +/// var headquarters: WorkLocation? +/// } +/// +/// struct WorkLocation: ReferenceableObject { +/// var locationName: String +/// var moreInfo: String // +/// +/// } +/// +/// var userProfile = ... +/// +/// // use projected value to load referenced employer +/// try await userProfile.$employer?.loadObject() +/// +/// // use projected value to load referenced workLocation +/// try await prof.$workLocation?.loadObject() +/// +/// +/// +/// ``` +/// +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@propertyWrapper +public struct FirestoreObjRef where T: ReferenceableObject { + private var objReference: ObjReference? + + public init(wrappedValue initialValue: T?) { + updateInitialValue(initialValue: initialValue) + } + + private mutating func updateInitialValue(initialValue: T?) { + if var initialValue { + var oid = initialValue.id + if oid == nil { + let docRef = Firestore.firestore().collection(T.parentCollection()).document() + oid = docRef.documentID + initialValue.id = oid + } + objReference = ObjReference( + documentId: oid!, + collection: T.parentCollection(), + referencedObj: initialValue + ) + } + } + + public var wrappedValue: T? { + get { + return objReference?.referencedObj + } + + set { + if objReference != nil { + objReference?.referencedObj = newValue + } else { + updateInitialValue(initialValue: newValue) + } + } + } + + public var projectedValue: ObjReference? { + get { + return objReference + } + set { + objReference = newValue + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension FirestoreObjRef: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let objRef = try container.decode(ObjReference.self) + objReference = objRef + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + if let objReference { + try container.encode(objReference) + + if let value = objReference.referencedObj { + Task { + try await ReferenceableObjectManager.instance.save(object: value) + } + } + + } else { + try container.encodeNil() + } + } +} diff --git a/Firestore/Swift/Source/ReferenceableObject/ReferenceableObject.swift b/Firestore/Swift/Source/ReferenceableObject/ReferenceableObject.swift new file mode 100644 index 00000000000..6f6f17a869f --- /dev/null +++ b/Firestore/Swift/Source/ReferenceableObject/ReferenceableObject.swift @@ -0,0 +1,138 @@ +/* + * Copyright 2023 Google LLC + * + * 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 FirebaseFirestore + +import CryptoKit +import OSLog + +/// A protocol that denotes an object that can be "stored by reference" in Firestore. +/// Structs that implement this and are contained in other Structs, are stored as a reference +/// instead of stored inline if the FirestoreObjRef propertyWrapper is used to annotate them. +/// +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public protocol ReferenceableObject: Codable, Identifiable, Hashable, Equatable { + // The Firestore collection where objects of this type are stored. + // If no value is specified, it defaults to Type name of the object + static func parentCollection() -> String + + var id: String? { get set } + + var path: String? { get } + + static func objectPath(objectId: String) -> String +} + +// default implementations +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public extension ReferenceableObject { + static func parentCollection() -> String { + let t = type(of: self) + return String(describing: t) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(path) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + if let lpath = lhs.path, + let rpath = rhs.path { + return lpath == rpath + } else { + return false + } + } + + var path: String? { + guard let id else { + return nil + } + + return Self.objectPath(objectId: id) + } + + // Helper function that creates a path for the object + static func objectPath(objectId: String) -> String { + let coll = Self.parentCollection() + if coll.hasSuffix("/") { + return coll + objectId + } else { + return coll + "/" + objectId + } + } +} + +// MARK: Contained Object Reference + +/// Struct used to store a reference to a ReferenceableObject. When a container Struct is +/// encoded, any FirestoreObjRef property wrappers are encoded as ObjReference objects and the referenced +/// objects are stored (if needed) in their intended location instead of inline. +/// +/// For example: +/// When storing UserProfile, which contains an Employer referenceable object, Employer object is stored +/// in the Employer collection and a reference to Employer is stored within the UserProfile encoded document. +/// +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct ObjReference: Codable { + public var documentId: String + public var collection: String + public var placeholder: Codable? + + /// + public var referencedObj: T? + + enum CodingKeys: String, CodingKey { + case documentId = "docId" + case collection + } + + /// Loads the referenced object from the db and assigns it to the referencedObj property + public mutating func loadObject() async throws { + let obj: T? = try await ReferenceableObjectManager.instance.getObject(objId: documentId) + referencedObj = obj + } +} + +// MARK: Logger + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension OSLog { + static var storeByReference = OSLog( + subsystem: "com.google.firebase.firestore", + category: "storeByReference" + ) + + func debug(_ message: StaticString, _ args: CVarArg...) { + os_log(message, log: self, type: .debug, args) + } + + func info(_ message: StaticString, _ args: CVarArg...) { + os_log(message, log: self, type: .info, args) + } + + func log(_ message: StaticString, _ args: CVarArg...) { + os_log(message, log: self, type: .default, args) + } + + func error(_ message: StaticString, _ args: CVarArg) { + os_log(message, log: self, type: .error, args) + } + + func fault(_ message: StaticString, args: CVarArg) { + os_log(message, log: self, type: .fault, args) + } +} diff --git a/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift b/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift new file mode 100644 index 00000000000..f35fe1ab058 --- /dev/null +++ b/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift @@ -0,0 +1,257 @@ +/* + * Copyright 2023 Google LLC + * + * 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 FirebaseFirestore + +import CryptoKit +import OSLog + +/// Used to fetch and save ReferenceableObjects. +/// +/// To prevent refetch of the same referenced object immediately, the manager +/// also momentarily caches the referenced object. This interval is configurable. +/// +/// To prevent writes of unmodified referenced objects, the manager compares checksums for the +/// object being written. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public class ReferenceableObjectManager { + public static var instance = ReferenceableObjectManager() + + static var settings = ReferenceableObjectManagerSettings() + + private var db = Firestore.firestore() + + private var objectCache = ReferenceableObjectCache() + + public func save(object: T) async throws { + do { + let encoder = Firestore.Encoder() + let json = try encoder.encode(object) + + if let docId = object.id { + guard let currentDigest = computeHash(obj: json), + await needsSave(object: object, currentDigest: currentDigest) else { + OSLog.storeByReference.debug("Object doesn't need to be saved") + return + } + + try await db.collection(T.parentCollection()).document(docId).setData(json) + await objectCache.add(object: object, digest: currentDigest) + } else { + try await db.collection(T.parentCollection()).document().setData(json) + } + + print("Save complete") + } + } + + public func getObject(objId: String) async throws -> T? { + do { + // first check cache + if let cacheEntry = await objectCache.get(for: T.objectPath(objectId: objId)) { + return cacheEntry.object as! T + } + + // get from db + let docRef = db.collection(T.parentCollection()).document(objId) + let doc = try await docRef.getDocument() + let obj = try doc.data(as: T.self) + + // cache the doc since we just fetched it from store + if let jsonData = doc.data(), + let digest = computeHash(obj: jsonData) { + await objectCache.add(object: obj, digest: digest) + } + + return obj + } + } + + public func fetchObjects(type: T.Type) async throws -> [T] { + var foundObjects = [T]() + do { + let collectionRef = db.collection(type.parentCollection()) + let docSnapshot = try await collectionRef.getDocuments() + + for document in docSnapshot.documents { + let refObj = try document.data(as: T.self) + foundObjects.append(refObj) + + let jsonData = document.data() + if let digest = computeHash(obj: jsonData) { + await objectCache.add(object: refObj, digest: digest) + } + } + } + + OSLog.storeByReference.debug("fetchObjects found %ld objects", foundObjects.count) + + return foundObjects + } + + public func fetchObjects(predicates: [QueryPredicate]) async throws + -> [T] { + var query: Query = db.collection(T.parentCollection()) + + query = createQuery(query: query, predicates: predicates) + + var foundObjects = [T]() + let snapshot = try await query.getDocuments() + for document in snapshot.documents { + let refObj = try document.data(as: T.self) + foundObjects.append(refObj) + + let jsonData = document.data() + if let digest = computeHash(obj: jsonData) { + await objectCache.add(object: refObj, digest: digest) + } + } + + return foundObjects + } + + // MARK: Internal helper functions + + private func needsSave(object: T, + currentDigest: Insecure.MD5Digest) async -> Bool { + guard let objPath = object.path else { + // we don't have an object path so can't find cached value + // save object + return true + } + + guard let cacheEntry = await objectCache.get(for: objPath) else { + // we don't have a cached entry for this object. + // save object + return true + } + + guard cacheEntry.digest == currentDigest else { + // digests of cached object and current object to be saved don't match + // save object + return true + } + + return false + } + + private func computeHash(obj: [String: Any]) -> Insecure.MD5Digest? { + do { + let objData = try PropertyListSerialization.data( + fromPropertyList: obj, + format: .binary, + options: .max + ) + + var md5 = Insecure.MD5() + md5.update(data: objData) + let digest = md5.finalize() + + return digest + } catch { + // this doesn't prevent functionality so not erroring here. + OSLog.storeByReference.info("Failed to compute hash") + return nil + } + } + + // logic copied from FirestoreQueryObservable.swift#createListener() + private func createQuery(query: Query, predicates: [QueryPredicate]) -> Query { + var query = query + + for predicate in predicates { + switch predicate { + case let .isEqualTo(field, value): + query = query.whereField(field, isEqualTo: value) + case let .isIn(field, values): + query = query.whereField(field, in: values) + case let .isNotIn(field, values): + query = query.whereField(field, notIn: values) + case let .arrayContains(field, value): + query = query.whereField(field, arrayContains: value) + case let .arrayContainsAny(field, values): + query = query.whereField(field, arrayContainsAny: values) + case let .isLessThan(field, value): + query = query.whereField(field, isLessThan: value) + case let .isGreaterThan(field, value): + query = query.whereField(field, isGreaterThan: value) + case let .isLessThanOrEqualTo(field, value): + query = query.whereField(field, isLessThanOrEqualTo: value) + case let .isGreaterThanOrEqualTo(field, value): + query = query.whereField(field, isGreaterThanOrEqualTo: value) + case let .orderBy(field, value): + query = query.order(by: field, descending: value) + case let .limitTo(field): + query = query.limit(to: field) + case let .limitToLast(field): + query = query.limit(toLast: field) + } + } + + return query + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +struct ReferenceableObjectManagerSettings { + // how long to cache object + // the purpose is not to cache for a long time + var cacheValidityInterval: TimeInterval = 5.0 // seconds +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +private struct ReferenceableObjectCacheEntry { + var digest: Insecure.MD5Digest + var object: any ReferenceableObject + var insertTime: TimeInterval +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +private actor ReferenceableObjectCache { + var cache = [String: ReferenceableObjectCacheEntry]() + + func add(object: T, digest: Insecure.MD5Digest) { + if let docId = object.id { + cache[docId] = ReferenceableObjectCacheEntry( + digest: digest, + object: object, + insertTime: Date().timeIntervalSince1970 + ) + // print("Added object to cache \(docId)") + OSLog.storeByReference.info("Added object to cache %@", docId) + } + } + + func get(for docId: String) -> ReferenceableObjectCacheEntry? { + guard let entry = cache[docId] else { + return nil + } + + let now = Date().timeIntervalSince1970 + let cacheTime = ReferenceableObjectManager.settings.cacheValidityInterval + guard now - entry.insertTime < cacheTime else { + // older entry - invalidate it + cache[docId] = nil + return nil + } + + return cache[docId] + } + + func removeAll() { + cache.removeAll() + } +} From 2fb23c05bdf1d7c5f01c198ce1850dd575ca8a18 Mon Sep 17 00:00:00 2001 From: Aashish Patil Date: Mon, 29 May 2023 07:22:34 +0530 Subject: [PATCH 2/4] Peter feedback --- .../Swift/Source/Common/FirestoreLogger.swift | 55 ++++++++++++++ .../FirestoreObjRefWrapper.swift | 73 +++++++++---------- .../ReferenceableObject.swift | 67 ++++++----------- .../ReferenceableObjectManager.swift | 45 ++++++++---- 4 files changed, 144 insertions(+), 96 deletions(-) create mode 100644 Firestore/Swift/Source/Common/FirestoreLogger.swift diff --git a/Firestore/Swift/Source/Common/FirestoreLogger.swift b/Firestore/Swift/Source/Common/FirestoreLogger.swift new file mode 100644 index 00000000000..88521c58be9 --- /dev/null +++ b/Firestore/Swift/Source/Common/FirestoreLogger.swift @@ -0,0 +1,55 @@ +// +// File.swift +// +// +// Created by Aashish Patil on 5/29/23. +// + +import Foundation +import OSLog + +/// Base FirestoreLogger class +/// Logging categories can be created by extending FirestoreLogger and defining a static FirestoreLogger var with your category +/// +/// ``` +/// extension FirestoreLogger { +/// static var myCategory = FirestoreLogger(category: "myCategory") +/// } +/// ``` +/// +/// To use your extension, call +/// ``` +/// FirestoreLogger.myCategory.log(msg, vars) +/// ``` +/// See ReferenceableObject.swift for the FirestoreLogger extension for an example + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +class FirestoreLogger: OSLog { + + private let firestoreSubsystem = "com.google.firebase.firestore" + + init(category: String) { + super.init(subsystem: firestoreSubsystem, category: category) + } + + func debug(_ message: StaticString, _ args: CVarArg...) { + os_log(message, log: self, type: .debug, args) + } + + func info(_ message: StaticString, _ args: CVarArg...) { + os_log(message, log: self, type: .info, args) + } + + func log(_ message: StaticString, _ args: CVarArg...) { + os_log(message, log: self, type: .default, args) + } + + func error(_ message: StaticString, _ args: CVarArg) { + os_log(message, log: self, type: .error, args) + } + + func fault(_ message: StaticString, _ args: CVarArg) { + os_log(message, log: self, type: .fault, args) + } + +} diff --git a/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift b/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift index 2f33f8b633e..6d2c628192a 100644 --- a/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift +++ b/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift @@ -23,9 +23,9 @@ import FirebaseFirestore /// When loading a parent object, any references are not loaded by default and can be loaded on demand /// using the projected value of the wrapper. /// -/// structs that can be stored as a reference must implement the `ReferenceableObject` protocol +/// Structs that can be stored as a reference must implement the `ReferenceableObject` protocol /// -/// variables that are annotated with the propertyWrapper should be marked as `Optional` since they can be nil when not loaded or set +/// Variables that are annotated with the propertyWrapper should be marked as `Optional` since they can be nil when not loaded or set /// /// Example: /// We have three structs representing a simplified UserProfile model - @@ -40,36 +40,36 @@ import FirebaseFirestore /// /// ``` /// struct UserProfile: ReferenceableObject { -/// var username: String +/// var username: String /// -/// @FirestoreObjRef -/// var employer: Employer? +/// @FirestoreObjectReference +/// var employer: Employer? /// -/// @FirestoreObjRef -/// var workLocation: WorkLocation? +/// @FirestoreObjectReference +/// var workLocation: WorkLocation? /// /// } /// /// struct Employer: ReferenceableObject { -/// var name: String +/// var name: String /// -/// @FirestoreObjRef -/// var headquarters: WorkLocation? +/// @FirestoreObjectReference +/// var headquarters: WorkLocation? /// } /// /// struct WorkLocation: ReferenceableObject { -/// var locationName: String -/// var moreInfo: String // +/// var locationName: String +/// var moreInfo: String /// /// } /// /// var userProfile = ... /// /// // use projected value to load referenced employer -/// try await userProfile.$employer?.loadObject() +/// try await userProfile.$employer?.loadReferencedObject() /// /// // use projected value to load referenced workLocation -/// try await prof.$workLocation?.loadObject() +/// try await prof.$workLocation?.loadReferencedObject() /// /// /// @@ -77,67 +77,66 @@ import FirebaseFirestore /// @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @propertyWrapper -public struct FirestoreObjRef where T: ReferenceableObject { - private var objReference: ObjReference? +public struct FirestoreObjectReference where T: ReferenceableObject { + private var objectReference: ObjectReference? public init(wrappedValue initialValue: T?) { updateInitialValue(initialValue: initialValue) } private mutating func updateInitialValue(initialValue: T?) { - if var initialValue { - var oid = initialValue.id - if oid == nil { + if let initialValue { + let objId = initialValue.id ?? { let docRef = Firestore.firestore().collection(T.parentCollection()).document() - oid = docRef.documentID - initialValue.id = oid - } - objReference = ObjReference( - documentId: oid!, + return docRef.documentID + }() + + objectReference = ObjectReference( + objectId: objId, collection: T.parentCollection(), - referencedObj: initialValue + referencedObject: initialValue ) } } public var wrappedValue: T? { get { - return objReference?.referencedObj + return objectReference?.referencedObject } set { - if objReference != nil { - objReference?.referencedObj = newValue + if objectReference != nil { + objectReference?.referencedObject = newValue } else { updateInitialValue(initialValue: newValue) } } } - public var projectedValue: ObjReference? { + public var projectedValue: ObjectReference? { get { - return objReference + return objectReference } set { - objReference = newValue + objectReference = newValue } } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension FirestoreObjRef: Codable { +extension FirestoreObjectReference: Codable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - let objRef = try container.decode(ObjReference.self) - objReference = objRef + let objRef = try container.decode(ObjectReference.self) + objectReference = objRef } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - if let objReference { - try container.encode(objReference) + if let objectReference { + try container.encode(objectReference) - if let value = objReference.referencedObj { + if let value = objectReference.referencedObject { Task { try await ReferenceableObjectManager.instance.save(object: value) } diff --git a/Firestore/Swift/Source/ReferenceableObject/ReferenceableObject.swift b/Firestore/Swift/Source/ReferenceableObject/ReferenceableObject.swift index 6f6f17a869f..16e0ae420d4 100644 --- a/Firestore/Swift/Source/ReferenceableObject/ReferenceableObject.swift +++ b/Firestore/Swift/Source/ReferenceableObject/ReferenceableObject.swift @@ -21,12 +21,12 @@ import OSLog /// A protocol that denotes an object that can be "stored by reference" in Firestore. /// Structs that implement this and are contained in other Structs, are stored as a reference -/// instead of stored inline if the FirestoreObjRef propertyWrapper is used to annotate them. +/// instead of stored inline if the FirestoreObjectReference propertyWrapper is used to annotate them. /// @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol ReferenceableObject: Codable, Identifiable, Hashable, Equatable { // The Firestore collection where objects of this type are stored. - // If no value is specified, it defaults to Type name of the object + // If no value is specified, it defaults to type name of the object static func parentCollection() -> String var id: String? { get set } @@ -67,72 +67,53 @@ public extension ReferenceableObject { // Helper function that creates a path for the object static func objectPath(objectId: String) -> String { - let coll = Self.parentCollection() - if coll.hasSuffix("/") { - return coll + objectId + let collection = Self.parentCollection() + if collection.hasSuffix("/") { + return collection + objectId } else { - return coll + "/" + objectId + return collection + "/" + objectId } } } // MARK: Contained Object Reference -/// Struct used to store a reference to a ReferenceableObject. When a container Struct is -/// encoded, any FirestoreObjRef property wrappers are encoded as ObjReference objects and the referenced +/// Struct used to store a reference to a ReferenceableObject. When a container struct is +/// encoded, any FirestoreObjectReference property wrappers are encoded as ObjectReference objects and the referenced /// objects are stored (if needed) in their intended location instead of inline. /// /// For example: /// When storing UserProfile, which contains an Employer referenceable object, Employer object is stored -/// in the Employer collection and a reference to Employer is stored within the UserProfile encoded document. +/// in the Employer collection and a reference to Employer object is stored within the UserProfile encoded document. /// @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct ObjReference: Codable { - public var documentId: String +public struct ObjectReference: Codable { + /// documentId of referenced object + public var objectId: String + + /// collection where the referenced object is stored public var collection: String - public var placeholder: Codable? - /// - public var referencedObj: T? + /// The referenced object. By default, it is not loaded. Apps can load this calling the loadReferencedObject() function. + public var referencedObject: T? enum CodingKeys: String, CodingKey { - case documentId = "docId" + case objectId case collection } - /// Loads the referenced object from the db and assigns it to the referencedObj property - public mutating func loadObject() async throws { - let obj: T? = try await ReferenceableObjectManager.instance.getObject(objId: documentId) - referencedObj = obj + /// Loads the referenced object from the db and assigns it to the referencedObject property + public mutating func loadReferencedObject() async throws { + let obj: T? = try await ReferenceableObjectManager.instance.getObject(objectId: objectId) + referencedObject = obj } } // MARK: Logger @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension OSLog { - static var storeByReference = OSLog( - subsystem: "com.google.firebase.firestore", - category: "storeByReference" +extension FirestoreLogger { + static var objectReference = FirestoreLogger( + category: "objectReference" ) - - func debug(_ message: StaticString, _ args: CVarArg...) { - os_log(message, log: self, type: .debug, args) - } - - func info(_ message: StaticString, _ args: CVarArg...) { - os_log(message, log: self, type: .info, args) - } - - func log(_ message: StaticString, _ args: CVarArg...) { - os_log(message, log: self, type: .default, args) - } - - func error(_ message: StaticString, _ args: CVarArg) { - os_log(message, log: self, type: .error, args) - } - - func fault(_ message: StaticString, args: CVarArg) { - os_log(message, log: self, type: .fault, args) - } } diff --git a/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift b/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift index f35fe1ab058..7780781d942 100644 --- a/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift +++ b/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift @@ -36,38 +36,43 @@ public class ReferenceableObjectManager { private var objectCache = ReferenceableObjectCache() + private let logPrefix = "ReferenceableObjectManager:" + public func save(object: T) async throws { do { - let encoder = Firestore.Encoder() - let json = try encoder.encode(object) + if let docId = object.id, + await objectCache.contains(for: docId) { + let encoder = Firestore.Encoder() + let json = try encoder.encode(object) - if let docId = object.id { guard let currentDigest = computeHash(obj: json), await needsSave(object: object, currentDigest: currentDigest) else { - OSLog.storeByReference.debug("Object doesn't need to be saved") + FirestoreLogger.objectReference.debug("%@ Object doesn't need to be saved", logPrefix) return } try await db.collection(T.parentCollection()).document(docId).setData(json) await objectCache.add(object: object, digest: currentDigest) } else { - try await db.collection(T.parentCollection()).document().setData(json) + + let documentReference = db.collection(T.parentCollection()).document() + try documentReference.setData(from: object) } - print("Save complete") + FirestoreLogger.objectReference.debug("%@ save object complete", logPrefix) } } - public func getObject(objId: String) async throws -> T? { + public func getObject(objectId: String) async throws -> T? { do { // first check cache - if let cacheEntry = await objectCache.get(for: T.objectPath(objectId: objId)) { + if let cacheEntry = await objectCache.get(for: T.objectPath(objectId: objectId)) { return cacheEntry.object as! T } // get from db - let docRef = db.collection(T.parentCollection()).document(objId) - let doc = try await docRef.getDocument() + let documentReference = db.collection(T.parentCollection()).document(objectId) + let doc = try await documentReference.getDocument() let obj = try doc.data(as: T.self) // cache the doc since we just fetched it from store @@ -80,7 +85,7 @@ public class ReferenceableObjectManager { } } - public func fetchObjects(type: T.Type) async throws -> [T] { + public func getObjects(type: T.Type) async throws -> [T] { var foundObjects = [T]() do { let collectionRef = db.collection(type.parentCollection()) @@ -97,12 +102,12 @@ public class ReferenceableObjectManager { } } - OSLog.storeByReference.debug("fetchObjects found %ld objects", foundObjects.count) + FirestoreLogger.objectReference.debug("%@ fetchObjects found %ld objects",logPrefix, foundObjects.count) return foundObjects } - public func fetchObjects(predicates: [QueryPredicate]) async throws + public func getObjects(predicates: [QueryPredicate]) async throws -> [T] { var query: Query = db.collection(T.parentCollection()) @@ -110,6 +115,7 @@ public class ReferenceableObjectManager { var foundObjects = [T]() let snapshot = try await query.getDocuments() + for document in snapshot.documents { let refObj = try document.data(as: T.self) foundObjects.append(refObj) @@ -163,7 +169,7 @@ public class ReferenceableObjectManager { return digest } catch { // this doesn't prevent functionality so not erroring here. - OSLog.storeByReference.info("Failed to compute hash") + FirestoreLogger.objectReference.info("Failed to compute hash") return nil } } @@ -230,8 +236,7 @@ private actor ReferenceableObjectCache { object: object, insertTime: Date().timeIntervalSince1970 ) - // print("Added object to cache \(docId)") - OSLog.storeByReference.info("Added object to cache %@", docId) + FirestoreLogger.objectReference.debug("Added object to cache %@", docId) } } @@ -251,6 +256,14 @@ private actor ReferenceableObjectCache { return cache[docId] } + func contains(for docId: String) -> Bool { + guard cache[docId] != nil else { + return false + } + + return true + } + func removeAll() { cache.removeAll() } From d6b297bd7d37ba1213544b2aff821afd355f6ed6 Mon Sep 17 00:00:00 2001 From: Aashish Patil Date: Mon, 29 May 2023 10:07:30 +0530 Subject: [PATCH 3/4] Styling, force cast --- Firestore/Swift/Source/Common/FirestoreLogger.swift | 6 ++---- .../ReferenceableObject/FirestoreObjRefWrapper.swift | 3 +-- .../ReferenceableObject/ReferenceableObjectManager.swift | 9 ++++++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Firestore/Swift/Source/Common/FirestoreLogger.swift b/Firestore/Swift/Source/Common/FirestoreLogger.swift index 88521c58be9..20c72549e6b 100644 --- a/Firestore/Swift/Source/Common/FirestoreLogger.swift +++ b/Firestore/Swift/Source/Common/FirestoreLogger.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Aashish Patil on 5/29/23. // @@ -10,7 +10,7 @@ import OSLog /// Base FirestoreLogger class /// Logging categories can be created by extending FirestoreLogger and defining a static FirestoreLogger var with your category -/// +/// /// ``` /// extension FirestoreLogger { /// static var myCategory = FirestoreLogger(category: "myCategory") @@ -25,7 +25,6 @@ import OSLog @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) class FirestoreLogger: OSLog { - private let firestoreSubsystem = "com.google.firebase.firestore" init(category: String) { @@ -51,5 +50,4 @@ class FirestoreLogger: OSLog { func fault(_ message: StaticString, _ args: CVarArg) { os_log(message, log: self, type: .fault, args) } - } diff --git a/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift b/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift index 6d2c628192a..ae3d8604497 100644 --- a/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift +++ b/Firestore/Swift/Source/ReferenceableObject/FirestoreObjRefWrapper.swift @@ -17,8 +17,7 @@ import FirebaseFirestore /// Property wrapper @FirestoreObjectReference, -/// Indicates that the specified property value should be stored by reference instead of by value, inline -/// with the parent object. +/// Indicates that the specified property value should be stored by reference instead of by value /// /// When loading a parent object, any references are not loaded by default and can be loaded on demand /// using the projected value of the wrapper. diff --git a/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift b/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift index 7780781d942..2070357419e 100644 --- a/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift +++ b/Firestore/Swift/Source/ReferenceableObject/ReferenceableObjectManager.swift @@ -54,7 +54,6 @@ public class ReferenceableObjectManager { try await db.collection(T.parentCollection()).document(docId).setData(json) await objectCache.add(object: object, digest: currentDigest) } else { - let documentReference = db.collection(T.parentCollection()).document() try documentReference.setData(from: object) } @@ -67,7 +66,7 @@ public class ReferenceableObjectManager { do { // first check cache if let cacheEntry = await objectCache.get(for: T.objectPath(objectId: objectId)) { - return cacheEntry.object as! T + return cacheEntry.object as? T } // get from db @@ -102,7 +101,11 @@ public class ReferenceableObjectManager { } } - FirestoreLogger.objectReference.debug("%@ fetchObjects found %ld objects",logPrefix, foundObjects.count) + FirestoreLogger.objectReference.debug( + "%@ fetchObjects found %ld objects", + logPrefix, + foundObjects.count + ) return foundObjects } From bdab2f04dff51f1e48180717cea20d4eeb1a1582 Mon Sep 17 00:00:00 2001 From: Aashish Patil Date: Mon, 29 May 2023 10:36:58 +0530 Subject: [PATCH 4/4] FirestoreLogger copyright notice --- .../Swift/Source/Common/FirestoreLogger.swift | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Firestore/Swift/Source/Common/FirestoreLogger.swift b/Firestore/Swift/Source/Common/FirestoreLogger.swift index 20c72549e6b..82568f29988 100644 --- a/Firestore/Swift/Source/Common/FirestoreLogger.swift +++ b/Firestore/Swift/Source/Common/FirestoreLogger.swift @@ -1,9 +1,18 @@ -// -// File.swift -// -// -// Created by Aashish Patil on 5/29/23. -// +/* + * Copyright 2023 Google LLC + * + * 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 import OSLog