Skip to content

Commit 8a7175f

Browse files
dani-garciacoroiu
andauthored
[PM-12612] SDK-Managed Repository support (#301)
## 🎟️ Tracking https://bitwarden.atlassian.net/browse/PM-12612 ## 📔 Objective A continuation of #213, this PR implements some simple SDK managed data store based on SQLite (on non-wasm) and IndexedDB (on wasm). Both databases are wrapped in a `Database` trait and conditionally compiled based on platform. Then from each database we can get multiple `Repository` implementations that can be used to read and write data persistently. Each repository is mapped to a separate table in SQLite and Object Store in IndexedDb. Some limitations of the current system: - The database currently needs to be initialized as a separate step to both the SdkClient and the client-managed repositories, I feel like ideally we can do that as part of Client initialization, but that would require us to make initialization async. I've left that for the future. - Currently we don't have any indexes beyond the main key, but both sqlite and indexeddb support adding more. - The current structure doesn't have any migration capabilities, which are required for indexeddb to create the object stores, this PR temporarily adds version numbers to the `register_repository_item` calls, just to get something working, but note that this is removed in #410 and replaced with a better migration system. ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes --------- Co-authored-by: Andreas Coroiu <[email protected]>
1 parent 03d02e6 commit 8a7175f

File tree

16 files changed

+924
-20
lines changed

16 files changed

+924
-20
lines changed

Cargo.lock

Lines changed: 95 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-core/src/platform/state_client.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use std::sync::Arc;
22

33
use bitwarden_state::{
4-
registry::RepositoryNotFoundError,
5-
repository::{Repository, RepositoryItem},
4+
registry::{RepositoryNotFoundError, StateRegistryError},
5+
repository::{Repository, RepositoryItem, RepositoryItemData},
6+
DatabaseConfiguration,
67
};
78

89
use crate::Client;
@@ -30,4 +31,26 @@ impl StateClient {
3031
) -> Result<Arc<dyn Repository<T>>, RepositoryNotFoundError> {
3132
self.client.internal.repository_map.get_client_managed()
3233
}
34+
35+
/// Initialize the database for SDK managed repositories.
36+
pub async fn initialize_database(
37+
&self,
38+
configuration: DatabaseConfiguration,
39+
repositories: Vec<RepositoryItemData>,
40+
) -> Result<(), StateRegistryError> {
41+
self.client
42+
.internal
43+
.repository_map
44+
.initialize_database(configuration, repositories)
45+
.await
46+
}
47+
48+
/// Get a SDK managed state repository for a specific type, if it exists.
49+
pub fn get_sdk_managed<
50+
T: RepositoryItem + serde::ser::Serialize + serde::de::DeserializeOwned,
51+
>(
52+
&self,
53+
) -> Result<impl Repository<T>, StateRegistryError> {
54+
self.client.internal.repository_map.get_sdk_managed()
55+
}
3356
}

crates/bitwarden-state/Cargo.toml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,24 @@ keywords.workspace = true
1111

1212
[features]
1313
uniffi = []
14-
wasm = []
14+
wasm = ["bitwarden-threading/wasm"]
1515

1616
[dependencies]
1717
async-trait = { workspace = true }
18+
bitwarden-error = { workspace = true }
19+
bitwarden-threading = { workspace = true }
20+
serde = { workspace = true }
21+
serde_json = { workspace = true }
1822
thiserror = { workspace = true }
23+
tokio = { workspace = true }
24+
25+
[target.'cfg(target_arch="wasm32")'.dependencies]
26+
indexed-db = ">=0.4.2, <0.5"
27+
js-sys = { workspace = true }
28+
tsify = { workspace = true }
29+
30+
[target.'cfg(not(target_arch="wasm32"))'.dependencies]
31+
rusqlite = { version = ">=0.37.0, <0.38", features = ["bundled"] }
1932

2033
[dev-dependencies]
2134
tokio = { workspace = true, features = ["rt"] }

crates/bitwarden-state/README.md

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ stored:
2727
- If the SDK itself will handle data storage, we call that approach `SDK-Managed State`. The
2828
implementation of this is will a work in progress.
2929

30+
Note that these approaches aren't mutually exclusive: a repository item can use both client and SDK
31+
managed state at the same time. However, this mixed approach is only recommended during migration
32+
scenarios to avoid potential confusion.
33+
3034
## Client-Managed State
3135

