1mod 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
17pub 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
24pub 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
31pub fn acl_allowed_principals() -> Vec<Principal> {
33 assert_caller_is_allowed();
34 ACL.with_borrow(|acl| acl.allowed_principals().to_vec())
35}
36
37pub 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
44pub 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
55pub 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
66pub 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
81pub 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
97pub 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
113pub 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#[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
141fn 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
151fn 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();
176 let bob = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
178 assert!(acl_add_principal(bob).is_ok());
179 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();
189 let bob = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
191 assert!(acl_add_principal(bob).is_ok());
192 assert!(acl_remove_principal(bob).is_ok());
194 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();
204 let allowed = acl_allowed_principals();
206 assert!(allowed.contains(&alice()));
208 }
209
210 #[test]
211 fn test_should_begin_transaction() {
212 init_acl();
214 let _tx_id = begin_transaction();
216 }
217
218 #[test]
219 fn test_should_commit_transaction() {
220 init_acl();
222 let tx_id = begin_transaction();
224 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();
233 let tx_id = begin_transaction();
235 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();
245 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();
259 load_fixtures();
260 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();
275 load_fixtures();
276
277 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();
291 load_fixtures();
292
293 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();
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 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}