Feat: add Docker+wasm examples (#309)
* Add a Docker+wasm sample application featuring a WasmEdge-based microservice, a MySQL database and an Nginx web server for frontend UI files. Signed-off-by: Michael Yuan <michael@secondstate.io> * Add a logo to indicate Docker+wasm compatibility. Add project descriptions to README. Signed-off-by: Michael Yuan <michael@secondstate.io> * Add the example for WasmEdge + Kafka / Redpanda + MySQL application to take messages from a queue and save into a database table. Signed-off-by: Michael Yuan <michael@secondstate.io> * Add a SVG icon to indicate Docker + Wasm req Signed-off-by: Michael Yuan <michael@michaelyuan.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update the docker compose files for the new Docker Desktop release Signed-off-by: Michael Yuan <michael@secondstate.io> * Use the correct platform to be compatible with Docker Desktop 4.15 Signed-off-by: Michael Yuan <michael@secondstate.io> * Update README.md Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-kafka-mysql/README.md Signed-off-by: Michael Yuan <michael@michaelyuan.com> Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-kafka-mysql/README.md Signed-off-by: Michael Yuan <michael@michaelyuan.com> Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-kafka-mysql/etl/Dockerfile Signed-off-by: Michael Yuan <michael@michaelyuan.com> Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-mysql-nginx/README.md Signed-off-by: Michael Yuan <michael@michaelyuan.com> Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-mysql-nginx/README.md Signed-off-by: Michael Yuan <michael@michaelyuan.com> Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-mysql-nginx/README.md Signed-off-by: Michael Yuan <michael@michaelyuan.com> Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-mysql-nginx/README.md Signed-off-by: Michael Yuan <michael@michaelyuan.com> Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-mysql-nginx/README.md Signed-off-by: Michael Yuan <michael@michaelyuan.com> Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-mysql-nginx/README.md Signed-off-by: Michael Yuan <michael@michaelyuan.com> Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-mysql-nginx/README.md Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-mysql-nginx/README.md Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-mysql-nginx/README.md Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Update wasmedge-mysql-nginx/README.md Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> * Change the Nginx port to the default non-privileged 8090 Signed-off-by: Michael Yuan <michael@secondstate.io> * My apologies. Need to correct the syntax for the Nginx port 8090. Signed-off-by: Michael Yuan <michael@secondstate.io> * Remove commented lines Signed-off-by: Michael Yuan <michael@secondstate.io> * Change wasi/wasm32 to wasi/wasm to conform with the latest spec Signed-off-by: Michael Yuan <michael@secondstate.io> * Update README.md Co-authored-by: Michael Irwin <mikesir87@gmail.com> Signed-off-by: Michael Yuan <michael@michaelyuan.com> Signed-off-by: Michael Yuan <michael@secondstate.io> Signed-off-by: Michael Yuan <michael@michaelyuan.com> Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> Co-authored-by: Michael Irwin <mikesir87@gmail.com>pull/319/head^2
parent
6f15838d24
commit
e6b1d2755f
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,36 @@ |
||||
services: |
||||
redpanda: |
||||
image: docker.redpanda.com/vectorized/redpanda:v22.2.2 |
||||
command: |
||||
- redpanda start |
||||
- --smp 1 |
||||
- --overprovisioned |
||||
- --node-id 0 |
||||
- --kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 |
||||
- --advertise-kafka-addr PLAINTEXT://redpanda:29092,OUTSIDE://redpanda:9092 |
||||
- --pandaproxy-addr 0.0.0.0:8082 |
||||
- --advertise-pandaproxy-addr localhost:8082 |
||||
ports: |
||||
- 8081:8081 |
||||
- 8082:8082 |
||||
- 9092:9092 |
||||
- 9644:9644 |
||||
- 29092:29092 |
||||
volumes: |
||||
- ./kafka:/app |
||||
etl: |
||||
image: etl-kafka |
||||
platform: wasi/wasm |
||||
build: |
||||
context: etl |
||||
environment: |
||||
DATABASE_URL: mysql://root:whalehello@db:3306/mysql |
||||
KAFKA_URL: kafka://redpanda:9092/order |
||||
RUST_BACKTRACE: full |
||||
RUST_LOG: info |
||||
restart: unless-stopped |
||||
runtime: io.containerd.wasmedge.v1 |
||||
db: |
||||
image: mariadb:10.9 |
||||
environment: |
||||
MYSQL_ROOT_PASSWORD: whalehello |
@ -0,0 +1,117 @@ |
||||
# Compose sample application |
||||
|
||||
![Compatible with Docker+Wasm](../icon_wasm.svg) |
||||
|
||||
This sample demonstrates a WebAssembly (Wasm) microservice written in Rust. It subscribes to a Kafka queue topic on a Redpanda server, and then transforms and saves each message into a MySQL (MariaDB) database table. The microservice is compiled into Wasm and runs in the WasmEdge runtime, which is a secure and lightweight alternative to natively compiled Rust apps in Linux containers. |
||||
|
||||
## Use with Docker Development Environments |
||||
|
||||
You will need a version of Docker Desktop or Docker CLI with Wasm support. |
||||
|
||||
* [Install Docker Desktop + Wasm (Beta)](https://docs.docker.com/desktop/wasm/) |
||||
* [Install Docker CLI + Wasm](https://github.com/chris-crone/wasm-day-na-22/tree/main/server) |
||||
|
||||
## WasmEdge server with Redpanda and MySQL database |
||||
|
||||
Project structure: |
||||
|
||||
``` |
||||
. |
||||
+-- compose.yml |
||||
|-- etl |
||||
|-- Dockerfile |
||||
|-- Cargo.toml |
||||
+-- src |
||||
|-- main.rs |
||||
|-- kafka |
||||
|-- order.json |
||||
|-- db |
||||
|-- db-password.txt |
||||
``` |
||||
|
||||
The [compose.yml](compose.yml) is as follows. |
||||
|
||||
```yaml |
||||
services: |
||||
redpanda: |
||||
image: docker.redpanda.com/vectorized/redpanda:v22.2.2 |
||||
command: |
||||
- redpanda start |
||||
- --smp 1 |
||||
- --overprovisioned |
||||
- --node-id 0 |
||||
- --kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 |
||||
- --advertise-kafka-addr PLAINTEXT://redpanda:29092,OUTSIDE://redpanda:9092 |
||||
- --pandaproxy-addr 0.0.0.0:8082 |
||||
- --advertise-pandaproxy-addr localhost:8082 |
||||
ports: |
||||
- 8081:8081 |
||||
- 8082:8082 |
||||
- 9092:9092 |
||||
- 9644:9644 |
||||
- 29092:29092 |
||||
volumes: |
||||
- ./kafka:/app |
||||
|
||||
etl: |
||||
image: etl-kafka |
||||
build: |
||||
context: etl |
||||
platforms: |
||||
- wasi/wasm32 |
||||
environment: |
||||
DATABASE_URL: mysql://root:whalehello@db:3306/mysql |
||||
KAFKA_URL: kafka://redpanda:9092/order |
||||
RUST_BACKTRACE: full |
||||
RUST_LOG: info |
||||
restart: unless-stopped |
||||
runtime: io.containerd.wasmedge.v1 |
||||
|
||||
db: |
||||
image: mariadb:10.9 |
||||
environment: |
||||
MYSQL_ROOT_PASSWORD: whalehello |
||||
``` |
||||
|
||||
The compose file defines an application with three services `redpanda`, `etl` and `db`. The `redpanda` service is a Kafka-compatible messaging server that produces messages in a queue topic. The `etl` service, in the WasmEdge container that subscribes to the queue topic and receives incoming messages. Each incoming message is parsed and stored in the `db` MySQL (MariaDB) database server. |
||||
|
||||
## Deploy with docker compose |
||||
|
||||
```bash |
||||
$ docker compose up -d |
||||
... |
||||
â ż Network wasmedge-kafka-mysql_default Created 0.1s |
||||
â ż Container wasmedge-kafka-mysql-redpanda-1 Created 0.3s |
||||
â ż Container wasmedge-kafka-mysql-etl-1 Created 0.3s |
||||
â ż Container wasmedge-kafka-mysql-db-1 Created 0.3s |
||||
``` |
||||
|
||||
## Expected result |
||||
|
||||
```bash |
||||
$ docker compose ps |
||||
NAME COMMAND SERVICE STATUS PORTS |
||||
wasmedge-kafka-mysql-db-1 "docker-entrypoint.s…" db running 3306/tcp |
||||
wasmedge-kafka-mysql-etl-1 "kafka.wasm" etl running |
||||
wasmedge-kafka-mysql-redpanda-1 "/entrypoint.sh 'red…" redpanda running 0.0.0.0:8081-8082->8081-8082/tcp, :::8081-8082->8081-8082/tcp, 0.0.0.0:9092->9092/tcp, :::9092->9092/tcp, 0.0.0.0:9644->9644/tcp, :::9644->9644/tcp, 0.0.0.0:29092->29092/tcp, :::29092->29092/tcp |
||||
``` |
||||
|
||||
After the application starts, |
||||
log into the Redpanda container and send a message to the queue topic `order` as follows. |
||||
|
||||
```bash |
||||
$ docker compose exec redpanda /bin/bash |
||||
redpanda@1add2615774b:/$ cd /app |
||||
redpanda@1add2615774b:/app$ cat order.json | rpk topic produce order |
||||
Produced to partition 0 at offset 0 with timestamp 1667922788523. |
||||
``` |
||||
|
||||
To see the data in the database container, you can use the following commands. |
||||
|
||||
```bash |
||||
$ docker compose exec db /bin/bash |
||||
root@c97c472db02e:/# mysql -u root -pwhalehello mysql |
||||
mysql> select * from orders; |
||||
... ... |
||||
``` |
||||
|
@ -0,0 +1,36 @@ |
||||
services: |
||||
redpanda: |
||||
image: docker.redpanda.com/vectorized/redpanda:v22.2.2 |
||||
command: |
||||
- redpanda start |
||||
- --smp 1 |
||||
- --overprovisioned |
||||
- --node-id 0 |
||||
- --kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 |
||||
- --advertise-kafka-addr PLAINTEXT://redpanda:29092,OUTSIDE://redpanda:9092 |
||||
- --pandaproxy-addr 0.0.0.0:8082 |
||||
- --advertise-pandaproxy-addr localhost:8082 |
||||
ports: |
||||
- 8081:8081 |
||||
- 8082:8082 |
||||
- 9092:9092 |
||||
- 9644:9644 |
||||
- 29092:29092 |
||||
volumes: |
||||
- ./kafka:/app |
||||
etl: |
||||
image: etl-kafka |
||||
platform: wasi/wasm |
||||
build: |
||||
context: etl |
||||
environment: |
||||
DATABASE_URL: mysql://root:whalehello@db:3306/mysql |
||||
KAFKA_URL: kafka://redpanda:9092/order |
||||
RUST_BACKTRACE: full |
||||
RUST_LOG: info |
||||
restart: unless-stopped |
||||
runtime: io.containerd.wasmedge.v1 |
||||
db: |
||||
image: mariadb:10.9 |
||||
environment: |
||||
MYSQL_ROOT_PASSWORD: whalehello |
@ -0,0 +1 @@ |
||||
whalehello |
@ -0,0 +1,17 @@ |
||||
[package] |
||||
name = "kafka" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[dependencies] |
||||
anyhow = "1.0.65" |
||||
mega_etl = {git = "https://github.com/second-state/MEGA.git"} |
||||
tokio_wasi = {version = '1.21', features = ["rt", "macros"]} |
||||
env_logger = "0.9" |
||||
log = "0.4" |
||||
serde = { version = "1.0", features = ["derive"] } |
||||
serde_json = "1.0" |
||||
http_req_wasi = "0.10" |
||||
lazy_static = "1.4.0" |
@ -0,0 +1,27 @@ |
||||
# syntax=docker/dockerfile:1 |
||||
FROM --platform=$BUILDPLATFORM rust:1.64 AS buildbase |
||||
RUN <<EOT bash |
||||
set -ex |
||||
apt-get update |
||||
apt-get install -y \ |
||||
git \ |
||||
clang |
||||
rustup target add wasm32-wasi |
||||
EOT |
||||
# This line installs WasmEdge including the AOT compiler |
||||
RUN curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash |
||||
|
||||
FROM buildbase AS build |
||||
COPY Cargo.toml . |
||||
COPY src ./src |
||||
# Build the Wasm binary |
||||
RUN --mount=type=cache,target=/usr/local/cargo/git/db \ |
||||
--mount=type=cache,target=/usr/local/cargo/registry/cache \ |
||||
--mount=type=cache,target=/usr/local/cargo/registry/index \ |
||||
cargo build --target wasm32-wasi --release |
||||
# This line builds the AOT Wasm binary |
||||
RUN /root/.wasmedge/bin/wasmedgec target/wasm32-wasi/release/kafka.wasm kafka.wasm |
||||
|
||||
FROM scratch |
||||
ENTRYPOINT [ "kafka.wasm" ] |
||||
COPY --link --from=build /kafka.wasm /kafka.wasm |
@ -0,0 +1,58 @@ |
||||
use mega_etl::{async_trait, Pipe, Transformer, TransformerError, TransformerResult}; |
||||
|
||||
use serde::{Deserialize, Serialize}; |
||||
#[derive(Serialize, Deserialize, Debug)] |
||||
struct Order { |
||||
order_id: i32, |
||||
product_id: i32, |
||||
quantity: i32, |
||||
amount: f32, |
||||
shipping: f32, |
||||
tax: f32, |
||||
shipping_address: String, |
||||
} |
||||
|
||||
#[async_trait] |
||||
impl Transformer for Order { |
||||
async fn transform(inbound_data: &Vec<u8>) -> TransformerResult<Vec<String>> { |
||||
let s = std::str::from_utf8(&inbound_data) |
||||
.map_err(|e| TransformerError::Custom(e.to_string()))?; |
||||
let order: Order = serde_json::from_str(String::from(s).as_str()) |
||||
.map_err(|e| TransformerError::Custom(e.to_string()))?; |
||||
log::info!("{:?}", &order); |
||||
let mut ret = vec![]; |
||||
let sql_string = format!( |
||||
r"INSERT INTO orders VALUES ({:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, CURRENT_TIMESTAMP);", |
||||
order.order_id, |
||||
order.product_id, |
||||
order.quantity, |
||||
order.amount, |
||||
order.shipping, |
||||
order.tax, |
||||
order.shipping_address, |
||||
); |
||||
dbg!(sql_string.clone()); |
||||
ret.push(sql_string); |
||||
Ok(ret) |
||||
} |
||||
|
||||
async fn init() -> TransformerResult<String> { |
||||
Ok(String::from( |
||||
r"CREATE TABLE IF NOT EXISTS orders (order_id INT, product_id INT, quantity INT, amount FLOAT, shipping FLOAT, tax FLOAT, shipping_address VARCHAR(50), date_registered TIMESTAMP DEFAULT CURRENT_TIMESTAMP);", |
||||
)) |
||||
} |
||||
} |
||||
|
||||
#[tokio::main(flavor = "current_thread")] |
||||
async fn main() -> anyhow::Result<()> { |
||||
env_logger::init(); |
||||
|
||||
// can use builder later
|
||||
let database_uri = std::env::var("DATABASE_URL")?; |
||||
let kafka_uri = std::env::var("KAFKA_URL")?; |
||||
let mut pipe = Pipe::new(database_uri, kafka_uri).await; |
||||
|
||||
// This is async because this calls the async transform() function in Order
|
||||
pipe.start::<Order>().await?; |
||||
Ok(()) |
||||
} |
@ -0,0 +1 @@ |
||||
{"order_id": 1,"product_id": 12,"quantity": 2,"amount": 56.0,"shipping": 15.0,"tax": 2.0,"shipping_address": "Mataderos 2312"} |
@ -0,0 +1,25 @@ |
||||
services: |
||||
frontend: |
||||
image: nginx:alpine |
||||
ports: |
||||
- 8090:80 |
||||
volumes: |
||||
- ./frontend:/usr/share/nginx/html |
||||
|
||||
backend: |
||||
image: demo-microservice |
||||
platform: wasi/wasm |
||||
build: |
||||
context: backend/ |
||||
ports: |
||||
- 8080:8080 |
||||
environment: |
||||
DATABASE_URL: mysql://root:whalehello@db:3306/mysql |
||||
RUST_BACKTRACE: full |
||||
restart: unless-stopped |
||||
runtime: io.containerd.wasmedge.v1 |
||||
|
||||
db: |
||||
image: mariadb:10.9 |
||||
environment: |
||||
MYSQL_ROOT_PASSWORD: whalehello |
@ -0,0 +1,125 @@ |
||||
# Compose sample application |
||||
|
||||
![Compatible with Docker+Wasm](../icon_wasm.svg) |
||||
|
||||
This sample demonstrates a web application with a WebAssembly (Wasm) microservice, written in Rust. The Wasm microservice is an HTTP API connected to a MySQL (MariaDB) database. The API is invoked via from JavaScript in a web interface serving static HTML. The microservice is compiled into WebAssembly (Wasm) and runs in the WasmEdge Runtime, a secure and lightweight alternative to natively compiled Rust apps in Linux containers. Checkout [this article](https://blog.logrocket.com/rust-microservices-server-side-webassembly/) or [this video](https://www.youtube.com/watch?v=VSqMPFr7SEs) to learn how the Rust code in this microservice works. |
||||
|
||||
## Use with Docker Development Environments |
||||
|
||||
You will need a version of Docker Desktop or Docker CLI with Wasm support. |
||||
|
||||
* [Install Docker Desktop + Wasm (Beta)](https://docs.docker.com/desktop/wasm/) |
||||
* [Install Docker CLI + Wasm](https://github.com/chris-crone/wasm-day-na-22/tree/main/server) |
||||
|
||||
## WasmEdge server with Nginx proxy and MySQL database |
||||
|
||||
Project structure: |
||||
|
||||
``` |
||||
. |
||||
+-- compose.yml |
||||
|-- backend |
||||
+-- Dockerfile |
||||
|-- Cargo.toml |
||||
|-- src |
||||
+-- main.rs |
||||
|-- frontend |
||||
+-- index.html |
||||
|-- js |
||||
+-- app.js |
||||
|-- db |
||||
+-- orders.json |
||||
|-- update_order.json |
||||
``` |
||||
|
||||
The [compose.yml](compose.yml) file: |
||||
|
||||
```yaml |
||||
services: |
||||
frontend: |
||||
image: nginx:alpine |
||||
ports: |
||||
- 8090:80 |
||||
volumes: |
||||
- ./frontend:/usr/share/nginx/html |
||||
|
||||
backend: |
||||
image: demo-microservice |
||||
build: |
||||
context: backend/ |
||||
platforms: |
||||
- wasi/wasm32 |
||||
ports: |
||||
- 8080:8080 |
||||
environment: |
||||
DATABASE_URL: mysql://root:whalehello@db:3306/mysql |
||||
RUST_BACKTRACE: full |
||||
restart: unless-stopped |
||||
runtime: io.containerd.wasmedge.v1 |
||||
|
||||
db: |
||||
image: mariadb:10.9 |
||||
environment: |
||||
MYSQL_ROOT_PASSWORD: whalehello |
||||
``` |
||||
|
||||
The compose file defines an application with three services `frontend`, `backend` and `db`. The `frontend` is a simple Nginx server that hosts static web pages that access the `backend` web service, in the WasmEdge container, via HTTP port 8080. When deploying the application, docker compose maps port 8090 of the `frontend` service container to port 8090 of the host as specified in the file. Make sure that ports 8090 and 8080 on the host are not already being used. |
||||
|
||||
## Deploy with docker compose |
||||
|
||||
```bash |
||||
$ docker compose up -d |
||||
... |
||||
â ż Network wasmedge-mysql-nginx_default Created |
||||
â ż Container wasmedge-mysql-nginx-db-1 Created |
||||
â ż Container wasmedge-mysql-nginx-frontend-1 Created |
||||
â ż Container wasmedge-mysql-nginx-backend-1 Created |
||||
``` |
||||
|
||||
## Expected result |
||||
|
||||
```bash |
||||
$ docker compose ps |
||||
NAME COMMAND SERVICE STATUS PORTS |
||||
wasmedge-mysql-nginx-backend-1 "order_demo_service.…" backend running 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp |
||||
wasmedge-mysql-nginx-db-1 "docker-entrypoint.s…" db running 3306/tcp |
||||
wasmedge-mysql-nginx-frontend-1 "/docker-entrypoint.…" frontend running 0.0.0.0:8090->80/tcp, :::8090->80/tcp |
||||
``` |
||||
|
||||
After the application starts, go to `http://localhost:8090` in your web browser to display the web frontend. |
||||
|
||||
### Using the API with `curl` |
||||
|
||||
As an alternative to the web frontend, you can use `curl` to interact with the WasmEdge API directly (the `backend` service). |
||||
|
||||
When the WasmEdge web service receives a GET request to the `/init` endpoint, it would initialize the database with the `orders` table. |
||||
|
||||
```bash |
||||
curl http://localhost:8080/init |
||||
``` |
||||
|
||||
When the WasmEdge web service receives a POST request to the `/create_order` endpoint, it extracts the JSON data from the POST body and inserts an `Order` record into the database table. |
||||
To insert multiple records, use the `/create_orders` endpoint and POST a JSON array of `Order` objects: |
||||
|
||||
```bash |
||||
curl http://localhost:8080/create_orders -X POST -d @db/orders.json |
||||
``` |
||||
|
||||
When the WasmEdge web service receives a GET request to the `/orders` endpoint, it gets all rows from the `orders` table and return the result set in a JSON array in the HTTP response. |
||||
|
||||
```bash |
||||
curl http://localhost:8080/orders |
||||
``` |
||||
|
||||
When the WasmEdge web service receives a POST request to the `/update_order` endpoint, it extracts the JSON data from the POST body and update the `Order` record in the database table that matches the `order_id` in the input data. |
||||
|
||||
```bash |
||||
curl http://localhost:8080/update_order -X POST -d @db/update_order.json |
||||
``` |
||||
|
||||
When the WasmEdge web service receives a GET request to the `/delete_order` endpoint, it deletes the row in the `orders` table that matches the `id` GET parameter. |
||||
|
||||
```bash |
||||
curl http://localhost:8080/delete_order?id=2 |
||||
``` |
||||
|
@ -0,0 +1,13 @@ |
||||
[package] |
||||
name = "order_demo_service" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
[dependencies] |
||||
anyhow = "1.0" |
||||
serde_json = "1.0" |
||||
serde = { version = "1.0", features = ["derive"] } |
||||
url = "2.3" |
||||
mysql_async_wasi = "0.30" |
||||
hyper_wasi = { version = "0.15", features = ["full"] } |
||||
tokio_wasi = { version = "1", features = ["io-util", "fs", "net", "time", "rt", "macros"] } |
@ -0,0 +1,29 @@ |
||||
# syntax=docker/dockerfile:1 |
||||
|
||||
FROM --platform=$BUILDPLATFORM rust:1.64 AS buildbase |
||||
WORKDIR /src |
||||
RUN <<EOT bash |
||||
set -ex |
||||
apt-get update |
||||
apt-get install -y \ |
||||
git \ |
||||
clang |
||||
rustup target add wasm32-wasi |
||||
EOT |
||||
# This line installs WasmEdge including the AOT compiler |
||||
RUN curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash |
||||
|
||||
FROM buildbase AS build |
||||
COPY Cargo.toml . |
||||
COPY src ./src |
||||
# Build the Wasm binary |
||||
RUN --mount=type=cache,target=/usr/local/cargo/git/db \ |
||||
--mount=type=cache,target=/usr/local/cargo/registry/cache \ |
||||
--mount=type=cache,target=/usr/local/cargo/registry/index \ |
||||
cargo build --target wasm32-wasi --release |
||||
# This line builds the AOT Wasm binary |
||||
RUN /root/.wasmedge/bin/wasmedgec target/wasm32-wasi/release/order_demo_service.wasm order_demo_service.wasm |
||||
|
||||
FROM scratch |
||||
ENTRYPOINT [ "order_demo_service.wasm" ] |
||||
COPY --link --from=build /src/order_demo_service.wasm /order_demo_service.wasm |
@ -0,0 +1,237 @@ |
||||
use hyper::service::{make_service_fn, service_fn}; |
||||
use hyper::{Body, Method, Request, Response, StatusCode, Server}; |
||||
pub use mysql_async::prelude::*; |
||||
pub use mysql_async::*; |
||||
use std::convert::Infallible; |
||||
use std::net::SocketAddr; |
||||
use std::result::Result; |
||||
use std::collections::HashMap; |
||||
use serde::{Deserialize, Serialize}; |
||||
|
||||
fn get_url() -> String { |
||||
if let Ok(url) = std::env::var("DATABASE_URL") { |
||||
let opts = Opts::from_url(&url).expect("DATABASE_URL invalid"); |
||||
if opts |
||||
.db_name() |
||||
.expect("a database name is required") |
||||
.is_empty() |
||||
{ |
||||
panic!("database name is empty"); |
||||
} |
||||
url |
||||
} else { |
||||
"mysql://root:pass@127.0.0.1:3306/mysql".into() |
||||
} |
||||
} |
||||
|
||||
#[derive(Serialize, Deserialize, Debug)] |
||||
struct Order { |
||||
order_id: i32, |
||||
product_id: i32, |
||||
quantity: i32, |
||||
amount: f32, |
||||
shipping: f32, |
||||
tax: f32, |
||||
shipping_address: String, |
||||
} |
||||
|
||||
impl Order { |
||||
fn new( |
||||
order_id: i32, |
||||
product_id: i32, |
||||
quantity: i32, |
||||
amount: f32, |
||||
shipping: f32, |
||||
tax: f32, |
||||
shipping_address: String, |
||||
) -> Self { |
||||
Self { |
||||
order_id, |
||||
product_id, |
||||
quantity, |
||||
amount, |
||||
shipping, |
||||
tax, |
||||
shipping_address, |
||||
} |
||||
} |
||||
} |
||||
|
||||
async fn handle_request(req: Request<Body>, pool: Pool) -> Result<Response<Body>, anyhow::Error> { |
||||
match (req.method(), req.uri().path()) { |
||||
(&Method::GET, "/") => Ok(Response::new(Body::from( |
||||
"The valid endpoints are /init /create_order /create_orders /update_order /orders /delete_order", |
||||
))), |
||||
|
||||
// Simply echo the body back to the client.
|
||||
(&Method::POST, "/echo") => Ok(Response::new(req.into_body())), |
||||
|
||||
// CORS OPTIONS
|
||||
(&Method::OPTIONS, "/init") => Ok(response_build(&String::from(""))), |
||||
(&Method::OPTIONS, "/create_order") => Ok(response_build(&String::from(""))), |
||||
(&Method::OPTIONS, "/create_orders") => Ok(response_build(&String::from(""))), |
||||
(&Method::OPTIONS, "/update_order") => Ok(response_build(&String::from(""))), |
||||
(&Method::OPTIONS, "/delete_order") => Ok(response_build(&String::from(""))), |
||||
(&Method::OPTIONS, "/orders") => Ok(response_build(&String::from(""))), |
||||
|
||||
(&Method::GET, "/init") => { |
||||
let mut conn = pool.get_conn().await.unwrap(); |
||||
"DROP TABLE IF EXISTS orders;".ignore(&mut conn).await?; |
||||
"CREATE TABLE orders (order_id INT, product_id INT, quantity INT, amount FLOAT, shipping FLOAT, tax FLOAT, shipping_address VARCHAR(20));".ignore(&mut conn).await?; |
||||
drop(conn); |
||||
Ok(response_build("{\"status\":true}")) |
||||
} |
||||
|
||||
(&Method::POST, "/create_order") => { |
||||
let mut conn = pool.get_conn().await.unwrap(); |
||||
|
||||
let byte_stream = hyper::body::to_bytes(req).await?; |
||||
let order: Order = serde_json::from_slice(&byte_stream).unwrap(); |
||||
|
||||
"INSERT INTO orders (order_id, product_id, quantity, amount, shipping, tax, shipping_address) VALUES (:order_id, :product_id, :quantity, :amount, :shipping, :tax, :shipping_address)" |
||||
.with(params! { |
||||
"order_id" => order.order_id, |
||||
"product_id" => order.product_id, |
||||
"quantity" => order.quantity, |
||||
"amount" => order.amount, |
||||
"shipping" => order.shipping, |
||||
"tax" => order.tax, |
||||
"shipping_address" => &order.shipping_address, |
||||
}) |
||||
.ignore(&mut conn) |
||||
.await?; |
||||
|
||||
drop(conn); |
||||
Ok(response_build("{\"status\":true}")) |
||||
} |
||||
|
||||
(&Method::POST, "/create_orders") => { |
||||
let mut conn = pool.get_conn().await.unwrap(); |
||||
|
||||
let byte_stream = hyper::body::to_bytes(req).await?; |
||||
let orders: Vec<Order> = serde_json::from_slice(&byte_stream).unwrap(); |
||||
|
||||
"INSERT INTO orders (order_id, product_id, quantity, amount, shipping, tax, shipping_address) VALUES (:order_id, :product_id, :quantity, :amount, :shipping, :tax, :shipping_address)" |
||||
.with(orders.iter().map(|order| { |
||||
params! { |
||||
"order_id" => order.order_id, |
||||
"product_id" => order.product_id, |
||||
"quantity" => order.quantity, |
||||
"amount" => order.amount, |
||||
"shipping" => order.shipping, |
||||
"tax" => order.tax, |
||||
"shipping_address" => &order.shipping_address, |
||||
} |
||||
})) |
||||
.batch(&mut conn) |
||||
.await?; |
||||
|
||||
drop(conn); |
||||
Ok(response_build("{\"status\":true}")) |
||||
} |
||||
|
||||
(&Method::POST, "/update_order") => { |
||||
let mut conn = pool.get_conn().await.unwrap(); |
||||
|
||||
let byte_stream = hyper::body::to_bytes(req).await?; |
||||
let order: Order = serde_json::from_slice(&byte_stream).unwrap(); |
||||
|
||||
"UPDATE orders SET product_id=:product_id, quantity=:quantity, amount=:amount, shipping=:shipping, tax=:tax, shipping_address=:shipping_address WHERE order_id=:order_id" |
||||
.with(params! { |
||||
"product_id" => order.product_id, |
||||
"quantity" => order.quantity, |
||||
"amount" => order.amount, |
||||
"shipping" => order.shipping, |
||||
"tax" => order.tax, |
||||
"shipping_address" => &order.shipping_address, |
||||
"order_id" => order.order_id, |
||||
}) |
||||
.ignore(&mut conn) |
||||
.await?; |
||||
|
||||
drop(conn); |
||||
Ok(response_build("{\"status\":true}")) |
||||
} |
||||
|
||||
(&Method::GET, "/orders") => { |
||||
let mut conn = pool.get_conn().await.unwrap(); |
||||
|
||||
let orders = "SELECT * FROM orders" |
||||
.with(()) |
||||
.map(&mut conn, |(order_id, product_id, quantity, amount, shipping, tax, shipping_address)| { |
||||
Order::new( |
||||
order_id, |
||||
product_id, |
||||
quantity, |
||||
amount, |
||||
shipping, |
||||
tax, |
||||
shipping_address, |
||||
)}, |
||||
).await?; |
||||
|
||||
drop(conn); |
||||
Ok(response_build(serde_json::to_string(&orders)?.as_str())) |
||||
}
|
||||
|
||||
(&Method::GET, "/delete_order") => { |
||||
let mut conn = pool.get_conn().await.unwrap(); |
||||
|
||||
let params: HashMap<String, String> = req.uri().query().map(|v| { |
||||
url::form_urlencoded::parse(v.as_bytes()).into_owned().collect() |
||||
}).unwrap_or_else(HashMap::new); |
||||
let order_id = params.get("id"); |
||||
|
||||
"DELETE FROM orders WHERE order_id=:order_id" |
||||
.with(params! { "order_id" => order_id, }) |
||||
.ignore(&mut conn) |
||||
.await?; |
||||
|
||||
drop(conn); |
||||
Ok(response_build("{\"status\":true}")) |
||||
} |
||||
|
||||
// Return the 404 Not Found for other routes.
|
||||
_ => { |
||||
let mut not_found = Response::default(); |
||||
*not_found.status_mut() = StatusCode::NOT_FOUND; |
||||
Ok(not_found) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// CORS headers
|
||||
fn response_build(body: &str) -> Response<Body> { |
||||
Response::builder() |
||||
.header("Access-Control-Allow-Origin", "*") |
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") |
||||
.header("Access-Control-Allow-Headers", "api,Keep-Alive,User-Agent,Content-Type") |
||||
.body(Body::from(body.to_owned())) |
||||
.unwrap() |
||||
} |
||||
|
||||
#[tokio::main(flavor = "current_thread")] |
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
||||
let opts = Opts::from_url(&*get_url()).unwrap(); |
||||
let builder = OptsBuilder::from_opts(opts); |
||||
// The connection pool will have a min of 5 and max of 10 connections.
|
||||
let constraints = PoolConstraints::new(5, 10).unwrap(); |
||||
let pool_opts = PoolOpts::default().with_constraints(constraints); |
||||
let pool = Pool::new(builder.pool_opts(pool_opts)); |
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], 8080)); |
||||
let make_svc = make_service_fn(|_| { |
||||
let pool = pool.clone(); |
||||
async move { |
||||
Ok::<_, Infallible>(service_fn(move |req| { |
||||
let pool = pool.clone(); |
||||
handle_request(req, pool) |
||||
})) |
||||
} |
||||
}); |
||||
let server = Server::bind(&addr).serve(make_svc); |
||||
if let Err(e) = server.await { |
||||
eprintln!("server error: {}", e); |
||||
} |
||||
Ok(()) |
||||
} |
@ -0,0 +1,25 @@ |
||||
services: |
||||
frontend: |
||||
image: nginx:alpine |
||||
ports: |
||||
- 8090:80 |
||||
volumes: |
||||
- ./frontend:/usr/share/nginx/html |
||||
|
||||
backend: |
||||
image: demo-microservice |
||||
platform: wasi/wasm |
||||
build: |
||||
context: backend/ |
||||
ports: |
||||
- 8080:8080 |
||||
environment: |
||||
DATABASE_URL: mysql://root:whalehello@db:3306/mysql |
||||
RUST_BACKTRACE: full |
||||
restart: unless-stopped |
||||
runtime: io.containerd.wasmedge.v1 |
||||
|
||||
db: |
||||
image: mariadb:10.9 |
||||
environment: |
||||
MYSQL_ROOT_PASSWORD: whalehello |
@ -0,0 +1,47 @@ |
||||
[ |
||||
{ |
||||
"order_id": 1, |
||||
"product_id": 12, |
||||
"quantity": 2, |
||||
"amount": 56.0, |
||||
"shipping": 15.0, |
||||
"tax": 2.0, |
||||
"shipping_address": "Mataderos 2312" |
||||
}, |
||||
{ |
||||
"order_id": 2, |
||||
"product_id": 15, |
||||
"quantity": 3, |
||||
"amount": 256.0, |
||||
"shipping": 30.0, |
||||
"tax": 16.0, |
||||
"shipping_address": "1234 NW Bobcat" |
||||
}, |
||||
{ |
||||
"order_id": 3, |
||||
"product_id": 11, |
||||
"quantity": 5, |
||||
"amount": 536.0, |
||||
"shipping": 50.0, |
||||
"tax": 24.0, |
||||
"shipping_address": "20 Havelock" |
||||
}, |
||||
{ |
||||
"order_id": 4, |
||||
"product_id": 8, |
||||
"quantity": 8, |
||||
"amount": 126.0, |
||||
"shipping": 20.0, |
||||
"tax": 12.0, |
||||
"shipping_address": "224 Pandan Loop" |
||||
}, |
||||
{ |
||||
"order_id": 5, |
||||
"product_id": 24, |
||||
"quantity": 1, |
||||
"amount": 46.0, |
||||
"shipping": 10.0, |
||||
"tax": 2.0, |
||||
"shipping_address": "No.10 Jalan Besar" |
||||
} |
||||
] |
@ -0,0 +1,9 @@ |
||||
{ |
||||
"order_id": 3, |
||||
"product_id": 12, |
||||
"quantity": 2, |
||||
"amount": 56.0, |
||||
"shipping": 15.0, |
||||
"tax": 2.0, |
||||
"shipping_address": "123 Main Street" |
||||
} |
@ -0,0 +1,102 @@ |
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<title>Demo App</title> |
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous" /> |
||||
<style>.d-none { display: none; }</style> |
||||
</head> |
||||
<body class="mb-5"> |
||||
<div class="container mt-5"> |
||||
<div id="app-loading-display"> |
||||
<h1>Loading...</h1> |
||||
</div> |
||||
|
||||
<div id="order-display" class="d-none"> |
||||
<h1>Welcome to the Demo!</h1> |
||||
<p>This application is served using nginx for the website, Wasm for the backend, and MariaDB for the database.</p> |
||||
|
||||
<div id="order-empty-text" class="d-none"> |
||||
<em>There are currently no orders to display!</em> |
||||
</div> |
||||
|
||||
<table id="order-table" class="d-none table table-striped"> |
||||
<thead> |
||||
<tr> |
||||
<th>Id</th> |
||||
<th>Product Id</th> |
||||
<th>Quantity</th> |
||||
<th>Amount</th> |
||||
<th>Shipping</th> |
||||
<th>Tax</th> |
||||
<th>Address</th> |
||||
<th></th> |
||||
</tr> |
||||
</thead> |
||||
<tbody></tbody> |
||||
</table> |
||||
|
||||
<hr /> |
||||
|
||||
<div id="add-order-wrapper" class="d-none row"> |
||||
<div class="col-6"> |
||||
<div class="accordion" id="accordionExample"> |
||||
<div class="accordion-item"> |
||||
<h2 class="accordion-header" id="addOrderHeading"> |
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#addOrder" aria-controls="addOrder"> |
||||
Add an order |
||||
</button> |
||||
</h2> |
||||
|
||||
<div id="addOrder" class="accordion-collapse collapse p-3" aria-labelledby="addOrderHeading" data-bs-parent="#accordionExample"> |
||||
<form id="add-order-form"> |
||||
<div class="mb-3"> |
||||
<label for="order-id" class="form-label">Order Id</label> |
||||
<input type="number" required class="form-control" id="order-id" aria-describedby="orderIdHelp"> |
||||
<div id="orderIdHelp" class="form-text">The ID of the order</div> |
||||
</div> |
||||
<div class="mb-3"> |
||||
<label for="product-id" class="form-label">Product Id</label> |
||||
<input type="number" required class="form-control" id="product-id" aria-describedby="productIdHelp"> |
||||
<div id="productIdHelp" class="form-text">The ID of the product</div> |
||||
</div> |
||||
<div class="mb-3"> |
||||
<label for="quantity" class="form-label">Quantity</label> |
||||
<input type="number" required class="form-control" id="quantity" aria-describedby="quantityHelp"> |
||||
<div id="quantityHelp" class="form-text">How many of the product?</div> |
||||
</div> |
||||
<div class="mb-3"> |
||||
<label for="amount" class="form-label">Amount</label> |
||||
<input type="number" required class="form-control" id="amount" aria-describedby="amountHelp"> |
||||
<div id="amountHelp" class="form-text">The total amount</div> |
||||
</div> |
||||
<div class="mb-3"> |
||||
<label for="tax" class="form-label">Tax</label> |
||||
<input type="number" required class="form-control" id="tax" aria-describedby="taxHelp"> |
||||
<div id="taxHelp" class="form-text">The total amount of tax</div> |
||||
</div> |
||||
<div class="mb-3"> |
||||
<label for="shippingAmount" class="form-label">Shipping Amount</label> |
||||
<input type="number" required class="form-control" id="shippingAmount" aria-describedby="shippingAmountHelp"> |
||||
<div id="shippingAmountHelp" class="form-text">The total amount for shipping</div> |
||||
</div> |
||||
<div class="mb-3"> |
||||
<label for="shippingAddress" class="form-label">Shipping Address</label> |
||||
<input type="text" required class="form-control" id="shippingAddress" aria-describedby="addressHelp"> |
||||
<div id="addressHelp" class="form-text">Where to send the order</div> |
||||
</div> |
||||
|
||||
<input type="submit" class="btn btn-success" value="Add Order" /> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
</div> |
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script> |
||||
<script type="text/javascript" src="/js/app.js"></script> |
||||
</body> |
||||
</html> |
@ -0,0 +1,135 @@ |
||||
(function() { |
||||
let orders = null; |
||||
const appLoadingEle = document.getElementById("app-loading-display"); |
||||
const orderWrapperEle = document.getElementById("order-display"); |
||||
const orderEmptyTextEle = document.getElementById("order-empty-text"); |
||||
const orderTableEle = document.getElementById("order-table"); |
||||
const orderTableBodyEle = document.querySelector("#order-table tbody"); |
||||
const addOrderEle = document.getElementById("add-order-wrapper"); |
||||
const addOrderForm = document.getElementById("add-order-form"); |
||||
|
||||
const orderIdField = document.getElementById("order-id"); |
||||
const productIdField = document.getElementById("product-id"); |
||||
const quantityField = document.getElementById("quantity"); |
||||
const amountField = document.getElementById("amount"); |
||||
const taxField = document.getElementById("tax"); |
||||
const shippingField = document.getElementById("shippingAmount"); |
||||
const shippingAddressField = document.getElementById("shippingAddress"); |
||||
|
||||
function fetchOrders() { |
||||
fetch("http://localhost:8080/orders") |
||||
.then(r => r.json()) |
||||
.then(r => orders = r) |
||||
.then(renderOrders) |
||||
.catch((e) => { |
||||
init(); |
||||
}); |
||||
} |
||||
|
||||
function init() { |
||||
fetch("http://localhost:8080/init") |
||||
.then(() => fetchOrders()) |
||||
.catch((e) => displayError(e)); |
||||
} |
||||
|
||||
function renderOrders() { |
||||
appLoadingEle.classList.add("d-none"); |
||||
orderWrapperEle.classList.remove("d-none"); |
||||
addOrderEle.classList.remove("d-none"); |
||||
|
||||
if (orders.length === 0) { |
||||
orderEmptyTextEle.classList.remove("d-none"); |
||||
orderTableEle.classList.add("d-none"); |
||||
return; |
||||
} |
||||
|
||||
orderEmptyTextEle.classList.add("d-none"); |
||||
orderTableEle.classList.remove("d-none"); |
||||
|
||||
while (orderTableBodyEle.firstChild) { |
||||
orderTableBodyEle.removeChild(orderTableBodyEle.firstChild); |
||||
} |
||||
|
||||
orders.forEach((order) => { |
||||
const orderId = order.order_id; |
||||
|
||||
const row = document.createElement("tr"); |
||||
|
||||
row.appendChild(createCell(order.order_id)); |
||||
row.appendChild(createCell(order.product_id)); |
||||
row.appendChild(createCell(order.quantity)); |
||||
row.appendChild(createCell(order.amount)); |
||||
row.appendChild(createCell(order.shipping)); |
||||
row.appendChild(createCell(order.tax)); |
||||
row.appendChild(createCell(order.shipping_address)); |
||||
|
||||
const actionCell = document.createElement("td"); |
||||
|
||||
const deleteButton = document.createElement("button"); |
||||
deleteButton.classList.add(...["btn","btn-sm","btn-danger"]); |
||||
deleteButton.innerText = "Delete"; |
||||
|
||||
deleteButton.addEventListener("click", (e) => { |
||||
e.preventDefault(); |
||||
deleteOrder(orderId); |
||||
}); |
||||
|
||||
actionCell.appendChild(deleteButton); |
||||
|
||||
row.appendChild(actionCell); |
||||
|
||||
orderTableBodyEle.appendChild(row); |
||||
}); |
||||
} |
||||
|
||||
function createCell(contents) { |
||||
const cell = document.createElement("td"); |
||||
cell.innerText = contents; |
||||
return cell; |
||||
} |
||||
|
||||
function deleteOrder(orderId) { |
||||
fetch(`http://localhost:8080/delete_order?id=${orderId}`) |
||||
.then(() => fetchOrders()); |
||||
} |
||||
|
||||
function displayError(err) { |
||||
alert("Error:" + err); |
||||
} |
||||
|
||||
function onAddFormSubmit(e) { |
||||
e.preventDefault(); |
||||
|
||||
const data = { |
||||
order_id : parseFloat(orderIdField.value), |
||||
product_id : parseFloat(productIdField.value), |
||||
quantity : parseFloat(quantityField.value), |
||||
amount : parseFloat(amountField.value), |
||||
shipping : parseFloat(shippingField.value), |
||||
tax : parseFloat(taxField.value), |
||||
shipping_address : shippingAddressField.value, |
||||
}; |
||||
|
||||
fetch("http://localhost:8080/create_order", { |
||||
method: "POST", |
||||
body: JSON.stringify(data), |
||||
headers: { "Content-type": "application/json" }, |
||||
}).then(() => fetchOrders()) |
||||
.then(() => resetAddOrderForm()); |
||||
|
||||
alert("Order added"); |
||||
} |
||||
|
||||
function resetAddOrderForm() { |
||||
orderIdField.value = ""; |
||||
productIdField.value = ""; |
||||
quantityField.value = ""; |
||||
amountField.value = ""; |
||||
shippingField.value = ""; |
||||
taxField.value = ""; |
||||
shippingAddressField.value = ""; |
||||
} |
||||
|
||||
fetchOrders(); |
||||
addOrderForm.addEventListener("submit", onAddFormSubmit); |
||||
})(); |
Loading…
Reference in new issue