generated from StanfordBDHG/SwiftPackageTemplate
-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce FirestoreAccountStorage (#24)
# Introduce FirestoreAccountStorage ## ♻️ Current situation & Problem This PR introduces the new FirestoreAccountStorage target that allows you to easily implement an [AccountStorageStandard](https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/accountstoragestandard) using Firestore. ## ⚙️ Release Notes * Introduce `FirestoreAccountStorage` to easily storage additional user data in firestore ## 📚 Documentation Extensive documentation was added. Further, I noticed that our documentation builds are not up to date in the Swift package index. This is probably due to an outdated spi.yml. This file was updated as part of this PR. ## ✅ Testing _TBA_ ## 📝 Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
- Loading branch information
Showing
7 changed files
with
295 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
Sources/SpeziFirebaseAccountStorage/FirestoreAccountStorage.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import FirebaseFirestore | ||
import Spezi | ||
import SpeziAccount | ||
import SpeziFirestore | ||
|
||
|
||
/// Store additional account details directly in firestore. | ||
/// | ||
/// Certain account services, like the account services provided by Firebase, can only store certain account details. | ||
/// The `FirestoreAccountStorage` can be used to store additional account details, that are not supported out of the box by your account services, | ||
/// inside Firestore in a custom user collection. | ||
/// | ||
/// - Note: The `FirestoreAccountStorage` relies on the primary [AccountId](https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/accountidkey) | ||
/// as the document identifier. Fore Firebase-based account service, this is the primary, firebase user identifier. Make sure to configure your firestore security rules respectively. | ||
/// | ||
/// Once you have [AccountConfiguration](https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/initial-setup#Account-Configuration) | ||
/// and the [FirebaseAccountConfiguration](https://swiftpackageindex.com/stanfordspezi/spezifirebase/documentation/spezifirebaseaccount/firebaseaccountconfiguration) | ||
/// set up, you can adopt the [AccountStorageStandard](https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/accountstoragestandard) | ||
/// protocol to provide a custom storage for SpeziAccount. | ||
/// | ||
/// - Important: In order to use the `FirestoreAccountStorage`, you must have [Firestore](https://swiftpackageindex.com/stanfordspezi/spezifirebase/main/documentation/spezifirestore/firestore) | ||
/// configured in your app. Refer to the documentation page for more information. | ||
/// | ||
/// ```swift | ||
/// import FirebaseFirestore | ||
/// import Spezi | ||
/// import SpeziAccount | ||
/// import SpeziFirebaseAccountStorage | ||
/// | ||
/// | ||
/// actor ExampleStandard: Standard, AccountStorageStandard { | ||
/// // Define the collection where you want to store your additional user data, ... | ||
/// static var collection: CollectionReference { | ||
/// Firestore.firestore().collection("users") | ||
/// } | ||
/// | ||
/// // ... define and initialize the `FirestoreAccountStorage` dependency ... | ||
/// @Dependency private var accountStorage = FirestoreAccountStorage(storedIn: Self.collection) | ||
/// | ||
/// | ||
/// // ... and forward all implementations of `AccountStorageStandard` to the `FirestoreAccountStorage`. | ||
/// | ||
/// public func create(_ identifier: AdditionalRecordId, _ details: SignupDetails) async throws { | ||
/// try await accountStorage.create(identifier, details) | ||
/// } | ||
/// | ||
/// public func load(_ identifier: AdditionalRecordId, _ keys: [any AccountKey.Type]) async throws -> PartialAccountDetails { | ||
/// try await accountStorage.load(identifier, keys) | ||
/// } | ||
/// | ||
/// public func modify(_ identifier: AdditionalRecordId, _ modifications: AccountModifications) async throws { | ||
/// try await accountStorage.modify(identifier, modifications) | ||
/// } | ||
/// | ||
/// public func clear(_ identifier: AdditionalRecordId) async { | ||
/// await accountStorage.clear(identifier) | ||
/// } | ||
/// | ||
/// public func delete(_ identifier: AdditionalRecordId) async throws { | ||
/// try await accountStorage.delete(identifier) | ||
/// } | ||
/// } | ||
/// ``` | ||
public actor FirestoreAccountStorage: Module, AccountStorageStandard { | ||
@Dependency private var firestore: SpeziFirestore.Firestore // ensure firestore is configured | ||
|
||
private let collection: CollectionReference | ||
|
||
|
||
public init(storeIn collection: CollectionReference) { | ||
self.collection = collection | ||
} | ||
|
||
private func userDocument(for accountId: String) -> DocumentReference { | ||
collection.document(accountId) | ||
} | ||
|
||
public func create(_ identifier: AdditionalRecordId, _ details: SignupDetails) async throws { | ||
let result = details.acceptAll(FirestoreEncodeVisitor()) | ||
|
||
do { | ||
switch result { | ||
case let .success(data): | ||
try await userDocument(for: identifier.accountId) | ||
.setData(data) | ||
case let .failure(error): | ||
throw error | ||
} | ||
} catch { | ||
throw FirestoreError(error) | ||
} | ||
} | ||
|
||
public func load(_ identifier: AdditionalRecordId, _ keys: [any AccountKey.Type]) async throws -> PartialAccountDetails { | ||
let builder = PartialAccountDetails.Builder() | ||
|
||
let document = userDocument(for: identifier.accountId) | ||
|
||
do { | ||
let data = try await document | ||
.getDocument() | ||
.data() | ||
|
||
if let data { | ||
for key in keys { | ||
guard let value = data[key.identifier] else { | ||
continue | ||
} | ||
|
||
let visitor = FirestoreDecodeVisitor(value: value, builder: builder, in: document) | ||
key.accept(visitor) | ||
if case let .failure(error) = visitor.final() { | ||
throw error | ||
} | ||
} | ||
} | ||
} catch { | ||
throw FirestoreError(error) | ||
} | ||
|
||
return builder.build() | ||
} | ||
|
||
public func modify(_ identifier: AdditionalRecordId, _ modifications: AccountModifications) async throws { | ||
let result = modifications.modifiedDetails.acceptAll(FirestoreEncodeVisitor()) | ||
|
||
do { | ||
switch result { | ||
case let .success(data): | ||
try await userDocument(for: identifier.accountId) | ||
.setData(data, merge: true) | ||
case let .failure(error): | ||
throw error | ||
} | ||
|
||
let removedFields: [String: Any] = modifications.removedAccountDetails.keys.reduce(into: [:]) { result, key in | ||
result[key.identifier] = FieldValue.delete() | ||
} | ||
|
||
try await userDocument(for: identifier.accountId) | ||
.updateData(removedFields) | ||
} catch { | ||
throw FirestoreError(error) | ||
} | ||
} | ||
|
||
public func clear(_ identifier: AdditionalRecordId) async { | ||
// nothing we can do ... | ||
} | ||
|
||
public func delete(_ identifier: AdditionalRecordId) async throws { | ||
do { | ||
try await userDocument(for: identifier.accountId) | ||
.delete() | ||
} catch { | ||
throw FirestoreError(error) | ||
} | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
Sources/SpeziFirebaseAccountStorage/Visitor/FirestoreDecodeVisitor.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import FirebaseFirestore | ||
import SpeziAccount | ||
|
||
|
||
class FirestoreDecodeVisitor: AccountKeyVisitor { | ||
private let builder: PartialAccountDetails.Builder | ||
private let value: Any | ||
private let reference: DocumentReference | ||
|
||
private var error: Error? | ||
|
||
|
||
init(value: Any, builder: PartialAccountDetails.Builder, in reference: DocumentReference) { | ||
self.value = value | ||
self.builder = builder | ||
self.reference = reference | ||
} | ||
|
||
|
||
func visit<Key: AccountKey>(_ key: Key.Type) { | ||
let decoder = Firestore.Decoder() | ||
|
||
do { | ||
try builder.set(key, value: decoder.decode(Key.Value.self, from: value, in: reference)) | ||
} catch { | ||
self.error = error | ||
} | ||
} | ||
|
||
func final() -> Result<Void, Error> { | ||
if let error { | ||
return .failure(error) | ||
} else { | ||
return .success(()) | ||
} | ||
} | ||
} |
39 changes: 39 additions & 0 deletions
39
Sources/SpeziFirebaseAccountStorage/Visitor/FirestoreEncodeVisitor.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
|
||
import FirebaseFirestore | ||
import SpeziAccount | ||
|
||
|
||
class FirestoreEncodeVisitor: AccountValueVisitor { | ||
typealias Data = [String: Any] | ||
|
||
private var values: Data = [:] | ||
private var errors: [String: Error] = [:] | ||
|
||
init() {} | ||
|
||
func visit<Key: AccountKey>(_ key: Key.Type, _ value: Key.Value) { | ||
let encoder = Firestore.Encoder() | ||
do { | ||
values["\(Key.self)"] = try encoder.encode(value) | ||
} catch { | ||
errors["\(Key.self)"] = error | ||
} | ||
} | ||
|
||
func final() -> Result<Data, Error> { | ||
if let first = errors.first { | ||
// we just report the first error, like in a typical do-catch setup | ||
return .failure(first.value) | ||
} else { | ||
return .success(values) | ||
} | ||
} | ||
} |
Oops, something went wrong.