refactor
This commit is contained in:
commit
c37f3df8de
14 changed files with 2295 additions and 0 deletions
9
.env
Normal file
9
.env
Normal 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
7
.env.example
Normal 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
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
1453
Cargo.lock
generated
Normal file
1453
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal 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
79
bacon.toml
Normal 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
50
src/app.rs
Normal 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
51
src/config.rs
Normal 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
75
src/main.rs
Normal 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
12
src/routes/manifest.rs
Normal 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
17
src/routes/mod.rs
Normal 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
43
src/routes/register.rs
Normal 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
0
src/routes/webhooks.rs
Normal file
477
src/saleor.rs
Normal file
477
src/saleor.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue