add: apps rust sdk
This commit is contained in:
parent
f05074ffc5
commit
fe1e53c47a
11 changed files with 461 additions and 11 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2669,6 +2669,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
@ -3317,6 +3318,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tetratto-l10n",
|
"tetratto-l10n",
|
||||||
"tetratto-shared",
|
"tetratto-shared",
|
||||||
|
"tokio",
|
||||||
"toml 0.9.2",
|
"toml 0.9.2",
|
||||||
"totp-rs",
|
"totp-rs",
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,6 +4,7 @@ members = ["crates/app", "crates/shared", "crates/core", "crates/l10n"]
|
||||||
package.authors = ["trisuaso"]
|
package.authors = ["trisuaso"]
|
||||||
package.repository = "https://trisua.com/t/tetratto"
|
package.repository = "https://trisua.com/t/tetratto"
|
||||||
package.license = "AGPL-3.0-or-later"
|
package.license = "AGPL-3.0-or-later"
|
||||||
|
package.homepage = "https://tetratto.com"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
incremental = true
|
incremental = true
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "11.0.0"
|
version = "11.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pathbufd = "0.1.4"
|
pathbufd = "0.1.4"
|
||||||
|
|
|
@ -193,7 +193,13 @@ macro_rules! user_banned {
|
||||||
macro_rules! check_user_blocked_or_private {
|
macro_rules! check_user_blocked_or_private {
|
||||||
($user:expr, $other_user:ident, $data:ident, $jar:ident) => {
|
($user:expr, $other_user:ident, $data:ident, $jar:ident) => {
|
||||||
// check is_deactivated
|
// check is_deactivated
|
||||||
if $other_user.is_deactivated {
|
if ($user.is_none() && $other_user.is_deactivated)
|
||||||
|
| !$user
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.permissions
|
||||||
|
.check(tetratto_core::model::permissions::FinePermission::MANAGE_USERS)
|
||||||
|
{
|
||||||
return Err(Html(
|
return Err(Html(
|
||||||
render_error(
|
render_error(
|
||||||
Error::GeneralNotFound("user".to_string()),
|
Error::GeneralNotFound("user".to_string()),
|
||||||
|
|
|
@ -117,6 +117,29 @@ export default function tetratto({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function update(id, value) {
|
||||||
|
if (!api_key) {
|
||||||
|
throw Error("No API key provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return api_promise(
|
||||||
|
json_parse(
|
||||||
|
await (
|
||||||
|
await fetch(`${host}/api/v1/app_data/${id}/value`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Atto-Secret-Key": api_key,
|
||||||
|
},
|
||||||
|
body: json_stringify({
|
||||||
|
value,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
).text(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function remove(id) {
|
async function remove(id) {
|
||||||
if (!api_key) {
|
if (!api_key) {
|
||||||
throw Error("No API key provided.");
|
throw Error("No API key provided.");
|
||||||
|
@ -241,6 +264,7 @@ export default function tetratto({
|
||||||
app,
|
app,
|
||||||
query,
|
query,
|
||||||
insert,
|
insert,
|
||||||
|
update,
|
||||||
remove,
|
remove,
|
||||||
remove_query,
|
remove_query,
|
||||||
// user connection
|
// user connection
|
||||||
|
|
|
@ -2,6 +2,16 @@
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "11.0.0"
|
version = "11.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
database = ["dep:oiseau", "dep:base64", "dep:base16ct", "dep:async-recursion", "dep:md-5"]
|
||||||
|
types = ["dep:totp-rs", "dep:paste", "dep:bitflags"]
|
||||||
|
sdk = ["types", "dep:reqwest"]
|
||||||
|
default = ["database", "types", "sdk"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pathbufd = "0.1.4"
|
pathbufd = "0.1.4"
|
||||||
|
@ -10,17 +20,20 @@ toml = "0.9.2"
|
||||||
tetratto-shared = { path = "../shared" }
|
tetratto-shared = { path = "../shared" }
|
||||||
tetratto-l10n = { path = "../l10n" }
|
tetratto-l10n = { path = "../l10n" }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] }
|
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"], optional = true }
|
||||||
reqwest = { version = "0.12.22", features = ["json"] }
|
reqwest = { version = "0.12.22", features = ["json", "multipart"], optional = true }
|
||||||
bitflags = "2.9.1"
|
bitflags = { version = "2.9.1", optional = true }
|
||||||
async-recursion = "1.1.1"
|
async-recursion = { version = "1.1.1", optional = true }
|
||||||
md-5 = "0.10.6"
|
md-5 = { version = "0.10.6", optional = true }
|
||||||
base16ct = { version = "0.2.0", features = ["alloc"] }
|
base16ct = { version = "0.2.0", features = ["alloc"], optional = true }
|
||||||
base64 = "0.22.1"
|
base64 = { version = "0.22.1", optional = true }
|
||||||
emojis = "0.7.0"
|
emojis = "0.7.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
oiseau = { version = "0.1.2", default-features = false, features = [
|
oiseau = { version = "0.1.2", default-features = false, features = [
|
||||||
"postgres",
|
"postgres",
|
||||||
"redis",
|
"redis",
|
||||||
] }
|
], optional = true }
|
||||||
paste = "1.0.15"
|
paste = { version = "1.0.15", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
|
65
crates/core/examples/sdk_db.rs
Normal file
65
crates/core/examples/sdk_db.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
extern crate tetratto_core;
|
||||||
|
use tetratto_core::{
|
||||||
|
model::apps::{AppDataSelectMode, AppDataSelectQuery, AppDataQueryResult},
|
||||||
|
sdk::{DataClient, SimplifiedQuery},
|
||||||
|
};
|
||||||
|
use std::env::var;
|
||||||
|
|
||||||
|
// mirror of https://trisua.com/t/tetratto/src/branch/master/example/app_sdk_test.js ... but in rust
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
let client = DataClient::new(
|
||||||
|
Some("http://localhost:4118".to_string()),
|
||||||
|
var("APP_API_KEY").unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("data used: {}", client.get_app().await.unwrap().data_used);
|
||||||
|
|
||||||
|
// record insert
|
||||||
|
client
|
||||||
|
.insert("rust_test".to_string(), "Hello, world!".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!("record created");
|
||||||
|
println!("data used: {}", client.get_app().await.unwrap().data_used);
|
||||||
|
|
||||||
|
// testing record query then delete
|
||||||
|
let record = match client
|
||||||
|
.query(&SimplifiedQuery {
|
||||||
|
query: AppDataSelectQuery::KeyIs("rust_test".to_string()),
|
||||||
|
mode: AppDataSelectMode::One(0),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
AppDataQueryResult::One(x) => x,
|
||||||
|
AppDataQueryResult::Many(_) => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{:?}", record);
|
||||||
|
|
||||||
|
client
|
||||||
|
.update(record.id, "Hello, world! 1".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!("record updated");
|
||||||
|
println!("data used: {}", client.get_app().await.unwrap().data_used);
|
||||||
|
|
||||||
|
let record = match client
|
||||||
|
.query(&SimplifiedQuery {
|
||||||
|
query: AppDataSelectQuery::KeyIs("rust_test".to_string()),
|
||||||
|
mode: AppDataSelectMode::One(0),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
AppDataQueryResult::One(x) => x,
|
||||||
|
AppDataQueryResult::Many(_) => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{:?}", record);
|
||||||
|
|
||||||
|
client.remove(record.id).await.unwrap();
|
||||||
|
println!("record deleted");
|
||||||
|
println!("data used: {}", client.get_app().await.unwrap().data_used);
|
||||||
|
}
|
|
@ -1,6 +1,13 @@
|
||||||
|
#[cfg(feature = "types")]
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
#[cfg(feature = "database")]
|
||||||
pub mod database;
|
pub mod database;
|
||||||
|
#[cfg(feature = "types")]
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
#[cfg(feature = "sdk")]
|
||||||
|
pub mod sdk;
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
pub use database::DataManager;
|
pub use database::DataManager;
|
||||||
|
#[cfg(feature = "database")]
|
||||||
pub use oiseau::cache;
|
pub use oiseau::cache;
|
||||||
|
|
313
crates/core/src/sdk.rs
Normal file
313
crates/core/src/sdk.rs
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
use crate::model::{
|
||||||
|
apps::{
|
||||||
|
AppDataQuery, AppDataQueryResult, AppDataSelectMode, AppDataSelectQuery, ThirdPartyApp,
|
||||||
|
},
|
||||||
|
ApiReturn, Error, Result,
|
||||||
|
};
|
||||||
|
use reqwest::{
|
||||||
|
multipart::{Form, Part},
|
||||||
|
Client as HttpClient,
|
||||||
|
};
|
||||||
|
pub use reqwest::Method;
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
|
||||||
|
macro_rules! api_return_ok {
|
||||||
|
($ret:ty, $res:ident) => {
|
||||||
|
match $res.json::<ApiReturn<$ret>>().await {
|
||||||
|
Ok(x) => {
|
||||||
|
if x.ok {
|
||||||
|
Ok(x.payload)
|
||||||
|
} else {
|
||||||
|
Err(Error::MiscError(x.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simplified app data query which matches what the API endpoint actually requires.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SimplifiedQuery {
|
||||||
|
pub query: AppDataSelectQuery,
|
||||||
|
pub mode: AppDataSelectMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The data client is used to access an app's data storage capabilities.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DataClient {
|
||||||
|
/// The HTTP client associated with this client.
|
||||||
|
pub http: HttpClient,
|
||||||
|
/// The app's API key. You can retrieve this from the web dashboard.
|
||||||
|
pub api_key: String,
|
||||||
|
/// The origin of the Tetratto server. When creating with [`DataClient::new`],
|
||||||
|
/// you can provide `None` to use `https://tetratto.com`.
|
||||||
|
pub host: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataClient {
|
||||||
|
/// Create a new [`DataClient`].
|
||||||
|
pub fn new(host: Option<String>, api_key: String) -> Self {
|
||||||
|
Self {
|
||||||
|
http: HttpClient::new(),
|
||||||
|
api_key,
|
||||||
|
host: host.unwrap_or("https://tetratto.com".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current app using the provided API key.
|
||||||
|
///
|
||||||
|
/// # Usage
|
||||||
|
/// ```rust
|
||||||
|
/// let client = DataClient::new("https://tetratto.com".to_string(), "...".to_string());
|
||||||
|
/// let app = client.get_app().await.expect("failed to get app");
|
||||||
|
/// ```
|
||||||
|
pub async fn get_app(&self) -> Result<ThirdPartyApp> {
|
||||||
|
match self
|
||||||
|
.http
|
||||||
|
.get(format!("{}/api/v1/app_data/app", self.host))
|
||||||
|
.header("Atto-Secret-Key", &self.api_key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => api_return_ok!(ThirdPartyApp, x),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query the app's data.
|
||||||
|
pub async fn query(&self, query: &SimplifiedQuery) -> Result<AppDataQueryResult> {
|
||||||
|
match self
|
||||||
|
.http
|
||||||
|
.post(format!("{}/api/v1/app_data/query", self.host))
|
||||||
|
.header("Atto-Secret-Key", &self.api_key)
|
||||||
|
.json(&query)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => api_return_ok!(AppDataQueryResult, x),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a key, value pair into the app's data.
|
||||||
|
pub async fn insert(&self, key: String, value: String) -> Result<String> {
|
||||||
|
match self
|
||||||
|
.http
|
||||||
|
.post(format!("{}/api/v1/app_data", self.host))
|
||||||
|
.header("Atto-Secret-Key", &self.api_key)
|
||||||
|
.json(&serde_json::Value::Object({
|
||||||
|
let mut map = serde_json::Map::new();
|
||||||
|
map.insert("key".to_string(), serde_json::Value::String(key));
|
||||||
|
map.insert("value".to_string(), serde_json::Value::String(value));
|
||||||
|
map
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => api_return_ok!(String, x),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update a record's value given its ID and the new value.
|
||||||
|
pub async fn update(&self, id: usize, value: String) -> Result<()> {
|
||||||
|
match self
|
||||||
|
.http
|
||||||
|
.post(format!("{}/api/v1/app_data/{id}/value", self.host))
|
||||||
|
.header("Atto-Secret-Key", &self.api_key)
|
||||||
|
.json(&serde_json::Value::Object({
|
||||||
|
let mut map = serde_json::Map::new();
|
||||||
|
map.insert("value".to_string(), serde_json::Value::String(value));
|
||||||
|
map
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => api_return_ok!((), x),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a row from the app's data by its `id`.
|
||||||
|
pub async fn remove(&self, id: usize) -> Result<()> {
|
||||||
|
match self
|
||||||
|
.http
|
||||||
|
.delete(format!("{}/api/v1/app_data/{id}", self.host))
|
||||||
|
.header("Atto-Secret-Key", &self.api_key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => api_return_ok!((), x),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete row(s) from the app's data by a query.
|
||||||
|
pub async fn remove_query(&self, query: &AppDataQuery) -> Result<()> {
|
||||||
|
match self
|
||||||
|
.http
|
||||||
|
.delete(format!("{}/api/v1/app_data/query", self.host))
|
||||||
|
.header("Atto-Secret-Key", &self.api_key)
|
||||||
|
.json(&query)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => api_return_ok!((), x),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The state of the [`ApiClient`].
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ApiClientState {
|
||||||
|
/// The token you received from an app grant request.
|
||||||
|
pub user_token: String,
|
||||||
|
/// The verifier you received from an app grant request.
|
||||||
|
pub user_verifier: String,
|
||||||
|
/// The ID of the user this client is connecting to.
|
||||||
|
pub user_id: usize,
|
||||||
|
/// The ID of the app that is being used for user grants.
|
||||||
|
///
|
||||||
|
/// You can get this from the web dashboard.
|
||||||
|
pub app_id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The API client is used to manage authentication flow and send requests on behalf of a user.
|
||||||
|
///
|
||||||
|
/// This client assumes you already have the required information for the given user.
|
||||||
|
/// If you don't, try using the JS SDK to extract this information.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ApiClient {
|
||||||
|
/// The HTTP client associated with this client.
|
||||||
|
pub http: HttpClient,
|
||||||
|
/// The general state of the client. Will be updated whenever you refresh the user's token.
|
||||||
|
pub state: ApiClientState,
|
||||||
|
/// The origin of the Tetratto server. When creating with [`ApiClient::new`],
|
||||||
|
/// you can provide `None` to use `https://tetratto.com`.
|
||||||
|
pub host: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiClient {
|
||||||
|
/// Create a new [`ApiClient`].
|
||||||
|
pub fn new(host: Option<String>, state: ApiClientState) -> Self {
|
||||||
|
Self {
|
||||||
|
http: HttpClient::new(),
|
||||||
|
state,
|
||||||
|
host: host.unwrap_or("https://tetratto.com".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh the client's user_token.
|
||||||
|
pub async fn refresh_token(&mut self) -> Result<String> {
|
||||||
|
match self
|
||||||
|
.http
|
||||||
|
.post(format!(
|
||||||
|
"{}/api/v1/auth/user/{}/grants/{}/refresh",
|
||||||
|
self.host, self.state.user_id, self.state.app_id
|
||||||
|
))
|
||||||
|
.header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token))
|
||||||
|
.json(&serde_json::Value::Object({
|
||||||
|
let mut map = serde_json::Map::new();
|
||||||
|
map.insert(
|
||||||
|
"verifier".to_string(),
|
||||||
|
serde_json::Value::String(self.state.user_verifier.to_owned()),
|
||||||
|
);
|
||||||
|
map
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => {
|
||||||
|
let ret = api_return_ok!(String, x)?;
|
||||||
|
self.state.user_token = ret.clone();
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a simple JSON request to the given endpoint.
|
||||||
|
pub async fn request<T, B>(
|
||||||
|
&self,
|
||||||
|
route: String,
|
||||||
|
method: Method,
|
||||||
|
body: Option<&B>,
|
||||||
|
) -> Result<ApiReturn<T>>
|
||||||
|
where
|
||||||
|
T: Serialize + DeserializeOwned,
|
||||||
|
B: Serialize + ?Sized,
|
||||||
|
{
|
||||||
|
if let Some(body) = body {
|
||||||
|
match self
|
||||||
|
.http
|
||||||
|
.request(method, format!("{}/api/v1/auth/{route}", self.host))
|
||||||
|
.header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token))
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => api_return_ok!(ApiReturn<T>, x),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match self
|
||||||
|
.http
|
||||||
|
.request(method, format!("{}/api/v1/auth/{route}", self.host))
|
||||||
|
.header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => api_return_ok!(ApiReturn<T>, x),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a JSON request with attachments to the given endpoint.
|
||||||
|
///
|
||||||
|
/// This type of request is only required for routes which use JsonMultipart,
|
||||||
|
/// such as `POST /api/v1/posts` (`create_post`).
|
||||||
|
///
|
||||||
|
/// Method is locked to `POST` for this type of request.
|
||||||
|
pub async fn request_attachments<T, B>(
|
||||||
|
&self,
|
||||||
|
route: String,
|
||||||
|
attachments: Vec<Vec<u8>>,
|
||||||
|
body: &B,
|
||||||
|
) -> Result<ApiReturn<T>>
|
||||||
|
where
|
||||||
|
T: Serialize + DeserializeOwned,
|
||||||
|
B: Serialize + ?Sized,
|
||||||
|
{
|
||||||
|
let mut multipart_body = Form::new();
|
||||||
|
|
||||||
|
// add attachments
|
||||||
|
for v in attachments.clone() {
|
||||||
|
// the file name doesn't matter
|
||||||
|
multipart_body = multipart_body.part(String::new(), Part::bytes(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(attachments);
|
||||||
|
|
||||||
|
// add json
|
||||||
|
multipart_body = multipart_body.part(
|
||||||
|
String::new(),
|
||||||
|
Part::text(serde_json::to_string(body).unwrap()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ...
|
||||||
|
match self
|
||||||
|
.http
|
||||||
|
.post(format!("{}/api/v1/auth/{route}", self.host))
|
||||||
|
.header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token))
|
||||||
|
.multipart(multipart_body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => api_return_ok!(ApiReturn<T>, x),
|
||||||
|
Err(e) => Err(Error::MiscError(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pathbufd = "0.1.4"
|
pathbufd = "0.1.4"
|
||||||
|
|
|
@ -15,7 +15,7 @@ console.log("record created");
|
||||||
console.log("data used:", (await sdk.app()).data_used);
|
console.log("data used:", (await sdk.app()).data_used);
|
||||||
|
|
||||||
// testing record query then delete
|
// testing record query then delete
|
||||||
const record = (
|
let record = (
|
||||||
await sdk.query({
|
await sdk.query({
|
||||||
query: { KeyIs: "deno_test" },
|
query: { KeyIs: "deno_test" },
|
||||||
mode: { One: 0 },
|
mode: { One: 0 },
|
||||||
|
@ -23,6 +23,20 @@ const record = (
|
||||||
).One;
|
).One;
|
||||||
|
|
||||||
console.log(record);
|
console.log(record);
|
||||||
|
|
||||||
|
await sdk.update("deno_test", "Hello, Deno! 1");
|
||||||
|
console.log("record updated");
|
||||||
|
console.log("data used:", (await sdk.app()).data_used);
|
||||||
|
|
||||||
|
record = (
|
||||||
|
await sdk.query({
|
||||||
|
query: { KeyIs: "deno_test" },
|
||||||
|
mode: { One: 0 },
|
||||||
|
})
|
||||||
|
).One;
|
||||||
|
|
||||||
|
console.log(record);
|
||||||
|
|
||||||
await sdk.remove(record.id);
|
await sdk.remove(record.id);
|
||||||
console.log("record deleted");
|
console.log("record deleted");
|
||||||
console.log("data used:", (await sdk.app()).data_used);
|
console.log("data used:", (await sdk.app()).data_used);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue