From f86fd8e39b64c685ee57e691b84f1cf851c146ae Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 14 Feb 2023 00:53:54 -0800 Subject: [PATCH] Adapter & Firebase Improvements (#48) --- Package.swift | 9 +++ Sources/Account/Login.swift | 4 +- Sources/Account/SignUp.swift | 4 +- Sources/CardinalKit/Adapter/Adapter.swift | 19 ++++++ .../FHIRFirestoreElement.swift | 33 ----------- .../FHIRFirestoreRemovalContext.swift | 25 -------- .../FHIRToFirestoreAdapter.swift | 21 +++++-- .../FirebaseAccountConfiguration.swift | 29 +++++++-- .../FirebaseAuthAuthenticationMethods.swift | 6 +- .../AnyFirestoreElement.swift | 14 +++++ .../AnyFirestoreRemovalContext.swift | 14 +++++ Sources/FirestoreDataStorage/Firestore.swift | 42 +++++++++---- .../FirestoreElement.swift | 31 ++++++++-- .../FirestoreElementAdapter.swift | 18 ------ .../FirestoreRemovalContext.swift | 22 +++++-- .../IdentityFirestoreElementAdapter.swift | 59 +++++-------------- .../FirestoreStoragePrefixUserIdAdapter.swift | 56 ++++++++++++++++++ ...storeStoragePrefixUserIdAdapterError.swift | 14 +++++ .../UITests/TestApp/GoogleService-Info.plist | 24 ++++---- .../TestApp/Shared/TestAppStandard.swift | 4 +- .../UITests/UITests.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/swiftpm/Package.resolved | 8 +-- 22 files changed, 282 insertions(+), 178 deletions(-) delete mode 100644 Sources/FHIRToFirestoreAdapter/FHIRFirestoreElement.swift delete mode 100644 Sources/FHIRToFirestoreAdapter/FHIRFirestoreRemovalContext.swift create mode 100644 Sources/FirestoreDataStorage/AnyFirestoreElement.swift create mode 100644 Sources/FirestoreDataStorage/AnyFirestoreRemovalContext.swift delete mode 100644 Sources/FirestoreDataStorage/FirestoreElementAdapter.swift create mode 100644 Sources/FirestoreStoragePrefixUserIdAdapter/FirestoreStoragePrefixUserIdAdapter.swift create mode 100644 Sources/FirestoreStoragePrefixUserIdAdapter/FirestoreStoragePrefixUserIdAdapterError.swift diff --git a/Package.swift b/Package.swift index 42332233..73835e38 100644 --- a/Package.swift +++ b/Package.swift @@ -24,6 +24,7 @@ let package = Package( .library(name: "FHIR", targets: ["FHIR"]), .library(name: "FHIRToFirestoreAdapter", targets: ["FHIRToFirestoreAdapter"]), .library(name: "FirestoreDataStorage", targets: ["FirestoreDataStorage"]), + .library(name: "FirestoreStoragePrefixUserIdAdapter", targets: ["FirestoreStoragePrefixUserIdAdapter"]), .library(name: "FirebaseConfiguration", targets: ["FirebaseConfiguration"]), .library(name: "FirebaseAccount", targets: ["FirebaseAccount"]), .library(name: "HealthKitDataSource", targets: ["HealthKitDataSource"]), @@ -131,6 +132,14 @@ let package = Package( .product(name: "FirebaseFirestoreSwift", package: "firebase-ios-sdk") ] ), + .target( + name: "FirestoreStoragePrefixUserIdAdapter", + dependencies: [ + .target(name: "CardinalKit"), + .target(name: "FirestoreDataStorage"), + .product(name: "FirebaseAuth", package: "firebase-ios-sdk") + ] + ), .target( name: "HealthKitDataSource", dependencies: [ diff --git a/Sources/Account/Login.swift b/Sources/Account/Login.swift index c5a2e5cc..2bc8701d 100644 --- a/Sources/Account/Login.swift +++ b/Sources/Account/Login.swift @@ -30,8 +30,8 @@ public struct Login: View { } /// - Parameter header: A SwiftUI `View` displayed as a header above all login buttons. - public init(header: Header) { - self.header = header + public init(@ViewBuilder header: () -> (Header)) { + self.header = header() } } diff --git a/Sources/Account/SignUp.swift b/Sources/Account/SignUp.swift index 892d8d0f..cb84c945 100644 --- a/Sources/Account/SignUp.swift +++ b/Sources/Account/SignUp.swift @@ -30,8 +30,8 @@ public struct SignUp: View { } /// - Parameter header: A SwiftUI `View` displayed as a header above all login buttons. - public init(header: Header) { - self.header = header + public init(@ViewBuilder header: () -> (Header)) { + self.header = header() } } diff --git a/Sources/CardinalKit/Adapter/Adapter.swift b/Sources/CardinalKit/Adapter/Adapter.swift index 5eb31847..32f09a9f 100644 --- a/Sources/CardinalKit/Adapter/Adapter.swift +++ b/Sources/CardinalKit/Adapter/Adapter.swift @@ -80,3 +80,22 @@ public protocol Adapter> ) async -> any TypedAsyncSequence> } + + +extension Adapter { + /// Map a collection of `DataChange` elements using the adapter to an ``TypedAsyncSequence``. + /// - Parameter dataChanges: The data changes that should be transformed. + /// - Returns: Returns the mapped elements using the ``Adapter``'s ``transform(_:)`` function. + public func transformDataChanges( + _ dataChanges: [DataChange] + ) async -> any TypedAsyncSequence> { + await transform( + AsyncStream { continuation in + for dataChange in dataChanges { + continuation.yield(dataChange) + } + continuation.finish() + } + ) + } +} diff --git a/Sources/FHIRToFirestoreAdapter/FHIRFirestoreElement.swift b/Sources/FHIRToFirestoreAdapter/FHIRFirestoreElement.swift deleted file mode 100644 index 01b4e009..00000000 --- a/Sources/FHIRToFirestoreAdapter/FHIRFirestoreElement.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// This source file is part of the CardinalKit open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import CardinalKit -import FHIR -import FirestoreDataStorage -import Foundation - - -/// Provides a mapping from a FHIR `Resource` to a type conforming to `FirestoreElement`. -public struct FHIRFirestoreElement: FirestoreElement, @unchecked Sendable { - let resource: Resource - - public let id: String - public let collectionPath: String - - - init(_ baseType: FHIR.BaseType) { - self.resource = baseType - self.id = baseType.id?.value?.string ?? UUID().uuidString - self.collectionPath = ResourceProxy(with: baseType).resourceType - } - - - public func encode(to encoder: Encoder) throws { - try resource.encode(to: encoder) - } -} diff --git a/Sources/FHIRToFirestoreAdapter/FHIRFirestoreRemovalContext.swift b/Sources/FHIRToFirestoreAdapter/FHIRFirestoreRemovalContext.swift deleted file mode 100644 index 651e2ab2..00000000 --- a/Sources/FHIRToFirestoreAdapter/FHIRFirestoreRemovalContext.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// This source file is part of the CardinalKit open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import CardinalKit -import FHIR -import FirestoreDataStorage -import Foundation - - -/// Provides a mapping from a FHIR standard `RemovalContext` to a type conforming to `FHIRFirestoreRemovalContext`. -public struct FHIRFirestoreRemovalContext: FirestoreRemovalContext, @unchecked Sendable { - public let id: String - public let collectionPath: String - - - init(_ removalContext: FHIR.RemovalContext) { - self.id = removalContext.id?.value?.string ?? UUID().uuidString - self.collectionPath = removalContext.resourceType.rawValue - } -} diff --git a/Sources/FHIRToFirestoreAdapter/FHIRToFirestoreAdapter.swift b/Sources/FHIRToFirestoreAdapter/FHIRToFirestoreAdapter.swift index 94584cd4..64e9655e 100644 --- a/Sources/FHIRToFirestoreAdapter/FHIRToFirestoreAdapter.swift +++ b/Sources/FHIRToFirestoreAdapter/FHIRToFirestoreAdapter.swift @@ -8,6 +8,8 @@ import CardinalKit import FHIR +import FirestoreDataStorage +import Foundation /// Adapts the output of the `FHIR` standard to be used with the `Firestore` data storage provider. @@ -28,18 +30,25 @@ import FHIR public actor FHIRToFirestoreAdapter: SingleValueAdapter { public typealias InputElement = FHIR.BaseType public typealias InputRemovalContext = FHIR.RemovalContext - public typealias OutputElement = FHIRFirestoreElement - public typealias OutputRemovalContext = FHIRFirestoreRemovalContext + public typealias OutputElement = FirestoreElement + public typealias OutputRemovalContext = FirestoreRemovalContext public init() {} - public func transform(element: InputElement) throws -> FHIRFirestoreElement { - FHIRFirestoreElement(element) + public func transform(element: InputElement) throws -> FirestoreElement { + FirestoreElement( + id: element.id?.value?.string ?? UUID().uuidString, + collectionPath: ResourceProxy(with: element).resourceType, + element + ) } - public func transform(removalContext: InputRemovalContext) throws -> FHIRFirestoreRemovalContext { - FHIRFirestoreRemovalContext(removalContext) + public func transform(removalContext: InputRemovalContext) throws -> FirestoreRemovalContext { + FirestoreRemovalContext( + id: removalContext.id?.value?.string ?? UUID().uuidString, + collectionPath: removalContext.resourceType.rawValue + ) } } diff --git a/Sources/FirebaseAccount/FirebaseAccountConfiguration.swift b/Sources/FirebaseAccount/FirebaseAccountConfiguration.swift index 5fb712c4..d60f00b6 100644 --- a/Sources/FirebaseAccount/FirebaseAccountConfiguration.swift +++ b/Sources/FirebaseAccount/FirebaseAccountConfiguration.swift @@ -8,13 +8,32 @@ import Account import CardinalKit -import FirebaseAuth +@_exported import class FirebaseAuth.User +import class FirebaseAuth.Auth +import protocol FirebaseAuth.AuthStateDidChangeListenerHandle import FirebaseConfiguration import FirebaseCore import Foundation -/// <#Description#> +/// Configures Firebase Auth `AccountService`s that can be used in any views of the `Account` module. +/// +/// The ``FirebaseAccountConfiguration`` offers a ``user`` property to access the current Firebase Auth user from, e.g., a SwiftUI view's environment: +/// ``` +/// @EnvironmentObject var firebaseAccountConfiguration: FirebaseAccountConfiguration +/// ``` +/// +/// The ``FirebaseAccountConfiguration`` can, e.g., be used to to connect to the Firebase Auth emulator: +/// ``` +/// class ExampleAppDelegate: CardinalKitAppDelegate { +/// override var configuration: Configuration { +/// Configuration(standard: /* ... */) { +/// FirebaseAccountConfiguration(emulatorSettings: (host: "localhost", port: 9099)) +/// // ... +/// } +/// } +/// } +/// ``` public final class FirebaseAccountConfiguration: Component, ObservableObject, ObservableObjectProvider { @Dependency private var configureFirebaseApp: ConfigureFirebaseApp @@ -35,10 +54,9 @@ public final class FirebaseAccountConfiguration: Co } - /// <#Description#> /// - Parameters: - /// - emulatorSettings: <#emulatorSettings description#> - /// - authenticationMethods: <#authenticationMethods description#> + /// - emulatorSettings: The emulator settings. The default value is `nil`, connecting the FirebaseAccount module to the Firebase Auth cloud instance. + /// - authenticationMethods: The authentication methods that should be supported. public init( emulatorSettings: (host: String, port: Int)? = nil, authenticationMethods: FirebaseAuthAuthenticationMethods = .all @@ -64,6 +82,7 @@ public final class FirebaseAccountConfiguration: Co guard let user else { Task { await MainActor.run { + self.user = nil self.account.signedIn = false } } diff --git a/Sources/FirebaseAccount/FirebaseAuthAuthenticationMethods.swift b/Sources/FirebaseAccount/FirebaseAuthAuthenticationMethods.swift index 042d779e..5b18d52b 100644 --- a/Sources/FirebaseAccount/FirebaseAuthAuthenticationMethods.swift +++ b/Sources/FirebaseAccount/FirebaseAuthAuthenticationMethods.swift @@ -7,12 +7,12 @@ // -/// <#Description#> +/// Definition of the authentication methods supported by the FirebaseAccount module. public struct FirebaseAuthAuthenticationMethods: OptionSet { - /// <#Description#> + /// E-Mail and password-based authentication. public static let emailAndPassword = FirebaseAuthAuthenticationMethods(rawValue: 1 << 0) - /// <#Description#> + /// All authentication methods. public static let all: FirebaseAuthAuthenticationMethods = [.emailAndPassword] diff --git a/Sources/FirestoreDataStorage/AnyFirestoreElement.swift b/Sources/FirestoreDataStorage/AnyFirestoreElement.swift new file mode 100644 index 00000000..eb6ec5f2 --- /dev/null +++ b/Sources/FirestoreDataStorage/AnyFirestoreElement.swift @@ -0,0 +1,14 @@ +// +// This source file is part of the CardinalKit open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +/// Provides the nescessary information context for the ``Firestore`` date storage component to store a new element or update an existing element. +public protocol AnyFirestoreElement: Encodable, Identifiable, Sendable where ID == String { + /// The collection path where the ``FirestoreElement`` should be stored at. + var collectionPath: String { get } +} diff --git a/Sources/FirestoreDataStorage/AnyFirestoreRemovalContext.swift b/Sources/FirestoreDataStorage/AnyFirestoreRemovalContext.swift new file mode 100644 index 00000000..b0fb7c1f --- /dev/null +++ b/Sources/FirestoreDataStorage/AnyFirestoreRemovalContext.swift @@ -0,0 +1,14 @@ +// +// This source file is part of the CardinalKit open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +/// Provides the nescessary removal context for the ``Firestore`` date storage component. +public protocol AnyFirestoreRemovalContext: Identifiable, Sendable where ID == String { + /// The collection path where the ``FirestoreElement`` is located at. + var collectionPath: String { get } +} diff --git a/Sources/FirestoreDataStorage/Firestore.swift b/Sources/FirestoreDataStorage/Firestore.swift index be0cee13..860c84bc 100644 --- a/Sources/FirestoreDataStorage/Firestore.swift +++ b/Sources/FirestoreDataStorage/Firestore.swift @@ -30,7 +30,12 @@ import SwiftUI /// } /// ``` public actor Firestore: Module, DataStorageProvider { - public typealias FirestoreAdapter = any FirestoreElementAdapter + public typealias FirestoreAdapter = any Adapter< + ComponentStandard.BaseType, + ComponentStandard.RemovalContext, + FirestoreElement, + FirestoreRemovalContext + > @Dependency private var configureFirebaseApp: ConfigureFirebaseApp @@ -41,8 +46,8 @@ public actor Firestore: Module, DataStorageProvider /// - Parameter settings: The firestore settings according to the [Firebase Firestore Swift Package](https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/FirestoreSettings) public init(settings: FirestoreSettings = FirestoreSettings()) - where ComponentStandard.BaseType: FirestoreElement, ComponentStandard.RemovalContext: FirestoreRemovalContext { - self.adapter = DefaultFirestoreElementAdapter() + where ComponentStandard.BaseType: AnyFirestoreElement, ComponentStandard.RemovalContext: AnyFirestoreRemovalContext { + self.adapter = DefaultFirestoreElementAdapter() self.settings = settings } @@ -50,8 +55,11 @@ public actor Firestore: Module, DataStorageProvider /// - adapter: A chain of adapter from your standard basetype and /// removal context to ``FirestoreElement`` and ``FirestoreRemovalContext`` instances. /// - settings: The firestore settings according to the [Firebase Firestore Swift Package](https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/FirestoreSettings) - public init(adapter: FirestoreAdapter, settings: FirestoreSettings = FirestoreSettings()) { - self.adapter = adapter + public init( + @AdapterBuilder adapter: () -> (FirestoreAdapter), + settings: FirestoreSettings = FirestoreSettings() + ) { + self.adapter = adapter() self.settings = settings } @@ -63,16 +71,25 @@ public actor Firestore: Module, DataStorageProvider } public func process(_ element: DataChange) async throws { - switch element { + try await process(asyncSequence: adapter.transformDataChanges([element])) + } + + private func process(asyncSequence: some TypedAsyncSequence>) async throws { + for try await dataChange in asyncSequence { + try await process(dataChange: dataChange) + } + } + + private func process(dataChange: DataChange) async throws { + switch dataChange { case let .addition(element): - let firebaseElement = try await adapter.transform(element: element) try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in do { let firestore = FirebaseFirestore.Firestore.firestore() try firestore - .collection(firebaseElement.collectionPath) - .document(firebaseElement.id) - .setData(from: firebaseElement, merge: false) { error in + .collection(element.collectionPath) + .document(element.id) + .setData(from: element, merge: false) { error in if let error { continuation.resume(throwing: error) } else { @@ -84,12 +101,11 @@ public actor Firestore: Module, DataStorageProvider } } case let .removal(removalContext): - let firebaseRemovalContext = try await adapter.transform(removalContext: removalContext) try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in let firestore = FirebaseFirestore.Firestore.firestore() firestore - .collection(firebaseRemovalContext.collectionPath) - .document(firebaseRemovalContext.id) + .collection(removalContext.collectionPath) + .document(removalContext.id) .delete { error in if let error { continuation.resume(throwing: error) diff --git a/Sources/FirestoreDataStorage/FirestoreElement.swift b/Sources/FirestoreDataStorage/FirestoreElement.swift index 4ea2cd5f..92b522c6 100644 --- a/Sources/FirestoreDataStorage/FirestoreElement.swift +++ b/Sources/FirestoreDataStorage/FirestoreElement.swift @@ -7,8 +7,31 @@ // -/// Provides the nescessary information context for the ``Firestore`` date storage component to store a new element or update an existing element. -public protocol FirestoreElement: Encodable, Identifiable, Sendable where ID == String { - /// The collection path where the ``FirestoreElement`` should be stored at. - var collectionPath: String { get } +/// Provides a mapping from a FHIR `Resource` to a type conforming to `FirestoreElement`. +public struct FirestoreElement: Encodable, Identifiable, Sendable { + /// The identifier that is used for the Firestore document. + public let id: String + /// The collection path that is used for the Firestore document. + public var collectionPath: String + let body: Encodable & Sendable + + + /// - Parameters: + /// - id: The identifier that is used for the Firestore document. + /// - collectionPath: The collection path that is used for the Firestore document. + /// - body: The body of the document, including all fields as defined by the `Encodable` implementation. + public init( + id: String, + collectionPath: String, + _ body: Body + ) { + self.id = id + self.collectionPath = collectionPath + self.body = body + } + + + public func encode(to encoder: Encoder) throws { + try body.encode(to: encoder) + } } diff --git a/Sources/FirestoreDataStorage/FirestoreElementAdapter.swift b/Sources/FirestoreDataStorage/FirestoreElementAdapter.swift deleted file mode 100644 index 57f2deb6..00000000 --- a/Sources/FirestoreDataStorage/FirestoreElementAdapter.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// This source file is part of the CardinalKit open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import CardinalKit - - -/// An adapter that a type conforms to when transforming elements and removal contexts for the ``Firestore`` data storage provider. -/// -/// Transforms an input element to a ``FirestoreElement`` and a removal context to a ``FirestoreRemovalContext``. -public protocol FirestoreElementAdapter< - InputElement, - InputRemovalContext ->: SingleValueAdapter where OutputElement: FirestoreElement, OutputRemovalContext: FirestoreRemovalContext { } diff --git a/Sources/FirestoreDataStorage/FirestoreRemovalContext.swift b/Sources/FirestoreDataStorage/FirestoreRemovalContext.swift index 7186e0f3..586577ec 100644 --- a/Sources/FirestoreDataStorage/FirestoreRemovalContext.swift +++ b/Sources/FirestoreDataStorage/FirestoreRemovalContext.swift @@ -7,8 +7,22 @@ // -/// Provides the nescessary removal context for the ``Firestore`` date storage component. -public protocol FirestoreRemovalContext: Identifiable, Sendable where ID == String { - /// The collection path where the ``FirestoreElement`` is located at. - var collectionPath: String { get } +/// Provides a mapping from a FHIR standard `RemovalContext` to a type conforming to `FHIRFirestoreRemovalContext`. +public struct FirestoreRemovalContext: Identifiable, Sendable { + /// The identifier that is used for the Firestore document. + public let id: String + /// The collection path that is used for the Firestore document. + public var collectionPath: String + + + /// - Parameters: + /// - id: The identifier that is used for the Firestore document. + /// - collectionPath: The collection path that is used for the Firestore document. + public init( + id: String, + collectionPath: String + ) { + self.id = id + self.collectionPath = collectionPath + } } diff --git a/Sources/FirestoreDataStorage/IdentityFirestoreElementAdapter.swift b/Sources/FirestoreDataStorage/IdentityFirestoreElementAdapter.swift index 4410343d..f2a1277a 100644 --- a/Sources/FirestoreDataStorage/IdentityFirestoreElementAdapter.swift +++ b/Sources/FirestoreDataStorage/IdentityFirestoreElementAdapter.swift @@ -6,57 +6,30 @@ // SPDX-License-Identifier: MIT // +import CardinalKit + /// Provides an identity mapping of a type already conforming to ``FirestoreElement``/``FirestoreRemovalContext`` to the type-erased counterparts (``AnyFirestoreElement``/``AnyFirestoreRemovalContext``) -public actor DefaultFirestoreElementAdapter: FirestoreElementAdapter { - public typealias OutputElement = AnyFirestoreElement - public typealias OutputRemovalContext = AnyFirestoreRemovalContext - - - /// Type-erased version of a ``FirestoreElement`` instance. - public struct AnyFirestoreElement: FirestoreElement { - fileprivate let element: any FirestoreElement - - - public var collectionPath: String { - element.collectionPath - } - public var id: String { - element.id - } - - - fileprivate init(element: some FirestoreElement) { - self.element = element - } - - - public func encode(to encoder: Encoder) throws { - try element.encode(to: encoder) - } - } - - /// Type-erased version of a ``FirestoreRemovalContext`` instance. - public struct AnyFirestoreRemovalContext: FirestoreRemovalContext { - public let collectionPath: String - public let id: String - - - fileprivate init(collectionPath: String, id: String) { - self.collectionPath = collectionPath - self.id = id - } - } +public actor DefaultFirestoreElementAdapter: SingleValueAdapter { + public typealias OutputElement = FirestoreElement + public typealias OutputRemovalContext = FirestoreRemovalContext public init() {} - public func transform(element: InputElement) throws -> AnyFirestoreElement { - AnyFirestoreElement(element: element) + public func transform(element: InputElement) throws -> FirestoreElement { + FirestoreElement( + id: element.id, + collectionPath: element.collectionPath, + element + ) } - public func transform(removalContext: InputRemovalContext) throws -> AnyFirestoreRemovalContext { - AnyFirestoreRemovalContext(collectionPath: removalContext.collectionPath, id: removalContext.id) + public func transform(removalContext: InputRemovalContext) throws -> FirestoreRemovalContext { + FirestoreRemovalContext( + id: removalContext.id, + collectionPath: removalContext.collectionPath + ) } } diff --git a/Sources/FirestoreStoragePrefixUserIdAdapter/FirestoreStoragePrefixUserIdAdapter.swift b/Sources/FirestoreStoragePrefixUserIdAdapter/FirestoreStoragePrefixUserIdAdapter.swift new file mode 100644 index 00000000..b42ab882 --- /dev/null +++ b/Sources/FirestoreStoragePrefixUserIdAdapter/FirestoreStoragePrefixUserIdAdapter.swift @@ -0,0 +1,56 @@ +// +// This source file is part of the CardinalKit open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import CardinalKit +import FirebaseAuth +import FirestoreDataStorage + + +/// Adds a `users//` prefix to any uploaded or removed firestore element. +/// +/// You can, e.g., use the ``FirestoreStoragePrefixUserIdAdapter`` as a final transformation step in the adapter chain to add the +/// `users//` prefix: +/// ``` +/// Firestore( +/// adapter: { +/// // ... +/// FirestoreStoragePrefixUserIdAdapter() +/// }, +/// settings: .emulator +/// ) +/// ``` +public actor FirestoreStoragePrefixUserIdAdapter: SingleValueAdapter { + public typealias InputElement = FirestoreElement + public typealias InputRemovalContext = FirestoreRemovalContext + public typealias OutputElement = FirestoreElement + public typealias OutputRemovalContext = FirestoreRemovalContext + + + public init() {} + + + public func transform(element: FirestoreElement) throws -> FirestoreElement { + guard let userID = Auth.auth().currentUser?.uid else { + throw FirestoreStoragePrefixUserIdAdapterError.userNotSignedIn + } + + var modifiedElement = element + modifiedElement.collectionPath = "users/" + userID.id + "/" + element.collectionPath + return modifiedElement + } + + public func transform(removalContext: FirestoreRemovalContext) throws -> FirestoreRemovalContext { + guard let userID = Auth.auth().currentUser?.uid else { + throw FirestoreStoragePrefixUserIdAdapterError.userNotSignedIn + } + + var modifiedRemovalContext = removalContext + modifiedRemovalContext.collectionPath = "users/" + userID.id + "/" + removalContext.collectionPath + return modifiedRemovalContext + } +} diff --git a/Sources/FirestoreStoragePrefixUserIdAdapter/FirestoreStoragePrefixUserIdAdapterError.swift b/Sources/FirestoreStoragePrefixUserIdAdapter/FirestoreStoragePrefixUserIdAdapterError.swift new file mode 100644 index 00000000..2a6ca026 --- /dev/null +++ b/Sources/FirestoreStoragePrefixUserIdAdapter/FirestoreStoragePrefixUserIdAdapterError.swift @@ -0,0 +1,14 @@ +// +// This source file is part of the CardinalKit open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +/// Indicates an error on the ``FirestoreStoragePrefixUserIdAdapter``. +public enum FirestoreStoragePrefixUserIdAdapterError: Error { + /// The user is not yet signed in. + case userNotSignedIn +} diff --git a/Tests/UITests/TestApp/GoogleService-Info.plist b/Tests/UITests/TestApp/GoogleService-Info.plist index c405d1bb..1265fc80 100644 --- a/Tests/UITests/TestApp/GoogleService-Info.plist +++ b/Tests/UITests/TestApp/GoogleService-Info.plist @@ -3,13 +3,13 @@ CLIENT_ID - 251345092840-dk8mbm59rs06ijhodncdqa22m95hgvlr.apps.googleusercontent.com + CLIENT_ID REVERSED_CLIENT_ID - com.googleusercontent.apps.251345092840-dk8mbm59rs06ijhodncdqa22m95hgvlr + REVERSED_CLIENT_ID API_KEY - AIzaSyCQGRnt7FC4Or7-gX97HoWUFiy7lpQesTc + API_KEY GCM_SENDER_ID - 251345092840 + GCM_SENDER_ID PLIST_VERSION 1 BUNDLE_ID @@ -17,18 +17,18 @@ PROJECT_ID cardinalkituitests STORAGE_BUCKET - cardinalkituitests.appspot.com + STORAGE_BUCKET IS_ADS_ENABLED - + IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED - + IS_GCM_ENABLED - + IS_SIGNIN_ENABLED - + GOOGLE_APP_ID - 1:251345092840:ios:0bc41b15e2e9623fe6095d + 1:123456789012:ios:1234567890123456789012 - \ No newline at end of file + diff --git a/Tests/UITests/TestApp/Shared/TestAppStandard.swift b/Tests/UITests/TestApp/Shared/TestAppStandard.swift index 77aca2e4..57909d6d 100644 --- a/Tests/UITests/TestApp/Shared/TestAppStandard.swift +++ b/Tests/UITests/TestApp/Shared/TestAppStandard.swift @@ -16,7 +16,7 @@ actor TestAppStandard: Standard, ObservableObjectProvider, ObservableObject { typealias RemovalContext = TestAppStandardRemovalContext - struct TestAppStandardBaseType: Identifiable, Sendable, FirestoreElement { + struct TestAppStandardBaseType: Identifiable, Sendable, AnyFirestoreElement { var id: String var content: Int var collectionPath: String @@ -34,7 +34,7 @@ actor TestAppStandard: Standard, ObservableObjectProvider, ObservableObject { } } - struct TestAppStandardRemovalContext: Identifiable, Sendable, FirestoreRemovalContext { + struct TestAppStandardRemovalContext: Identifiable, Sendable, AnyFirestoreRemovalContext { var id: TestAppStandardBaseType.ID var collectionPath: String diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 5a1eeeb0..4b19ae0a 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -136,7 +136,7 @@ 2FC246D52941720600F75383 /* AccountSignUpTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSignUpTests.swift; sourceTree = ""; }; 2FC2C405291D84A600712676 /* HealthKitTestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitTestsView.swift; sourceTree = ""; }; 2FC2C407291DCC8200712676 /* HealthKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitTests.swift; sourceTree = ""; }; - 2FC42FD7290ADD5E00B08F18 /* CardinalKitSPM */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = CardinalKitSPM; path = ../..; sourceTree = ""; }; + 2FC42FD7290ADD5E00B08F18 /* CardinalKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = CardinalKit; path = ../..; sourceTree = ""; }; 2FD6390B294B0A4B008BADFC /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; 2FE122B1294247480016B162 /* XCUIApplication+TextEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+TextEntry.swift"; sourceTree = ""; }; 2FE62C3C2966074F00FCBE7F /* FirestoreDataStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreDataStorageTests.swift; sourceTree = ""; }; @@ -217,7 +217,7 @@ isa = PBXGroup; children = ( 2F7B6CB4294C03C800FDC494 /* TestApp.xctestplan */, - 2FC42FD7290ADD5E00B08F18 /* CardinalKitSPM */, + 2FC42FD7290ADD5E00B08F18 /* CardinalKit */, 2F6D139428F5F384007C25D6 /* TestApp */, 2F6D13AF28F5F386007C25D6 /* TestAppUITests */, 2F6D139328F5F384007C25D6 /* Products */, diff --git a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a1ed2699..586deaaa 100644 --- a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "3e4e743631e86c8c70dbc6efdc7beaa6e90fd3bb", - "version" : "2.1.1" + "revision" : "ec957ccddbcc710ccc64c9dcbd4c7006fcf8b73a", + "version" : "2.2.0" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/StanfordBDHG/XCTHealthKit", "state" : { - "revision" : "37ef9a48fcbdd5a301e8482a4bdbf61ef987a4f0", - "version" : "0.3.0" + "revision" : "70b79e67339da1174e6f7dcd58d6c041388ded96", + "version" : "0.3.1" } } ],