Skip to main content

junobuild_satellite/db/
store.rs

1use crate::auth::store::get_config as get_auth_config;
2use crate::controllers::store::get_controllers;
3use crate::db::assert::{
4    assert_delete_doc, assert_get_doc, assert_get_docs, assert_set_config, assert_set_doc,
5};
6use crate::db::state::{
7    count_docs_heap, count_docs_stable, delete_collection as delete_state_collection,
8    delete_doc as delete_state_doc, get_config, get_doc as get_state_doc, get_docs_heap,
9    get_docs_stable, get_rule as get_state_rule, init_collection as init_state_collection,
10    insert_config, insert_doc as insert_state_doc,
11    is_collection_empty as is_state_collection_empty,
12};
13use crate::db::types::config::DbConfig;
14use crate::db::types::interface::{DelDoc, SetDbConfig, SetDoc};
15use crate::db::types::state::{Doc, DocContext, DocUpsert};
16use crate::db::types::store::AssertSetDocOptions;
17use crate::db::utils::filter_values;
18use crate::memory::state::STATE;
19use crate::types::store::{AssertContext, StoreContext};
20use candid::Principal;
21use junobuild_collections::msg::msg_db_collection_not_empty;
22use junobuild_collections::types::core::CollectionKey;
23use junobuild_collections::types::rules::{Memory, Rule};
24use junobuild_shared::list::list_values;
25use junobuild_shared::types::core::Key;
26use junobuild_shared::types::list::{ListParams, ListResults};
27use junobuild_shared::types::state::{Controllers, UserId};
28// ---------------------------------------------------------
29// Collection
30// ---------------------------------------------------------
31
32pub fn init_collection_store(collection: &CollectionKey, memory: &Memory) {
33    init_state_collection(collection, memory);
34}
35
36pub fn delete_collection_store(collection: &CollectionKey) -> Result<(), String> {
37    secure_delete_collection(collection)
38}
39
40fn secure_delete_collection(collection: &CollectionKey) -> Result<(), String> {
41    let rule = get_state_rule(collection)?;
42    delete_collection_impl(collection, &rule.memory)
43}
44
45fn delete_collection_impl(
46    collection: &CollectionKey,
47    memory: &Option<Memory>,
48) -> Result<(), String> {
49    let empty = is_state_collection_empty(collection, memory)?;
50
51    if !empty {
52        return Err(msg_db_collection_not_empty(collection));
53    }
54
55    delete_state_collection(collection, memory)?;
56
57    Ok(())
58}
59
60// ---------------------------------------------------------
61// Get
62// ---------------------------------------------------------
63
64/// Get a document from a collection's store.
65///
66/// This function retrieves a document from a collection's store based on the specified parameters.
67/// It returns a `Result<Option<Doc>, String>` where `Ok(Some(Doc))` indicates a successfully retrieved
68/// document, `Ok(None)` represents no document found, or an error message as `Err(String)` if retrieval
69/// encounters issues.
70///
71/// # Parameters
72/// - `caller`: The `UserId` representing the caller requesting the document.
73/// - `collection`: A `CollectionKey` representing the collection from which to retrieve the document.
74/// - `key`: A `Key` identifying the document to be retrieved.
75///
76/// # Returns
77/// - `Ok(Some(Doc))`: Indicates successful retrieval of a document.
78/// - `Ok(None)`: Indicates no document found for the specified key.
79/// - `Err(String)`: An error message if retrieval fails.
80///
81/// This function allows you to securely retrieve a document from a Juno collection's store.
82pub fn get_doc_store(
83    caller: UserId,
84    collection: CollectionKey,
85    key: Key,
86) -> Result<Option<Doc>, String> {
87    let controllers: Controllers = get_controllers();
88
89    let context = StoreContext {
90        caller,
91        controllers: &controllers,
92        collection: &collection,
93    };
94
95    secure_get_doc(&context, key)
96}
97
98fn secure_get_doc(context: &StoreContext, key: Key) -> Result<Option<Doc>, String> {
99    let rule = get_state_rule(context.collection)?;
100    let auth_config = get_auth_config();
101
102    let assert_context = AssertContext {
103        rule: &rule,
104        auth_config: &auth_config,
105    };
106
107    get_doc_impl(context, &assert_context, key)
108}
109
110fn get_doc_impl(
111    context: &StoreContext,
112    assert_context: &AssertContext,
113    key: Key,
114) -> Result<Option<Doc>, String> {
115    let value = get_state_doc(context.collection, &key, assert_context.rule)?;
116
117    match value {
118        None => Ok(None),
119        Some(value) => {
120            if assert_get_doc(context, assert_context, &value).is_err() {
121                return Ok(None);
122            }
123
124            Ok(Some(value))
125        }
126    }
127}
128
129// ---------------------------------------------------------
130// Insert
131// ---------------------------------------------------------
132
133/// Set a document in a collection's store.
134///
135/// This function sets a document in a collection's store based on the specified parameters.
136/// It returns a `Result<DocContext<DocUpsert>, String>` where `Ok(DocContext)` indicates successful
137/// insertion or update of the document, or an error message as `Err(String)` if the operation encounters
138/// issues.
139///
140/// # Parameters
141/// - `caller`: The `UserId` representing the caller initiating the operation.
142/// - `collection`: A `CollectionKey` representing the collection in which to set the document.
143/// - `key`: A `Key` identifying the document to be set.
144/// - `value`: An instance of `SetDoc` representing the document to be inserted or updated.
145///
146/// # Returns
147/// - `Ok(DocContext<DocUpsert>)`: Indicates successful insertion or update of the document.
148/// - `Err(String)`: An error message if the operation fails.
149///
150/// This function allows you to securely insert or update documents in a Juno collection's store.
151pub fn set_doc_store(
152    caller: UserId,
153    collection: CollectionKey,
154    key: Key,
155    value: SetDoc,
156) -> Result<DocContext<DocUpsert>, String> {
157    let controllers: Controllers = get_controllers();
158    let config = get_config();
159
160    let context = StoreContext {
161        caller,
162        controllers: &controllers,
163        collection: &collection,
164    };
165
166    let assert_options = AssertSetDocOptions {
167        with_assert_rate: true,
168    };
169
170    let data = secure_set_doc(&context, &config, &assert_options, key.clone(), value)?;
171
172    Ok(DocContext {
173        key,
174        collection,
175        data,
176    })
177}
178
179/// Internal variant of `set_doc_store`.
180///
181/// Performs a secure insert or update of a document in the specified collection,
182/// using custom assertion options.
183///
184/// Useful for the authentication flow using prepare delegation.
185pub fn internal_set_doc_store(
186    caller: UserId,
187    collection: CollectionKey,
188    key: Key,
189    value: SetDoc,
190    assert_options: &AssertSetDocOptions,
191) -> Result<DocContext<DocUpsert>, String> {
192    let controllers: Controllers = get_controllers();
193    let config = get_config();
194
195    let context = StoreContext {
196        caller,
197        controllers: &controllers,
198        collection: &collection,
199    };
200
201    let data = secure_set_doc(&context, &config, assert_options, key.clone(), value)?;
202
203    Ok(DocContext {
204        key,
205        collection,
206        data,
207    })
208}
209
210fn secure_set_doc(
211    context: &StoreContext,
212    config: &Option<DbConfig>,
213    assert_options: &AssertSetDocOptions,
214    key: Key,
215    value: SetDoc,
216) -> Result<DocUpsert, String> {
217    let rule = get_state_rule(context.collection)?;
218    let auth_config = get_auth_config();
219
220    let assert_context = AssertContext {
221        rule: &rule,
222        auth_config: &auth_config,
223    };
224
225    set_doc_impl(context, &assert_context, config, assert_options, key, value)
226}
227
228fn set_doc_impl(
229    context: &StoreContext,
230    assert_context: &AssertContext,
231    config: &Option<DbConfig>,
232    assert_options: &AssertSetDocOptions,
233    key: Key,
234    value: SetDoc,
235) -> Result<DocUpsert, String> {
236    let current_doc = get_state_doc(context.collection, &key, assert_context.rule)?;
237
238    assert_set_doc(
239        context,
240        assert_context,
241        config,
242        assert_options,
243        &key,
244        &value,
245        &current_doc,
246    )?;
247
248    let doc: Doc = Doc::prepare(context.caller, &current_doc, value);
249
250    let (_evicted_doc, after) =
251        insert_state_doc(context.collection, &key, &doc, assert_context.rule)?;
252
253    Ok(DocUpsert {
254        before: current_doc,
255        after,
256    })
257}
258
259// ---------------------------------------------------------
260// List
261// ---------------------------------------------------------
262
263/// List documents in a collection.
264///
265/// This function retrieves a list of documents from a collection's store based on the specified parameters.
266/// It returns a `Result<ListResults<Doc>, String>` where `Ok(ListResults)` contains the retrieved documents,
267/// or an error message as `Err(String)` if the operation encounters issues.
268///
269/// # Parameters
270/// - `caller`: The `Principal` representing the caller initiating the operation. If used in serverless functions, you can use `junobuild_satellite::id()` to pass an administrator controller.
271/// - `collection`: A `CollectionKey` representing the collection from which to list the documents.
272/// - `filter`: A reference to `ListParams` containing the filter criteria for listing the documents.
273///
274/// # Returns
275/// - `Ok(ListResults<Doc>)`: Contains the list of retrieved documents matching the filter criteria.
276/// - `Err(String)`: An error message if the operation fails.
277///
278/// This function retrieves documents from a Juno collection's store, applying the specified filter criteria.
279pub fn list_docs_store(
280    caller: Principal,
281    collection: CollectionKey,
282    filter: &ListParams,
283) -> Result<ListResults<Doc>, String> {
284    let controllers: Controllers = get_controllers();
285
286    secure_get_docs(caller, &controllers, collection, filter)
287}
288
289/// Count documents in a collection.
290///
291/// This function retrieves the count of documents from a collection's store based on the specified parameters.
292/// It returns a `Result<usize, String>` where `Ok(usize)` contains the count of documents matching the filter criteria,
293/// or an error message as `Err(String)` if the operation encounters issues.
294///
295/// # Parameters
296/// - `caller`: The `Principal` representing the caller initiating the operation. If used in serverless functions, you can use `junobuild_satellite::id()` to pass an administrator controller.
297/// - `collection`: A `CollectionKey` representing the collection from which to count the documents.
298/// - `filter`: A reference to `ListParams` containing the filter criteria for counting the documents.
299///
300/// # Returns
301/// - `Ok(usize)`: Contains the count of documents matching the filter criteria.
302/// - `Err(String)`: An error message if the operation fails.
303///
304/// This function counts documents in a Juno collection's store by listing them and then determining the length of the result set.
305///
306/// # Note
307/// This implementation can be improved, as it currently relies on `list_docs_store` underneath, meaning that all documents matching the filter criteria are still read from the store. This might lead to unnecessary overhead, especially for large collections. Optimizing this function to count documents directly without retrieving them could enhance performance.
308pub fn count_docs_store(
309    caller: Principal,
310    collection: CollectionKey,
311    filter: &ListParams,
312) -> Result<usize, String> {
313    let results = list_docs_store(caller, collection, filter)?;
314    Ok(results.items_length)
315}
316
317fn secure_get_docs(
318    caller: Principal,
319    controllers: &Controllers,
320    collection: CollectionKey,
321    filter: &ListParams,
322) -> Result<ListResults<Doc>, String> {
323    let context: StoreContext = StoreContext {
324        caller,
325        collection: &collection,
326        controllers,
327    };
328
329    let rule = get_state_rule(&collection)?;
330    let auth_config = get_auth_config();
331
332    let assert_context = AssertContext {
333        rule: &rule,
334        auth_config: &auth_config,
335    };
336
337    assert_get_docs(&context, &assert_context)?;
338
339    match rule.mem() {
340        Memory::Heap => STATE.with(|state| {
341            let state_ref = state.borrow();
342            let docs = get_docs_heap(&collection, &state_ref.heap.db.db)?;
343            get_docs_impl(&docs, caller, controllers, filter, &rule)
344        }),
345        Memory::Stable => STATE.with(|state| {
346            let stable = get_docs_stable(&collection, &state.borrow().stable.db)?;
347            let docs: Vec<(&Key, &Doc)> = stable.iter().map(|(key, doc)| (&key.key, doc)).collect();
348            get_docs_impl(&docs, caller, controllers, filter, &rule)
349        }),
350    }
351}
352
353fn get_docs_impl<'a>(
354    docs: &[(&'a Key, &'a Doc)],
355    caller: Principal,
356    controllers: &Controllers,
357    filters: &ListParams,
358    rule: &Rule,
359) -> Result<ListResults<Doc>, String> {
360    let matches = filter_values(caller, controllers, &rule.read, docs, filters)?;
361
362    let results = list_values(&matches, filters);
363
364    Ok(results)
365}
366
367// ---------------------------------------------------------
368// Delete
369// ---------------------------------------------------------
370
371/// Delete a document from a collection's store.
372///
373/// This function deletes a document from a collection's store based on the specified parameters.
374/// It returns a `Result` with `Ok(DocContext<Option<Doc>>)` on success, containing information
375/// about the deleted document, or an error message as `Err(String)` if the deletion encounters issues.
376///
377/// # Parameters
378/// - `caller`: The `UserId` representing the caller initiating the deletion.
379/// - `collection`: A `CollectionKey` representing the collection from which to delete the document.
380/// - `key`: A `Key` identifying the document to be deleted.
381/// - `value`: An instance of `DelDoc` representing the deletion action.
382///
383/// # Returns
384/// - `Ok(DocContext<Option<Doc>>)`:
385///   - `key`: The `Key` of the deleted document.
386///   - `collection`: The `CollectionKey` from which the document was deleted.
387///   - `data`: An `Option<Doc>` representing the deleted document data, if available.
388/// - `Err(String)`: An error message if the deletion operation fails.
389///
390/// This function allows you to securely delete documents from a Juno collection's store, returning
391/// relevant context information or error messages.
392pub fn delete_doc_store(
393    caller: UserId,
394    collection: CollectionKey,
395    key: Key,
396    value: DelDoc,
397) -> Result<DocContext<Option<Doc>>, String> {
398    let controllers: Controllers = get_controllers();
399
400    let context = StoreContext {
401        caller,
402        controllers: &controllers,
403        collection: &collection,
404    };
405
406    let doc = secure_delete_doc(&context, key.clone(), value)?;
407
408    Ok(DocContext {
409        key,
410        collection,
411        data: doc,
412    })
413}
414
415fn secure_delete_doc(
416    context: &StoreContext,
417    key: Key,
418    value: DelDoc,
419) -> Result<Option<Doc>, String> {
420    let rule = get_state_rule(context.collection)?;
421    let auth_config = get_auth_config();
422
423    let assert_context = AssertContext {
424        rule: &rule,
425        auth_config: &auth_config,
426    };
427
428    delete_doc_impl(context, &assert_context, key, value)
429}
430
431fn delete_doc_impl(
432    context: &StoreContext,
433    assert_context: &AssertContext,
434    key: Key,
435    value: DelDoc,
436) -> Result<Option<Doc>, String> {
437    let current_doc = get_state_doc(context.collection, &key, assert_context.rule)?;
438
439    assert_delete_doc(context, assert_context, &key, &value, &current_doc)?;
440
441    delete_state_doc(context.collection, &key, assert_context.rule)
442}
443
444/// Delete multiple documents from a collection's store.
445///
446/// This function deletes multiple documents from a collection's store based on the specified collection key.
447/// It returns a `Result<(), String>` where `Ok(())` indicates successful deletion, or an error message
448/// as `Err(String)` if the deletion encounters issues.
449///
450/// # Parameters
451/// - `collection`: A reference to the `CollectionKey` representing the collection from which to delete documents.
452///
453/// # Returns
454/// - `Ok(())`: Indicates successful deletion of documents.
455/// - `Err(String)`: An error message if the deletion operation fails.
456///
457/// This function allows you to securely delete multiple documents from a Juno collection's store.
458pub fn delete_docs_store(collection: &CollectionKey) -> Result<(), String> {
459    let rule = get_state_rule(collection)?;
460
461    let keys = match rule.mem() {
462        Memory::Heap => STATE.with(|state| {
463            get_docs_heap(collection, &state.borrow().heap.db.db)
464                .map(|docs| docs.into_iter().map(|(key, _)| key.clone()).collect())
465        }),
466        Memory::Stable => STATE.with(|state| {
467            get_docs_stable(collection, &state.borrow().stable.db)
468                .map(|docs| docs.iter().map(|(key, _)| key.key.clone()).collect())
469        }),
470    }?;
471
472    delete_docs_impl(&keys, collection, &rule)
473}
474
475fn delete_docs_impl(
476    keys: &Vec<Key>,
477    collection: &CollectionKey,
478    rule: &Rule,
479) -> Result<(), String> {
480    for key in keys {
481        delete_state_doc(collection, key, rule)?;
482    }
483
484    Ok(())
485}
486
487/// Count the number of documents in a collection's store.
488///
489/// This function retrieves the state rule for the specified collection and counts the documents
490/// based on the memory type (Heap or Stable). It returns the count as a `Result` with `Ok(usize)`
491/// on success, or an error message as `Err(String)` if an issue occurs during counting.
492///
493/// # Parameters
494/// - `collection`: A reference to the `CollectionKey` representing the collection to count documents in.
495///
496/// # Returns
497/// - `Ok(usize)`: The count of documents in the collection.
498/// - `Err(String)`: An error message if counting fails.
499///
500/// This function provides a convenient way to determine the number of documents in a Juno collection's store.
501pub fn count_collection_docs_store(collection: &CollectionKey) -> Result<usize, String> {
502    let rule = get_state_rule(collection)?;
503
504    match rule.mem() {
505        Memory::Heap => STATE.with(|state| {
506            let state_ref = state.borrow();
507            let length = count_docs_heap(collection, &state_ref.heap.db.db)?;
508            Ok(length)
509        }),
510        Memory::Stable => STATE.with(|state| {
511            let length = count_docs_stable(collection, &state.borrow().stable.db)?;
512            Ok(length)
513        }),
514    }
515}
516
517/// Delete multiple documents from a collection's store based on filter criteria.
518///
519/// This function deletes documents from a collection's store that match the specified filter criteria.
520/// It returns a `Result` with `Ok(Vec<DocContext<Option<Doc>>>)` on success, containing information
521/// about each deleted document, or an error message as `Err(String)` if the deletion encounters issues.
522///
523/// # Parameters
524/// - `caller`: The `Principal` representing the caller initiating the deletion.
525/// - `collection`: A `CollectionKey` representing the collection from which to delete the documents.
526/// - `filter`: A reference to `ListParams`, defining the criteria to filter documents for deletion.
527///
528/// # Returns
529/// - `Ok(Vec<DocContext<Option<Doc>>>)`:
530///   - Each element in the vector represents the context of a deleted document, with:
531///     - `key`: The `Key` of the deleted document.
532///     - `collection`: The `CollectionKey` from which the document was deleted.
533///     - `data`: An `Option<Doc>` representing the deleted document data, if available.
534/// - `Err(String)`: An error message if the deletion operation fails.
535///
536/// This function enables batch deletion of documents in a Juno collection's store that match the given
537/// filter criteria, providing context information for each deleted document or error messages.
538pub fn delete_filtered_docs_store(
539    caller: Principal,
540    collection: CollectionKey,
541    filter: &ListParams,
542) -> Result<Vec<DocContext<Option<Doc>>>, String> {
543    let controllers: Controllers = get_controllers();
544
545    let docs = secure_get_docs(caller, &controllers, collection.clone(), filter)?;
546
547    let context = StoreContext {
548        caller,
549        controllers: &controllers,
550        collection: &collection,
551    };
552
553    delete_filtered_docs_store_impl(&context, &docs)
554}
555
556fn delete_filtered_docs_store_impl(
557    context: &StoreContext,
558    docs: &ListResults<Doc>,
559) -> Result<Vec<DocContext<Option<Doc>>>, String> {
560    let rule = get_state_rule(context.collection)?;
561    let auth_config = get_auth_config();
562
563    let assert_context = AssertContext {
564        rule: &rule,
565        auth_config: &auth_config,
566    };
567
568    let mut results: Vec<DocContext<Option<Doc>>> = Vec::new();
569
570    for (key, doc) in &docs.items {
571        let value = DelDoc {
572            version: doc.version,
573        };
574
575        let deleted_doc = delete_doc_impl(context, &assert_context, key.clone(), value)?;
576
577        let doc_context = DocContext {
578            key: key.clone(),
579            collection: context.collection.clone(),
580            data: deleted_doc,
581        };
582
583        results.push(doc_context);
584    }
585
586    Ok(results)
587}
588
589// ---------------------------------------------------------
590// Config
591// ---------------------------------------------------------
592
593pub fn set_config_store(proposed_config: &SetDbConfig) -> Result<DbConfig, String> {
594    let current_config = get_config();
595
596    assert_set_config(proposed_config, &current_config)?;
597
598    let config = DbConfig::prepare(&current_config, proposed_config);
599
600    insert_config(&config);
601
602    Ok(config)
603}
604
605pub fn get_config_store() -> Option<DbConfig> {
606    get_config()
607}