sitemap app, lots of other stuff lol
This commit is contained in:
parent
4a9ef170fe
commit
ba432d9aa5
61 changed files with 76492 additions and 549 deletions
11
.env
11
.env
|
@ -1,6 +1,15 @@
|
||||||
REQUIRED_SALEOR_VERSION="^3.13"
|
REQUIRED_SALEOR_VERSION="^3.13"
|
||||||
SALEOR_APP_ID="dummy-saleor-app-rs"
|
|
||||||
APP_API_BASE_URL="http://10.100.110.234:3000"
|
APP_API_BASE_URL="http://10.100.110.234:3000"
|
||||||
APL="Redis"
|
APL="Redis"
|
||||||
APL_URL="redis://localhost:6380/2"
|
APL_URL="redis://localhost:6380/2"
|
||||||
LOG_LEVEL="DEBUG"
|
LOG_LEVEL="DEBUG"
|
||||||
|
SITEMAP_TARGET_FOLDER="./temp"
|
||||||
|
# Available fields can be found in ./sitemap-generator/src/queries/event_subjects_updated.rs: ProductUpdate
|
||||||
|
SITEMAP_PRODUCT_TEMPLATE="https://example.com/{product.category.slug}/{product.slug}"
|
||||||
|
# Available fields can be found in ./sitemap-generator/src/queries/event_subjects_updated.rs: CategoryUpdate
|
||||||
|
SITEMAP_CATEGORY_TEMPLATE="https://example.com/{category.slug}"
|
||||||
|
# Available fields can be found in ./sitemap-generator/src/queries/event_subjects_updated.rs: CollectionUpdate
|
||||||
|
SITEMAP_COLLECTION_TEMPLATE="https://example.com/collection/{collection.slug}"
|
||||||
|
# Available fields can be found in ./sitemap-generator/src/queries/event_subjects_updated.rs: PageUpdate
|
||||||
|
SITEMAP_PAGES_TEMPLATE="https://example.com/{page.slug}"
|
||||||
|
SITEMAP_INDEX_HOSTNAME="https://example.com/"
|
||||||
|
|
12
.env.example
12
.env.example
|
@ -1,6 +1,14 @@
|
||||||
REQUIRED_SALEOR_VERSION="^3.13"
|
REQUIRED_SALEOR_VERSION="^3.13"
|
||||||
SALEOR_APP_ID="dummy-saleor-app-rs"
|
APP_API_BASE_URL="http://0.0.0.0:3000"
|
||||||
APP_API_BASE_URL="http://10.100.110.234:3000"
|
|
||||||
APL="Redis"
|
APL="Redis"
|
||||||
APL_URL="redis://localhost:6380/2"
|
APL_URL="redis://localhost:6380/2"
|
||||||
LOG_LEVEL="DEBUG"
|
LOG_LEVEL="DEBUG"
|
||||||
|
# Available fields can be found in ./sitemap-generator/src/queries/event_subjects_updated.rs: ProductUpdate
|
||||||
|
SITEMAP_PRODUCT_TEMPLATE="https://example.com/{product.category.slug}/{product.slug}"
|
||||||
|
# Available fields can be found in ./sitemap-generator/src/queries/event_subjects_updated.rs: CategoryUpdate
|
||||||
|
SITEMAP_CATEGORY_TEMPLATE="https://example.com/{category.slug}"
|
||||||
|
# Available fields can be found in ./sitemap-generator/src/queries/event_subjects_updated.rs: CollectionUpdate
|
||||||
|
SITEMAP_COLLECTION_TEMPLATE="https://example.com/collection/{collection.slug}"
|
||||||
|
# Available fields can be found in ./sitemap-generator/src/queries/event_subjects_updated.rs: PageUpdate
|
||||||
|
SITEMAP_PAGES_TEMPLATE="https://example.com/{page.slug}"
|
||||||
|
SITEMAP_INDEX_HOSTNAME="https://example.com/"
|
||||||
|
|
2183
Cargo.lock
generated
2183
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,10 +1,11 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["saleor-app-sdk", "saleor-app-template"]
|
members = ["sdk", "app-template", "sitemap-generator"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0.79"
|
anyhow = "1.0.79"
|
||||||
cynic = "3.4.3"
|
cynic = {version="3.4.3", features = ["http-surf"]}
|
||||||
|
surf = "2.3.2"
|
||||||
serde = "1.0.196"
|
serde = "1.0.196"
|
||||||
serde_json = "1.0.113"
|
serde_json = "1.0.113"
|
||||||
tokio = {version = "1.36.0", features = ["full"]}
|
tokio = {version = "1.36.0", features = ["full"]}
|
||||||
|
@ -15,6 +16,7 @@ tracing-serde = "0.1.3"
|
||||||
tracing-subscriber = { version = "0.3.18" }
|
tracing-subscriber = { version = "0.3.18" }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
axum = "0.7.4"
|
axum = "0.7.4"
|
||||||
saleor-app-sdk = {path = "saleor-app-sdk"}
|
saleor-app-sdk = {path = "sdk"}
|
||||||
tower = { version = "0.4.13", features = ["util"] }
|
tower = { version = "0.4.13", features = ["util"] }
|
||||||
tower-http = { version = "0.5.2", features = ["fs", "trace"] }
|
tower-http = { version = "0.5.2", features = ["fs", "trace"] }
|
||||||
|
cynic-codegen= "3.4.3"
|
||||||
|
|
110
FSL-1.1-MIT.md
110
FSL-1.1-MIT.md
|
@ -1,110 +0,0 @@
|
||||||
# Functional Source License, Version 1.1, MIT Future License
|
|
||||||
|
|
||||||
## Abbreviation
|
|
||||||
|
|
||||||
FSL-1.1-MIT
|
|
||||||
|
|
||||||
## Notice
|
|
||||||
|
|
||||||
Copyright 2024 Radovan Katrenčik
|
|
||||||
|
|
||||||
## Terms and Conditions
|
|
||||||
|
|
||||||
### Licensor ("We")
|
|
||||||
|
|
||||||
The party offering the Software under these Terms and Conditions.
|
|
||||||
|
|
||||||
### The Software
|
|
||||||
|
|
||||||
The "Software" is each version of the software that we make available under
|
|
||||||
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
||||||
Conditions with the Software.
|
|
||||||
|
|
||||||
### License Grant
|
|
||||||
|
|
||||||
Subject to your compliance with this License Grant and the Patents,
|
|
||||||
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
||||||
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
||||||
and redistribute the Software for any Permitted Purpose identified below.
|
|
||||||
|
|
||||||
### Permitted Purpose
|
|
||||||
|
|
||||||
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
||||||
means making the Software available to others in a commercial product or
|
|
||||||
service that:
|
|
||||||
|
|
||||||
1. substitutes for the Software;
|
|
||||||
|
|
||||||
2. substitutes for any other product or service we offer using the Software
|
|
||||||
that exists as of the date we make the Software available; or
|
|
||||||
|
|
||||||
3. offers the same or substantially similar functionality as the Software.
|
|
||||||
|
|
||||||
Permitted Purposes specifically include using the Software:
|
|
||||||
|
|
||||||
1. for your internal use and access;
|
|
||||||
|
|
||||||
2. for non-commercial education;
|
|
||||||
|
|
||||||
3. for non-commercial research; and
|
|
||||||
|
|
||||||
4. in connection with professional services that you provide to a licensee
|
|
||||||
using the Software in accordance with these Terms and Conditions.
|
|
||||||
|
|
||||||
### Patents
|
|
||||||
|
|
||||||
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
||||||
patents, the license grant above includes a license under our patents. If you
|
|
||||||
make a claim against any party that the Software infringes or contributes to
|
|
||||||
the infringement of any patent, then your patent license to the Software ends
|
|
||||||
immediately.
|
|
||||||
|
|
||||||
### Redistribution
|
|
||||||
|
|
||||||
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
||||||
the Software.
|
|
||||||
|
|
||||||
If you redistribute any copies, modifications or derivatives of the Software,
|
|
||||||
you must include a copy of or a link to these Terms and Conditions and not
|
|
||||||
remove any copyright notices provided in or with the Software.
|
|
||||||
|
|
||||||
### Disclaimer
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
||||||
|
|
||||||
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
||||||
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
||||||
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
||||||
|
|
||||||
### Trademarks
|
|
||||||
|
|
||||||
Except for displaying the License Details and identifying us as the origin of
|
|
||||||
the Software, you have no right under these Terms and Conditions to use our
|
|
||||||
trademarks, trade names, service marks or product names.
|
|
||||||
|
|
||||||
## Grant of Future License
|
|
||||||
|
|
||||||
We hereby irrevocably grant you an additional license to use the Software under
|
|
||||||
the MIT license that is effective on the second anniversary of the date we make
|
|
||||||
the Software available. On or after that date, you may use the Software under
|
|
||||||
the MIT license, in which case the following will apply:
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
73
PolyForm-Noncommercial-1.0.0.md
Normal file
73
PolyForm-Noncommercial-1.0.0.md
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
# PolyForm Noncommercial License 1.0.0
|
||||||
|
|
||||||
|
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
||||||
|
|
||||||
|
## Copyright License
|
||||||
|
|
||||||
|
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
||||||
|
|
||||||
|
## Distribution License
|
||||||
|
|
||||||
|
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
||||||
|
|
||||||
|
## Notices
|
||||||
|
|
||||||
|
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
||||||
|
|
||||||
|
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
||||||
|
|
||||||
|
## Changes and New Works License
|
||||||
|
|
||||||
|
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
||||||
|
|
||||||
|
## Patent License
|
||||||
|
|
||||||
|
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
||||||
|
|
||||||
|
## Noncommercial Purposes
|
||||||
|
|
||||||
|
Any noncommercial purpose is a permitted purpose.
|
||||||
|
|
||||||
|
## Personal Uses
|
||||||
|
|
||||||
|
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
|
||||||
|
|
||||||
|
## Noncommercial Organizations
|
||||||
|
|
||||||
|
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
|
||||||
|
|
||||||
|
## Fair Use
|
||||||
|
|
||||||
|
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
||||||
|
|
||||||
|
## No Other Rights
|
||||||
|
|
||||||
|
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
||||||
|
|
||||||
|
## Patent Defense
|
||||||
|
|
||||||
|
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
||||||
|
|
||||||
|
## Violations
|
||||||
|
|
||||||
|
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
||||||
|
|
||||||
|
## No Liability
|
||||||
|
|
||||||
|
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
|
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
||||||
|
|
||||||
|
**You** refers to the individual or entity agreeing to these terms.
|
||||||
|
|
||||||
|
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||||
|
|
||||||
|
**Your licenses** are all the licenses granted to you for the software under these terms.
|
||||||
|
|
||||||
|
**Use** means anything you do with the software requiring one of your licenses.
|
|
@ -31,6 +31,10 @@ Workspace dependencies need to be managed manually. If you wanna add a new depen
|
||||||
If you want to use a shared dependency, add it to the root level `Cargo.toml`,
|
If you want to use a shared dependency, add it to the root level `Cargo.toml`,
|
||||||
then inside your member `Cargo.toml`add it under depencency like: `<dependency> = { workspace = true, features = [ "..." ] }`.
|
then inside your member `Cargo.toml`add it under depencency like: `<dependency> = { workspace = true, features = [ "..." ] }`.
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
To have the app rebuild during development, install bacon `cargo install bacon`, then run `bacon run -- <app-name>` to have bacon watch your code and rerun it on save!
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Each workspace member has it's licensed in it's own directory.
|
Each workspace member has it's licensed in it's own directory.
|
||||||
|
@ -38,4 +42,4 @@ Each workspace member has it's licensed in it's own directory.
|
||||||
### TL;DR:
|
### TL;DR:
|
||||||
|
|
||||||
- saleor-app-sdk, saleor-app-template and the root structure fall under either MIT or Apache 2.0 at your convenience.
|
- saleor-app-sdk, saleor-app-template and the root structure fall under either MIT or Apache 2.0 at your convenience.
|
||||||
- Any other workspace members fall under FSL-1.1-MIT. If you want to use my apps in commercial environment, each app costs 10€ (or voluntarily more). Upon payment/donation you can automatically use the given app as if it had MIT-1 or Apache 2.0.
|
- Rest of the apps in this repo fall under `PolyForm-Noncommercial-1.0.md`. If you want to use my apps commercially, each app costs 10€ (or voluntarily more). Upon payment/donation you are allowed to use the given app commercially.
|
||||||
|
|
|
@ -13,7 +13,6 @@ license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
cynic.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tokio = { workspace = true, features = ["full"] }
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
@ -27,3 +26,9 @@ axum.workspace = true
|
||||||
saleor-app-sdk.workspace = true
|
saleor-app-sdk.workspace = true
|
||||||
tower = { workspace = true, features = ["util"] }
|
tower = { workspace = true, features = ["util"] }
|
||||||
tower-http = { workspace = true , features = ["fs", "trace"] }
|
tower-http = { workspace = true , features = ["fs", "trace"] }
|
||||||
|
surf.workspace = true
|
||||||
|
cynic = {workspace = true, features = ["http-surf"]}
|
||||||
|
cynic-codegen.workspace = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cynic-codegen.workspace = true
|
4
app-template/README.md
Normal file
4
app-template/README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Unofficial Saleor App Template
|
||||||
|
|
||||||
|
To update the saleor schema, you can download it from [here](https://raw.githubusercontent.com/saleor/saleor/main/saleor/graphql/schema.graphql) and put into schema/schema.graphql
|
||||||
|
To generate typings for events and gql queries, use: https://generator.cynic-rs.dev/
|
7
app-template/build.rs
Normal file
7
app-template/build.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
fn main() {
|
||||||
|
cynic_codegen::register_schema("saleor")
|
||||||
|
.from_sdl_file("schema/schema.graphql")
|
||||||
|
.unwrap()
|
||||||
|
.as_default()
|
||||||
|
.unwrap();
|
||||||
|
}
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
1
app-template/schema/note.txt
Normal file
1
app-template/schema/note.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
To update the schema, you can download it from https://raw.githubusercontent.com/saleor/saleor/main/saleor/graphql/schema.graphql
|
36207
app-template/schema/schema.graphql
Normal file
36207
app-template/schema/schema.graphql
Normal file
File diff suppressed because it is too large
Load diff
|
@ -5,8 +5,7 @@ use axum::{
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::Config;
|
use saleor_app_sdk::{config::Config, manifest::AppManifest, SaleorApp};
|
||||||
use saleor_app_sdk::{apl::APL, manifest::AppManifest, SaleorApp};
|
|
||||||
// Make our own error that wraps `anyhow::Error`.
|
// Make our own error that wraps `anyhow::Error`.
|
||||||
pub struct AppError(anyhow::Error);
|
pub struct AppError(anyhow::Error);
|
||||||
|
|
||||||
|
@ -40,8 +39,8 @@ pub fn trace_to_std(config: &Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AppState<A: APL> {
|
pub struct AppState {
|
||||||
pub saleor_app: Arc<tokio::sync::Mutex<SaleorApp<A>>>,
|
pub saleor_app: Arc<tokio::sync::Mutex<SaleorApp>>,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub manifest: AppManifest,
|
pub manifest: AppManifest,
|
||||||
}
|
}
|
89
app-template/src/main.rs
Normal file
89
app-template/src/main.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
mod app;
|
||||||
|
mod queries;
|
||||||
|
mod routes;
|
||||||
|
|
||||||
|
use saleor_app_sdk::{
|
||||||
|
config::Config,
|
||||||
|
manifest::{AppManifest, AppPermission},
|
||||||
|
webhooks::{AsyncWebhookEventType, WebhookManifest},
|
||||||
|
SaleorApp,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{trace_to_std, AppState},
|
||||||
|
routes::create_routes,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let config = Config::load()?;
|
||||||
|
trace_to_std(&config);
|
||||||
|
|
||||||
|
let saleor_app = SaleorApp::new(&config)?;
|
||||||
|
|
||||||
|
let app_manifest = AppManifest::new(&config)
|
||||||
|
.add_webhook(
|
||||||
|
WebhookManifest::new(&config)
|
||||||
|
.set_query(
|
||||||
|
r#"
|
||||||
|
subscription QueryProductsChanged {
|
||||||
|
event {
|
||||||
|
... on ProductUpdated {
|
||||||
|
product {
|
||||||
|
... BaseProduct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on ProductCreated {
|
||||||
|
product {
|
||||||
|
... BaseProduct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on ProductDeleted {
|
||||||
|
product {
|
||||||
|
... BaseProduct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment BaseProduct on Product {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
category {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.add_async_events(vec![
|
||||||
|
AsyncWebhookEventType::ProductCreated,
|
||||||
|
AsyncWebhookEventType::ProductUpdated,
|
||||||
|
AsyncWebhookEventType::ProductDeleted,
|
||||||
|
])
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.add_permission(AppPermission::ManageProducts)
|
||||||
|
.build();
|
||||||
|
let app_state = AppState {
|
||||||
|
manifest: app_manifest,
|
||||||
|
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());
|
||||||
|
match axum::serve(listener, app).await {
|
||||||
|
Ok(o) => Ok(o),
|
||||||
|
Err(e) => anyhow::bail!(e),
|
||||||
|
}
|
||||||
|
}
|
45
app-template/src/queries/event_products_updated.rs
Normal file
45
app-template/src/queries/event_products_updated.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
#[cynic::schema("saleor")]
|
||||||
|
mod schema {}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
#[cynic(graphql_type = "Subscription")]
|
||||||
|
pub struct QueryProductsChanged {
|
||||||
|
pub event: Option<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductUpdated {
|
||||||
|
pub product: Option<Product>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductDeleted {
|
||||||
|
pub product: Option<Product>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductCreated {
|
||||||
|
pub product: Option<Product>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct Product {
|
||||||
|
pub id: cynic::Id,
|
||||||
|
pub slug: String,
|
||||||
|
pub name: String,
|
||||||
|
pub category: Option<Category>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct Category {
|
||||||
|
pub slug: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::InlineFragments, Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
ProductUpdated(ProductUpdated),
|
||||||
|
ProductCreated(ProductCreated),
|
||||||
|
ProductDeleted(ProductDeleted),
|
||||||
|
#[cynic(fallback)]
|
||||||
|
Unknown,
|
||||||
|
}
|
2
app-template/src/queries/mod.rs
Normal file
2
app-template/src/queries/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod event_products_updated;
|
||||||
|
pub mod product_metadata_update;
|
75
app-template/src/queries/product_metadata_update.rs
Normal file
75
app-template/src/queries/product_metadata_update.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
#[cynic::schema("saleor")]
|
||||||
|
mod schema {}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryVariables, Debug)]
|
||||||
|
pub struct UpdateProductMetadataVariables<'a> {
|
||||||
|
pub metadata: Option<Vec<MetadataInput<'a>>>,
|
||||||
|
pub product_id: &'a cynic::Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
#[cynic(
|
||||||
|
graphql_type = "Mutation",
|
||||||
|
variables = "UpdateProductMetadataVariables"
|
||||||
|
)]
|
||||||
|
pub struct UpdateProductMetadata {
|
||||||
|
#[arguments(id: $product_id, input: { metadata: $metadata })]
|
||||||
|
pub product_update: Option<ProductUpdate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductUpdate {
|
||||||
|
pub errors: Vec<ProductError>,
|
||||||
|
pub product: Option<Product>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct Product {
|
||||||
|
pub id: cynic::Id,
|
||||||
|
pub metadata: Vec<MetadataItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductError {
|
||||||
|
pub field: Option<String>,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub code: ProductErrorCode,
|
||||||
|
pub attributes: Option<Vec<cynic::Id>>,
|
||||||
|
pub values: Option<Vec<cynic::Id>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct MetadataItem {
|
||||||
|
pub key: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::Enum, Clone, Copy, Debug)]
|
||||||
|
pub enum ProductErrorCode {
|
||||||
|
AlreadyExists,
|
||||||
|
AttributeAlreadyAssigned,
|
||||||
|
AttributeCannotBeAssigned,
|
||||||
|
AttributeVariantsDisabled,
|
||||||
|
MediaAlreadyAssigned,
|
||||||
|
DuplicatedInputItem,
|
||||||
|
GraphqlError,
|
||||||
|
Invalid,
|
||||||
|
InvalidPrice,
|
||||||
|
ProductWithoutCategory,
|
||||||
|
NotProductsImage,
|
||||||
|
NotProductsVariant,
|
||||||
|
NotFound,
|
||||||
|
Required,
|
||||||
|
Unique,
|
||||||
|
VariantNoDigitalContent,
|
||||||
|
CannotManageProductWithoutVariant,
|
||||||
|
ProductNotAssignedToChannel,
|
||||||
|
UnsupportedMediaProvider,
|
||||||
|
PreorderVariantCannotBeDeactivated,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::InputObject, Debug)]
|
||||||
|
pub struct MetadataInput<'a> {
|
||||||
|
pub key: &'a str,
|
||||||
|
pub value: &'a str,
|
||||||
|
}
|
8
app-template/src/routes/manifest.rs
Normal file
8
app-template/src/routes/manifest.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
use axum::{extract::State, Json};
|
||||||
|
use saleor_app_sdk::{manifest::AppManifest};
|
||||||
|
|
||||||
|
use crate::app::{AppError, AppState};
|
||||||
|
|
||||||
|
pub async fn manifest(State(state): State<AppState>) -> Result<Json<AppManifest>, AppError> {
|
||||||
|
Ok(Json(state.manifest))
|
||||||
|
}
|
|
@ -4,17 +4,18 @@ use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use saleor_app_sdk::apl::APL;
|
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
use crate::app::AppState;
|
use crate::app::AppState;
|
||||||
|
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
pub mod register;
|
pub mod register;
|
||||||
|
pub mod webhooks;
|
||||||
use manifest::manifest;
|
use manifest::manifest;
|
||||||
use register::register;
|
use register::register;
|
||||||
|
use webhooks::webhooks;
|
||||||
|
|
||||||
pub fn create_routes<T: APL + 'static>(state: AppState<T>) -> Router {
|
pub fn create_routes(state: AppState) -> Router {
|
||||||
async fn handle_404() -> (StatusCode, &'static str) {
|
async fn handle_404() -> (StatusCode, &'static str) {
|
||||||
(StatusCode::NOT_FOUND, "Not found")
|
(StatusCode::NOT_FOUND, "Not found")
|
||||||
}
|
}
|
||||||
|
@ -31,5 +32,6 @@ pub fn create_routes<T: APL + 'static>(state: AppState<T>) -> Router {
|
||||||
.fallback_service(serve_dir)
|
.fallback_service(serve_dir)
|
||||||
.route("/api/manifest", get(manifest))
|
.route("/api/manifest", get(manifest))
|
||||||
.route("/api/register", post(register))
|
.route("/api/register", post(register))
|
||||||
|
.route("/api/webhooks", post(webhooks))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
|
@ -4,14 +4,14 @@ use axum::{
|
||||||
extract::State,
|
extract::State,
|
||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
};
|
};
|
||||||
use saleor_app_sdk::{apl::APL, AuthData, AuthToken};
|
use saleor_app_sdk::{AuthData, AuthToken};
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::app::{AppError, AppState};
|
use crate::app::{AppError, AppState};
|
||||||
|
|
||||||
pub async fn register<A: APL>(
|
pub async fn register(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
State(state): State<AppState<A>>,
|
State(state): State<AppState>,
|
||||||
Json(auth_token): Json<AuthToken>,
|
Json(auth_token): Json<AuthToken>,
|
||||||
) -> Result<StatusCode, AppError> {
|
) -> Result<StatusCode, AppError> {
|
||||||
debug!(
|
debug!(
|
79
app-template/src/routes/webhooks.rs
Normal file
79
app-template/src/routes/webhooks.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
use anyhow::Context;
|
||||||
|
use axum::{
|
||||||
|
extract::{Json, State},
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
};
|
||||||
|
use cynic::{http::SurfExt, MutationBuilder};
|
||||||
|
use saleor_app_sdk::{
|
||||||
|
headers::SALEOR_API_URL_HEADER,
|
||||||
|
webhooks::{
|
||||||
|
utils::{get_webhook_event_type, EitherWebhookType},
|
||||||
|
AsyncWebhookEventType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{AppError, AppState},
|
||||||
|
queries::{
|
||||||
|
event_products_updated::ProductUpdated,
|
||||||
|
product_metadata_update::{
|
||||||
|
MetadataInput, MetadataItem, UpdateProductMetadata, UpdateProductMetadataVariables,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn webhooks(
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
//Will try to convert req body to ProductUpdated type, else returns 400
|
||||||
|
Json(product): Json<ProductUpdated>,
|
||||||
|
) -> Result<StatusCode, AppError> {
|
||||||
|
debug!("/api/webhooks");
|
||||||
|
debug!("req: {:?}", product);
|
||||||
|
debug!("headers: {:?}", headers);
|
||||||
|
|
||||||
|
let url = headers
|
||||||
|
.get(SALEOR_API_URL_HEADER)
|
||||||
|
.context("missing saleor api url header")?;
|
||||||
|
let event_type = get_webhook_event_type(&headers)?;
|
||||||
|
match event_type {
|
||||||
|
EitherWebhookType::Async(a) => match a {
|
||||||
|
AsyncWebhookEventType::ProductUpdated
|
||||||
|
| AsyncWebhookEventType::ProductCreated
|
||||||
|
| AsyncWebhookEventType::ProductDeleted => {
|
||||||
|
update_product(product, url.to_str()?, state).await?
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("got webhooks!");
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_product(
|
||||||
|
product: ProductUpdated,
|
||||||
|
saleor_api_url: &str,
|
||||||
|
state: AppState,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
debug!("Product got changed!");
|
||||||
|
if let Some(product) = product.product {
|
||||||
|
let operation = UpdateProductMetadata::build(UpdateProductMetadataVariables {
|
||||||
|
product_id: &product.id,
|
||||||
|
metadata: Some(vec![MetadataInput {
|
||||||
|
key: "helloloo",
|
||||||
|
value: "hiiiihii",
|
||||||
|
}]),
|
||||||
|
});
|
||||||
|
let saleor_app = state.saleor_app.lock().await;
|
||||||
|
let auth_data = saleor_app.apl.get(saleor_api_url).await?;
|
||||||
|
let result = surf::post(saleor_api_url)
|
||||||
|
.header("Authorization", format!("bearer {}", auth_data.token))
|
||||||
|
.run_graphql(operation)
|
||||||
|
.await;
|
||||||
|
debug!("update product result : {:?}", result);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
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";
|
|
|
@ -1,165 +0,0 @@
|
||||||
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>,
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
use saleor_app_sdk::apl::AplType;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
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 {
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
mod app;
|
|
||||||
mod config;
|
|
||||||
mod routes;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
app::{trace_to_std, AppState},
|
|
||||||
config::Config,
|
|
||||||
routes::create_routes,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> anyhow::Result<()> {
|
|
||||||
let config = Config::load()?;
|
|
||||||
trace_to_std(&config);
|
|
||||||
|
|
||||||
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: 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()),
|
|
||||||
extensions: None,
|
|
||||||
permissions: vec![AppPermission::ManageProducts],
|
|
||||||
support_url: None,
|
|
||||||
data_privacy: 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#"
|
|
||||||
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);
|
|
||||||
/* 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());
|
|
||||||
match axum::serve(listener, app).await {
|
|
||||||
Ok(o) => Ok(o),
|
|
||||||
Err(e) => anyhow::bail!(e),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
use axum::{extract::State, Json};
|
|
||||||
use saleor_app_sdk::{apl::APL, manifest::AppManifest};
|
|
||||||
|
|
||||||
use crate::app::{AppError, AppState};
|
|
||||||
|
|
||||||
pub async fn manifest<A: APL>(
|
|
||||||
State(state): State<AppState<A>>,
|
|
||||||
) -> Result<Json<AppManifest>, AppError> {
|
|
||||||
Ok(Json(state.manifest))
|
|
||||||
}
|
|
|
@ -20,3 +20,11 @@ tracing-subscriber.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
envy.workspace = true
|
envy.workspace = true
|
||||||
dotenvy.workspace = true
|
dotenvy.workspace = true
|
||||||
|
async-trait = "0.1.77"
|
||||||
|
jose-jwk = "0.1.2"
|
||||||
|
tower = { workspace = true }
|
||||||
|
jose-jws = "0.1.2"
|
||||||
|
http = "1.0.0"
|
||||||
|
jose-b64 = {version = "0.1.2", features =["serde"] }
|
||||||
|
strum = "0.26.0"
|
||||||
|
strum_macros = "0.26.1"
|
|
@ -1,5 +1,6 @@
|
||||||
use super::APL;
|
use super::APL;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
/**
|
/**
|
||||||
|
@ -7,17 +8,18 @@ is not implemented yet!
|
||||||
*/
|
*/
|
||||||
pub struct EnvApl {}
|
pub struct EnvApl {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl APL for EnvApl {
|
impl APL for EnvApl {
|
||||||
async fn set(&self, auth_data: crate::AuthData) -> Result<()> {
|
async fn set(&self, _auth_data: crate::AuthData) -> Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
async fn get(&self, saleor_api_url: &str) -> Result<crate::AuthData> {
|
async fn get(&self, _saleor_api_url: &str) -> Result<crate::AuthData> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
async fn get_all(&self) -> Result<Vec<crate::AuthData>> {
|
async fn get_all(&self) -> Result<Vec<crate::AuthData>> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
async fn delete(&self, saleor_api_url: &str) -> Result<()> {
|
async fn delete(&self, _saleor_api_url: &str) -> Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
async fn is_ready(&self) -> Result<()> {
|
async fn is_ready(&self) -> Result<()> {
|
|
@ -1,8 +1,9 @@
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use super::APL;
|
use super::APL;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{Result};
|
||||||
use std::fs::{read, write};
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
/**
|
/**
|
||||||
|
@ -12,17 +13,18 @@ pub struct FileApl {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl APL for FileApl {
|
impl APL for FileApl {
|
||||||
async fn set(&self, auth_data: crate::AuthData) -> Result<()> {
|
async fn set(&self, _auth_data: crate::AuthData) -> Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
async fn get(&self, saleor_api_url: &str) -> Result<crate::AuthData> {
|
async fn get(&self, _saleor_api_url: &str) -> Result<crate::AuthData> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
async fn get_all(&self) -> Result<Vec<crate::AuthData>> {
|
async fn get_all(&self) -> Result<Vec<crate::AuthData>> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
async fn delete(&self, saleor_api_url: &str) -> Result<()> {
|
async fn delete(&self, _saleor_api_url: &str) -> Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
async fn is_ready(&self) -> Result<()> {
|
async fn is_ready(&self) -> Result<()> {
|
26
sdk/src/apl/mod.rs
Normal file
26
sdk/src/apl/mod.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
pub mod env_apl;
|
||||||
|
pub mod file_apl;
|
||||||
|
pub mod redis_apl;
|
||||||
|
|
||||||
|
use crate::AuthData;
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum AplType {
|
||||||
|
Redis,
|
||||||
|
File,
|
||||||
|
Env,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait APL: Send + Sync + 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<()>;
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
|
@ -13,6 +14,7 @@ pub struct RedisApl {
|
||||||
pub app_api_base_url: String,
|
pub app_api_base_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
impl APL for RedisApl {
|
impl APL for RedisApl {
|
||||||
async fn get(&self, saleor_api_url: &str) -> Result<AuthData> {
|
async fn get(&self, saleor_api_url: &str) -> Result<AuthData> {
|
||||||
debug!(" get()");
|
debug!(" get()");
|
||||||
|
@ -75,7 +77,8 @@ impl APL for RedisApl {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RedisApl {
|
impl RedisApl {
|
||||||
pub fn new(redis_url: String, app_api_base_url: String) -> Result<Self> {
|
pub fn new(redis_url: &str, app_api_base_url: &str) -> Result<Self> {
|
||||||
|
debug!("creating redis apl...");
|
||||||
let client = redis::Client::open(redis_url)?;
|
let client = redis::Client::open(redis_url)?;
|
||||||
let mut conn = client.get_connection_with_timeout(Duration::from_secs(3))?;
|
let mut conn = client.get_connection_with_timeout(Duration::from_secs(3))?;
|
||||||
let val: Result<String, redis::RedisError> =
|
let val: Result<String, redis::RedisError> =
|
||||||
|
@ -84,7 +87,7 @@ impl RedisApl {
|
||||||
match val {
|
match val {
|
||||||
Ok(_) => Ok(Self {
|
Ok(_) => Ok(Self {
|
||||||
client,
|
client,
|
||||||
app_api_base_url,
|
app_api_base_url: app_api_base_url.to_owned(),
|
||||||
}),
|
}),
|
||||||
Err(e) => bail!("failed redis connection, {:?}", e),
|
Err(e) => bail!("failed redis connection, {:?}", e),
|
||||||
}
|
}
|
|
@ -23,7 +23,6 @@ fn version_default() -> String {
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "version_default")]
|
#[serde(default = "version_default")]
|
||||||
pub required_saleor_version: String,
|
pub required_saleor_version: String,
|
||||||
pub saleor_app_id: String,
|
|
||||||
pub app_api_base_url: String,
|
pub app_api_base_url: String,
|
||||||
pub apl: AplType,
|
pub apl: AplType,
|
||||||
pub apl_url: String,
|
pub apl_url: String,
|
2
sdk/src/fetch_jwks.rs
Normal file
2
sdk/src/fetch_jwks.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
|
51
sdk/src/headers.rs
Normal file
51
sdk/src/headers.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct SaleorHeaders<'a> {
|
||||||
|
#[serde(rename = "saleor-domain")]
|
||||||
|
#[serde(alias = "x-saleor-domain")]
|
||||||
|
domain: Option<&'a str>,
|
||||||
|
#[serde(rename = "saleor-domain")]
|
||||||
|
#[serde(alias = "x-saleor-domain")]
|
||||||
|
authorization_bearer: Option<&'a str>,
|
||||||
|
#[serde(rename = "saleor-domain")]
|
||||||
|
#[serde(alias = "x-saleor-domain")]
|
||||||
|
signature: Option<&'a str>,
|
||||||
|
#[serde(rename = "saleor-domain")]
|
||||||
|
#[serde(alias = "x-saleor-domain")]
|
||||||
|
event: Option<&'a str>,
|
||||||
|
saleor_api_url: Option<&'a str>,
|
||||||
|
#[serde(rename = "content-length")]
|
||||||
|
content_length: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO!
|
||||||
|
impl SaleorHeaders {
|
||||||
|
pub fn verify(&self, payload: &str) -> anyhow::Result<()> {
|
||||||
|
/*
|
||||||
|
if let Some(saleor_signature) = self.signature {
|
||||||
|
let split: Vec<String> = saleor_signature.split(".").collect();
|
||||||
|
let header = split.get(0);
|
||||||
|
let signature = split.get(2);
|
||||||
|
if let Some(header) = header {
|
||||||
|
if let Some(signature) = signature {
|
||||||
|
let jws = jose_jws::Signature {
|
||||||
|
signature: signature.into(),
|
||||||
|
header,
|
||||||
|
protected: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
|
@ -1,11 +1,16 @@
|
||||||
pub mod apl;
|
pub mod apl;
|
||||||
|
pub mod config;
|
||||||
pub mod headers;
|
pub mod headers;
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
|
pub mod middleware;
|
||||||
pub mod webhooks;
|
pub mod webhooks;
|
||||||
|
|
||||||
use apl::APL;
|
use apl::{AplType, APL};
|
||||||
|
use config::Config;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::apl::{env_apl::EnvApl, file_apl::FileApl, redis_apl::RedisApl};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AuthToken {
|
pub struct AuthToken {
|
||||||
pub auth_token: String,
|
pub auth_token: String,
|
||||||
|
@ -34,7 +39,22 @@ impl std::fmt::Display for AuthData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug)]
|
||||||
pub struct SaleorApp<A: APL> {
|
pub struct SaleorApp {
|
||||||
pub apl: A,
|
pub apl: Box<dyn APL>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SaleorApp {
|
||||||
|
pub fn new(config: &Config) -> anyhow::Result<SaleorApp> {
|
||||||
|
use AplType::{Env, File, Redis};
|
||||||
|
Ok(SaleorApp {
|
||||||
|
apl: match config.apl {
|
||||||
|
Redis => Box::new(RedisApl::new(&config.apl_url, &config.app_api_base_url)?),
|
||||||
|
Env => Box::new(EnvApl {}),
|
||||||
|
File => Box::new(FileApl {
|
||||||
|
path: "apl.txt".to_owned(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::webhooks::WebhookManifest;
|
use crate::{config::Config, webhooks::WebhookManifest};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
@ -76,7 +76,7 @@ pub struct AppExtension {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AppManifest {
|
pub struct AppManifest {
|
||||||
/** ID of the application used internally by Saleor */
|
/** ID of the application used internally by Saleor */
|
||||||
|
@ -162,6 +162,75 @@ pub struct AppManifest {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub brand: Option<SaleorAppBranding>,
|
pub brand: Option<SaleorAppBranding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AppManifestBuilder {
|
||||||
|
pub manifest: AppManifest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppManifestBuilder {
|
||||||
|
/**
|
||||||
|
* to simply create a webhook manifest, you can use WebhookManifest::new()
|
||||||
|
*/
|
||||||
|
pub fn add_webhook(mut self, webhook: WebhookManifest) -> Self {
|
||||||
|
if let Some(webhooks) = &mut self.manifest.webhooks {
|
||||||
|
webhooks.push(webhook)
|
||||||
|
} else {
|
||||||
|
self.manifest.webhooks = Some(vec![webhook]);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn add_permission(mut self, permissions: AppPermission) -> Self {
|
||||||
|
self.manifest.permissions.push(permissions);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn add_permissions(mut self, mut permissions: Vec<AppPermission>) -> Self {
|
||||||
|
self.manifest.permissions.append(&mut permissions);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn build(self) -> AppManifest {
|
||||||
|
self.manifest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppManifest {
|
||||||
|
/**
|
||||||
|
* Builder for AppManifest
|
||||||
|
*
|
||||||
|
* Takes these out of config:
|
||||||
|
* - Takes fields id, saleor_version, logo, token_target_url
|
||||||
|
* And these out of the environment:
|
||||||
|
* - name(CARGO_PKG_NAME), about(CARGO_PKG_DESCRIPTION), author(CARGO_PKG_AUTHORS),
|
||||||
|
* version(CARGO_PKG_VERSION), homepage_url(CARGO_PKG_HOMEPAGE)
|
||||||
|
*
|
||||||
|
* To set webhooks and permissions use the add_webhook() and add_permissions()
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
pub fn new(config: &Config) -> AppManifestBuilder {
|
||||||
|
AppManifestBuilder {
|
||||||
|
manifest: AppManifest {
|
||||||
|
id: env!("CARGO_PKG_NAME").to_owned(),
|
||||||
|
required_saleor_version: Some(config.required_saleor_version.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(),
|
||||||
|
configuration_url: Some(config.app_api_base_url.clone()),
|
||||||
|
token_target_url: format!("{}/api/register", config.app_api_base_url.clone()),
|
||||||
|
permissions: vec![],
|
||||||
|
homepage_url: Some(env!("CARGO_PKG_HOMEPAGE").to_owned()),
|
||||||
|
data_privacy_url: Some(env!("CARGO_PKG_HOMEPAGE").to_owned()),
|
||||||
|
support_url: Some(env!("CARGO_PKG_HOMEPAGE").to_owned()),
|
||||||
|
brand: Some(SaleorAppBranding {
|
||||||
|
logo: SaleorAppBrandingDefault {
|
||||||
|
default: format!("{}/logo.png", config.app_api_base_url),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SaleorAppBranding {
|
pub struct SaleorAppBranding {
|
1
sdk/src/middleware/mod.rs
Normal file
1
sdk/src/middleware/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod verify_webhook_signature;
|
59
sdk/src/middleware/verify_webhook_signature.rs
Normal file
59
sdk/src/middleware/verify_webhook_signature.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/*use http::{Request, Response};
|
||||||
|
use std::{
|
||||||
|
str::Bytes,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
use tower::Service;
|
||||||
|
|
||||||
|
use crate::headers::SALEOR_SIGNATURE_HEADER;
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WebhookVerifyJWTs<S> {
|
||||||
|
inner: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> WebhookVerifyJWTs<S> {
|
||||||
|
pub fn new(inner: S) -> Self {
|
||||||
|
WebhookVerifyJWTs { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for WebhookVerifyJWTs<S>
|
||||||
|
where
|
||||||
|
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
|
||||||
|
{
|
||||||
|
type Response = S::Response;
|
||||||
|
type Error = S::Error;
|
||||||
|
type Future = S::Future;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
self.inner.poll_ready(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
|
||||||
|
/*
|
||||||
|
if let Some(signature_header) = req.headers().get(SALEOR_SIGNATURE_HEADER) {
|
||||||
|
if let Ok(saleor_signature) = signature_header.to_str() {
|
||||||
|
let split: Vec<&str> = saleor_signature.split(".").collect();
|
||||||
|
let header = split.get(0);
|
||||||
|
let signature = split.get(2);
|
||||||
|
if let Some(signature) = signature {
|
||||||
|
let jws = jose_jws::Signature {
|
||||||
|
signature: signature.parse().unwrap(),
|
||||||
|
header:,
|
||||||
|
protected: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if req.extensions().get::<RequestId>().is_none() {
|
||||||
|
let request_id = request_id.clone();
|
||||||
|
req.extensions_mut().insert(RequestId::new(request_id));
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
self.inner.call(req)
|
||||||
|
*/
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}*/
|
285
sdk/src/webhooks/mod.rs
Normal file
285
sdk/src/webhooks/mod.rs
Normal file
|
@ -0,0 +1,285 @@
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum_macros::EnumString;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, EnumString)]
|
||||||
|
//kinda annoying that in an apps manifest, the `AsyncWebhookEventType` is in SCREAMING_SNAKE_CASE,
|
||||||
|
//but when receiving saleors webhook the header `saleor-event` is in snake_case,
|
||||||
|
//have to serialize and deserialize the enum two different ways
|
||||||
|
#[serde(rename_all(deserialize = "snake_case", serialize = "SCREAMING_SNAKE_CASE"))]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum AsyncWebhookEventType {
|
||||||
|
AnyEvents,
|
||||||
|
AccountConfirmationRequested,
|
||||||
|
AccountChangeEmailRequested,
|
||||||
|
AccountEmailChanged,
|
||||||
|
AccountSetPasswordRequested,
|
||||||
|
AccountConfirmed,
|
||||||
|
AccountDeleteRequested,
|
||||||
|
AccountDeleted,
|
||||||
|
AddressCreated,
|
||||||
|
AddressUpdated,
|
||||||
|
AddressDeleted,
|
||||||
|
AppInstalled,
|
||||||
|
AppUpdated,
|
||||||
|
AppDeleted,
|
||||||
|
AppStatusChanged,
|
||||||
|
AttributeCreated,
|
||||||
|
AttributeUpdated,
|
||||||
|
AttributeDeleted,
|
||||||
|
AttributeValueCreated,
|
||||||
|
AttributeValueUpdated,
|
||||||
|
AttributeValueDeleted,
|
||||||
|
CategoryCreated,
|
||||||
|
CategoryUpdated,
|
||||||
|
CategoryDeleted,
|
||||||
|
ChannelCreated,
|
||||||
|
ChannelUpdated,
|
||||||
|
ChannelDeleted,
|
||||||
|
ChannelStatusChanged,
|
||||||
|
ChannelMetadataUpdated,
|
||||||
|
GiftCardCreated,
|
||||||
|
GiftCardUpdated,
|
||||||
|
GiftCardDeleted,
|
||||||
|
GiftCardSent,
|
||||||
|
GiftCardStatusChanged,
|
||||||
|
GiftCardMetadataUpdated,
|
||||||
|
GiftCardExportCompleted,
|
||||||
|
MenuCreated,
|
||||||
|
MenuUpdated,
|
||||||
|
MenuDeleted,
|
||||||
|
MenuItemCreated,
|
||||||
|
MenuItemUpdated,
|
||||||
|
MenuItemDeleted,
|
||||||
|
OrderCreated,
|
||||||
|
OrderConfirmed,
|
||||||
|
OrderPaid,
|
||||||
|
OrderFullyPaid,
|
||||||
|
OrderRefunded,
|
||||||
|
OrderFullyRefunded,
|
||||||
|
OrderUpdated,
|
||||||
|
OrderCancelled,
|
||||||
|
OrderExpired,
|
||||||
|
OrderFulfilled,
|
||||||
|
OrderMetadataUpdated,
|
||||||
|
OrderBulkCreated,
|
||||||
|
FulfillmentCreated,
|
||||||
|
FulfillmentCanceled,
|
||||||
|
FulfillmentApproved,
|
||||||
|
FulfillmentMetadataUpdated,
|
||||||
|
FulfillmentTrackingNumberUpdated,
|
||||||
|
DraftOrderCreated,
|
||||||
|
DraftOrderUpdated,
|
||||||
|
DraftOrderDeleted,
|
||||||
|
SaleCreated,
|
||||||
|
SaleUpdated,
|
||||||
|
SaleDeleted,
|
||||||
|
SaleToggle,
|
||||||
|
PromotionCreated,
|
||||||
|
PromotionUpdated,
|
||||||
|
PromotionDeleted,
|
||||||
|
PromotionStarted,
|
||||||
|
PromotionEnded,
|
||||||
|
PromotionRuleCreated,
|
||||||
|
PromotionRuleUpdated,
|
||||||
|
PromotionRuleDeleted,
|
||||||
|
InvoiceRequested,
|
||||||
|
InvoiceDeleted,
|
||||||
|
InvoiceSent,
|
||||||
|
CustomerCreated,
|
||||||
|
CustomerUpdated,
|
||||||
|
CustomerDeleted,
|
||||||
|
CustomerMetadataUpdated,
|
||||||
|
CollectionCreated,
|
||||||
|
CollectionUpdated,
|
||||||
|
CollectionDeleted,
|
||||||
|
CollectionMetadataUpdated,
|
||||||
|
ProductCreated,
|
||||||
|
ProductUpdated,
|
||||||
|
ProductDeleted,
|
||||||
|
ProductMetadataUpdated,
|
||||||
|
ProductExportCompleted,
|
||||||
|
ProductMediaCreated,
|
||||||
|
ProductMediaUpdated,
|
||||||
|
ProductMediaDeleted,
|
||||||
|
ProductVariantCreated,
|
||||||
|
ProductVariantUpdated,
|
||||||
|
ProductVariantDeleted,
|
||||||
|
ProductVariantMetadataUpdated,
|
||||||
|
ProductVariantOutOfStock,
|
||||||
|
ProductVariantBackInStock,
|
||||||
|
ProductVariantStockUpdated,
|
||||||
|
CheckoutCreated,
|
||||||
|
CheckoutUpdated,
|
||||||
|
CheckoutFullyPaid,
|
||||||
|
CheckoutMetadataUpdated,
|
||||||
|
NotifyUser,
|
||||||
|
PageCreated,
|
||||||
|
PageUpdated,
|
||||||
|
PageDeleted,
|
||||||
|
PageTypeCreated,
|
||||||
|
PageTypeUpdated,
|
||||||
|
PageTypeDeleted,
|
||||||
|
PermissionGroupCreated,
|
||||||
|
PermissionGroupUpdated,
|
||||||
|
PermissionGroupDeleted,
|
||||||
|
ShippingPriceCreated,
|
||||||
|
ShippingPriceUpdated,
|
||||||
|
ShippingPriceDeleted,
|
||||||
|
ShippingZoneCreated,
|
||||||
|
ShippingZoneUpdated,
|
||||||
|
ShippingZoneDeleted,
|
||||||
|
ShippingZoneMetadataUpdated,
|
||||||
|
StaffCreated,
|
||||||
|
StaffUpdated,
|
||||||
|
StaffDeleted,
|
||||||
|
StaffSetPasswordRequested,
|
||||||
|
TransactionItemMetadataUpdated,
|
||||||
|
TranslationCreated,
|
||||||
|
TranslationUpdated,
|
||||||
|
WarehouseCreated,
|
||||||
|
WarehouseUpdated,
|
||||||
|
WarehouseDeleted,
|
||||||
|
WarehouseMetadataUpdated,
|
||||||
|
VoucherCreated,
|
||||||
|
VoucherUpdated,
|
||||||
|
VoucherDeleted,
|
||||||
|
VoucherMetadataUpdated,
|
||||||
|
VoucherCodeExportCompleted,
|
||||||
|
Observability,
|
||||||
|
ThumbnailCreated,
|
||||||
|
ShopMetadataUpdated,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, EnumString)]
|
||||||
|
//kinda annoying that in an apps manifest, the `AsyncWebhookEventType` is in SCREAMING_SNAKE_CASE,
|
||||||
|
//but when receiving saleors webhook the header `saleor-event` is in snake_case,
|
||||||
|
//have to serialize and deserialize the enum two different ways
|
||||||
|
#[serde(rename_all(deserialize = "snake_case", serialize = "SCREAMING_SNAKE_CASE"))]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum SyncWebhookEventType {
|
||||||
|
PaymentListGateways,
|
||||||
|
PaymentAuthorize,
|
||||||
|
PaymentCapture,
|
||||||
|
PaymentRefund,
|
||||||
|
PaymentVoid,
|
||||||
|
PaymentConfirm,
|
||||||
|
PaymentProcess,
|
||||||
|
CheckoutCalculateTaxes,
|
||||||
|
OrderCalculateTaxes,
|
||||||
|
TransactionChargeRequested,
|
||||||
|
TransactionRefundRequested,
|
||||||
|
TransactionCancelationRequested,
|
||||||
|
ShippingListMethodsForCheckout,
|
||||||
|
CheckoutFilterShippingMethods,
|
||||||
|
OrderFilterShippingMethods,
|
||||||
|
PaymentGatewayInitializeSession,
|
||||||
|
TransactionInitializeSession,
|
||||||
|
TransactionProcessSession,
|
||||||
|
ListStoredPaymentMethods,
|
||||||
|
StoredPaymentMethodDeleteRequested,
|
||||||
|
PaymentGatewayInitializeTokenizationSession,
|
||||||
|
PaymentMethodInitializeTokenizationSession,
|
||||||
|
PaymentMethodProcessTokenizationSession,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
#[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(Default)]
|
||||||
|
pub struct WebhookManifestBuilder {
|
||||||
|
pub webhook_manifest: WebhookManifest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebhookManifestBuilder {
|
||||||
|
pub fn set_name(mut self, name: &str) -> Self {
|
||||||
|
self.webhook_manifest.name = name.to_owned();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn set_query(mut self, query: &str) -> Self {
|
||||||
|
self.webhook_manifest.query = query.to_owned();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn add_async_event(mut self, async_event: AsyncWebhookEventType) -> Self {
|
||||||
|
if let Some(curr_events) = &mut self.webhook_manifest.async_events {
|
||||||
|
curr_events.push(async_event);
|
||||||
|
} else {
|
||||||
|
self.webhook_manifest.async_events = Some(vec![async_event]);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn add_async_events(mut self, mut async_events: Vec<AsyncWebhookEventType>) -> Self {
|
||||||
|
if let Some(curr_events) = &mut self.webhook_manifest.async_events {
|
||||||
|
curr_events.append(&mut async_events);
|
||||||
|
} else {
|
||||||
|
self.webhook_manifest.async_events = Some(async_events);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn add_sync_event(mut self, sync_event: SyncWebhookEventType) -> Self {
|
||||||
|
if let Some(curr_events) = &mut self.webhook_manifest.sync_events {
|
||||||
|
curr_events.push(sync_event);
|
||||||
|
} else {
|
||||||
|
self.webhook_manifest.sync_events = Some(vec![sync_event]);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn add_sync_events(mut self, mut sync_events: Vec<SyncWebhookEventType>) -> Self {
|
||||||
|
if let Some(curr_events) = &mut self.webhook_manifest.sync_events {
|
||||||
|
curr_events.append(&mut sync_events);
|
||||||
|
} else {
|
||||||
|
self.webhook_manifest.sync_events = Some(sync_events);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn set_target_url(mut self, url: &str) -> Self {
|
||||||
|
self.webhook_manifest.target_url = url.to_owned();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn set_is_active(mut self, active: bool) -> Self {
|
||||||
|
self.webhook_manifest.is_active = Some(active);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn build(self) -> WebhookManifest {
|
||||||
|
self.webhook_manifest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebhookManifest {
|
||||||
|
/**
|
||||||
|
* Creates defaults of name(<cargo_app_name> webhook) and target url(/api/webhooks) from config and env.
|
||||||
|
*/
|
||||||
|
pub fn new(config: &Config) -> WebhookManifestBuilder {
|
||||||
|
WebhookManifestBuilder {
|
||||||
|
webhook_manifest: WebhookManifest {
|
||||||
|
target_url: format!("{}/api/webhooks", config.app_api_base_url),
|
||||||
|
name: env!("CARGO_PKG_NAME").to_owned() + " webhook",
|
||||||
|
is_active: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
sdk/src/webhooks/utils.rs
Normal file
28
sdk/src/webhooks/utils.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use http::HeaderMap;
|
||||||
|
|
||||||
|
use crate::headers::SALEOR_EVENT_HEADER;
|
||||||
|
|
||||||
|
use super::{AsyncWebhookEventType, SyncWebhookEventType};
|
||||||
|
|
||||||
|
pub enum EitherWebhookType {
|
||||||
|
Sync(SyncWebhookEventType),
|
||||||
|
Async(AsyncWebhookEventType),
|
||||||
|
}
|
||||||
|
|
||||||
|
//header "saleor-event" can have either sync or async type, so we return enum witch has either or
|
||||||
|
pub fn get_webhook_event_type(header: &HeaderMap) -> anyhow::Result<EitherWebhookType> {
|
||||||
|
if let Some(event) = header.get(SALEOR_EVENT_HEADER) {
|
||||||
|
let event = event.to_str()?;
|
||||||
|
let s_event: Result<SyncWebhookEventType, _> = SyncWebhookEventType::try_from(event);
|
||||||
|
let a_event: Result<AsyncWebhookEventType, _> = AsyncWebhookEventType::try_from(event);
|
||||||
|
let event = match s_event {
|
||||||
|
Ok(s) => EitherWebhookType::Sync(s),
|
||||||
|
Err(_) => match a_event {
|
||||||
|
Ok(a) => EitherWebhookType::Async(a),
|
||||||
|
Err(e) => anyhow::bail!(e),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return Ok(event);
|
||||||
|
}
|
||||||
|
anyhow::bail!("Missing event type header")
|
||||||
|
}
|
45
sitemap-generator/Cargo.toml
Normal file
45
sitemap-generator/Cargo.toml
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
[package]
|
||||||
|
name = "sitemap-generator"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Djkáťo <djkatovfx@gmail.com>"]
|
||||||
|
description = "Creates and keeps Sitemap.xml uptodate with Saleor."
|
||||||
|
homepage = "https://github.com/djkato/saleor-apps-rs"
|
||||||
|
repository = "https://github.com/djkato/saleor-apps-rs"
|
||||||
|
documentation = "https://github.com/djkato/saleor-apps-rs"
|
||||||
|
keywords = ["saleor", "plugin"]
|
||||||
|
categories = ["web-programming::http-server"]
|
||||||
|
license = "PolyForm Noncommercial License 1.0.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.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"] }
|
||||||
|
surf.workspace = true
|
||||||
|
cynic = { workspace = true, features = ["http-surf"] }
|
||||||
|
cynic-codegen.workspace = true
|
||||||
|
tera = { version = "1.19.1", default-features = false }
|
||||||
|
fd-lock = "4.0.2"
|
||||||
|
quick-xml = { version = "0.31.0", features = ["serialize"] }
|
||||||
|
flate2 = "1.0.28"
|
||||||
|
tinytemplate = "1.2.1"
|
||||||
|
sitemap-rs = "0.2.1"
|
||||||
|
chrono = "0.4.34"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cynic-codegen.workspace = true
|
19
sitemap-generator/Dockerfile
Normal file
19
sitemap-generator/Dockerfile
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
FROM lukemathwalker/cargo-chef:latest as chef
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM chef AS planner
|
||||||
|
COPY ./Cargo.toml ./Cargo.lock ./
|
||||||
|
COPY ./src ./src
|
||||||
|
RUN cargo chef prepare
|
||||||
|
|
||||||
|
FROM chef AS builder
|
||||||
|
COPY --from=planner /app/recipe.json .
|
||||||
|
RUN cargo chef cook --release
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build --release
|
||||||
|
RUN mv ./target/release/saleor-app-template ./app
|
||||||
|
|
||||||
|
FROM debian:stable-slim AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/app /usr/local/bin/
|
||||||
|
ENTRYPOINT ["/usr/local/bin/app"]
|
4
sitemap-generator/README.md
Normal file
4
sitemap-generator/README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Unofficial Saleor App Template
|
||||||
|
|
||||||
|
To update the saleor schema, you can download it from [here](https://raw.githubusercontent.com/saleor/saleor/main/saleor/graphql/schema.graphql) and put into schema/schema.graphql
|
||||||
|
To generate typings for events and gql queries, use: https://generator.cynic-rs.dev/
|
7
sitemap-generator/build.rs
Normal file
7
sitemap-generator/build.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
fn main() {
|
||||||
|
cynic_codegen::register_schema("saleor")
|
||||||
|
.from_sdl_file("schema/schema.graphql")
|
||||||
|
.unwrap()
|
||||||
|
.as_default()
|
||||||
|
.unwrap();
|
||||||
|
}
|
BIN
sitemap-generator/public/logo.png
Normal file
BIN
sitemap-generator/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
36207
sitemap-generator/schema/schema.graphql
Normal file
36207
sitemap-generator/schema/schema.graphql
Normal file
File diff suppressed because it is too large
Load diff
151
sitemap-generator/src/app.rs
Normal file
151
sitemap-generator/src/app.rs
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
use anyhow::bail;
|
||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use fd_lock::RwLock;
|
||||||
|
use std::{fs::File, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use redis::{AsyncCommands, Client};
|
||||||
|
use saleor_app_sdk::{config::Config, manifest::AppManifest, SaleorApp};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::{debug, info};
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sitemaps have a limit of 10mb, so we create an index and split all paths between multiple
|
||||||
|
* sitemaps.
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub sitemap_file_products: Vec<Arc<RwLock<File>>>,
|
||||||
|
pub sitemap_file_categories: Vec<Arc<RwLock<File>>>,
|
||||||
|
pub sitemap_file_collections: Vec<Arc<RwLock<File>>>,
|
||||||
|
pub sitemap_file_pages: Vec<Arc<RwLock<File>>>,
|
||||||
|
pub sitemap_file_index: Arc<RwLock<File>>,
|
||||||
|
pub xml_cache: XmlCache,
|
||||||
|
pub saleor_app: Arc<tokio::sync::Mutex<SaleorApp>>,
|
||||||
|
pub config: Config,
|
||||||
|
pub sitemap_config: SitemapConfig,
|
||||||
|
pub manifest: AppManifest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SitemapConfig {
|
||||||
|
#[serde(rename = "sitemap_target_folder")]
|
||||||
|
pub target_folder: String,
|
||||||
|
#[serde(rename = "sitemap_product_template")]
|
||||||
|
pub product_template: String,
|
||||||
|
#[serde(rename = "sitemap_category_template")]
|
||||||
|
pub category_template: String,
|
||||||
|
#[serde(rename = "sitemap_pages_template")]
|
||||||
|
pub pages_template: String,
|
||||||
|
#[serde(rename = "sitemap_index_hostname")]
|
||||||
|
pub index_hostname: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SitemapConfig {
|
||||||
|
pub fn load() -> Result<Self, envy::Error> {
|
||||||
|
dotenvy::dotenv().unwrap();
|
||||||
|
let env = envy::from_env::<SitemapConfig>();
|
||||||
|
env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct XmlCache {
|
||||||
|
client: Client,
|
||||||
|
app_api_base_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct XmlData {
|
||||||
|
pub id: cynic::Id,
|
||||||
|
pub slug: String,
|
||||||
|
pub relations: Vec<cynic::Id>,
|
||||||
|
pub data_type: XmlDataType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
pub enum XmlDataType {
|
||||||
|
Category,
|
||||||
|
Product,
|
||||||
|
Page,
|
||||||
|
Collection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl XmlCache {
|
||||||
|
pub fn new(redis_url: &str, app_api_base_url: &str) -> anyhow::Result<Self> {
|
||||||
|
debug!("creating XmlCache...");
|
||||||
|
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: app_api_base_url.to_owned(),
|
||||||
|
}),
|
||||||
|
Err(e) => bail!("failed redis connection(XmlCache), {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all(&self, saleor_api_url: &str) -> anyhow::Result<Vec<XmlData>> {
|
||||||
|
debug!("xml data get_all()");
|
||||||
|
let mut conn = self.client.get_async_connection().await?;
|
||||||
|
let res: String = conn.get(self.prepare_key(saleor_api_url)).await?;
|
||||||
|
let cache: Vec<XmlData> = serde_json::from_str(&res)?;
|
||||||
|
|
||||||
|
info!("sucessful cache get");
|
||||||
|
|
||||||
|
Ok(cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set(&self, data: Vec<XmlData>, saleor_api_url: &str) -> anyhow::Result<()> {
|
||||||
|
debug!("xml data set(), {:?}", data);
|
||||||
|
let mut conn = self.client.get_async_connection().await?;
|
||||||
|
conn.set(
|
||||||
|
self.prepare_key(saleor_api_url),
|
||||||
|
serde_json::to_string(&data)?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
info!("sucessful cache set");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare_key(&self, saleor_api_url: &str) -> String {
|
||||||
|
let key = format!("{}:{saleor_api_url}", self.app_api_base_url);
|
||||||
|
key
|
||||||
|
}
|
||||||
|
}
|
92
sitemap-generator/src/main.rs
Normal file
92
sitemap-generator/src/main.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
mod app;
|
||||||
|
mod queries;
|
||||||
|
mod routes;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use fd_lock::RwLock;
|
||||||
|
use saleor_app_sdk::{
|
||||||
|
config::Config,
|
||||||
|
manifest::{AppManifest, AppPermission},
|
||||||
|
webhooks::{AsyncWebhookEventType, WebhookManifest},
|
||||||
|
SaleorApp,
|
||||||
|
};
|
||||||
|
use std::{fs::File, sync::Arc};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{trace_to_std, AppState, SitemapConfig, XmlCache},
|
||||||
|
queries::event_subjects_updated::EVENTS_QUERY,
|
||||||
|
routes::create_routes,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let config = Config::load()?;
|
||||||
|
trace_to_std(&config);
|
||||||
|
let sitemap_config = SitemapConfig::load()?;
|
||||||
|
debug!("Creating configs...");
|
||||||
|
|
||||||
|
let saleor_app = SaleorApp::new(&config)?;
|
||||||
|
|
||||||
|
debug!("Creating saleor App...");
|
||||||
|
|
||||||
|
let app_manifest = AppManifest::new(&config)
|
||||||
|
.add_permissions(vec![
|
||||||
|
AppPermission::ManageProducts,
|
||||||
|
AppPermission::ManagePages,
|
||||||
|
])
|
||||||
|
.add_webhook(
|
||||||
|
WebhookManifest::new(&config)
|
||||||
|
.set_query(EVENTS_QUERY)
|
||||||
|
.add_async_events(vec![
|
||||||
|
AsyncWebhookEventType::ProductCreated,
|
||||||
|
AsyncWebhookEventType::ProductUpdated,
|
||||||
|
AsyncWebhookEventType::ProductDeleted,
|
||||||
|
AsyncWebhookEventType::CategoryCreated,
|
||||||
|
AsyncWebhookEventType::CategoryUpdated,
|
||||||
|
AsyncWebhookEventType::CategoryDeleted,
|
||||||
|
AsyncWebhookEventType::PageCreated,
|
||||||
|
AsyncWebhookEventType::PageUpdated,
|
||||||
|
AsyncWebhookEventType::PageDeleted,
|
||||||
|
AsyncWebhookEventType::CollectionCreated,
|
||||||
|
AsyncWebhookEventType::CollectionUpdated,
|
||||||
|
AsyncWebhookEventType::CollectionDeleted,
|
||||||
|
])
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
debug!("Created AppManifest...");
|
||||||
|
|
||||||
|
debug!("{}/sitemap_index.xml.gz", sitemap_config.target_folder);
|
||||||
|
let app_state = AppState {
|
||||||
|
sitemap_file_index: Arc::new(RwLock::new(File::options().write(true).create(true).open(
|
||||||
|
format!("{}/sitemap_index.xml", sitemap_config.target_folder),
|
||||||
|
)?)),
|
||||||
|
sitemap_file_products: vec![],
|
||||||
|
sitemap_file_categories: vec![],
|
||||||
|
sitemap_file_collections: vec![],
|
||||||
|
sitemap_file_pages: vec![],
|
||||||
|
sitemap_config,
|
||||||
|
xml_cache: XmlCache::new(&config.apl_url, &config.app_api_base_url)?,
|
||||||
|
manifest: app_manifest,
|
||||||
|
config: config.clone(),
|
||||||
|
saleor_app: Arc::new(Mutex::new(saleor_app)),
|
||||||
|
};
|
||||||
|
debug!("Created AppState...");
|
||||||
|
let app = create_routes(app_state);
|
||||||
|
let listener = tokio::net::TcpListener::bind(
|
||||||
|
&config
|
||||||
|
.app_api_base_url
|
||||||
|
.split("//")
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.get(1)
|
||||||
|
.context("APP_API_BASE_URL invalid format")?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
tracing::debug!("listening on {}", listener.local_addr().unwrap());
|
||||||
|
match axum::serve(listener, app).await {
|
||||||
|
Ok(o) => Ok(o),
|
||||||
|
Err(e) => anyhow::bail!(e),
|
||||||
|
}
|
||||||
|
}
|
245
sitemap-generator/src/queries/event_subjects_updated.rs
Normal file
245
sitemap-generator/src/queries/event_subjects_updated.rs
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[cynic::schema("saleor")]
|
||||||
|
mod schema {}
|
||||||
|
|
||||||
|
pub const EVENTS_QUERY: &str = r#"
|
||||||
|
subscription QueryProductsChanged {
|
||||||
|
event {
|
||||||
|
... on ProductUpdated {
|
||||||
|
product {
|
||||||
|
...BaseProduct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on ProductCreated {
|
||||||
|
product {
|
||||||
|
...BaseProduct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on ProductDeleted {
|
||||||
|
product {
|
||||||
|
...BaseProduct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on CategoryCreated {
|
||||||
|
category {
|
||||||
|
...BaseCategory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on CategoryUpdated {
|
||||||
|
category {
|
||||||
|
...BaseCategory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on CategoryDeleted {
|
||||||
|
category {
|
||||||
|
...BaseCategory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on PageCreated {
|
||||||
|
page {
|
||||||
|
slug
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on PageUpdated {
|
||||||
|
page {
|
||||||
|
slug
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on PageDeleted {
|
||||||
|
page {
|
||||||
|
slug
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on CollectionCreated {
|
||||||
|
collection {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on CollectionUpdated {
|
||||||
|
collection {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on CollectionDeleted {
|
||||||
|
collection {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment BaseCategory on Category {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
products(first: 100) {
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment BaseProduct on Product {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
category {
|
||||||
|
slug
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
#[cynic(graphql_type = "Subscription")]
|
||||||
|
pub struct QueryProductsChanged {
|
||||||
|
pub event: Option<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug, Serialize)]
|
||||||
|
pub struct ProductUpdated {
|
||||||
|
pub product: Option<Product>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductDeleted {
|
||||||
|
pub product: Option<Product>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductCreated {
|
||||||
|
pub product: Option<Product>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug, Serialize)]
|
||||||
|
pub struct Product {
|
||||||
|
pub id: cynic::Id,
|
||||||
|
pub slug: String,
|
||||||
|
pub category: Option<Category>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct PageUpdated {
|
||||||
|
pub page: Option<Page>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct PageDeleted {
|
||||||
|
pub page: Option<Page>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct PageCreated {
|
||||||
|
pub page: Option<Page>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct Page {
|
||||||
|
pub slug: String,
|
||||||
|
pub id: cynic::Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct CollectionUpdated {
|
||||||
|
pub collection: Option<Collection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct CollectionDeleted {
|
||||||
|
pub collection: Option<Collection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct CollectionCreated {
|
||||||
|
pub collection: Option<Collection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct Collection {
|
||||||
|
pub id: cynic::Id,
|
||||||
|
pub slug: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct CategoryUpdated {
|
||||||
|
pub category: Option<Category2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct CategoryDeleted {
|
||||||
|
pub category: Option<Category2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct CategoryCreated {
|
||||||
|
pub category: Option<Category2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug, Serialize)]
|
||||||
|
pub struct Category {
|
||||||
|
pub slug: String,
|
||||||
|
pub id: cynic::Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
#[cynic(graphql_type = "Category")]
|
||||||
|
pub struct Category2 {
|
||||||
|
pub id: cynic::Id,
|
||||||
|
pub slug: String,
|
||||||
|
#[arguments(first: 100)]
|
||||||
|
pub products: Option<ProductCountableConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductCountableConnection {
|
||||||
|
pub page_info: PageInfo,
|
||||||
|
pub edges: Vec<ProductCountableEdge>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductCountableEdge {
|
||||||
|
pub node: Product2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
#[cynic(graphql_type = "Product")]
|
||||||
|
pub struct Product2 {
|
||||||
|
pub id: cynic::Id,
|
||||||
|
pub slug: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct PageInfo {
|
||||||
|
pub end_cursor: Option<String>,
|
||||||
|
pub has_next_page: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::InlineFragments, Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
ProductUpdated(ProductUpdated),
|
||||||
|
ProductCreated(ProductCreated),
|
||||||
|
ProductDeleted(ProductDeleted),
|
||||||
|
CategoryCreated(CategoryCreated),
|
||||||
|
CategoryUpdated(CategoryUpdated),
|
||||||
|
CategoryDeleted(CategoryDeleted),
|
||||||
|
PageCreated(PageCreated),
|
||||||
|
PageUpdated(PageUpdated),
|
||||||
|
PageDeleted(PageDeleted),
|
||||||
|
CollectionCreated(CollectionCreated),
|
||||||
|
CollectionUpdated(CollectionUpdated),
|
||||||
|
CollectionDeleted(CollectionDeleted),
|
||||||
|
#[cynic(fallback)]
|
||||||
|
Unknown,
|
||||||
|
}
|
2
sitemap-generator/src/queries/mod.rs
Normal file
2
sitemap-generator/src/queries/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod event_subjects_updated;
|
||||||
|
pub mod product_metadata_update;
|
75
sitemap-generator/src/queries/product_metadata_update.rs
Normal file
75
sitemap-generator/src/queries/product_metadata_update.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
#[cynic::schema("saleor")]
|
||||||
|
mod schema {}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryVariables, Debug)]
|
||||||
|
pub struct UpdateProductMetadataVariables<'a> {
|
||||||
|
pub metadata: Option<Vec<MetadataInput<'a>>>,
|
||||||
|
pub product_id: &'a cynic::Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
#[cynic(
|
||||||
|
graphql_type = "Mutation",
|
||||||
|
variables = "UpdateProductMetadataVariables"
|
||||||
|
)]
|
||||||
|
pub struct UpdateProductMetadata {
|
||||||
|
#[arguments(id: $product_id, input: { metadata: $metadata })]
|
||||||
|
pub product_update: Option<ProductUpdate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductUpdate {
|
||||||
|
pub errors: Vec<ProductError>,
|
||||||
|
pub product: Option<Product>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct Product {
|
||||||
|
pub id: cynic::Id,
|
||||||
|
pub metadata: Vec<MetadataItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct ProductError {
|
||||||
|
pub field: Option<String>,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub code: ProductErrorCode,
|
||||||
|
pub attributes: Option<Vec<cynic::Id>>,
|
||||||
|
pub values: Option<Vec<cynic::Id>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::QueryFragment, Debug)]
|
||||||
|
pub struct MetadataItem {
|
||||||
|
pub key: String,
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::Enum, Clone, Copy, Debug)]
|
||||||
|
pub enum ProductErrorCode {
|
||||||
|
AlreadyExists,
|
||||||
|
AttributeAlreadyAssigned,
|
||||||
|
AttributeCannotBeAssigned,
|
||||||
|
AttributeVariantsDisabled,
|
||||||
|
MediaAlreadyAssigned,
|
||||||
|
DuplicatedInputItem,
|
||||||
|
GraphqlError,
|
||||||
|
Invalid,
|
||||||
|
InvalidPrice,
|
||||||
|
ProductWithoutCategory,
|
||||||
|
NotProductsImage,
|
||||||
|
NotProductsVariant,
|
||||||
|
NotFound,
|
||||||
|
Required,
|
||||||
|
Unique,
|
||||||
|
VariantNoDigitalContent,
|
||||||
|
CannotManageProductWithoutVariant,
|
||||||
|
ProductNotAssignedToChannel,
|
||||||
|
UnsupportedMediaProvider,
|
||||||
|
PreorderVariantCannotBeDeactivated,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(cynic::InputObject, Debug)]
|
||||||
|
pub struct MetadataInput<'a> {
|
||||||
|
pub key: &'a str,
|
||||||
|
pub value: &'a str,
|
||||||
|
}
|
8
sitemap-generator/src/routes/manifest.rs
Normal file
8
sitemap-generator/src/routes/manifest.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
use axum::{extract::State, Json};
|
||||||
|
use saleor_app_sdk::{manifest::AppManifest};
|
||||||
|
|
||||||
|
use crate::app::{AppError, AppState};
|
||||||
|
|
||||||
|
pub async fn manifest(State(state): State<AppState>) -> Result<Json<AppManifest>, AppError> {
|
||||||
|
Ok(Json(state.manifest))
|
||||||
|
}
|
38
sitemap-generator/src/routes/mod.rs
Normal file
38
sitemap-generator/src/routes/mod.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use axum::{
|
||||||
|
handler::HandlerWithoutStateExt,
|
||||||
|
http::StatusCode,
|
||||||
|
routing::{any, get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
|
use crate::app::AppState;
|
||||||
|
|
||||||
|
pub mod manifest;
|
||||||
|
pub mod register;
|
||||||
|
pub mod webhooks;
|
||||||
|
use manifest::manifest;
|
||||||
|
use register::register;
|
||||||
|
use webhooks::webhooks;
|
||||||
|
|
||||||
|
pub fn create_routes(state: AppState) -> Router {
|
||||||
|
async fn handle_404() -> (StatusCode, &'static str) {
|
||||||
|
(StatusCode::NOT_FOUND, "Not found")
|
||||||
|
}
|
||||||
|
let service = handle_404.into_service();
|
||||||
|
//TODO : Fix this relative path issue in workspaces
|
||||||
|
let serve_dir = ServeDir::new("./sitemap-generator/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))
|
||||||
|
.route("/api/webhooks", any(webhooks))
|
||||||
|
.with_state(state)
|
||||||
|
}
|
40
sitemap-generator/src/routes/register.rs
Normal file
40
sitemap-generator/src/routes/register.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
use anyhow::Context;
|
||||||
|
use axum::{
|
||||||
|
extract::Json,
|
||||||
|
extract::State,
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
};
|
||||||
|
use saleor_app_sdk::{AuthData, AuthToken};
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
use crate::app::{AppError, AppState};
|
||||||
|
|
||||||
|
pub async fn register(
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(auth_token): Json<AuthToken>,
|
||||||
|
) -> 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.manifest.id,
|
||||||
|
saleor_api_url: saleor_api_url.clone(),
|
||||||
|
};
|
||||||
|
app.apl.set(auth_data).await?;
|
||||||
|
|
||||||
|
info!("registered app for{:?}", &saleor_api_url);
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
221
sitemap-generator/src/routes/webhooks.rs
Normal file
221
sitemap-generator/src/routes/webhooks.rs
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
use std::{fs::File, io::Write};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
};
|
||||||
|
use chrono::TimeZone;
|
||||||
|
use fd_lock::RwLock;
|
||||||
|
use flate2::{write::GzEncoder, Compression};
|
||||||
|
use saleor_app_sdk::{
|
||||||
|
headers::SALEOR_API_URL_HEADER,
|
||||||
|
webhooks::{
|
||||||
|
utils::{get_webhook_event_type, EitherWebhookType},
|
||||||
|
AsyncWebhookEventType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use sitemap_rs::{
|
||||||
|
url::{ChangeFrequency, Url},
|
||||||
|
url_set::UrlSet,
|
||||||
|
};
|
||||||
|
use tinytemplate::TinyTemplate;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{AppError, AppState, XmlData, XmlDataType},
|
||||||
|
queries::event_subjects_updated::{
|
||||||
|
Category, CategoryUpdated, CollectionUpdated, PageUpdated, Product, ProductUpdated,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn webhooks(
|
||||||
|
headers: HeaderMap,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
data: String,
|
||||||
|
) -> Result<StatusCode, AppError> {
|
||||||
|
debug!("/api/webhooks");
|
||||||
|
debug!("req: {:?}", data);
|
||||||
|
debug!("headers: {:?}", headers);
|
||||||
|
|
||||||
|
let url = headers
|
||||||
|
.get(SALEOR_API_URL_HEADER)
|
||||||
|
.context("missing saleor api url header")?
|
||||||
|
.to_str()?
|
||||||
|
.to_owned();
|
||||||
|
let event_type = get_webhook_event_type(&headers)?;
|
||||||
|
match event_type {
|
||||||
|
EitherWebhookType::Async(a) => match a {
|
||||||
|
AsyncWebhookEventType::ProductUpdated
|
||||||
|
| AsyncWebhookEventType::ProductCreated
|
||||||
|
| AsyncWebhookEventType::ProductDeleted => {
|
||||||
|
let product: ProductUpdated = serde_json::from_str(&data)?;
|
||||||
|
spawn(async move { update_sitemap_product(product, &url, state).await });
|
||||||
|
}
|
||||||
|
AsyncWebhookEventType::CategoryCreated
|
||||||
|
| AsyncWebhookEventType::CategoryUpdated
|
||||||
|
| AsyncWebhookEventType::CategoryDeleted => {
|
||||||
|
let category: CategoryUpdated = serde_json::from_str(&data)?;
|
||||||
|
spawn(async move { update_sitemap_category(category, &url, state).await });
|
||||||
|
}
|
||||||
|
AsyncWebhookEventType::PageCreated
|
||||||
|
| AsyncWebhookEventType::PageUpdated
|
||||||
|
| AsyncWebhookEventType::PageDeleted => {
|
||||||
|
let page: PageUpdated = serde_json::from_str(&data)?;
|
||||||
|
spawn(async move { update_sitemap_page(page, &url, state).await });
|
||||||
|
}
|
||||||
|
AsyncWebhookEventType::CollectionCreated
|
||||||
|
| AsyncWebhookEventType::CollectionUpdated
|
||||||
|
| AsyncWebhookEventType::CollectionDeleted => {
|
||||||
|
let collection: CollectionUpdated = serde_json::from_str(&data)?;
|
||||||
|
spawn(async move { update_sitemap_collection(collection, &url, state).await });
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("got webhooks!");
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_sitemap_product(
|
||||||
|
product: ProductUpdated,
|
||||||
|
saleor_api_url: &str,
|
||||||
|
state: AppState,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
debug!("Product got changed!, {:?}", &product);
|
||||||
|
if let Some(product) = product.product {
|
||||||
|
// Update or add the product
|
||||||
|
// TODO: when there are no keys, this will error. Work around that
|
||||||
|
let mut xml_data = state.xml_cache.get_all(saleor_api_url).await?;
|
||||||
|
let mut new_data = vec![];
|
||||||
|
for x in xml_data.iter_mut() {
|
||||||
|
if x.id == product.id && x.data_type == XmlDataType::Product {
|
||||||
|
debug!(
|
||||||
|
"changed product {} found in xml_data, updating...",
|
||||||
|
product.slug
|
||||||
|
);
|
||||||
|
x.slug = product.slug.clone();
|
||||||
|
x.relations = match &product.category {
|
||||||
|
Some(c) => vec![c.id.clone()],
|
||||||
|
None => vec![],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"changed product {} not found in xml_data, adding...",
|
||||||
|
product.slug
|
||||||
|
);
|
||||||
|
new_data.push(XmlData {
|
||||||
|
relations: match &product.category {
|
||||||
|
Some(c) => vec![c.id.clone()],
|
||||||
|
None => vec![],
|
||||||
|
},
|
||||||
|
id: product.id.clone(),
|
||||||
|
data_type: XmlDataType::Product,
|
||||||
|
slug: product.slug.clone(),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
xml_data.append(&mut new_data);
|
||||||
|
debug!("new xml_data : {:?}", &xml_data);
|
||||||
|
//create urls
|
||||||
|
let mut urls = vec![];
|
||||||
|
for x in xml_data.iter() {
|
||||||
|
if x.data_type == XmlDataType::Product {
|
||||||
|
let mut tt = TinyTemplate::new();
|
||||||
|
tt.add_template("product_url", &state.sitemap_config.product_template)?;
|
||||||
|
let context = ProductUpdated {
|
||||||
|
product: Some(Product {
|
||||||
|
id: x.id.clone(),
|
||||||
|
slug: x.slug.clone(),
|
||||||
|
category: match x.relations.is_empty() {
|
||||||
|
false => {
|
||||||
|
let data = xml_data
|
||||||
|
.iter()
|
||||||
|
.find(|d| x.relations.iter().find(|r| **r == d.id).is_some());
|
||||||
|
match data {
|
||||||
|
Some(d) => Some(Category {
|
||||||
|
slug: d.slug.clone(),
|
||||||
|
id: d.id.clone(),
|
||||||
|
}),
|
||||||
|
None => Some(Category {
|
||||||
|
slug: "unknown".to_owned(),
|
||||||
|
id: cynic::Id::new("unknown".to_owned()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true => Some(Category {
|
||||||
|
slug: "unknown".to_owned(),
|
||||||
|
id: cynic::Id::new("unknown".to_owned()),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
urls.push(tt.render("product_url", &context)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("new urls:{:?}", &urls);
|
||||||
|
|
||||||
|
write_xml(
|
||||||
|
urls,
|
||||||
|
RwLock::new(
|
||||||
|
File::options()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.open("./sitemap.xml")?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
error!("Failed to update product, e: {:?}", product);
|
||||||
|
anyhow::bail!("product not present in body");
|
||||||
|
}
|
||||||
|
debug!("Sitemap updated");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_sitemap_category(
|
||||||
|
category: CategoryUpdated,
|
||||||
|
saleor_api_url: &str,
|
||||||
|
state: AppState,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
async fn update_sitemap_collection(
|
||||||
|
collection: CollectionUpdated,
|
||||||
|
saleor_api_url: &str,
|
||||||
|
state: AppState,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
async fn update_sitemap_page(
|
||||||
|
page: PageUpdated,
|
||||||
|
saleor_api_url: &str,
|
||||||
|
state: AppState,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_xml(urls: Vec<String>, mut file: RwLock<File>) -> anyhow::Result<()> {
|
||||||
|
let mut f = file.write()?;
|
||||||
|
let mut sitemap_urls: Vec<Url> = vec![];
|
||||||
|
for url in urls {
|
||||||
|
sitemap_urls.push(
|
||||||
|
Url::builder(url)
|
||||||
|
.change_frequency(ChangeFrequency::Weekly)
|
||||||
|
.last_modified(chrono::offset::Utc::now().fixed_offset())
|
||||||
|
.build()?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let url_set: UrlSet = UrlSet::new(sitemap_urls)?;
|
||||||
|
debug!("Writing xml into file");
|
||||||
|
f.set_len(0)?;
|
||||||
|
let mut buf = Vec::<u8>::new();
|
||||||
|
url_set.write(&mut buf)?;
|
||||||
|
f.write_all(&buf)?;
|
||||||
|
//let mut gzip = GzEncoder::new(f, Compression::default());
|
||||||
|
todo!()
|
||||||
|
}
|
Loading…
Reference in a new issue