3236
With `Client-Managed State` the application and SDK will both access the same data pool, which
@@ -53,7 +57,7 @@ impl StateClient {
5357
}
5458
```
5559

56-
#### How to use it on web clients
60+
#### How to initialize Client-Managed State on the web clients
5761

5862
Once we have the function defined in `bitwarden-wasm-internal`, we can use it from the web clients.
5963
For that, the first thing we need to do is create a mapper between the client and SDK types. This
@@ -113,7 +117,7 @@ impl StateClient {
113117
}
114118
```
115119

116-
#### How to use it on iOS
120+
#### How to initialize Client-Managed State on iOS
117121

118122
Once we have the function defined in `bitwarden-uniffi`, we can use it from the iOS application:
119123

@@ -144,7 +148,7 @@ let store = CipherStoreImpl(cipherDataStore: self.cipherDataStore, userId: userI
144148
try await self.clientService.platform().store().registerCipherStore(store: store);
145149
```
146150

147-
### How to use it on Android
151+
### How to initialize Client-Managed State on Android
148152

149153
Once we have the function defined in `bitwarden-uniffi`, we can use it from the Android application:
150154

@@ -173,3 +177,70 @@ class CipherStoreImpl: CipherStore {
173177

174178
getClient(userId = userId).platform().store().registerCipherStore(CipherStoreImpl());
175179
```
180+
181+
## SDK-Managed State
182+
183+
With `SDK-Managed State`, the SDK will be exclusively responsible for the data storage. This means
184+
that the clients don't need to make any changes themselves, as the implementation is internal to the
185+
SDK. To add support for an SDK managed `Repository`, it needs to be added to the initialization code
186+
for WASM and UniFFI. This example shows how to add support for `Cipher`s.
187+
188+
### How to initialize SDK-Managed State on WASM
189+
190+
Go to `crates/bitwarden-wasm-internal/src/platform/mod.rs` and add a line with your type, as shown
191+
below. In this example we're registering `Cipher` as both client and SDK managed to show how both
192+
are done, but you can also just do one or the other.
193+
194+
```rust,ignore
195+
pub async fn initialize_state(
196+
&self,
197+
cipher_repository: CipherRepository,
198+
) -> Result<(), bitwarden_state::registry::StateRegistryError> {
199+
let cipher = cipher_repository.into_channel_impl();
200+
// Register the provided repository as client managed state
201+
self.0.platform().state().register_client_managed(cipher);
202+
203+
let sdk_managed_repositories = vec![
204+
// This should list all the SDK-managed repositories
205+
<Cipher as RepositoryItem>::data(),
206+
// Add your type here
207+
];
208+
209+
self.0
210+
.platform()
211+
.state()
212+
.initialize_database(sdk_managed_repositories)
213+
.await
214+
}
215+
```
216+
217+
### How to initialize SDK-Managed State on UniFFI
218+
219+
Go to `crates/bitwarden-uniffi/src/platform/mod.rs` and add a line with your type, as shown below.
220+
In this example we're registering `Cipher` as both client and SDK managed to show how both are done,
221+
but you can also just do one or the other.
222+
223+
```rust,ignore
224+
pub async fn initialize_state(
225+
&self,
226+
cipher_repository: Arc<dyn CipherRepository>,
227+
) -> Result<()> {
228+
let cipher = UniffiRepositoryBridge::new(cipher_repository);
229+
// Register the provided repository as client managed state
230+
self.0.platform().state().register_client_managed(cipher);
231+
232+
let sdk_managed_repositories = vec![
233+
// This should list all the SDK-managed repositories
234+
<Cipher as RepositoryItem>::data(),
235+
// Add your type here
236+
];
237+
238+
self.0
239+
.platform()
240+
.state()
241+
.initialize_database(sdk_managed_repositories)
242+
.await
243+
.map_err(Error::StateRegistry)?;
244+
Ok(())
245+
}
246+
```

crates/bitwarden-state/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ pub mod repository;
55

66
/// This module provides a registry for managing repositories of different types.
77
pub mod registry;
8+
9+
pub(crate) mod sdk_managed;
10+
11+
pub use sdk_managed::DatabaseConfiguration;

0 commit comments

Comments
 (0)