Database setup and first queries

demo-mode
Wes Holland 1 year ago
parent dfd7a9b6a8
commit 949b6b0238

19
Cargo.lock generated

@ -181,7 +181,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper 1.0.1",
"tokio",
"tower",
"tower 0.5.1",
"tower-layer",
"tower-service",
"tracing",
@ -224,7 +224,7 @@ dependencies = [
"mime",
"pin-project-lite",
"serde",
"tower",
"tower 0.5.1",
"tower-layer",
"tower-service",
"tracing",
@ -238,7 +238,10 @@ checksum = "36cdb6062317f732ed3acf4e9c28c3824092e226726616f46ebdd8cd32c82a41"
dependencies = [
"async-trait",
"axum-core",
"futures",
"http 1.1.0",
"tokio",
"tower 0.4.13",
]
[[package]]
@ -1137,7 +1140,7 @@ dependencies = [
"sqlx",
"time",
"tokio",
"tower",
"tower 0.5.1",
"tower-http",
"tower-sessions",
"tower-sessions-sqlx-store",
@ -2619,6 +2622,16 @@ dependencies = [
"tokio",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"tower-layer",
"tower-service",
]
[[package]]
name = "tower"
version = "0.5.1"

@ -7,7 +7,7 @@ edition = "2021"
anyhow = "1.0.91"
askama = { version = "0.12.1", features = ["with-axum"] }
axum = { version = "0.7.7", features = ["macros"] }
axum-htmx = "0.6.0"
axum-htmx = { version = "0.6.0", features = ["auto-vary"] }
dotenvy = "0.15.7"
oauth2 = "4.4.2"
sqlx = { version = "0.8.2", features = ["runtime-tokio", "sqlite"] }

@ -1,7 +1,7 @@
# Copy this to .env and change OAUTH Values
RUST_LOG=debug,tower_http=info
DATABASE_URI=inventory-app.db
SESSION_DATABASE_URI=session.db
DATABASE_URL=sqlite://inventory-app.db
SESSION_DATABASE_URL=sqlite://session.db
OAUTH_CLIENT_ID=changeme
OAUTH_CLIENT_SECRET=changme
OAUTH_AUTH_URL=https://accounts.google.com/o/oauth2/auth

@ -0,0 +1,41 @@
-- Add migration script here
CREATE TABLE IF NOT EXISTS User (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL,
CHECK(role = 'admin' OR role = 'editor' OR role = 'viewer')
);
CREATE TABLE IF NOT EXISTS InventoryItem (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
reorder_point INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS PositiveAdjustment (
id INTEGER PRIMARY KEY NOT NULL,
user INTEGER NOT NULL,
create_date TIMESTAMP NOT NULL,
target_date TIMESTAMP NOT NULL,
amount INTEGER NOT NULL,
unit_price INTEGER NOT NULL,
FOREIGN KEY(user) REFERENCES User(id)
);
CREATE TABLE IF NOT EXISTS NegativeAdjustment (
id INTEGER PRIMARY KEY NOT NULL,
user INTEGER NOT NULL,
create_date TIMESTAMP NOT NULL,
target_date TIMESTAMP NOT NULL,
amount INTEGER NOT NULL,
reason INTEGER NOT NULL,
FOREIGN KEY(user) REFERENCES User(id),
FOREIGN KEY(reason) REFERENCES NegativeAdjustmentReason(id)
);
CREATE TABLE IF NOT EXISTS NegativeAdjustmentReason (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL
);

@ -0,0 +1,67 @@
use askama::Template;
use askama_axum::IntoResponse;
use axum::extract::{Path, State};
use sqlx::SqlitePool;
use axum::response::Response;
use axum_htmx::HxRequest;
use serde::Deserialize;
use crate::db::inventory_item::{inventory_item_get_all, inventory_item_get_by_id, inventory_item_get_search, DbInventoryItem};
use crate::error::{AppError, QueryExtractor};
#[derive(Template)]
#[template(path = "item_list.html")]
struct ItemListTemplate {
items: Vec<DbInventoryItem>,
query: ItemsQueryArgs,
}
#[derive(Template)]
#[template(path = "item_list_fragment.html")]
struct ItemListFragmentTemplate {
items: Vec<DbInventoryItem>
}
/// Query string response for "authorized" endpoint
#[derive(Debug, Deserialize)]
pub struct ItemsQueryArgs {
#[serde(rename = "q")]
pub search: Option<String>,
#[serde(alias = "p")]
pub page: Option<i64>,
#[serde(rename = "size")]
pub page_size: Option<i64>,
}
#[axum::debug_handler]
pub async fn item_list(
QueryExtractor(query): QueryExtractor<ItemsQueryArgs>,
HxRequest(hx_request): HxRequest,
State(db): State<SqlitePool>
) -> Result<Response, AppError> {
let page = query.page.unwrap_or(0);
let page_size = query.page_size.unwrap_or(100);
let items = match query.search.as_ref() {
Some(s) => inventory_item_get_search(&db, &s, page_size, page).await?,
None => inventory_item_get_all(&db, page_size, page).await?,
};
if hx_request {
Ok(ItemListFragmentTemplate { items }.into_response())
}
else {
Ok(ItemListTemplate { items, query }.into_response())
}
}
#[derive(Template)]
#[template(path = "item.html")]
struct ItemTemplate {
item: DbInventoryItem
}
pub async fn item(State(db): State<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
let item = inventory_item_get_by_id(&db, id).await?;
Ok(ItemTemplate { item }.into_response())
}

@ -1,27 +1,24 @@
use askama::Template;
use askama_axum::IntoResponse;
use axum::middleware::from_extractor;
use axum::Router;
use axum::routing::get;
use axum_htmx::{AutoVaryLayer};
use crate::app::state::AppState;
use crate::auth::User;
pub mod state;
pub mod items;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/", get(index))
.route("/index.html", get(index))
.route("/", get(items::item_list))
.route("/index.html", get(items::item_list))
.route("/items", get(items::item_list))
.route("/items/", get(items::item_list))
.route("/item/:item_id", get(items::item))
.route("/item/:item_id/", get(items::item))
// Ensure that all routes here require an authenticated user
// whether explicitly asked or not
.route_layer(from_extractor::<User>())
}
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate;
async fn index() -> impl IntoResponse {
IndexTemplate.into_response()
.layer(AutoVaryLayer)
}

@ -1,14 +0,0 @@
use sqlx::SqlitePool;
use sqlx::sqlite::SqliteConnectOptions;
pub async fn init(filename: &str) -> anyhow::Result<SqlitePool> {
let options = SqliteConnectOptions::new()
.filename(filename)
.create_if_missing(true);
let db = SqlitePool::connect_with(options).await?;
tracing::info!("Database connected {}", filename);
Ok(db)
}

@ -0,0 +1,69 @@
use serde::Serialize;
use anyhow::Result;
use sqlx::SqlitePool;
#[derive(Debug, Serialize)]
#[derive(sqlx::FromRow)]
pub struct DbInventoryItem {
pub id: i64,
pub name: String,
pub reorder_point: i64,
}
pub async fn inventory_item_get_all(db: &SqlitePool, page_size: i64, page_num: i64) -> Result<Vec<DbInventoryItem>> {
let offset = page_num * page_size;
sqlx::query_as!(
DbInventoryItem,
r#"
SELECT
id, name, reorder_point
FROM
InventoryItem
LIMIT ? OFFSET ?
"#,
page_size, offset
)
.fetch_all(db).await
.map_err(From::from)
}
pub async fn inventory_item_get_search(db: &SqlitePool,
search_term: &str,
page_size: i64,
page_num: i64) -> Result<Vec<DbInventoryItem>> {
let offset = page_num * page_size;
let search = String::from("%") + search_term + "%";
sqlx::query_as!(
DbInventoryItem,
r#"
SELECT
id, name, reorder_point
FROM
InventoryItem
WHERE InventoryItem.name LIKE ?
LIMIT ? OFFSET ?
"#,
search,
page_size, offset
)
.fetch_all(db).await
.map_err(From::from)
}
pub async fn inventory_item_get_by_id(db: &SqlitePool, id: i64) -> Result<DbInventoryItem> {
sqlx::query_as!(
DbInventoryItem,
r#"
SELECT
id, name, reorder_point
FROM
InventoryItem
WHERE id = ?
"#,
id
)
.fetch_one(db).await
.map_err(From::from)
}

@ -0,0 +1,30 @@
pub mod inventory_item;
use std::str::FromStr;
use anyhow::Context;
use sqlx::SqlitePool;
use sqlx::sqlite::SqliteConnectOptions;
pub async fn init() -> anyhow::Result<SqlitePool> {
let db_url = std::env::var("DATABASE_URL")
.context("DATABASE_URI not set")?;
let db = connect_db(&db_url).await?;
sqlx::migrate!().run(&db).await?;
Ok(db)
}
pub async fn connect_db(url: &str) -> anyhow::Result<SqlitePool> {
let options = SqliteConnectOptions::from_str(url)?
.create_if_missing(true);
let db = SqlitePool::connect_with(options).await?;
tracing::info!("Database connected {}", url);
Ok(db)
}

@ -46,9 +46,7 @@ async fn main() -> Result<()>{
// Application database. What you would expect. Access
// through the application state
let db_file = std::env::var("DATABASE_URI")
.context("DATABASE_URI not set")?;
let db = db::init(&db_file).await?;
let db = db::init().await?;
// OAUTH2 Client
let oauth_client = auth::init_client()?;

@ -9,9 +9,9 @@ use crate::db;
pub async fn init() -> Result<(SessionManagerLayer<SqliteStore>, JoinHandle<Result<()>>)> {
// Session store is a session aware database backing for the session data
let session_db_location = std::env::var("SESSION_DATABASE_URI")
.context("SESSION_DATABASE_URI not set")?;
let session_db = db::init(&session_db_location).await?;
let session_db_location = std::env::var("SESSION_DATABASE_URL")
.context("SESSION_DATABASE_URL not set")?;
let session_db = db::connect_db(&session_db_location).await?;
let session_store = SqliteStore::new(session_db);
session_store.migrate().await?;

@ -1,14 +0,0 @@
{% extends "main.html" %}
{% block title %} Inventory App {% endblock %}
{% block content %}
<p>
<input name="search"
placeholder="Search"
aria-label="Search"
type="search">
</p>
{% endblock %}

@ -0,0 +1,10 @@
{% extends "main.html" %}
{% block title %} Items {% endblock %}
{% block content %}
<h3>{{item.name}}</h3>
<p>Reorder at: {{item.reorder_point}}</p>
{% endblock %}

@ -0,0 +1,22 @@
{% extends "main.html" %}
{% block title %} Items {% endblock %}
{% block content %}
<p>
<input id="search" type="search" name="q"
placeholder="Search"
aria-label="Search"
value='{{ query.search.as_deref().unwrap_or("") }}'
hx-get="/items"
hx-trigger="search, keyup delay:200ms changed"
hx-target="#items"
/>
</p>
<div id="items" class="container">
{% include "item_list_fragment.html" %}
</div>
{% endblock %}

@ -0,0 +1,9 @@
{% for item in items %}
<article>
<div class="grid">
<div><a href="/item/{{item.id}}/">{{ item.name }}</a></div>
<div>Reorder Point: {{ item.reorder_point }}</div>
</div>
</article>
{% endfor %}

@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="/css/pico.min.css">
<link rel="stylesheet" href="/css/custom.css">
<script src="/js/htmx.min.js"></script>
<title>{% block title %}Title{% endblock %}</title>
</head>
@ -17,6 +16,7 @@
</ul>
<ul>
<li><a class="secondary" href="#">Overview</a></li>
<li><a class="secondary" href="/items/">Items</a></li>
<li><a class="secondary" href="#">Receiving</a></li>
<li><a class="secondary" href="#">Reports</a></li>
<li><a class="secondary" href="#">Adjustments</a></li>

Loading…
Cancel
Save

Powered by TurnKey Linux.