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