This commit is contained in:
Djkáťo 2024-02-21 22:37:06 +01:00
commit c37f3df8de
14 changed files with 2295 additions and 0 deletions

9
.env Normal file
View file

@ -0,0 +1,9 @@
APP_AUTHOR="Djkáťo"
APP_DESCRIPTION="A simple rust app for doing many things"
APP_NAME="Rust test app"
REQUIRED_SALEOR_VERSION="^3.13"
SALEOR_APP_ID="dummy-saleor-app-rs"
APP_API_BASE_URL="http://10.100.110.234:3000"
APL="Redis"
APL_URL="redis://localhost:6380/2"
LOG_LEVEL="DEBUG"

7
.env.example Normal file
View file

@ -0,0 +1,7 @@
APP_NAME="Rust test app"
REQUIRED_SALEOR_VERSION=">=3.11.7 <4"
SALEOR_APP_ID="dummy-saleor-app-rs"
APP_API_BASE_URL="http://localhost:8000/graphql/"
APL="redis"
APL_URL="redis://redis:6379/2"
LOG_LEVEL="DEBUG"

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1453
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "saleor-app-rs"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.79"
#color-eyre = "0.6.2"
cynic = "3.4.3"
#dotenvy_macro = "0.15.7"
#poem = {version = "2.0.0", features = ["redis-session"]}
serde = "1.0.196"
serde_json = "1.0.113"
tokio = {version = "1.36.0", features = ["full"]}
redis = { version = "0.23.0", features = ["aio", "tokio-comp", "connection-manager"] }
envy = "0.4.2"
tracing = "0.1.40"
tracing-serde = "0.1.3"
tracing-subscriber = { version = "0.3.18" }
dotenvy = "0.15.7"
axum = "0.7.4"

79
bacon.toml Normal file
View file

