reorganised into a workspace
This commit is contained in:
parent
c37f3df8de
commit
ad720187c5
26 changed files with 783 additions and 546 deletions
3
.env
3
.env
|
@ -1,6 +1,3 @@
|
|||
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"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
APP_NAME="Rust test app"
|
||||
REQUIRED_SALEOR_VERSION=">=3.11.7 <4"
|
||||
REQUIRED_SALEOR_VERSION="^3.13"
|
||||
SALEOR_APP_ID="dummy-saleor-app-rs"
|
||||
APP_API_BASE_URL="http://localhost:8000/graphql/"
|
||||
APL="redis"
|
||||
APL_URL="redis://redis:6379/2"
|
||||
APP_API_BASE_URL="http://10.100.110.234:3000"
|
||||
APL="Redis"
|
||||
APL_URL="redis://localhost:6380/2"
|
||||
LOG_LEVEL="DEBUG"
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/target
|
||||
.env
|
||||
|
|
75
Cargo.lock
generated
75
Cargo.lock
generated
|
@ -134,6 +134,12 @@ version = "1.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
|
@ -504,6 +510,12 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
|
@ -638,6 +650,16 @@ version = "0.3.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.2"
|
||||
|
@ -891,7 +913,7 @@ version = "0.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -933,7 +955,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
||||
|
||||
[[package]]
|
||||
name = "saleor-app-rs"
|
||||
name = "saleor-app-sdk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"redis",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "saleor-app-template"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
|
@ -942,9 +976,12 @@ dependencies = [
|
|||
"dotenvy",
|
||||
"envy",
|
||||
"redis",
|
||||
"saleor-app-sdk",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-serde",
|
||||
"tracing-subscriber",
|
||||
|
@ -1213,6 +1250,31 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.2"
|
||||
|
@ -1293,6 +1355,15 @@ dependencies = [
|
|||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.15"
|
||||
|
|
15
Cargo.toml
15
Cargo.toml
|
@ -1,14 +1,10 @@
|
|||
[package]
|
||||
name = "saleor-app-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
[workspace]
|
||||
members = ["saleor-app-sdk", "saleor-app-template"]
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
[workspace.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"]}
|
||||
|
@ -19,3 +15,6 @@ tracing-serde = "0.1.3"
|
|||
tracing-subscriber = { version = "0.3.18" }
|
||||
dotenvy = "0.15.7"
|
||||
axum = "0.7.4"
|
||||
saleor-app-sdk = {path = "saleor-app-sdk"}
|
||||
tower = { version = "0.4.13", features = ["util"] }
|
||||
tower-http = { version = "0.5.2", features = ["fs", "trace"] }
|
||||
|
|
BIN
app logo template.xcf
Normal file
BIN
app logo template.xcf
Normal file
Binary file not shown.
20
saleor-app-sdk/Cargo.toml
Normal file
20
saleor-app-sdk/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "saleor-app-sdk"
|
||||
authors = ["Djkáťo <djkatovfx@gmail.com>"]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Unofficial Saleor App SDK like library, made to work with rust."
|
||||
keywords = ["saleor", "sdk", "plugin"]
|
||||
categories = [ "api-bindings", "web-programming::http-server"]
|
||||
homepage = "https://github.com/djkato/saleor-app-rs-template"
|
||||
repository = "https://github.com/djkato/saleor-app-rs-template"
|
||||
documentation = "https://github.com/djkato/saleor-app-rs-template"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
redis = { workspace=true, features = ["aio", "tokio-comp", "connection-manager"] }
|
||||
serde.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
serde_json.workspace = true
|
29
saleor-app-sdk/src/apl/env_apl.rs
Normal file
29
saleor-app-sdk/src/apl/env_apl.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use super::APL;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/**
|
||||
is not implemented yet!
|
||||
*/
|
||||
pub struct EnvApl {}
|
||||
|
||||
impl APL for EnvApl {
|
||||
async fn set(&self, auth_data: crate::AuthData) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn get(&self, saleor_api_url: &str) -> Result<crate::AuthData> {
|
||||
todo!()
|
||||
}
|
||||
async fn get_all(&self) -> Result<Vec<crate::AuthData>> {
|
||||
todo!()
|
||||
}
|
||||
async fn delete(&self, saleor_api_url: &str) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn is_ready(&self) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn is_configured(&self) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
34
saleor-app-sdk/src/apl/file_apl.rs
Normal file
34
saleor-app-sdk/src/apl/file_apl.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use std::path::Path;
|
||||
|
||||
use super::APL;
|
||||
use anyhow::{bail, Result};
|
||||
use std::fs::{read, write};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/**
|
||||
is not implemented yet!
|
||||
*/
|
||||
pub struct FileApl {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl APL for FileApl {
|
||||
async fn set(&self, auth_data: crate::AuthData) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn get(&self, saleor_api_url: &str) -> Result<crate::AuthData> {
|
||||
todo!()
|
||||
}
|
||||
async fn get_all(&self) -> Result<Vec<crate::AuthData>> {
|
||||
todo!()
|
||||
}
|
||||
async fn delete(&self, saleor_api_url: &str) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn is_ready(&self) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
async fn is_configured(&self) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
24
saleor-app-sdk/src/apl/mod.rs
Normal file
24
saleor-app-sdk/src/apl/mod.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
pub mod env_apl;
|
||||
pub mod file_apl;
|
||||
pub mod redis_apl;
|
||||
|
||||
use crate::AuthData;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::future::Future;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum AplType {
|
||||
Redis,
|
||||
File,
|
||||
Env,
|
||||
}
|
||||
|
||||
pub trait APL: Sized + Send + Sync + Clone + std::fmt::Debug {
|
||||
fn get(&self, saleor_api_url: &str) -> impl Future<Output = Result<AuthData>> + Send;
|
||||
fn set(&self, auth_data: AuthData) -> impl Future<Output = Result<()>> + Send;
|
||||
fn delete(&self, saleor_api_url: &str) -> impl Future<Output = Result<()>> + Send;
|
||||
fn get_all(&self) -> impl Future<Output = Result<Vec<AuthData>>> + Send;
|
||||
fn is_ready(&self) -> impl Future<Output = Result<()>> + Send;
|
||||
fn is_configured(&self) -> impl Future<Output = Result<()>> + Send;
|
||||
}
|
97
saleor-app-sdk/src/apl/redis_apl.rs
Normal file
97
saleor-app-sdk/src/apl/redis_apl.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use redis::AsyncCommands;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use super::APL;
|
||||
use crate::AuthData;
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
6
saleor-app-sdk/src/headers.rs
Normal file
6
saleor-app-sdk/src/headers.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub const SALEOR_DOMAIN_HEADER: &str = "saleor-domain";
|
||||
pub const SALEOR_EVENT_HEADER: &str = "saleor-event";
|
||||
pub const SALEOR_SIGNATURE_HEADER: &str = "saleor-signature";
|
||||
pub const SALEOR_AUTHORIZATION_BEARER_HEADER: &str = "authorization-bearer";
|
||||
pub const SALEOR_API_URL_HEADER: &str = "saleor-api-url";
|
||||
pub const SALEOR_SCHEMA_VERSION: &str = "saleor-schema-version";
|
40
saleor-app-sdk/src/lib.rs
Normal file
40
saleor-app-sdk/src/lib.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
pub mod apl;
|
||||
pub mod headers;
|
||||
pub mod manifest;
|
||||
pub mod webhooks;
|
||||
|
||||
use apl::APL;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthToken {
|
||||
pub auth_token: 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)]
|
||||
pub struct SaleorApp<A: APL> {
|
||||
pub apl: A,
|
||||
}
|
174
saleor-app-sdk/src/manifest.rs
Normal file
174
saleor-app-sdk/src/manifest.rs
Normal file
|
@ -0,0 +1,174 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::webhooks::WebhookManifest;
|
||||
|
||||
#[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,
|
||||
}
|
165
saleor-app-sdk/src/webhooks.rs
Normal file
165
saleor-app-sdk/src/webhooks.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[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>,
|
||||
}
|
29
saleor-app-template/Cargo.toml
Normal file
29
saleor-app-template/Cargo.toml
Normal file
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "saleor-app-template"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Djkáťo <djkatovfx@gmail.com>"]
|
||||
description = "A simple rust app template for Saleor using axum"
|
||||
homepage = "https://github.com/djkato/saleor-app-rs-template"
|
||||
repository = "https://github.com/djkato/saleor-app-rs-template"
|
||||
documentation = "https://github.com/djkato/saleor-app-rs-template"
|
||||
keywords = ["saleor", "sdk", "plugin", "template"]
|
||||
categories = [ "api-bindings", "web-programming::http-server"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
cynic.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
redis = { workspace = true, features = ["aio", "tokio-comp", "connection-manager"] }
|
||||
envy.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-serde.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
dotenvy.workspace = true
|
||||
axum.workspace = true
|
||||
saleor-app-sdk.workspace = true
|
||||
tower = { workspace = true, features = ["util"] }
|
||||
tower-http = { workspace = true , features = ["fs", "trace"] }
|
BIN
saleor-app-template/public/logo.png
Normal file
BIN
saleor-app-template/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -5,11 +5,8 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
saleor::{AppManifest, SaleorApp, APL},
|
||||
};
|
||||
|
||||
use crate::config::Config;
|
||||
use saleor_app_sdk::{apl::APL, manifest::AppManifest, SaleorApp};
|
||||
// Make our own error that wraps `anyhow::Error`.
|
||||
pub struct AppError(anyhow::Error);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use saleor_app_sdk::apl::AplType;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::saleor::AplType;
|
||||
use tracing::{debug, Level};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -20,7 +20,6 @@ fn version_default() -> String {
|
|||
#[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,
|
||||
|
@ -29,8 +28,6 @@ pub struct Config {
|
|||
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 {
|
|
@ -1,8 +1,13 @@
|
|||
mod app;
|
||||
mod config;
|
||||
mod routes;
|
||||
mod saleor;
|
||||
|
||||
use saleor_app_sdk::{
|
||||
apl::{env_apl::EnvApl, file_apl::FileApl, redis_apl::RedisApl, AplType, APL},
|
||||
manifest::{AppManifest, AppPermission, SaleorAppBranding, SaleorAppBrandingDefault},
|
||||
webhooks::{AsyncWebhookEventType, WebhookManifest},
|
||||
SaleorApp,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
|
@ -10,25 +15,29 @@ 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 apl: Box<dyn APL> = match config.apl {
|
||||
AplType::File => FileApl {
|
||||
path: "apl.json".to_owned(),
|
||||
},
|
||||
AplType::Redis => RedisApl::new(config.apl_url, config.app_api_base_url),
|
||||
AplType::Env => EnvApl {},
|
||||
};
|
||||
|
||||
let saleor_app = SaleorApp { apl };
|
||||
|
||||
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(),
|
||||
name: env!("CARGO_PKG_NAME").to_owned(),
|
||||
about: Some(env!("CARGO_PKG_DESCRIPTION").to_owned()),
|
||||
author: Some(env!("CARGO_PKG_AUTHORS").to_owned()),
|
||||
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()),
|
||||
|
@ -36,9 +45,14 @@ async fn main() -> anyhow::Result<()> {
|
|||
permissions: vec![AppPermission::ManageProducts],
|
||||
support_url: None,
|
||||
data_privacy: None,
|
||||
homepage_url: None,
|
||||
homepage_url: Some(env!("CARGO_PKG_HOMEPAGE").to_owned()),
|
||||
data_privacy_url: None,
|
||||
configuration_url: None,
|
||||
brand: Some(SaleorAppBranding {
|
||||
logo: SaleorAppBrandingDefault {
|
||||
default: format!("{}/logo.png", config.app_api_base_url),
|
||||
},
|
||||
}),
|
||||
webhooks: Some(vec![WebhookManifest {
|
||||
name: "GetProducts for demo rust app".to_owned(),
|
||||
query: r#"
|
||||
|
@ -66,10 +80,18 @@ async fn main() -> anyhow::Result<()> {
|
|||
config,
|
||||
saleor_app: Arc::new(Mutex::new(saleor_app)),
|
||||
};
|
||||
|
||||
let app = create_routes(app_state);
|
||||
/* Router::new()
|
||||
.route("/api/manifest", get(manifest))
|
||||
.route("/api/register", post(register))
|
||||
.with_state(app_state);
|
||||
*/
|
||||
|
||||
// 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(())
|
||||
match axum::serve(listener, app).await {
|
||||
Ok(o) => Ok(o),
|
||||
Err(e) => anyhow::bail!(e),
|
||||
}
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
use axum::{extract::State, Json};
|
||||
use saleor_app_sdk::{apl::APL, manifest::AppManifest};
|
||||
|
||||
use crate::{
|
||||
app::{AppError, AppState},
|
||||
saleor::{AppManifest, APL},
|
||||
};
|
||||
use crate::app::{AppError, AppState};
|
||||
|
||||
pub async fn manifest<A: APL>(
|
||||
State(state): State<AppState<A>>,
|
35
saleor-app-template/src/routes/mod.rs
Normal file
35
saleor-app-template/src/routes/mod.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use axum::{
|
||||
handler::HandlerWithoutStateExt,
|
||||
http::StatusCode,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use saleor_app_sdk::apl::APL;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::app::AppState;
|
||||
|
||||
pub mod manifest;
|
||||
pub mod register;
|
||||
use manifest::manifest;
|
||||
use register::register;
|
||||
|
||||
pub fn create_routes<T: APL + 'static>(state: AppState<T>) -> Router {
|
||||
async fn handle_404() -> (StatusCode, &'static str) {
|
||||
(StatusCode::NOT_FOUND, "Not found")
|
||||
}
|
||||
let service = handle_404.into_service();
|
||||
let serve_dir = ServeDir::new("saleor-app-template/public").not_found_service(service);
|
||||
|
||||
Router::new()
|
||||
//handles just path, eg. localhost:3000/
|
||||
.route(
|
||||
"/",
|
||||
get(|| async { "Your app got installed successfully!" }),
|
||||
)
|
||||
//handles files, eg. localhost:3000/logo.png
|
||||
.fallback_service(serve_dir)
|
||||
.route("/api/manifest", get(manifest))
|
||||
.route("/api/register", post(register))
|
||||
.with_state(state)
|
||||
}
|
|
@ -1,21 +1,18 @@
|
|||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::Json,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::Json,
|
||||
};
|
||||
use saleor_app_sdk::{apl::APL, AuthData, AuthToken};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{
|
||||
app::AppError,
|
||||
saleor::{AuthData, AuthToken, APL},
|
||||
AppState,
|
||||
};
|
||||
use crate::app::{AppError, AppState};
|
||||
|
||||
pub async fn register<A: APL>(
|
||||
headers: HeaderMap,
|
||||
Json(auth_token): Json<AuthToken>,
|
||||
State(state): State<AppState<A>>,
|
||||
Json(auth_token): Json<AuthToken>,
|
||||
) -> Result<StatusCode, AppError> {
|
||||
debug!(
|
||||
"/api/register:\nsaleor_api_url:{:?}\nauth_token:{:?}",
|
|
@ -1,17 +0,0 @@
|
|||
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)
|
||||
}
|
477
src/saleor.rs
477
src/saleor.rs
|
@ -1,477 +0,0 @@
|
|||
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