ic_dbms_canister/
api.rs

1//! API generic interface to be used by different DBMS canisters.
2
3mod inspect;
4
5use candid::Principal;
6use ic_dbms_api::prelude::{
7    Database as _, DeleteBehavior, Filter, IcDbmsError, IcDbmsResult, InsertRecord, Query,
8    TableSchema, TransactionId, UpdateRecord,
9};
10
11pub use self::inspect::inspect;
12use crate::dbms::IcDbmsDatabase;
13use crate::memory::ACL;
14use crate::prelude::{DatabaseSchema, TRANSACTION_SESSION};
15use crate::trap;
16
17/// Adds the given principal to the ACL of the canister.
18pub fn acl_add_principal(principal: Principal) -> IcDbmsResult<()> {
19    assert_caller_is_allowed();
20    ACL.with_borrow_mut(|acl| acl.add_principal(principal))
21        .map_err(IcDbmsError::from)
22}
23
24/// Removes the given principal from the ACL of the canister.
25pub fn acl_remove_principal(principal: Principal) -> IcDbmsResult<()> {
26    assert_caller_is_allowed();
27    ACL.with_borrow_mut(|acl| acl.remove_principal(&principal))
28        .map_err(IcDbmsError::from)
29}
30
31/// Lists all principals in the ACL of the canister.
32pub fn acl_allowed_principals() -> Vec<Principal> {
33    assert_caller_is_allowed();
34    ACL.with_borrow(|acl| acl.allowed_principals().to_vec())
35}
36
37/// Begins a new transaction and returns its ID.
38pub fn begin_transaction() -> TransactionId {
39    assert_caller_is_allowed();
40    let owner = crate::utils::caller();
41    TRANSACTION_SESSION.with_borrow_mut(|ts| ts.begin_transaction(owner))
42}
43
44/// Commits the transaction with the given ID.
45pub fn commit(
46    transaction_id: TransactionId,
47    database_schema: impl DatabaseSchema + 'static,
48) -> IcDbmsResult<()> {
49    assert_caller_is_allowed();
50    assert_caller_owns_transaction(Some(&transaction_id));
51    let mut database = IcDbmsDatabase::from_transaction(database_schema, transaction_id);
52    database.commit()
53}
54
55/// Rolls back the transaction with the given ID.
56pub fn rollback(
57    transaction_id: TransactionId,
58    database_schema: impl DatabaseSchema + 'static,
59) -> IcDbmsResult<()> {
60    assert_caller_is_allowed();
61    assert_caller_owns_transaction(Some(&transaction_id));
62    let mut database = IcDbmsDatabase::from_transaction(database_schema, transaction_id);
63    database.rollback()
64}
65
66/// Executes a select query against the database schema, optionally within a transaction.
67pub fn select<T>(
68    query: Query<T>,
69    transaction_id: Option<TransactionId>,
70    database_schema: impl DatabaseSchema + 'static,
71) -> IcDbmsResult<Vec<T::Record>>
72where
73    T: TableSchema,
74{
75    assert_caller_is_allowed();
76    assert_caller_owns_transaction(transaction_id.as_ref());
77    let database = database(transaction_id, database_schema);
78    database.select(query)
79}
80
81/// Executes an insert query against the database schema, optionally within a transaction.
82pub fn insert<T>(
83    record: T::Insert,
84    transaction_id: Option<TransactionId>,
85    database_schema: impl DatabaseSchema + 'static,
86) -> IcDbmsResult<()>
87where
88    T: TableSchema,
89    T::Insert: InsertRecord<Schema = T>,
90{
91    assert_caller_is_allowed();
92    assert_caller_owns_transaction(transaction_id.as_ref());
93    let database = database(transaction_id, database_schema);
94    database.insert::<T>(record)
95}
96
97/// Executes an update query against the database schema, optionally within a transaction.
98pub fn update<T>(
99    patch: T::Update,
100    transaction_id: Option<TransactionId>,
101    database_schema: impl DatabaseSchema + 'static,
102) -> IcDbmsResult<u64>
103where
104    T: TableSchema,
105    T::Update: UpdateRecord<Schema = T>,
106{
107    assert_caller_is_allowed();
108    assert_caller_owns_transaction(transaction_id.as_ref());
109    let database = database(transaction_id, database_schema);
110    database.update::<T>(patch)
111}
112
113/// Executes a delete query against the database schema, optionally within a transaction.
114pub fn delete<T>(
115    behaviour: DeleteBehavior,
116    filter: Option<Filter>,
117    transaction_id: Option<TransactionId>,
118    database_schema: impl DatabaseSchema + 'static,
119) -> IcDbmsResult<u64>
120where
121    T: TableSchema,
122{
123    assert_caller_is_allowed();
124    assert_caller_owns_transaction(transaction_id.as_ref());
125    let database = database(transaction_id, database_schema);
126    database.delete::<T>(behaviour, filter)
127}
128
129/// Helper function to get the database, either in a transaction or as a one-shot.
130#[inline]
131fn database(
132    transaction_id: Option<TransactionId>,
133    database_schema: impl DatabaseSchema + 'static,
134) -> IcDbmsDatabase {
135    match transaction_id {
136        Some(tx_id) => IcDbmsDatabase::from_transaction(database_schema, tx_id),
137        None => IcDbmsDatabase::oneshot(database_schema),
138    }
139}
140
141/// Asserts that the caller is in the ACL of the canister.
142///
143/// If not it traps.
144fn assert_caller_is_allowed() {
145    let caller = crate::utils::caller();
146    if !ACL.with_borrow(|acl| acl.is_allowed(&caller)) {
147        trap!("Caller {caller} is not allowed to perform this operation");
148    }
149}
150
151/// Asserts that the caller owns the given transaction ID.
152fn assert_caller_owns_transaction(transaction_id: Option<&TransactionId>) {
153    let Some(tx_id) = transaction_id else {
154        return;
155    };
156    let caller = crate::utils::caller();
157    TRANSACTION_SESSION.with_borrow(|ts| {
158        if !ts.has_transaction(tx_id, caller) {
159            trap!("Caller {caller} does not own transaction {tx_id}");
160        }
161    });
162}
163
164#[cfg(test)]
165mod tests {
166
167    use ic_dbms_api::prelude::Uint32;
168
169    use super::*;
170    use crate::tests::{UserInsertRequest, load_fixtures};
171
172    #[test]
173    fn test_should_insert_into_acl() {
174        // init ACL
175        init_acl();
176        // try to add a new principal
177        let bob = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
178        assert!(acl_add_principal(bob).is_ok());
179        // check if bob is in the ACL
180        let allowed = acl_allowed_principals();
181        assert!(allowed.contains(&bob));
182        assert!(allowed.contains(&alice()));
183    }
184
185    #[test]
186    fn test_should_remove_from_acl() {
187        // init ACL
188        init_acl();
189        // add a new principal
190        let bob = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
191        assert!(acl_add_principal(bob).is_ok());
192        // remove bob
193        assert!(acl_remove_principal(bob).is_ok());
194        // check if bob is not in the ACL
195        let allowed = acl_allowed_principals();
196        assert!(!allowed.contains(&bob));
197        assert!(allowed.contains(&alice()));
198    }
199
200    #[test]
201    fn test_should_list_acl_principals() {
202        // init ACL
203        init_acl();
204        // list principals
205        let allowed = acl_allowed_principals();
206        // check if alice is in the ACL
207        assert!(allowed.contains(&alice()));
208    }
209
210    #[test]
211    fn test_should_begin_transaction() {
212        // init ACL
213        init_acl();
214        // begin transaction
215        let _tx_id = begin_transaction();
216    }
217
218    #[test]
219    fn test_should_commit_transaction() {
220        // init ACL
221        init_acl();
222        // begin transaction
223        let tx_id = begin_transaction();
224        // commit transaction
225        let res = commit(tx_id, crate::tests::TestDatabaseSchema);
226        assert!(res.is_ok());
227    }
228
229    #[test]
230    fn test_should_rollback_transaction() {
231        // init ACL
232        init_acl();
233        // begin transaction
234        let tx_id = begin_transaction();
235        // rollback transaction
236        let res = rollback(tx_id, crate::tests::TestDatabaseSchema);
237        assert!(res.is_ok());
238    }
239
240    #[test]
241    fn test_should_insert_record() {
242        load_fixtures();
243        // init ACL
244        init_acl();
245        // insert record
246        let record = UserInsertRequest {
247            id: 100u32.into(),
248            name: "Alice".to_string().into(),
249        };
250
251        let res = insert::<crate::tests::User>(record, None, crate::tests::TestDatabaseSchema);
252        assert!(res.is_ok());
253    }
254
255    #[test]
256    fn test_should_select_record() {
257        // init ACL
258        init_acl();
259        load_fixtures();
260        // select record
261        let query = Query::<crate::tests::User>::builder()
262            .all()
263            .limit(10)
264            .build();
265        let res = select::<crate::tests::User>(query, None, crate::tests::TestDatabaseSchema);
266        assert!(res.is_ok());
267        let records = res.unwrap();
268        assert!(!records.is_empty());
269    }
270
271    #[test]
272    fn test_should_update_record() {
273        // init ACL
274        init_acl();
275        load_fixtures();
276
277        // update record
278        let patch = crate::tests::UserUpdateRequest {
279            id: None,
280            name: Some("Robert".to_string().into()),
281            where_clause: Some(Filter::Eq("id".to_string(), Uint32::from(1u32).into())),
282        };
283        let res = update::<crate::tests::User>(patch, None, crate::tests::TestDatabaseSchema);
284        assert!(res.is_ok());
285    }
286
287    #[test]
288    fn test_should_delete_record() {
289        // init ACL
290        init_acl();
291        load_fixtures();
292
293        // delete record
294        let filter = Some(Filter::Eq("id".to_string(), Uint32::from(2u32).into()));
295        let res = delete::<crate::tests::User>(
296            DeleteBehavior::Cascade,
297            filter,
298            None,
299            crate::tests::TestDatabaseSchema,
300        );
301        assert!(res.is_ok());
302    }
303
304    #[test]
305    #[should_panic = "Caller ghsi2-tqaaa-aaaan-aaaca-cai does not own transaction 0"]
306    fn test_should_not_allow_operating_wrong_tx() {
307        // init ACL
308        init_acl();
309        load_fixtures();
310
311        let tx_id = TRANSACTION_SESSION.with_borrow_mut(|ts| {
312            ts.begin_transaction(Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap())
313        });
314
315        // try to commit the transaction started by alice
316        let _ = commit(tx_id, crate::tests::TestDatabaseSchema);
317    }
318
319    fn alice() -> Principal {
320        crate::utils::caller()
321    }
322
323    fn init_acl() {
324        ACL.with_borrow_mut(|acl| {
325            acl.add_principal(alice()).unwrap();
326        });
327    }
328}