@ -0,0 +1,79 @@
# This is a configuration file for the bacon tool
#
# Bacon repository: https://github.com/Canop/bacon
# Complete help on configuration: https://dystroy.org/bacon/config/
# You can also check bacon's own bacon.toml file
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
default_job = "check"
[jobs.check]
command = ["cargo", "check", "--color", "always"]
need_stdout = false
[jobs.check-all]
command = ["cargo", "check", "--all-targets", "--color", "always"]
need_stdout = false
[jobs.clippy]
command = [
"cargo", "clippy",
"--all-targets",
"--color", "always",
]
need_stdout = false
# This job lets you run
# - all tests: bacon test
# - a specific test: bacon test -- config::test_default_files
# - the tests of a package: bacon test -- -- -p config
[jobs.test]
command = [
"cargo", "test", "--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.doc]
command = ["cargo", "doc", "--color", "always", "--no-deps"]
need_stdout = false
# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change
# You can run your application and have the result displayed in bacon,
# *if* it makes sense for this crate.
# Don't forget the `--color always` part or the errors won't be
# properly parsed.
# If your program never stops (eg a server), you may set `background`
# to false to have the cargo run output immediately displayed instead
# of waiting for program's end.
[jobs.run]
command = [
"cargo", "run",
"--color", "always",
# put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
background = true
# This parameterized job runs the example of your choice, as soon
# as the code compiles.
# Call it as
# bacon ex -- my-example
[jobs.ex]
command = ["cargo", "run", "--color", "always", "--example"]
need_stdout = true
allow_warnings = true
# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"

50
src/app.rs Normal file
View file

@ -0,0 +1,50 @@
use std::sync::Arc;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use crate::{
config::Config,
saleor::{AppManifest, SaleorApp, APL},
};
// Make our own error that wraps `anyhow::Error`.
pub struct AppError(anyhow::Error);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
pub fn trace_to_std(config: &Config) {
tracing_subscriber::fmt()
.with_max_level(config.log_level)
.with_target(false)
.init();
}
#[derive(Debug, Clone)]
pub struct AppState<A: APL> {
pub saleor_app: Arc<tokio::sync::Mutex<SaleorApp<A>>>,
pub config: Config,
pub manifest: AppManifest,
}

51
src/config.rs Normal file
View file

@ -0,0 +1,51 @@
use serde::Deserialize;
use crate::saleor::AplType;
use tracing::{debug, Level};
#[derive(Debug, Deserialize)]
#[serde(remote = "Level")]
pub enum LocalTracingLevel {
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
}
fn version_default() -> String {
">=3.11.7<4".to_owned()
}
#[derive(Deserialize, Debug, Clone)]
//#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub struct Config {
pub app_name: String,
#[serde(default = "version_default")]
pub required_saleor_version: String,
pub saleor_app_id: String,
pub app_api_base_url: String,
pub apl: AplType,
pub apl_url: String,
#[serde(with = "LocalTracingLevel")]
pub log_level: tracing::Level,
pub app_description: Option<String>,
pub app_author: Option<String>,
}
impl std::fmt::Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl Config {
pub fn load() -> Result<Self, envy::Error> {
dotenvy::dotenv().unwrap();
let env = envy::from_env::<Config>();
if let Ok(e) = &env {
debug!("{}", e);
}
env
}
}

75
src/main.rs Normal file
View file

@ -0,0 +1,75 @@
mod app;
mod config;
mod routes;
mod saleor;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::{
app::{trace_to_std, AppState},
config::Config,
routes::create_routes,
saleor::{
AppManifest, AppPermission, AsyncWebhookEventType, RedisApl, SaleorApp, WebhookManifest,
},
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = Config::load()?;
trace_to_std(&config);
let saleor_app = SaleorApp::<RedisApl> {
apl: RedisApl::new(config.apl_url.clone(), config.app_api_base_url.clone())?,
};
let app_manifest = AppManifest {
id: config.saleor_app_id.clone(),
required_saleor_version: Some(config.required_saleor_version.clone()),
name: config.app_name.clone(),
about: config.app_description.clone(),
brand: None,
author: config.app_author.clone(),
version: env!("CARGO_PKG_VERSION").to_owned(),
app_url: config.app_api_base_url.clone(),
token_target_url: format!("{}/api/register", config.app_api_base_url.clone()),
extensions: None,
permissions: vec![AppPermission::ManageProducts],
support_url: None,
data_privacy: None,
homepage_url: None,
data_privacy_url: None,
configuration_url: None,
webhooks: Some(vec![WebhookManifest {
name: "GetProducts for demo rust app".to_owned(),
query: r#"
subscription {
event {
... on ProductUpdated {
product {
id
name
}
}
}
}
"#
.to_owned(),
is_active: Some(true),
target_url: format!("{}/api/webhooks", config.app_api_base_url),
sync_events: None,
async_events: Some(vec![AsyncWebhookEventType::ProductCreated]),
}]),
};
let app_state = AppState {
manifest: app_manifest,
config,
saleor_app: Arc::new(Mutex::new(saleor_app)),
};
let app = create_routes(app_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app);
Ok(())
}

12
src/routes/manifest.rs Normal file
View file

@ -0,0 +1,12 @@
use axum::{extract::State, Json};
use crate::{
app::{AppError, AppState},
saleor::{AppManifest, APL},
};
pub async fn manifest<A: APL>(
State(state): State<AppState<A>>,
) -> Result<Json<AppManifest>, AppError> {
Ok(Json(state.manifest))
}

17
src/routes/mod.rs Normal file
View file

@ -0,0 +1,17 @@
mod manifest;
mod register;
use axum::{
routing::{get, post},
Router,
};
use manifest::manifest;
use register::register;
use crate::{app::AppState, saleor::APL};
pub fn create_routes<T: APL>(state: AppState<T>) -> Router<AppState<T>> {
Router::new()
.route("/api/manifest", get(manifest))
.route("/api/register", post(register))
.with_state(state)
}

43
src/routes/register.rs Normal file
View file

@ -0,0 +1,43 @@
use anyhow::Context;
use axum::{
extract::State,
http::{HeaderMap, StatusCode},
response::Json,
};
use tracing::{debug, info};
use crate::{
app::AppError,
saleor::{AuthData, AuthToken, APL},
AppState,
};
pub async fn register<A: APL>(
headers: HeaderMap,
Json(auth_token): Json<AuthToken>,
State(state): State<AppState<A>>,
) -> Result<StatusCode, AppError> {
debug!(
"/api/register:\nsaleor_api_url:{:?}\nauth_token:{:?}",
headers.get("saleor-api-url"),
auth_token
);
if auth_token.auth_token.is_empty() {
return Err(anyhow::anyhow!("missing auth_token").into());
}
let app = state.saleor_app.lock().await;
let saleor_api_url = headers.get("saleor-api-url").context("missing api field")?;
let saleor_api_url = saleor_api_url.to_str()?.to_owned();
let auth_data = AuthData {
jwks: None,
token: auth_token.auth_token,
domain: Some(state.config.app_api_base_url),
app_id: state.config.saleor_app_id,
saleor_api_url: saleor_api_url.clone(),
};
app.apl.set(auth_data).await?;
info!("registered app for{:?}", &saleor_api_url);
Ok(StatusCode::OK)
}

0
src/routes/webhooks.rs Normal file
View file

477
src/saleor.rs Normal file
View file

@ -0,0 +1,477 @@
use std::time::Duration;
use anyhow::{bail, Result};
use redis::{AsyncCommands, Commands, ConnectionLike, RedisError};
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthToken {
pub auth_token: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AsyncWebhookEventType {
AccountConfirmationRequested,
AccountDeleteRequested,
AddressCreated,
AddressUpdated,
AddressDeleted,
AppInstalled,
AppUpdated,
AppDeleted,
AppStatusChanged,
AttributeCreated,
AttributeUpdated,
AttributeDeleted,
AttributeValueCreated,
AttributeValueUpdated,
AttributeValueDeleted,
CategoryCreated,
CategoryUpdated,
CategoryDeleted,
ChannelCreated,
ChannelUpdated,
ChannelDeleted,
ChannelStatusChanged,
GiftCardCreated,
GiftCardUpdated,
GiftCardDeleted,
GiftCardSent,
GiftCardStatusChanged,
GiftCardMetadataUpdated,
MenuCreated,
MenuUpdated,
MenuDeleted,
MenuItemCreated,
MenuItemUpdated,
MenuItemDeleted,
OrderCreated,
OrderConfirmed,
OrderPaid,
OrderFullyPaid,
OrderRefunded,
OrderFullyRefunded,
OrderUpdated,
OrderCancelled,
OrderExpired,
OrderFulfilled,
OrderMetadataUpdated,
OrderBulkCreated,
DraftOrderCreated,
DraftOrderUpdated,
DraftOrderDeleted,
SaleCreated,
SaleUpdated,
SaleDeleted,
SaleToggle,
InvoiceRequested,
InvoiceDeleted,
InvoiceSent,
CustomerCreated,
CustomerUpdated,
CustomerDeleted,
CustomerMetadataUpdated,
CollectionCreated,
CollectionUpdated,
CollectionDeleted,
CollectionMetadataUpdated,
ProductCreated,
ProductUpdated,
ProductDeleted,
ProductMediaCreated,
ProductMediaUpdated,
ProductMediaDeleted,
ProductMetadataUpdated,
ProductVariantCreated,
ProductVariantUpdated,
ProductVariantDeleted,
ProductVariantOutOfStock,
ProductVariantBackInStock,
ProductVariantStockUpdated,
ProductVariantMetadataUpdated,
CheckoutCreated,
CheckoutUpdated,
CheckoutFullyPaid,
CheckoutMetadataUpdated,
FulfillmentCreated,
FulfillmentCanceled,
FulfillmentApproved,
FulfillmentMetadataUpdated,
NotifyUser,
PageCreated,
PageUpdated,
PageDeleted,
PageTypeCreated,
PageTypeUpdated,
PageTypeDeleted,
PermissionGroupCreated,
PermissionGroupUpdated,
PermissionGroupDeleted,
ShippingPriceCreated,
ShippingPriceUpdated,
ShippingPriceDeleted,
ShippingZoneCreated,
ShippingZoneUpdated,
ShippingZoneDeleted,
ShippingZoneMetadataUpdated,
StaffCreated,
StaffUpdated,
StaffDeleted,
TransactionActionRequest,
TransactionItemMetadataUpdated,
TranslationCreated,
TranslationUpdated,
WarehouseCreated,
WarehouseUpdated,
WarehouseDeleted,
WarehouseMetadataUpdated,
VoucherCreated,
VoucherUpdated,
VoucherDeleted,
VoucherMetadataUpdated,
OBSERVABILITY,
ThumbnailCreated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SyncWebhookEventType {
CheckoutCalculateTaxes,
OrderCalculateTaxes,
ShippingListMethodsForCheckout,
CheckoutFilterShippingMethods,
OrderFilterShippingMethods,
TransactionChargeRequested,
TransactionRefundRequested,
TransactionCancelationRequested,
PaymentGatewayInitializeSession,
TransactionInitializeSession,
TransactionProcessSession,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebhookManifest {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub async_events: Option<Vec<AsyncWebhookEventType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sync_events: Option<Vec<SyncWebhookEventType>>,
/**
* Query is required for a subscription.
* If you don't need a payload, you can provide empty query like this:
*
* subscription {
* event {
* __typename
* }
* }
*/
pub query: String,
/** The full URL of the endpoint where request will be sent */
pub target_url: String,
pub is_active: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AppPermission {
ManageUsers,
ManageStaff,
ImpersonateUser,
ManageObservability,
ManageCheckouts,
HandleCheckouts,
HandleTaxes,
ManageTaxes,
ManageChannels,
ManageDiscounts,
ManageGiftCard,
ManageMenus,
ManageOrders,
ManagePages,
ManagePageTypesAndAttributes,
HandlePayments,
ManagePlugins,
ManageProducts,
ManageProductTypesAndAttributes,
ManageShipping,
ManageSettings,
ManageTranslations,
ManageApps,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AppExtensionMount {
ProductDetailsMoreActions,
ProductOverviewCreate,
ProductOverviewMoreActions,
NavigationCatalog,
NavigationOrders,
NavigationCustomers,
NavigationDiscounts,
NavigationTranslations,
NavigationPages,
OrderDetailsMoreActions,
OrderOverviewCreate,
OrderOverviewMoreActions,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AppExtensionTarget {
Popup,
AppPage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppExtension {
/** Name which will be displayed in the dashboard */
pub label: String,
/** the place where the extension will be mounted */
pub mount: AppExtensionMount,
/** Method of presenting the interface
`POPUP` will present the interface in a modal overlay
`APP_PAGE` will navigate to the application page
@default `POPUP`
*/
pub target: AppExtensionTarget,
pub permissions: Vec<AppPermission>,
/** URL of the view to display,
you can skip the domain and protocol when target is set to `APP_PAGE`, or when your manifest defines an `appUrl`.
When target is set to `POPUP`, the url will be used to render an `<iframe>`.
*/
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppManifest {
/** ID of the application used internally by Saleor */
pub id: String,
pub version: String,
/** App's name displayed in the dashboard */
pub name: String,
/** Description of the app displayed in the dashboard */
#[serde(skip_serializing_if = "Option::is_none")]
pub about: Option<String>,
/** Array of permissions requested by the app */
pub permissions: Vec<AppPermission>,
/** App website rendered in the dashboard */
pub app_url: String,
/** Address to the app configuration page, which is rendered in the dashboard
@deprecated in Saleor 3.5, use appUrl instead
*/
#[serde(skip_serializing_if = "Option::is_none")]
pub configuration_url: Option<String>,
/** Endpoint used during process of app installation
@see [Installing an app](https://docs.saleor.io/docs/3.x/developer/extending/apps/installing-apps#installing-an-app)
*/
pub token_target_url: String,
/** Short description of privacy policy displayed in the dashboard
@deprecated in Saleor 3.5, use dataPrivacyUrl instead
*/
#[serde(skip_serializing_if = "Option::is_none")]
pub data_privacy: Option<String>,
/** URL to the full privacy policy */
#[serde(skip_serializing_if = "Option::is_none")]
pub data_privacy_url: Option<String>,
/** External URL to the app homepage */
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage_url: Option<String>,
/** External URL to the page where app users can find support */
#[serde(skip_serializing_if = "Option::is_none")]
pub support_url: Option<String>,
/** List of extensions that will be mounted in Saleor's dashboard
@see For details, please see the [extension section](https://docs.saleor.io/docs/3.x/developer/extending/apps/extending-dashboard-with-apps#key-concepts)
*/
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<Vec<AppExtension>>,
/** List of webhooks that will be set.
@see For details, please look at [asynchronous webhooks](https://docs.saleor.io/docs/3.x/developer/extending/apps/asynchronous-webhooks),
[synchronous-webhooks](https://docs.saleor.io/docs/3.x/developer/extending/apps/synchronous-webhooks/key-concepts)
and [webhooks' subscription](https://docs.saleor.io/docs/3.x/developer/extending/apps/subscription-webhook-payloads)
Be aware that subscription queries are required in manifest sections
*/
#[serde(skip_serializing_if = "Option::is_none")]
pub webhooks: Option<Vec<WebhookManifest>>,
/**
* Allows app installation for specific Saleor versions, using semver.
* https://github.com/npm/node-semver#versions
*
* If not set, Saleor will allow installation for every version
*
* In Saleor versions lower than 3.13, this field will be ignored
*
* Examples:
* ">=3.10" - allow for versions 3.10 or newer
* ">=3.10 <4" - allow for versions 3.10 and newer, but not 4.0 and newer
* ">=3.10 <4 || 4.0.0" - 3.10 and newer, less than 4, but allow exactly 4.0.0
*/
#[serde(skip_serializing_if = "Option::is_none")]
pub required_saleor_version: Option<String>,
/**
* App author name displayed in the dashboard
*
* In Saleor versions lower than 3.13, this field will be ignored
*/
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
/**
* Add brand-specific metadata to the app
*
* Available from Saleor 3.15. In previous versions will be ignored
*/
#[serde(skip_serializing_if = "Option::is_none")]
pub brand: Option<SaleorAppBranding>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaleorAppBranding {
pub logo: SaleorAppBrandingDefault,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaleorAppBrandingDefault {
pub default: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthData {
pub domain: Option<String>,
pub token: String,
pub saleor_api_url: String,
pub app_id: String,
pub jwks: Option<String>,
}
impl std::fmt::Display for AuthData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"(domain:{}\ntoken:{}\nsaleor_api_url:{}\napp_id:{}\njwks:{})",
self.domain.clone().unwrap_or_default(),
self.token,
self.saleor_api_url,
self.app_id,
self.jwks.clone().unwrap_or_default()
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AplType {
Redis,
}
pub trait APL: Sized + Send + Sync + Clone + std::fmt::Debug {
async fn get(&self, saleor_api_url: &str) -> Result<AuthData>;
async fn set(&self, auth_data: AuthData) -> Result<()>;
async fn delete(&self, saleor_api_url: &str) -> Result<()>;
async fn get_all(&self) -> Result<Vec<AuthData>>;
async fn is_ready(&self) -> Result<()>;
async fn is_configured(&self) -> Result<()>;
}
#[derive(Debug, Clone)]
pub struct SaleorApp<A: APL> {
pub apl: A,
}
#[derive(Debug, Clone)]
pub struct RedisApl {
pub client: redis::Client,
pub app_api_base_url: String,
}
impl APL for RedisApl {
async fn get(&self, saleor_api_url: &str) -> Result<AuthData> {
debug!(" get()");
let mut conn = self.client.get_async_connection().await?;
let val: String = conn.get(self.prepare_key(saleor_api_url)).await?;
debug!("received {val}");
let val: AuthData = serde_json::from_str(&val)?;
info!("sucessful get");
debug!("parsed {val}");
Ok(val)
}
async fn set(&self, auth_data: AuthData) -> Result<()> {
debug!("set(), {}", auth_data);
let mut conn = self.client.get_async_connection().await?;
conn.set(
self.prepare_key(&auth_data.saleor_api_url),
serde_json::to_string(&auth_data)?,
)
.await?;
info!("sucessful set");
Ok(())
}
async fn delete(&self, saleor_api_url: &str) -> Result<()> {
debug!("delete(), {}", saleor_api_url);
let mut conn = self.client.get_async_connection().await?;
let val: String = conn.get_del(self.prepare_key(saleor_api_url)).await?;
debug!("sucessful delete(), {}", val);
info!("sucessful del");
Ok(())
}
async fn is_ready(&self) -> Result<()> {
debug!("is_ready()");
let mut conn = self.client.get_async_connection().await?;
let val: String = redis::cmd("INFO")
.arg("server")
.query_async(&mut conn)
.await?;
debug!("sucessful is_ready(), info: {}", val);
info!("sucessful is_ready");
Ok(())
}
async fn is_configured(&self) -> Result<()> {
debug!("is_configured()");
let mut conn = self.client.get_async_connection().await?;
let val: String = redis::cmd("INFO")
.arg("server")
.query_async(&mut conn)
.await?;
debug!("sucessful is_configured(), info: {}", val);
info!("sucessful is_configured");
Ok(())
}
async fn get_all(&self) -> Result<Vec<AuthData>> {
anyhow::bail!("Redis doens't support getall")
}
}
impl RedisApl {
pub fn new(redis_url: String, app_api_base_url: String) -> Result<Self> {
let client = redis::Client::open(redis_url)?;
let mut conn = client.get_connection_with_timeout(Duration::from_secs(3))?;
let val: Result<String, redis::RedisError> =
redis::cmd("INFO").arg("server").query(&mut conn);
match val {
Ok(_) => Ok(Self {
client,
app_api_base_url,
}),
Err(e) => bail!("failed redis connection, {:?}", e),
}
}
pub fn prepare_key(&self, saleor_api_url: &str) -> String {
let key = format!("{}:{saleor_api_url}", self.app_api_base_url);
debug!("made key:'{}'", key);
key
}
}