├── src ├── rts.rs ├── settings.rs ├── lib.rs ├── admin.rs ├── httpc.rs ├── client.rs ├── logs.rs ├── collections.rs └── records.rs ├── .gitignore ├── TODO.org ├── examples ├── healthcheck.rs ├── logs.rs ├── collections.rs └── records.rs ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── LICENSE ├── tests ├── authenticate_admin.rs ├── authenticate_record.rs ├── records.rs └── collections.rs └── README.md /src/rts.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /pb_data 4 | /pb_migrations 5 | /pocketbase 6 | -------------------------------------------------------------------------------- /TODO.org: -------------------------------------------------------------------------------- 1 | * List of things to complete for rewrite launch 2 | 3 | ** TODO Implement Create APIs for Collections 4 | ** TODO Implement View 5 | -------------------------------------------------------------------------------- /examples/healthcheck.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use pocketbase_sdk::client::Client; 3 | 4 | fn main() -> Result<()> { 5 | let client = Client::new("http://localhost:8090"); 6 | let health_check_response = client.health_check()?; 7 | dbg!(health_check_response); 8 | 9 | Ok(()) 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build x86 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Non Admin Client 2 | pub mod client; 3 | 4 | /// Admin Client - Mirror of Client but with admin authentication token 5 | pub mod admin; 6 | 7 | /// Records Related Operations 8 | pub mod records; 9 | 10 | /// Collections Related Operations 11 | pub mod collections; 12 | 13 | /// Logs Related Operations 14 | pub mod logs; 15 | 16 | /// Settings Related Operations 17 | pub mod settings; 18 | 19 | /// Realtime Server [Not Available] 20 | pub mod rts; 21 | 22 | mod httpc; 23 | -------------------------------------------------------------------------------- /examples/logs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use pocketbase_sdk::admin::Admin; 3 | 4 | fn main() -> Result<()> { 5 | env_logger::init(); 6 | 7 | // admin authentication 8 | let admin = Admin::new("http://localhost:8090") 9 | .auth_with_password("sreedev@icloud.com", "Sreedev123")?; 10 | 11 | // list logs 12 | let logs = admin.logs().list().page(1).per_page(10).call()?; 13 | dbg!(&logs); 14 | 15 | // view log 16 | let somelogid = &logs.items[0].id; 17 | let logitem = admin.logs().view(somelogid).call()?; 18 | dbg!(logitem); 19 | 20 | // view log statistics data points 21 | let logstats = admin.logs().statistics().call()?; 22 | dbg!(logstats); 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /examples/collections.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use pocketbase_sdk::admin::Admin; 3 | 4 | fn main() -> Result<()> { 5 | env_logger::init(); 6 | 7 | // admin authentication 8 | let authenticated_admin_client = Admin::new("http://localhost:8090") 9 | .auth_with_password("sreedev@icloud.com", "Sreedev123")?; 10 | 11 | // collections list + Filter 12 | let collections = authenticated_admin_client 13 | .collections() 14 | .list() 15 | .page(1) 16 | .filter("name = 'employees'".to_string()) 17 | .per_page(100) 18 | .call()?; 19 | 20 | dbg!(collections); 21 | 22 | // view collection 23 | let user_collection = authenticated_admin_client 24 | .collections() 25 | .view("users") 26 | .call()?; 27 | 28 | dbg!(user_collection); 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pocketbase-sdk" 3 | version = "0.1.1" 4 | edition = "2021" 5 | authors = ["Sreedev Kodichath ", "CubeCoder "] 6 | license = "MIT" 7 | description = "Pocketbase SDK" 8 | readme = "README.md" 9 | homepage = "https://github.com/sreedevk/pocketbase-sdk-rust" 10 | categories = ["development-tools"] 11 | keywords = [ 12 | "pocketbase", 13 | "pocketbase-sdk", 14 | "rust-pocketbase", 15 | "pocketbase-rs" 16 | ] 17 | 18 | [dependencies] 19 | anyhow = "1.0.70" 20 | chrono = { version = "0.4.24", features = ["serde"] } 21 | env_logger = "0.10.0" 22 | log = "0.4.17" 23 | serde = { version = "1.0.145", features = ["derive"] } 24 | serde_json = "1.0.85" 25 | ureq = { version = "2.6.2", features = ["json", "charset"] } 26 | url = "2.3.1" 27 | 28 | [dev-dependencies] 29 | httpmock = "0.6" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sreedev Kodichath 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/admin.rs: -------------------------------------------------------------------------------- 1 | use crate::client::Auth; 2 | use crate::client::Client; 3 | use crate::httpc::Httpc; 4 | use anyhow::{anyhow, Result}; 5 | use serde::Deserialize; 6 | use serde_json::json; 7 | 8 | pub struct Admin<'a> { 9 | pub base_url: &'a str, 10 | } 11 | 12 | #[derive(Debug, Clone, Deserialize)] 13 | struct AuthSuccessResponse { 14 | token: String, 15 | } 16 | 17 | impl<'a> Admin<'a> { 18 | pub fn auth_with_password(&self, identifier: &str, secret: &str) -> Result> { 19 | let url = format!("{}/api/admins/auth-with-password", self.base_url); 20 | let credentials = json!({ 21 | "identity": identifier, 22 | "password": secret, 23 | }); 24 | let client = Client::new(self.base_url); 25 | match Httpc::post(&client, &url, credentials.to_string()) { 26 | Ok(response) => { 27 | let raw_response = response.into_json::(); 28 | match raw_response { 29 | Ok(AuthSuccessResponse { token }) => Ok(Client { 30 | base_url: self.base_url.to_string(), 31 | state: Auth, 32 | auth_token: Some(token), 33 | }), 34 | Err(e) => Err(anyhow!("{}", e)), 35 | } 36 | } 37 | Err(e) => Err(anyhow!("{}", e)), 38 | } 39 | } 40 | 41 | pub fn new(base_url: &'a str) -> Admin<'a> { 42 | Admin { base_url } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/httpc.rs: -------------------------------------------------------------------------------- 1 | use ureq::{Request, Response}; 2 | 3 | use crate::client::Client; 4 | use anyhow::Result; 5 | 6 | pub struct Httpc; 7 | 8 | impl Httpc { 9 | fn attach_auth_info(partial_request: Request, client: &Client) -> Result { 10 | match client.auth_token.as_ref() { 11 | Some(token) => Ok(partial_request.set("Authorization", token)), 12 | None => Ok(partial_request), 13 | } 14 | } 15 | 16 | pub fn get( 17 | client: &Client, 18 | url: &str, 19 | query_params: Option>, 20 | ) -> Result { 21 | Ok(ureq::get(url)) 22 | .and_then(|request| Self::attach_auth_info(request, client)) 23 | .map(|request| { 24 | if let Some(pairs) = query_params { 25 | request.query_pairs(pairs) 26 | } else { 27 | request 28 | } 29 | }) 30 | .and_then(|request| Ok(request.call()?)) 31 | } 32 | 33 | pub fn post(client: &Client, url: &str, body_content: String) -> Result { 34 | Ok(ureq::post(url)) 35 | .map(|request| request.set("Content-Type", "application/json")) 36 | .and_then(|request| Self::attach_auth_info(request, client)) 37 | .and_then(|request| Ok(request.send_string(body_content.as_str())?)) 38 | } 39 | 40 | pub fn delete(client: &Client, url: &str) -> Result { 41 | Ok(ureq::delete(url)) 42 | .and_then(|request| Self::attach_auth_info(request, client)) 43 | .and_then(|request| Ok(request.call()?)) 44 | } 45 | 46 | pub fn patch(client: &Client, url: &str, body_content: String) -> Result { 47 | Ok(ureq::patch(url)) 48 | .map(|request| request.set("Content-Type", "application/json")) 49 | .and_then(|request| Self::attach_auth_info(request, client)) 50 | .and_then(|request| Ok(request.send_string(body_content.as_str())?)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/records.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use pocketbase_sdk::client::Client; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Clone, Deserialize, Default)] 6 | pub struct Product { 7 | pub id: String, 8 | pub name: String, 9 | pub count: i32, 10 | pub created: chrono::DateTime, 11 | } 12 | 13 | #[derive(Debug, Clone, Serialize)] 14 | pub struct NewProduct { 15 | pub name: String, 16 | pub count: i32, 17 | } 18 | 19 | fn main() -> Result<()> { 20 | env_logger::init(); 21 | 22 | /* Authenticate Client */ 23 | let authenticated_client = Client::new("http://localhost:8090").auth_with_password( 24 | "users", 25 | "sreedev@icloud.com", 26 | "Sreedev123", 27 | )?; 28 | 29 | /* List Products */ 30 | let products = authenticated_client 31 | .records("products") 32 | .list() 33 | .call::()?; 34 | dbg!(products); 35 | 36 | /* List Products with filter */ 37 | let filtered_products = authenticated_client.records("products").list().filter("count < 6000").call::()?; 38 | dbg!(filtered_products); 39 | 40 | /* View Product */ 41 | let product = authenticated_client 42 | .records("products") 43 | .view("jme4ixxqie2f9ho") 44 | .call::()?; 45 | dbg!(product); 46 | 47 | /* Create Product */ 48 | let new_product = NewProduct { 49 | name: String::from("bingo"), 50 | count: 69420, 51 | }; 52 | let create_response = authenticated_client 53 | .records("products") 54 | .create(new_product) 55 | .call()?; 56 | dbg!(&create_response); 57 | 58 | /* Update Product */ 59 | let updated_product = NewProduct { 60 | name: String::from("bango"), 61 | count: 69420, 62 | }; 63 | let update_response = authenticated_client 64 | .records("products") 65 | .update(create_response.id.as_str(), updated_product) 66 | .call()?; 67 | 68 | dbg!(update_response); 69 | 70 | /* Delete Product */ 71 | authenticated_client 72 | .records("products") 73 | .destroy(create_response.id.as_str()) 74 | .call()?; 75 | 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /tests/authenticate_admin.rs: -------------------------------------------------------------------------------- 1 | use httpmock::prelude::*; 2 | use pocketbase_sdk::admin::Admin; 3 | use serde_json::json; 4 | 5 | #[test] 6 | pub fn authenticate_admin_success() { 7 | let mockserver = mock_admin_login(); 8 | let client = Admin::new(mockserver.base_url().as_str()) 9 | .auth_with_password("sreedev@icloud.com", "Sreedev123"); 10 | assert!(client.is_ok()); 11 | } 12 | 13 | #[test] 14 | pub fn authenticate_admin_failure() { 15 | let mockserver = mock_admin_login(); 16 | let client = Admin::new(mockserver.base_url().as_str()) 17 | .auth_with_password("wrongidentity@wrongidentity.com", "wrongpassword"); 18 | assert!(client.is_err()); 19 | } 20 | 21 | fn mock_admin_login() -> MockServer { 22 | let server = MockServer::start(); 23 | server.mock(|when, then| { 24 | when.method(POST).json_body(json!({ 25 | "identity": "wrongidentity@wrongidentity.com", 26 | "password": "wrongpassword" 27 | })); 28 | 29 | then.status(400) 30 | .header("content-type", "application/json") 31 | .json_body(json!({ 32 | "code": 400, 33 | "message": "An error occurred while submitting the form.", 34 | "data": { 35 | "password": { 36 | "code": "validation_required", 37 | "message": "Missing required value." 38 | } 39 | } 40 | })); 41 | }); 42 | 43 | server.mock(|when, then| { 44 | when 45 | .method(POST) 46 | .json_body(json!({ 47 | "identity": "sreedev@icloud.com", 48 | "password": "Sreedev123" 49 | })) 50 | .path("/api/admins/auth-with-password"); 51 | 52 | then 53 | .status(200) 54 | .header("content-type", "application/json") 55 | .json_body(json!({ 56 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4MTYwMH0.han3_sG65zLddpcX2ic78qgy7FKecuPfOpFa8Dvi5Bg", 57 | "admin": { 58 | "id": "b6e4b08274f34e9", 59 | "created": "2022-06-22 07:13:09.735Z", 60 | "updated": "2022-06-22 07:13:09.735Z", 61 | "email": "test@example.com", 62 | "avatar": 0 63 | } 64 | })); 65 | }); 66 | 67 | server 68 | } 69 | -------------------------------------------------------------------------------- /tests/authenticate_record.rs: -------------------------------------------------------------------------------- 1 | use httpmock::prelude::*; 2 | use pocketbase_sdk::client::Client; 3 | use serde_json::json; 4 | 5 | #[test] 6 | pub fn authenticate_record_success() { 7 | let mockserver = mock_admin_login(); 8 | let client = Client::new(mockserver.base_url().as_str()).auth_with_password( 9 | "users", 10 | "sreedev@icloud.com", 11 | "Sreedev123", 12 | ); 13 | assert!(client.is_ok()); 14 | } 15 | 16 | #[test] 17 | pub fn authenticate_record_error() { 18 | let mockserver = mock_admin_login(); 19 | let client = Client::new(mockserver.base_url().as_str()).auth_with_password( 20 | "users", 21 | "bingo", 22 | "bango", 23 | ); 24 | assert!(client.is_err()); 25 | } 26 | 27 | fn mock_admin_login() -> MockServer { 28 | let server = MockServer::start(); 29 | server.mock(|when, then| { 30 | when.method(POST) 31 | .json_body(json!({ 32 | "identity": "bingo", 33 | "password": "bango" 34 | })) 35 | .path("/api/collections/users/auth-with-password"); 36 | 37 | then.status(400) 38 | .header("content-type", "application/json") 39 | .json_body(json!({ 40 | "code": 400, 41 | "message": "An error occurred while submitting the form.", 42 | "data": { 43 | "password": { 44 | "code": "validation_required", 45 | "message": "Missing required value." 46 | } 47 | } 48 | })); 49 | }); 50 | server.mock(|when, then| { 51 | when 52 | .method(POST) 53 | .json_body(json!({ 54 | "identity": "sreedev@icloud.com", 55 | "password": "Sreedev123" 56 | })) 57 | .path("/api/collections/users/auth-with-password"); 58 | 59 | then 60 | .status(200) 61 | .header("content-type", "application/json") 62 | .json_body(json!({ 63 | "token": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", 64 | "record": { 65 | "id": "8171022dc95a4ed", 66 | "collectionId": "d2972397d45614e", 67 | "collectionName": "users", 68 | "created": "2022-06-24 06:24:18.434Z", 69 | "updated": "2022-06-24 06:24:18.889Z", 70 | "username": "test@example.com", 71 | "email": "test@example.com", 72 | "verified": false, 73 | "emailVisibility": true, 74 | "someCustomField": "example 123" 75 | } 76 | })); 77 | }); 78 | 79 | server 80 | } 81 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use crate::{collections::CollectionsManager, httpc::Httpc}; 2 | use crate::{logs::LogsManager, records::RecordsManager}; 3 | use anyhow::{anyhow, Result}; 4 | use serde::Deserialize; 5 | use serde_json::json; 6 | 7 | #[derive(Debug, Deserialize)] 8 | struct AuthSuccessResponse { 9 | token: String, 10 | } 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct NoAuth; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct Auth; 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct Client { 20 | pub base_url: String, 21 | pub auth_token: Option, 22 | pub state: State, 23 | } 24 | 25 | #[derive(Debug, Clone, Deserialize)] 26 | pub struct HealthCheckResponse { 27 | pub code: i32, 28 | pub message: String, 29 | } 30 | 31 | impl Client { 32 | pub fn collections(&self) -> CollectionsManager { 33 | CollectionsManager { client: self } 34 | } 35 | 36 | pub fn health_check(&self) -> Result { 37 | let url = format!("{}/api/health", self.base_url); 38 | match Httpc::get(self, &url, None) { 39 | Ok(response) => Ok(response.into_json::()?), 40 | Err(e) => Err(anyhow!("{}", e)) 41 | } 42 | } 43 | 44 | pub fn logs(&self) -> LogsManager { 45 | LogsManager { client: self } 46 | } 47 | 48 | pub fn records(&self, record_name: &'static str) -> RecordsManager { 49 | RecordsManager { 50 | client: self, 51 | name: record_name, 52 | } 53 | } 54 | } 55 | 56 | impl Client { 57 | pub fn new(base_url: &str) -> Self { 58 | Self { 59 | base_url: base_url.to_string(), 60 | auth_token: None, 61 | state: NoAuth, 62 | } 63 | } 64 | 65 | pub fn health_check(&self) -> Result { 66 | let url = format!("{}/api/health", self.base_url); 67 | match Httpc::get(self, &url, None) { 68 | Ok(response) => Ok(response.into_json::()?), 69 | Err(e) => Err(anyhow!("{}", e)) 70 | } 71 | } 72 | 73 | pub fn auth_with_password(&self, collection: &str, identifier: &str, secret: &str) -> Result> { 74 | let url = format!( 75 | "{}/api/collections/{}/auth-with-password", 76 | self.base_url, collection 77 | ); 78 | 79 | let auth_payload = json!({ 80 | "identity": identifier, 81 | "password": secret 82 | }); 83 | 84 | match Httpc::post(self, &url, auth_payload.to_string()) { 85 | Ok(response) => { 86 | let raw_response = response.into_json::(); 87 | match raw_response { 88 | Ok(AuthSuccessResponse { token }) => Ok(Client { 89 | base_url: self.base_url.clone(), 90 | state: Auth, 91 | auth_token: Some(token), 92 | }), 93 | Err(e) => Err(anyhow!("{}", e)), 94 | } 95 | } 96 | Err(e) => Err(anyhow!("{}", e)), 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/records.rs: -------------------------------------------------------------------------------- 1 | use httpmock::prelude::*; 2 | use pocketbase_sdk::client::Client; 3 | use serde::{Serialize, Deserialize}; 4 | use serde_json::json; 5 | 6 | #[derive(Clone, Debug, Serialize, Default, Deserialize)] 7 | pub struct Record { 8 | pub id: String, 9 | pub title: String, 10 | } 11 | 12 | #[test] 13 | fn list_records_success() { 14 | let mockserver = mock_records_server(); 15 | let client = Client::new(mockserver.base_url().as_str()).auth_with_password( 16 | "users", 17 | "sreedev@icloud.com", 18 | "Sreedev123", 19 | ).unwrap(); 20 | 21 | let records = client.records("posts").list().per_page(1010).call::(); 22 | assert!(records.is_ok()); 23 | } 24 | 25 | fn mock_records_server() -> MockServer { 26 | let server = MockServer::start(); 27 | server.mock(|when, then| { 28 | when.method(GET) 29 | .path("/api/collections/posts/records") 30 | .query_param("page", "1") 31 | .query_param("per_page", "1010") 32 | .header("Authorization", "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc"); 33 | then.header("Content-Type", "application/json") 34 | .json_body(json!({ 35 | "page": 1, 36 | "perPage": 100, 37 | "totalItems": 2, 38 | "items": [ 39 | { 40 | "id": "ae40239d2bc4477", 41 | "collectionId": "a98f514eb05f454", 42 | "collectionName": "posts", 43 | "updated": "2022-06-25 11:03:50.052", 44 | "created": "2022-06-25 11:03:35.163", 45 | "title": "test1" 46 | }, 47 | { 48 | "id": "d08dfc4f4d84419", 49 | "collectionId": "a98f514eb05f454", 50 | "collectionName": "posts", 51 | "updated": "2022-06-25 11:03:45.876", 52 | "created": "2022-06-25 11:03:45.876", 53 | "title": "test2" 54 | } 55 | ] 56 | })); 57 | }); 58 | 59 | server.mock(|when, then| { 60 | when 61 | .method(POST) 62 | .json_body(json!({ 63 | "identity": "sreedev@icloud.com", 64 | "password": "Sreedev123" 65 | })) 66 | .path("/api/collections/users/auth-with-password"); 67 | 68 | then 69 | .status(200) 70 | .header("content-type", "application/json") 71 | .json_body(json!({ 72 | "token": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc", 73 | "record": { 74 | "id": "8171022dc95a4ed", 75 | "collectionId": "d2972397d45614e", 76 | "collectionName": "users", 77 | "created": "2022-06-24 06:24:18.434Z", 78 | "updated": "2022-06-24 06:24:18.889Z", 79 | "username": "test@example.com", 80 | "email": "test@example.com", 81 | "verified": false, 82 | "emailVisibility": true, 83 | "someCustomField": "example 123" 84 | } 85 | })); 86 | }); 87 | server 88 | } 89 | -------------------------------------------------------------------------------- /src/logs.rs: -------------------------------------------------------------------------------- 1 | use crate::client::{Auth, Client}; 2 | use crate::httpc::Httpc; 3 | use anyhow::Result; 4 | use serde::Deserialize; 5 | use std::collections::HashMap; 6 | use chrono::{DateTime, Utc}; 7 | 8 | pub struct LogsManager<'a> { 9 | pub client: &'a Client, 10 | } 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct LogListRequestBuilder<'a> { 14 | pub client: &'a Client, 15 | pub page: i32, 16 | pub per_page: i32, 17 | pub sort: Option<&'a str>, 18 | pub filter: Option<&'a str>, 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct LogViewRequestBuilder<'a> { 23 | pub client: &'a Client, 24 | pub id: &'a str, 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub struct LogStatisticsRequestBuilder<'a> { 29 | pub client: &'a Client, 30 | pub filter: Option<&'a str>, 31 | } 32 | 33 | #[derive(Debug, Clone, Deserialize)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct LogListItem { 36 | pub id: String, 37 | pub created: DateTime, 38 | pub updated: DateTime, 39 | pub url: String, 40 | pub method: String, 41 | pub status: i32, 42 | pub ip: Option, 43 | pub referer: String, 44 | pub user_agent: String, 45 | pub meta: HashMap, 46 | } 47 | 48 | #[derive(Debug, Clone, Deserialize)] 49 | #[serde(rename_all = "camelCase")] 50 | pub struct LogList { 51 | pub page: i32, 52 | pub per_page: i32, 53 | pub total_items: i32, 54 | pub items: Vec, 55 | } 56 | 57 | #[derive(Debug, Clone, Deserialize)] 58 | pub struct LogStatDataPoint { 59 | pub total: i32, 60 | pub date: String, 61 | } 62 | 63 | impl<'a> LogStatisticsRequestBuilder<'a> { 64 | pub fn filter(&self, filter_query: &'a str) -> Self { 65 | Self { 66 | filter: Some(filter_query), 67 | ..self.clone() 68 | } 69 | } 70 | 71 | pub fn call(&self) -> Result> { 72 | let url = format!("{}/api/logs/requests/stats", self.client.base_url); 73 | let mut build_opts = Vec::new(); 74 | if let Some(filter_opts) = &self.filter { 75 | build_opts.push(("filter", filter_opts.to_owned())); 76 | } 77 | 78 | match Httpc::get(self.client, &url, Some(build_opts)) { 79 | Ok(result) => { 80 | let response = result.into_json::>()?; 81 | Ok(response) 82 | } 83 | Err(e) => Err(e), 84 | } 85 | } 86 | } 87 | 88 | impl<'a> LogViewRequestBuilder<'a> { 89 | pub fn call(&self) -> Result { 90 | let url = format!("{}/api/logs/requests/{}", self.client.base_url, self.id); 91 | match Httpc::get(self.client, &url, None) { 92 | Ok(result) => { 93 | let response = result.into_json::()?; 94 | Ok(response) 95 | } 96 | Err(e) => Err(e), 97 | } 98 | } 99 | } 100 | 101 | impl<'a> LogListRequestBuilder<'a> { 102 | pub fn page(&self, page_count: i32) -> Self { 103 | LogListRequestBuilder { 104 | page: page_count, 105 | ..self.clone() 106 | } 107 | } 108 | 109 | pub fn per_page(&self, per_page_count: i32) -> Self { 110 | LogListRequestBuilder { 111 | per_page: per_page_count, 112 | ..self.clone() 113 | } 114 | } 115 | 116 | pub fn filter(&self, filter_opts: &'a str) -> Self { 117 | LogListRequestBuilder { 118 | filter: Some(filter_opts), 119 | ..self.clone() 120 | } 121 | } 122 | 123 | pub fn sort(&self, sort_opts: &'a str) -> Self { 124 | LogListRequestBuilder { 125 | sort: Some(sort_opts), 126 | ..self.clone() 127 | } 128 | } 129 | 130 | pub fn call(&self) -> Result { 131 | let url = format!("{}/api/logs/requests", self.client.base_url); 132 | let mut build_opts = Vec::new(); 133 | 134 | if let Some(sort_opts) = &self.sort { build_opts.push(("sort", sort_opts.to_owned())) } 135 | if let Some(filter_opts) = &self.filter { build_opts.push(("filter", filter_opts.to_owned())) } 136 | let per_page_opts = self.per_page.to_string(); 137 | let page_opts = self.page.to_string(); 138 | build_opts.push(("per_page", per_page_opts.as_str())); 139 | build_opts.push(("page", page_opts.as_str())); 140 | 141 | match Httpc::get(self.client, &url, Some(build_opts)) { 142 | Ok(result) => { 143 | let response = result.into_json::()?; 144 | Ok(response) 145 | } 146 | Err(e) => Err(e), 147 | } 148 | } 149 | } 150 | 151 | impl<'a> LogsManager<'a> { 152 | pub fn list(&self) -> LogListRequestBuilder<'a> { 153 | LogListRequestBuilder { 154 | client: self.client, 155 | page: 1, 156 | per_page: 100, 157 | sort: None, 158 | filter: None, 159 | } 160 | } 161 | 162 | pub fn view(&self, id: &'a str) -> LogViewRequestBuilder<'a> { 163 | LogViewRequestBuilder { 164 | client: self.client, 165 | id, 166 | } 167 | } 168 | 169 | pub fn statistics(&self) -> LogStatisticsRequestBuilder<'a> { 170 | LogStatisticsRequestBuilder { client: self.client, filter: None } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/collections.rs: -------------------------------------------------------------------------------- 1 | use crate::client::{Auth, Client}; 2 | use crate::httpc::Httpc; 3 | use anyhow::Result; 4 | use serde::{Deserialize, Serialize}; 5 | use chrono::{DateTime, Utc}; 6 | 7 | #[derive(Debug, Deserialize, Clone, Serialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct Field { 10 | pub system: bool, 11 | pub id: String, 12 | pub name: String, 13 | pub r#type: String, 14 | pub required: bool, 15 | pub unique: bool, 16 | } 17 | 18 | #[derive(Debug, Clone, Deserialize, Serialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct FieldDeclaration<'a> { 21 | pub name: &'a str, 22 | pub r#type: &'a str, 23 | pub required: bool, 24 | } 25 | 26 | #[derive(Debug, Deserialize)] 27 | #[serde(rename_all = "camelCase")] 28 | pub struct CollectionList { 29 | pub page: i32, 30 | pub per_page: i32, 31 | pub total_items: i32, 32 | pub items: Vec, 33 | } 34 | 35 | #[derive(Debug, Deserialize, Clone)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct Collection { 38 | pub id: String, 39 | pub created: DateTime, 40 | pub r#type: String, 41 | pub updated: DateTime, 42 | pub name: String, 43 | pub schema: Vec, 44 | } 45 | 46 | #[derive(Clone, Debug)] 47 | pub struct CollectionsManager<'a> { 48 | pub client: &'a Client, 49 | } 50 | 51 | /*TODO: Add Auth Options & View Options for View & Auth Types*/ 52 | #[derive(Debug, Clone, Serialize)] 53 | #[serde(rename_all = "camelCase")] 54 | pub struct CollectionDetails<'a> { 55 | pub id: Option<&'a str>, 56 | pub name: Option<&'a str>, 57 | pub r#type: Option<&'a str>, 58 | pub schema: Vec>, 59 | pub system: bool, 60 | pub list_rule: Option, 61 | pub view_rule: Option, 62 | pub create_rule: Option, 63 | pub update_rule: Option, 64 | pub delete_rule: Option, 65 | pub indexes: Vec, 66 | } 67 | 68 | #[derive(Debug, Clone)] 69 | pub struct CollectionCreateRequestBuilder<'a> { 70 | pub client: &'a Client, 71 | pub collection_name: &'a str, 72 | pub collection_details: Option>, 73 | } 74 | 75 | #[derive(Clone, Debug)] 76 | pub struct CollectionViewRequestBuilder<'a> { 77 | pub client: &'a Client, 78 | pub name: &'a str, 79 | } 80 | 81 | #[derive(Clone, Debug)] 82 | pub struct CollectionListRequestBuilder<'a> { 83 | pub client: &'a Client, 84 | pub filter: Option, 85 | pub sort: Option, 86 | pub per_page: i32, 87 | pub page: i32, 88 | } 89 | 90 | impl<'a> CollectionListRequestBuilder<'a> { 91 | pub fn call(&self) -> Result { 92 | let url = format!("{}/api/collections", self.client.base_url); 93 | let mut build_opts: Vec<(&str, &str)> = Vec::new(); 94 | 95 | if let Some(filter_opts) = &self.filter { 96 | build_opts.push(("filter", filter_opts)) 97 | } 98 | if let Some(sort_opts) = &self.sort { 99 | build_opts.push(("sort", sort_opts)) 100 | } 101 | let per_page_opts = self.per_page.to_string(); 102 | let page_opts = self.page.to_string(); 103 | build_opts.push(("per_page", per_page_opts.as_str())); 104 | build_opts.push(("page", page_opts.as_str())); 105 | 106 | match Httpc::get(self.client, &url, Some(build_opts)) { 107 | Ok(result) => { 108 | let response = result.into_json::()?; 109 | Ok(response) 110 | } 111 | Err(e) => Err(e), 112 | } 113 | } 114 | 115 | pub fn filter(&self, filter_opts: String) -> Self { 116 | Self { 117 | filter: Some(filter_opts), 118 | ..self.clone() 119 | } 120 | } 121 | 122 | pub fn per_page(&self, per_page_count: i32) -> Self { 123 | Self { 124 | per_page: per_page_count, 125 | ..self.clone() 126 | } 127 | } 128 | 129 | pub fn page(&self, page_count: i32) -> Self { 130 | Self { 131 | page: page_count, 132 | ..self.clone() 133 | } 134 | } 135 | 136 | pub fn sort(&self, sort_opts: String) -> Self { 137 | Self { 138 | sort: Some(sort_opts), 139 | ..self.clone() 140 | } 141 | } 142 | } 143 | 144 | impl<'a> CollectionsManager<'a> { 145 | pub fn view(&self, name: &'a str) -> CollectionViewRequestBuilder { 146 | CollectionViewRequestBuilder { 147 | client: self.client, 148 | name, 149 | } 150 | } 151 | 152 | pub fn create(&self, name: &'a str) -> CollectionCreateRequestBuilder { 153 | CollectionCreateRequestBuilder { 154 | client: self.client, 155 | collection_details: None, 156 | collection_name: name, 157 | } 158 | } 159 | 160 | pub fn list(&self) -> CollectionListRequestBuilder { 161 | CollectionListRequestBuilder { 162 | client: self.client, 163 | filter: None, 164 | sort: None, 165 | per_page: 100, 166 | page: 1, 167 | } 168 | } 169 | } 170 | 171 | impl<'a> CollectionViewRequestBuilder<'a> { 172 | pub fn call(&self) -> Result { 173 | let url = format!("{}/api/collections/{}", self.client.base_url, self.name); 174 | match Httpc::get(self.client, &url, None) { 175 | Ok(result) => { 176 | let response = result.into_json::()?; 177 | Ok(response) 178 | } 179 | Err(e) => Err(e), 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Pocketbase SDK 2 | 3 | A Rust SDK for Pocketbase Clients. Pocketbase is an open source backend for your SaaS & Mobile Applications. The Goal of this project is to create a wrapper around the APIs that Pocketbase exposes to abstract away unnecessary details of implementation, so that you can focus on building your app and not worry about integration with pocketbase. 4 | 5 | #### Currently Compatible with Pocketbase Version 0.15.1 6 | 7 | #### NOTE 8 | Version 0.1.1 of pocketbase SDK is complete reimplementation and is not compatible with the previous versions. The sytax has modified to be more minimalistic. This has been done to make pocketbase-sdk more user-friendly & to facilitate continued maintenance of pocketbase-sdk. 9 | 10 | # Installation 11 | 12 | ```bash 13 | $ cargo add pocketbase-sdk 14 | $ cargo add serde 15 | ``` 16 | or add the following to your `Cargo.toml` 17 | 18 | ```toml 19 | [dependencies] 20 | pocketbase-sdk = "0.1.1" 21 | serde = { version = "1.0.145", features = ["derive"] } 22 | ``` 23 | 24 | # Usage 25 | 26 | ```rust 27 | use anyhow::Result; 28 | use pocketbase_sdk::admin::Admin; 29 | 30 | fn main() -> Result<()> { 31 | env_logger::init(); 32 | 33 | // admin authentication 34 | let authenticated_admin_client = Admin::new("http://localhost:8090") 35 | .auth_with_password("sreedev@icloud.com", "Sreedev123")?; 36 | 37 | // collections list + Filter 38 | let collections = authenticated_admin_client 39 | .collections() 40 | .list() 41 | .page(1) 42 | .per_page(100) 43 | .call()?; 44 | 45 | dbg!(collections); 46 | 47 | // view collection 48 | let user_collection = authenticated_admin_client 49 | .collections() 50 | .view("users") 51 | .call()?; 52 | 53 | dbg!(user_collection); 54 | 55 | Ok(()) 56 | } 57 | ``` 58 | 59 | ### Records 60 | ```rust 61 | use anyhow::Result; 62 | use pocketbase_sdk::client::Client; 63 | use serde::{Deserialize, Serialize}; 64 | 65 | #[derive(Debug, Clone, Deserialize, Default)] 66 | pub struct Product { 67 | pub id: String, 68 | pub name: String, 69 | pub count: i32, 70 | } 71 | 72 | #[derive(Debug, Clone, Serialize)] 73 | pub struct NewProduct { 74 | pub name: String, 75 | pub count: i32, 76 | } 77 | 78 | fn main() -> Result<()> { 79 | env_logger::init(); 80 | 81 | /* Authenticate Client */ 82 | let authenticated_client = Client::new("http://localhost:8090").auth_with_password( 83 | "users", 84 | "sreedev@icloud.com", 85 | "Sreedev123", 86 | )?; 87 | 88 | /* List Products */ 89 | let products = authenticated_client 90 | .records("products") 91 | .list() 92 | .call::()?; 93 | dbg!(products); 94 | 95 | /* View Product */ 96 | let product = authenticated_client 97 | .records("products") 98 | .view("jme4ixxqie2f9ho") 99 | .call::()?; 100 | dbg!(product); 101 | 102 | /* Create Product */ 103 | let new_product = NewProduct { 104 | name: String::from("bingo"), 105 | count: 69420, 106 | }; 107 | let create_response = authenticated_client 108 | .records("products") 109 | .create(new_product) 110 | .call()?; 111 | dbg!(&create_response); 112 | 113 | /* Update Product */ 114 | let updated_product = NewProduct { 115 | name: String::from("bango"), 116 | count: 69420, 117 | }; 118 | let update_response = authenticated_client 119 | .records("products") 120 | .update(create_response.id.as_str(), updated_product) 121 | .call()?; 122 | 123 | dbg!(update_response); 124 | 125 | /* Delete Product */ 126 | authenticated_client 127 | .records("products") 128 | .destroy(create_response.id.as_str()) 129 | .call()?; 130 | 131 | Ok(()) 132 | } 133 | ``` 134 | 135 | ### Logs 136 | 137 | ```rust 138 | use anyhow::Result; 139 | use pocketbase_sdk::admin::Admin; 140 | 141 | fn main() -> Result<()> { 142 | env_logger::init(); 143 | 144 | // admin authentication 145 | let admin = Admin::new("http://localhost:8090") 146 | .auth_with_password("sreedev@icloud.com", "Sreedev123")?; 147 | 148 | // list logs 149 | let logs = admin.logs().list().page(1).per_page(10).call()?; 150 | dbg!(&logs); 151 | 152 | // view log 153 | let somelogid = &logs.items[0].id; 154 | let logitem = admin.logs().view(somelogid).call()?; 155 | dbg!(logitem); 156 | 157 | // view log statistics data points 158 | let logstats = admin.logs().statistics().call()?; 159 | dbg!(logstats); 160 | 161 | Ok(()) 162 | } 163 | ``` 164 | 165 | ### HealthCheck 166 | 167 | ```rust 168 | use anyhow::Result; 169 | use pocketbase_sdk::client::Client; 170 | 171 | fn main() -> Result<()> { 172 | let client = Client::new("http://localhost:8090"); 173 | let health_check_response = client.health_check()?; 174 | dbg!(health_check_response); 175 | 176 | Ok(()) 177 | } 178 | ``` 179 | 180 | # Development TODOs 181 | * [ ] Improve Test Coverage 182 | * [ ] Collections 183 | * [x] List Collections 184 | * [x] View Collection 185 | * [ ] Create Collection 186 | * [ ] Auth Refresh 187 | * [ ] Request Password Reset 188 | * [ ] Confirm Password Reset 189 | * [ ] List Admins 190 | * [ ] View Admin 191 | * [ ] Create Admin 192 | * [ ] Update Admin 193 | * [ ] Delete Admin 194 | * [ ] Files 195 | * [ ] Download / Fetch File 196 | * [ ] Generate Protected File Token 197 | * [ ] Records 198 | * [x] Create Records 199 | * [x] Update Records 200 | * [x] Delete Records 201 | * [ ] Bulk Delete Records 202 | * [ ] List Auth Methods 203 | * [ ] Auth with OAuth2 204 | * [ ] Auth Refresh 205 | * [ ] Request Verification 206 | * [ ] Confirm Verification 207 | * [ ] Request Password Reset 208 | * [ ] Request Email Change 209 | * [ ] Confirm Email Change 210 | * [ ] List Linked External Auth Providers 211 | * [ ] Unlink External Auth Provider 212 | * [ ] Real Time APIs 213 | * [ ] WebAsm Support 214 | * [ ] Settings 215 | * [ ] List 216 | * [ ] Update 217 | * [x] Health Check 218 | -------------------------------------------------------------------------------- /tests/collections.rs: -------------------------------------------------------------------------------- 1 | use httpmock::prelude::*; 2 | use pocketbase_sdk::admin::Admin; 3 | use serde_json::json; 4 | 5 | #[test] 6 | fn collections_list_success() { 7 | let mockserver_url = mockserver().base_url(); 8 | let admin_client = Admin::new(mockserver_url.as_str()) 9 | .auth_with_password("sreedev@icloud.com", "Sreedev123") 10 | .unwrap(); 11 | 12 | let collections_list = admin_client.collections().list().call(); 13 | assert!(collections_list.is_ok()) 14 | } 15 | 16 | #[test] 17 | fn colletion_view_succes() { 18 | let mockserver_url = mockserver().base_url(); 19 | let admin_client = Admin::new(mockserver_url.as_str()) 20 | .auth_with_password("sreedev@icloud.com", "Sreedev123") 21 | .unwrap(); 22 | let collection = admin_client.collections().view("posts").call(); 23 | assert!(collection.is_ok()) 24 | } 25 | 26 | fn mockserver() -> MockServer { 27 | let server = MockServer::start(); 28 | server.mock(|when, then| { 29 | when.method(GET) 30 | .path("/api/collections/posts") 31 | .header("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4MTYwMH0.han3_sG65zLddpcX2ic78qgy7FKecuPfOpFa8Dvi5Bg"); 32 | then.status(200).header("content-type", "application/json") 33 | .json_body(json!({ 34 | "id": "d2972397d45614e", 35 | "created": "2022-06-22 07:13:00.643Z", 36 | "updated": "2022-06-22 07:13:00.643Z", 37 | "name": "posts", 38 | "type": "base", 39 | "schema": [ 40 | { 41 | "system": false, 42 | "id": "njnkhxa2", 43 | "name": "title", 44 | "type": "text", 45 | "required": false, 46 | "unique": false, 47 | "options": { 48 | "min": null, 49 | "max": null, 50 | "pattern": "" 51 | } 52 | }, 53 | { 54 | "system": false, 55 | "id": "9gvv0jkj", 56 | "name": "image", 57 | "type": "file", 58 | "required": false, 59 | "unique": false, 60 | "options": { 61 | "maxSelect": 1, 62 | "maxSize": 5242880, 63 | "mimeTypes": [ 64 | "image/jpg", 65 | "image/jpeg", 66 | "image/png", 67 | "image/svg+xml", 68 | "image/gif" 69 | ], 70 | "thumbs": null 71 | } 72 | } 73 | ], 74 | "listRule": "id = @request.user.id", 75 | "viewRule": "id = @request.user.id", 76 | "createRule": "id = @request.user.id", 77 | "updateRule": "id = @request.user.id", 78 | "deleteRule": null, 79 | "options": {}, 80 | "indexes": ["create index title_idx on posts (title)"] 81 | })); 82 | }); 83 | server.mock(|when, then| { 84 | when.method(GET) 85 | .path("/api/collections") 86 | .header("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4MTYwMH0.han3_sG65zLddpcX2ic78qgy7FKecuPfOpFa8Dvi5Bg"); 87 | 88 | then.status(200) 89 | .header("content-type", "application/json") 90 | .json_body(json!( 91 | { 92 | "page": 1, 93 | "perPage": 100, 94 | "totalItems": 3, 95 | "items": [ 96 | { 97 | "id": "d2972397d45614e", 98 | "created": "2022-06-22 07:13:00.643Z", 99 | "updated": "2022-06-22 07:13:00.643Z", 100 | "name": "users", 101 | "type": "base", 102 | "system": true, 103 | "schema": [ 104 | { 105 | "system": false, 106 | "id": "njnkhxa2", 107 | "name": "title", 108 | "type": "text", 109 | "required": false, 110 | "unique": false, 111 | "options": { 112 | "min": "", 113 | "max": "", 114 | "pattern": "" 115 | } 116 | }, 117 | { 118 | "system": false, 119 | "id": "9gvv0jkj", 120 | "name": "avatar", 121 | "type": "file", 122 | "required": false, 123 | "unique": false, 124 | "options": { 125 | "maxSelect": 1, 126 | "maxSize": 5242880, 127 | "mimeTypes": [ 128 | "image/jpg", 129 | "image/jpeg", 130 | "image/png", 131 | "image/svg+xml", 132 | "image/gif" 133 | ], 134 | "thumbs": null 135 | } 136 | } 137 | ], 138 | "listRule": "id = @request.user.id", 139 | "viewRule": "id = @request.user.id", 140 | "createRule": "id = @request.user.id", 141 | "updateRule": "id = @request.user.id", 142 | "deleteRule": null, 143 | "options": { 144 | "manageRule": null, 145 | "allowOAuth2Auth": true, 146 | "allowUsernameAuth": true, 147 | "allowEmailAuth": true, 148 | "requireEmail": true, 149 | "exceptEmailDomains": [], 150 | "onlyEmailDomains": [], 151 | "minPasswordLength": 8 152 | }, 153 | "indexes": ["create index title_idx on users (title)"] 154 | }, 155 | ] 156 | } 157 | )); 158 | }); 159 | server.mock(|when, then| { 160 | when 161 | .method(POST) 162 | .json_body(json!({ 163 | "identity": "sreedev@icloud.com", 164 | "password": "Sreedev123" 165 | })) 166 | .path("/api/admins/auth-with-password"); 167 | 168 | then 169 | .status(200) 170 | .header("content-type", "application/json") 171 | .json_body(json!({ 172 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4MTYwMH0.han3_sG65zLddpcX2ic78qgy7FKecuPfOpFa8Dvi5Bg", 173 | "admin": { 174 | "id": "b6e4b08274f34e9", 175 | "created": "2022-06-22 07:13:09.735Z", 176 | "updated": "2022-06-22 07:13:09.735Z", 177 | "email": "test@example.com", 178 | "avatar": 0 179 | } 180 | })); 181 | }); 182 | 183 | server 184 | } 185 | -------------------------------------------------------------------------------- /src/records.rs: -------------------------------------------------------------------------------- 1 | use crate::client::{Auth, Client}; 2 | use crate::httpc::Httpc; 3 | use anyhow::{anyhow, Result}; 4 | use serde::Serialize; 5 | use serde::{de::DeserializeOwned, Deserialize}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct RecordsManager<'a> { 9 | pub client: &'a Client, 10 | pub name: &'a str, 11 | } 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct RecordsListRequestBuilder<'a> { 15 | pub client: &'a Client, 16 | pub collection_name: &'a str, 17 | pub filter: Option, 18 | pub sort: Option, 19 | pub page: i32, 20 | pub per_page: i32, 21 | } 22 | 23 | #[derive(Debug, Clone, Deserialize)] 24 | #[serde(rename_all = "camelCase")] 25 | pub struct RecordList { 26 | pub page: i32, 27 | pub per_page: i32, 28 | pub total_items: i32, 29 | pub items: Vec, 30 | } 31 | 32 | impl<'a> RecordsListRequestBuilder<'a> { 33 | pub fn call(&self) -> Result> { 34 | let url = format!( 35 | "{}/api/collections/{}/records", 36 | self.client.base_url, self.collection_name 37 | ); 38 | 39 | let mut build_opts: Vec<(&str, &str)> = vec![]; 40 | if let Some(filter_opts) = &self.filter { 41 | build_opts.push(("filter", filter_opts)) 42 | } 43 | if let Some(sort_opts) = &self.sort { 44 | build_opts.push(("sort", sort_opts)) 45 | } 46 | let per_page_opts = self.per_page.to_string(); 47 | let page_opts = self.page.to_string(); 48 | build_opts.push(("per_page", per_page_opts.as_str())); 49 | build_opts.push(("page", page_opts.as_str())); 50 | 51 | match Httpc::get(self.client, &url, Some(build_opts)) { 52 | Ok(result) => { 53 | let response = result.into_json::>()?; 54 | Ok(response) 55 | } 56 | Err(e) => Err(e), 57 | } 58 | } 59 | 60 | pub fn filter(&self, filter_opts: &str) -> Self { 61 | Self { 62 | filter: Some(filter_opts.to_string()), 63 | ..self.clone() 64 | } 65 | } 66 | 67 | pub fn sort(&self, sort_opts: &str) -> Self { 68 | Self { 69 | sort: Some(sort_opts.to_string()), 70 | ..self.clone() 71 | } 72 | } 73 | 74 | pub fn page(&self, page: i32) -> Self { 75 | Self { 76 | page, 77 | ..self.clone() 78 | } 79 | } 80 | 81 | pub fn per_page(&self, per_page: i32) -> Self { 82 | Self { 83 | per_page, 84 | ..self.clone() 85 | } 86 | } 87 | } 88 | 89 | pub struct RecordViewRequestBuilder<'a> { 90 | pub client: &'a Client, 91 | pub collection_name: &'a str, 92 | pub identifier: &'a str, 93 | } 94 | 95 | impl<'a> RecordViewRequestBuilder<'a> { 96 | pub fn call(&self) -> Result { 97 | let url = format!( 98 | "{}/api/collections/{}/records/{}", 99 | self.client.base_url, self.collection_name, self.identifier 100 | ); 101 | match Httpc::get(self.client, &url, None) { 102 | Ok(result) => { 103 | let response = result.into_json::()?; 104 | Ok(response) 105 | } 106 | Err(e) => Err(anyhow!("error: {}", e)), 107 | } 108 | } 109 | } 110 | 111 | impl<'a> RecordDestroyRequestBuilder<'a> { 112 | pub fn call(&self) -> Result<()> { 113 | let url = format!( 114 | "{}/api/collections/{}/records/{}", 115 | self.client.base_url, self.collection_name, self.identifier 116 | ); 117 | match Httpc::delete(self.client, url.as_str()) { 118 | Ok(result) => { 119 | if result.status() == 204 { 120 | Ok(()) 121 | } else { 122 | Err(anyhow!("Failed to delete")) 123 | } 124 | } 125 | Err(e) => Err(anyhow!("error: {}", e)), 126 | } 127 | } 128 | } 129 | 130 | #[derive(Clone, Debug)] 131 | pub struct RecordDestroyRequestBuilder<'a> { 132 | pub identifier: &'a str, 133 | pub client: &'a Client, 134 | pub collection_name: &'a str, 135 | } 136 | 137 | #[derive(Debug, Clone)] 138 | pub struct RecordDeleteAllRequestBuilder<'a> { 139 | pub client: &'a Client, 140 | pub collection_name: &'a str, 141 | pub filter: Option<&'a str>, 142 | } 143 | 144 | #[derive(Debug, Clone)] 145 | pub struct RecordCreateRequestBuilder<'a, T: Serialize + Clone> { 146 | pub client: &'a Client, 147 | pub collection_name: &'a str, 148 | pub record: T, 149 | } 150 | 151 | #[derive(Deserialize, Clone, Debug)] 152 | pub struct CreateResponse { 153 | #[serde(rename = "@collectionName")] 154 | pub collection_name: Option, 155 | #[serde(rename = "@collectionId")] 156 | pub collection_id: Option, 157 | pub id: String, 158 | pub updated: String, 159 | pub created: String, 160 | } 161 | 162 | impl<'a, T: Serialize + Clone> RecordCreateRequestBuilder<'a, T> { 163 | pub fn call(&self) -> Result { 164 | let url = format!( 165 | "{}/api/collections/{}/records", 166 | self.client.base_url, self.collection_name 167 | ); 168 | let payload = serde_json::to_string(&self.record).map_err(anyhow::Error::from)?; 169 | match Httpc::post(self.client, &url, payload) { 170 | Ok(result) => { 171 | let response = result.into_json::()?; 172 | Ok(response) 173 | } 174 | Err(e) => Err(anyhow!("error: {}", e)), 175 | } 176 | } 177 | } 178 | 179 | pub struct RecordUpdateRequestBuilder<'a, T: Serialize + Clone> { 180 | pub record: T, 181 | pub collection_name: &'a str, 182 | pub client: &'a Client, 183 | pub id: &'a str, 184 | } 185 | 186 | impl<'a, T: Serialize + Clone> RecordUpdateRequestBuilder<'a, T> { 187 | pub fn call(&self) -> Result { 188 | let url = format!( 189 | "{}/api/collections/{}/records/{}", 190 | self.client.base_url, self.collection_name, self.id 191 | ); 192 | let payload = serde_json::to_string(&self.record).map_err(anyhow::Error::from)?; 193 | match Httpc::patch(self.client, &url, payload) { 194 | Ok(result) => { 195 | result.into_json::()?; 196 | Ok(self.record.clone()) 197 | } 198 | Err(e) => Err(anyhow!("error: {}", e)), 199 | } 200 | } 201 | } 202 | 203 | impl<'a> RecordsManager<'a> { 204 | pub fn view(&self, identifier: &'a str) -> RecordViewRequestBuilder<'a> { 205 | RecordViewRequestBuilder { 206 | identifier, 207 | client: self.client, 208 | collection_name: self.name, 209 | } 210 | } 211 | 212 | pub fn destroy(&self, identifier: &'a str) -> RecordDestroyRequestBuilder<'a> { 213 | RecordDestroyRequestBuilder { 214 | identifier, 215 | client: self.client, 216 | collection_name: self.name, 217 | } 218 | } 219 | 220 | pub fn update( 221 | &self, 222 | identifier: &'a str, 223 | record: T, 224 | ) -> RecordUpdateRequestBuilder<'a, T> { 225 | RecordUpdateRequestBuilder { 226 | client: self.client, 227 | collection_name: self.name, 228 | id: identifier, 229 | record, 230 | } 231 | } 232 | 233 | pub fn create(&self, record: T) -> RecordCreateRequestBuilder<'a, T> { 234 | RecordCreateRequestBuilder { 235 | record, 236 | client: self.client, 237 | collection_name: self.name, 238 | } 239 | } 240 | 241 | pub fn list(&self) -> RecordsListRequestBuilder<'a> { 242 | RecordsListRequestBuilder { 243 | client: self.client, 244 | collection_name: self.name, 245 | filter: None, 246 | sort: None, 247 | page: 1, 248 | per_page: 100, 249 | } 250 | } 251 | } 252 | --------------------------------------------------------------------------------