Compare commits

...

10 Commits

@ -1,16 +0,0 @@
[package]
name = "fast-insiders"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace]
members = ["server", "client"]
[workspace.dependencies]
chrono = { version = "0.4.23", features = ["serde"] }
serde = { version = "1.0.152", features = ["derive"] }
dotenvy = "0.15.6"
envy = "0.4.2"
serde_json = "1.0.91"

1
client/.gitignore vendored

@ -1,2 +1,3 @@
target/
dist/ dist/
pkg/ pkg/

2269
client/Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -6,16 +6,17 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
chrono = { workspace = true, features = ["serde"] } chrono = { version = "0.4.23", features = ["serde"] }
serde = { workspace = true, features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] }
serde_json = { workspace = true } serde_json = "1.0.91"
perseus = { version = "0.4", features = ["hydrate"] } perseus = { version = "0.4.1", features = ["hydrate"] }
sycamore = { version = "^0.8.1", features = [ sycamore = { version = "^0.8.1", features = [
"ssr", "ssr",
"serde", "serde",
"suspense", "suspense",
"hydrate", "hydrate",
] } ] }
lazy_static = "1"
[target.'cfg(engine)'.dev-dependencies] [target.'cfg(engine)'.dev-dependencies]
fantoccini = "^0.19.3" fantoccini = "^0.19.3"

@ -7,7 +7,7 @@ RUN apt-get update \
build-essential curl wget pkg-config build-essential curl wget pkg-config
# vars # vars
ENV PERSEUS_VERSION=0.4.0 \ ENV PERSEUS_VERSION=0.4.1 \
PERSEUS_SIZE_OPT_VERSION=0.1.9 \ PERSEUS_SIZE_OPT_VERSION=0.1.9 \
ESBUILD_VERSION=0.15.18 \ ESBUILD_VERSION=0.15.18 \
BINARYEN_VERSION=112 BINARYEN_VERSION=112
@ -21,43 +21,22 @@ RUN rustup target add wasm32-unknown-unknown;\
# retrieve the src dir # retrieve the src dir
COPY . . COPY . .
# RUN curl https://git.albv.org/alban/fast-insiders/archive/master.tar.gz | tar -xz
# go to src dir
WORKDIR /app/client
# install perseus-cli # install perseus-cli
RUN cargo install perseus-cli --version $PERSEUS_VERSION;\ RUN cargo install perseus-cli --version $PERSEUS_VERSION;\
perseus clean perseus clean
# specify deps in app config
# RUN sed -i s"/perseus = .*/perseus = \"${PERSEUS_VERSION}\"/" ./Cargo.toml \
# && sed -i s"/perseus-size-opt = .*/perseus-size-opt = \"${PERSEUS_SIZE_OPT_VERSION}\"/" ./Cargo.toml \
# && cat ./Cargo.toml
#
# # modify main.rs
# RUN sed -i s'/SizeOpts::default()/SizeOpts { wee_alloc: true, lto: true, opt_level: "z".to_string(), codegen_units: 1, enable_fluent_bundle_patch: false, }/' ./src/main.rs \
# && cat ./src/main.rs
# # run plugin(s) to adjust app
# RUN perseus tinker
# single-threaded perseus CLI mode required for low memory environments
#ENV PERSEUS_CLI_SEQUENTIAL=true
# deploy app # deploy app
RUN perseus deploy RUN perseus deploy
# go back to app dir
WORKDIR /app
# download and unpack esbuild # download and unpack esbuild
RUN curl -O https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-${ESBUILD_VERSION}.tgz \ RUN curl -O https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-${ESBUILD_VERSION}.tgz \
&& tar xf esbuild-linux-64-${ESBUILD_VERSION}.tgz \ && tar xf esbuild-linux-64-${ESBUILD_VERSION}.tgz \
&& ./package/bin/esbuild --version && ./package/bin/esbuild --version
# run esbuild against bundle.js # run esbuild against bundle.js
RUN ./package/bin/esbuild ./client/pkg/dist/pkg/perseus_engine.js --minify --target=es6 --outfile=./client/pkg/dist/pkg/perseus_engine.js --allow-overwrite \ RUN ./package/bin/esbuild ./pkg/dist/pkg/perseus_engine.js --minify --target=es6 --outfile=./pkg/dist/pkg/perseus_engine.js --allow-overwrite \
&& ls -lha ./client/pkg/dist/pkg && ls -lha ./pkg/dist/pkg
# download and unpack binaryen # download and unpack binaryen
RUN wget -nv https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-x86_64-linux.tar.gz \ RUN wget -nv https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-x86_64-linux.tar.gz \
@ -65,14 +44,14 @@ RUN wget -nv https://github.com/WebAssembly/binaryen/releases/download/version_$
&& ./binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt --version && ./binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt --version
# run wasm-opt against bundle.wasm # run wasm-opt against bundle.wasm
RUN ./binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt -Os ./client/pkg/dist/pkg/perseus_engine_bg.wasm -o ./client/pkg/dist/pkg/perseus_engine_bg.wasm RUN ./binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt -Os ./pkg/dist/pkg/perseus_engine_bg.wasm -o ./pkg/dist/pkg/perseus_engine_bg.wasm
# prepare deployment image # prepare deployment image
FROM debian:stable-slim FROM debian:stable-slim
WORKDIR /app WORKDIR /app
COPY --from=build /app/client/pkg /app/ COPY --from=build /app/pkg /app/
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0

@ -1,7 +1,9 @@
use crate::api::{ use crate::api::{
types::{ types::{
paginated_response::PaginatedResponse, paginated_response::PaginatedResponse,
transaction::{LatestTransaction, TransactionCompany, TransactionsAggregated}, transaction::{
LatestTransaction, MajorTransactions, TransactionCompany, TransactionsAggregated,
},
}, },
FastInsidersApi, FastInsidersApi,
}; };
@ -38,7 +40,7 @@ impl FastInsidersApi {
.await .await
.map_err(|_| ())?; .map_err(|_| ())?;
return Ok(res); Ok(res)
} }
pub async fn get_aggregated_transactions( pub async fn get_aggregated_transactions(
@ -72,7 +74,7 @@ impl FastInsidersApi {
.await .await
.map_err(|_| ())?; .map_err(|_| ())?;
return Ok(res); Ok(res)
} }
pub async fn get_latest_transactions( pub async fn get_latest_transactions(
@ -103,6 +105,34 @@ impl FastInsidersApi {
.await .await
.map_err(|_| ())?; .map_err(|_| ())?;
return Ok(res); Ok(res)
}
pub async fn get_major_transactions(
&self,
_: Option<String>,
page: i64,
size: i64,
) -> Result<PaginatedResponse<MajorTransactions>, ()> {
let route = &format!("{}/transaction/major?page={}&size={}", self.url, page, size,);
#[cfg(client)]
let res = reqwasm::http::Request::get(route)
.send()
.await
.map_err(|_| ())?
.json::<PaginatedResponse<MajorTransactions>>()
.await
.map_err(|_| ())?;
#[cfg(engine)]
let res = reqwest::get(route)
.await
.map_err(|_| ())?
.json::<PaginatedResponse<MajorTransactions>>()
.await
.map_err(|_| ())?;
Ok(res)
} }
} }

@ -3,13 +3,13 @@ use sycamore::prelude::*;
use crate::components::base_table::TableContent; use crate::components::base_table::TableContent;
use super::transaction::{TransactionCompany, TransactionsAggregated, LatestTransaction}; use super::transaction::{TransactionCompany, TransactionsAggregated, LatestTransaction, MajorTransactions};
pub trait IntoTableData<G> pub trait IntoTableData<G>
where where
G: GenericNode, G: GenericNode,
{ {
fn into_table_data<'a>(self, cx: Scope<'a>) -> TableContent<G>; fn into_table_data(self, cx: Scope) -> TableContent<G>;
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
@ -23,7 +23,7 @@ impl<G> IntoTableData<G> for PaginatedResponse<TransactionCompany>
where where
G: GenericNode, G: GenericNode,
{ {
fn into_table_data<'a>(self, cx: Scope<'a>) -> TableContent<G> { fn into_table_data(self, cx: Scope) -> TableContent<G> {
let headers_view = vec![ let headers_view = vec![
view! {cx, "Company" }, view! {cx, "Company" },
view! {cx, "Date published" }, view! {cx, "Date published" },
@ -45,7 +45,7 @@ where
let mut res = vec![]; let mut res = vec![];
res.push(view! {cx, res.push(view! {cx,
a (href=format!("transactions/{}", t.company.slug), a (href=format!("transactions/{}", t.company.slug),
class="text-indigo-800 dark:text-indigo-300 hover:text-indigo-500 dark:hover:text-indigo-600 hover:underline", class="text-indigo-800 dark:text-indigo-300 hover:text-indigo-500 hover:underline dark:hover:text-indigo-600",
) { ) {
(t.company.name.to_owned()) (t.company.name.to_owned())
} }
@ -76,7 +76,7 @@ impl<G> IntoTableData<G> for PaginatedResponse<TransactionsAggregated>
where where
G: GenericNode, G: GenericNode,
{ {
fn into_table_data<'a>(self, cx: Scope<'a>) -> TableContent<G> { fn into_table_data(self, cx: Scope) -> TableContent<G> {
let headers_view = vec![ let headers_view = vec![
view! {cx, "Company" }, view! {cx, "Company" },
view! {cx, "Transactions" }, view! {cx, "Transactions" },
@ -89,7 +89,7 @@ where
let mut res = vec![]; let mut res = vec![];
res.push(view! {cx, res.push(view! {cx,
a (href=format!("transactions/{}", t.slug), a (href=format!("transactions/{}", t.slug),
class="text-indigo-800 dark:text-indigo-300 hover:text-indigo-500 dark:hover:text-indigo-600 hover:underline", class="text-indigo-800 dark:text-indigo-300 hover:text-indigo-500 hover:underline dark:hover:text-indigo-600",
) { ) {
(t.name.to_owned()) (t.name.to_owned())
} }
@ -111,7 +111,7 @@ impl<G> IntoTableData<G> for PaginatedResponse<LatestTransaction>
where where
G: GenericNode, G: GenericNode,
{ {
fn into_table_data<'a>(self, cx: Scope<'a>) -> TableContent<G> { fn into_table_data(self, cx: Scope) -> TableContent<G> {
let headers_view = vec![ let headers_view = vec![
view! {cx, "Company" }, view! {cx, "Company" },
view! {cx, "nature" }, view! {cx, "nature" },
@ -125,7 +125,7 @@ where
let mut res = vec![]; let mut res = vec![];
res.push(view! {cx, res.push(view! {cx,
a (href=format!("transactions/{}", t.slug), a (href=format!("transactions/{}", t.slug),
class="text-indigo-800 dark:text-indigo-300 hover:text-indigo-500 dark:hover:text-indigo-600 hover:underline", class="text-indigo-800 dark:text-indigo-300 hover:text-indigo-500 hover:underline dark:hover:text-indigo-600",
) { ) {
(t.company_name.to_owned()) (t.company_name.to_owned())
} }
@ -143,3 +143,50 @@ where
} }
} }
} }
impl<G> IntoTableData<G> for PaginatedResponse<MajorTransactions>
where
G: GenericNode,
{
fn into_table_data(self, cx: Scope) -> TableContent<G> {
let headers_view = vec![
view! {cx, "Company" },
view! {cx, "Date published" },
view! {cx, "Date executed" },
view! {cx, "Nature" },
view! {cx, "Instrument" },
view! {cx, "Volume" },
view! {cx, "Unit price" },
view! {cx, "Total" },
];
let data_view: Vec<Vec<View<G>>> = self
.list
.into_iter()
.map(|t| {
let mut res = vec![];
res.push(view! {cx,
a (href=format!("transactions/{}", t.slug),
class="text-indigo-800 dark:text-indigo-300 hover:text-indigo-500 hover:underline dark:hover:text-indigo-600",
) {
(t.company_name.to_owned())
}
});
res.push(view! {cx, (t.date_published.to_owned()) });
res.push(view! {cx, (t.date_executed.to_owned()) });
res.push(view! {cx, (t.nature.to_owned()) });
res.push(view! {cx, (t.instrument.to_owned()) });
res.push(view! {cx, (t.volume.to_owned()) });
res.push(view! {cx, (t.unit_price.to_owned()) });
res.push(view! {cx, (t.total.to_string()) });
res
})
.collect();
TableContent {
headers_view,
data_view,
}
}
}

@ -52,3 +52,16 @@ pub struct LatestTransaction {
pub nature: String, pub nature: String,
pub total: f32, pub total: f32,
} }
#[derive(Deserialize, Clone)]
pub struct MajorTransactions {
pub company_name: String,
pub slug: String,
pub date_published: NaiveDate,
pub date_executed: NaiveDate,
pub nature: String,
pub instrument: String,
pub volume: i32,
pub unit_price: f32,
pub total: f32,
}

@ -0,0 +1,36 @@
use lazy_static::lazy_static;
use perseus::prelude::*;
use sycamore::prelude::*;
use crate::global_state::AppStateRx;
lazy_static! {
pub static ref DARK_MODE_BTN: Capsule<PerseusNodeType, ()> = get_capsule();
}
fn dark_mode_btn<G: Html>(cx: Scope, _props: ()) -> View<G> {
let global_state = Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx);
let toggle_dark_mode = move |_| {
global_state.dark_mode.set(!*global_state.dark_mode.get());
};
view! { cx,
button (on:click=toggle_dark_mode, class="py-1 px-2 mx-1 rounded-full bg-slate-200 dark:bg-slate-800")
{ "Toggle dark mode" }
}
}
fn fallback<G: Html>(cx: Scope, _props: ()) -> View<G> {
view! { cx,
button (class="py-1 px-2 mx-1 rounded-full bg-slate-200 dark:bg-slate-800")
{ "Toggle dark mode" }
}
}
pub fn get_capsule<G: Html>() -> Capsule<G, ()> {
Capsule::build(Template::build("dark_mode_btn"))
.fallback(fallback)
.view(dark_mode_btn)
.build()
}

@ -0,0 +1 @@
pub mod dark_mode_btn;

@ -1,32 +1,12 @@
use perseus::prelude::*; use perseus::prelude::*;
use sycamore::prelude::*; use sycamore::prelude::*;
use crate::global_state::AppStateRx; use crate::capsules::dark_mode_btn::DARK_MODE_BTN;
#[component] #[component]
pub fn TheHeader<'a, G: Html>(cx: Scope<'a>) -> View<G> { pub fn TheHeader<G: Html>(cx: Scope) -> View<G> {
// This is ugly and is only caused by the get_global_state function panicking when running on the server at build time
let global_state_sig: &Signal<Option<&AppStateRx>> = create_signal(cx, None);
#[cfg(client)]
global_state_sig.set(Some(
Reactor::<G>::from_cx(cx).get_global_state::<AppStateRx>(cx),
));
let dark_mode = create_signal(cx, true);
create_effect(cx, move || {
if let Some(gstate) = (*global_state_sig.get()).clone() {
dark_mode.set(*gstate.dark_mode.get());
}
});
let toggle_dark_mode = move |_| {
if let Some(gstate) = *global_state_sig.get() {
gstate.dark_mode.set(!*dark_mode.get());
}
};
view! { cx, view! { cx,
header (class="shadow-md h-11 p-2 align-middle w-full bg-gray-100 dark:bg-slate-500/30 backdrop-blur-lg") { header (class="p-2 w-full h-11 align-middle bg-gray-100 shadow-md backdrop-blur-lg dark:bg-slate-500/30") {
div (class="flex") { div (class="flex") {
div (class="flex-none mr-12") { div (class="flex-none mr-12") {
a (href="/", class="hover:underline") { a (href="/", class="hover:underline") {
@ -39,8 +19,7 @@ pub fn TheHeader<'a, G: Html>(cx: Scope<'a>) -> View<G> {
} }
} }
div (class="flex-none") { div (class="flex-none") {
button (on:click=toggle_dark_mode, class="py-1 px-2 mx-1 rounded-full bg-slate-200 dark:bg-slate-800") (DARK_MODE_BTN.widget(cx,"",()))
{ "Toggle dark mode" }
} }
} }
} }

@ -2,6 +2,7 @@ use perseus::prelude::*;
use sycamore::view; use sycamore::view;
mod api; mod api;
pub mod capsules;
mod components; mod components;
mod env; mod env;
pub mod error_pages; pub mod error_pages;
@ -13,6 +14,7 @@ pub fn main<G: Html>() -> PerseusApp<G> {
PerseusApp::new() PerseusApp::new()
.template(crate::templates::index::get_template()) .template(crate::templates::index::get_template())
.template(crate::templates::transactions::get_template()) .template(crate::templates::transactions::get_template())
.capsule_ref(&*crate::capsules::dark_mode_btn::DARK_MODE_BTN)
.global_state_creator(crate::global_state::get_global_state_creator()) .global_state_creator(crate::global_state::get_global_state_creator())
.error_views(crate::error_pages::get_error_views()) .error_views(crate::error_pages::get_error_views())
.index_view(|cx| { .index_view(|cx| {

@ -2,7 +2,7 @@ use perseus::prelude::*;
use sycamore::prelude::*; use sycamore::prelude::*;
use crate::{ use crate::{
api::types::transaction::{LatestTransaction, TransactionsAggregated}, api::types::transaction::{LatestTransaction, MajorTransactions, TransactionsAggregated},
components::{ components::{
main_content_container::MainContentContainer, main_content_container::MainContentContainer,
paginated_data_table::{PaginatedTable, PaginatedTableStateRx}, paginated_data_table::{PaginatedTable, PaginatedTableStateRx},
@ -42,6 +42,17 @@ fn index_page<G: Html>(cx: BoundedScope) -> View<G> {
table_class: table_classes, table_class: table_classes,
}; };
let route_ref = create_ref(cx, move |c, p, s| {
api_scope_ref.get_major_transactions(c, p, s)
});
let table_transactions_major: PaginatedTableStateRx<MajorTransactions, _, _> =
PaginatedTableStateRx {
record_label: "transactions".to_owned(),
route: route_ref,
filter: Some((24 * 30).to_string()),
table_class: table_classes,
};
let dark_mode_class = create_memo(cx, || { let dark_mode_class = create_memo(cx, || {
if *global_state.dark_mode.get() { if *global_state.dark_mode.get() {
"dark" "dark"
@ -68,6 +79,12 @@ fn index_page<G: Html>(cx: BoundedScope) -> View<G> {
} }
PaginatedTable(table_transactions_month) PaginatedTable(table_transactions_month)
} }
div (class="flex-grow") {
h1 (class="mb-1 text-center") {
"Major transactions in the past 30 days"
}
PaginatedTable(table_transactions_major)
}
} }
} }
} }

@ -49,7 +49,7 @@ fn transactions_page<'a, G: Html>(cx: Scope, state: &TransactionsPageStateRx) ->
let route_ref = create_ref(cx, |n, l| api_scope_ref.get_company_by_name(n, l)); let route_ref = create_ref(cx, |n, l| api_scope_ref.get_company_by_name(n, l));
let async_select_prop: AsyncSelectRx<_, _, _> = AsyncSelectRx { let async_select_prop: AsyncSelectRx<_, _, _> = AsyncSelectRx {
route: create_ref(cx, |n, l| api_scope_ref.get_company_by_name(n, l)), route: route_ref,
selected_item: create_signal(cx, None), selected_item: create_signal(cx, None),
}; };

File diff suppressed because one or more lines are too long

@ -52,7 +52,7 @@ async fn index(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
.select_by_value(&page_size.to_string()) .select_by_value(&page_size.to_string())
.await?; .await?;
let table_rows = c.find_all(Locator::Css("table tr")).await?.iter().count(); let table_rows = c.find_all(Locator::Css("table tr")).await?.len();
let page_size_select = c.find(Locator::Css("#size-select")).await?; let page_size_select = c.find(Locator::Css("#size-select")).await?;
let browser_page_size = page_size_select let browser_page_size = page_size_select
.prop("value") .prop("value")
@ -71,7 +71,7 @@ async fn index(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
.await? .await?
.text() .text()
.await? .await?
.split("/") .split('/')
.map(|x| x.parse().unwrap()) .map(|x| x.parse().unwrap())
.collect::<Vec<usize>>(); .collect::<Vec<usize>>();
assert_eq!(page_numbers[0], 1); assert_eq!(page_numbers[0], 1);
@ -81,7 +81,7 @@ async fn index(c: &mut Client) -> Result<(), fantoccini::error::CmdError> {
.await? .await?
.text() .text()
.await? .await?
.split("/") .split('/')
.map(|x| x.parse().unwrap()) .map(|x| x.parse().unwrap())
.collect::<Vec<usize>>(); .collect::<Vec<usize>>();
assert_eq!(new_page_numbers[0], page_numbers[0] + 1); assert_eq!(new_page_numbers[0], page_numbers[0] + 1);

@ -3,8 +3,7 @@ services:
client: client:
container_name: fast-insiders-client container_name: fast-insiders-client
image: docker.albv.org/root/fast-insiders-client:latest image: docker.albv.org/root/fast-insiders-client:latest
build: build: ./client
dockerfile: ./client/Dockerfile
restart: always restart: always
ports: ports:
- 8080:8080 - 8080:8080
@ -14,8 +13,7 @@ services:
server: server:
container_name: fast-insiders-server container_name: fast-insiders-server
image: docker.albv.org/root/fast-insiders-server:latest image: docker.albv.org/root/fast-insiders-server:latest
build: build: ./server
dockerfile: ./server/Dockerfile
restart: always restart: always
ports: ports:
- 8000:8000 - 8000:8000

1
server/.gitignore vendored

@ -0,0 +1 @@
target/

File diff suppressed because it is too large Load Diff

@ -6,12 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
chrono = { version = "0.4.23", features = ["serde"] }
serde = { version = "1.0.152", features = ["derive"] }
dotenvy = "0.15.6"
envy = "0.4.2"
serde_json = "1.0.91"
migration = { version = "0.1.0", path = "./migration" } migration = { version = "0.1.0", path = "./migration" }
chrono = { workspace = true, features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
dotenvy = { workspace = true }
envy = { workspace = true }
tokio = { version = "^1.20.1", features = ["full"] } tokio = { version = "^1.20.1", features = ["full"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"] } reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
axum = "0.6.12" axum = "0.6.12"
@ -25,7 +25,6 @@ sea-orm = { version = "0.11.0", features = [
lopdf = "0.29.0" lopdf = "0.29.0"
bytes = { version = "1.3.0", features = ["serde"] } bytes = { version = "1.3.0", features = ["serde"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
pretty_env_logger = "0.4.0"
log = "0.4.17" log = "0.4.17"
futures = "0.3.25" futures = "0.3.25"
async-trait = "0.1.61" async-trait = "0.1.61"

@ -1,4 +1,4 @@
FROM rust:1.69-slim as build FROM rust:1.84-slim as build
# Install build dependencies # Install build dependencies
RUN apt-get update \ RUN apt-get update \
@ -12,13 +12,7 @@ WORKDIR /app
COPY . . COPY . .
# Build failed when not using nightly though I don't know which crate is responsible # Build failed when not using nightly though I don't know which crate is responsible
RUN rustup default nightly RUN rustup default nightly; cargo build --release
# go to src dir
WORKDIR /app/server
# Build the final binary
RUN cargo build --release
# prepare deployment image # prepare deployment image
FROM debian:stable-slim FROM debian:stable-slim

@ -10,7 +10,6 @@ path = "src/lib.rs"
[dependencies] [dependencies]
async-std = { version = "^1", features = ["attributes", "tokio1"] } async-std = { version = "^1", features = ["attributes", "tokio1"] }
chrono.workspace = true
[dependencies.sea-orm-migration] [dependencies.sea-orm-migration]
version = "0.11.0" version = "0.11.0"

@ -1,7 +1,7 @@
use crate::{ use crate::{
amf::{ amf::{
types::{ types::{
amf_response::{Document, Hit}, amf_response::{Document, Source},
AMFResponse, TransactionData, AMFResponse, TransactionData,
}, },
TransactionDataTrait, TransactionDataTrait,
@ -12,16 +12,16 @@ use crate::{
use super::pdf::AMFPdf; use super::pdf::AMFPdf;
impl AMFResponse { impl AMFResponse {
pub fn get_hits(&self) -> &Vec<Hit> { pub fn get_hits(&self) -> &Vec<Source> {
&self.hits.hits &self.hits
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl TransactionDataTrait for Hit { impl TransactionDataTrait for Source {
type Err = GetAMFTransactionsError; type Err = GetAMFTransactionsError;
async fn get_transaction_data(&self) -> Result<TransactionData, Self::Err> { async fn get_transaction_data(&self) -> Result<TransactionData, Self::Err> {
let foreign_id = self.source.numero.to_owned(); let foreign_id = self.numero.to_owned();
let docs = self.get_documents(); let docs = self.get_documents();
if docs.len() > 1 { if docs.len() > 1 {
warn!("Transaction number {} contains more than one document, only the first document will be parsed for information", foreign_id); warn!("Transaction number {} contains more than one document, only the first document will be parsed for information", foreign_id);
@ -54,12 +54,12 @@ impl TransactionDataTrait for Hit {
} }
} }
impl Hit { impl Source {
pub fn get_documents(&self) -> &Vec<Document> { pub fn get_documents(&self) -> &Vec<Document> {
&self.source.documents &self.documents
} }
pub fn get_foreign_id(&self) -> String { pub fn get_foreign_id(&self) -> String {
self.source.numero.to_owned() self.numero.to_owned()
} }
} }

@ -41,16 +41,12 @@ impl AMFRequest {
} }
} }
#[derive(Default)]
pub enum AMFRequestType { pub enum AMFRequestType {
#[default]
DD, DD,
} }
impl Default for AMFRequestType {
fn default() -> Self {
AMFRequestType::DD
}
}
impl fmt::Display for AMFRequestType { impl fmt::Display for AMFRequestType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {

@ -4,32 +4,8 @@ use serde_json::Value;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AMFResponse { pub struct AMFResponse {
pub hits: Hits, #[serde(rename = "result")]
pub aggregations: Aggregations, pub hits: Vec<Source>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Hits {
pub total: Total,
pub hits: Vec<Hit>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Total {
pub value: i64,
pub relation: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Hit {
#[serde(rename = "_ignored")]
pub ignored: Option<Vec<String>>,
#[serde(rename = "_source")]
pub source: Source,
pub sort: Vec<i64>,
} }
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -60,128 +36,12 @@ pub struct Source {
pub version: i64, pub version: i64,
pub regulateur: String, pub regulateur: String,
pub relations: Vec<Value>, pub relations: Vec<Value>,
pub societes: Vec<Societe>,
pub annee_comptable: Value, pub annee_comptable: Value,
} }
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Document { pub struct Document {
pub accessible: bool,
pub issuer_id: Option<String>,
pub path: String, pub path: String,
pub numero: Value, pub numero: Value,
pub signature: Option<String>,
pub format: Value,
pub details: Details,
pub doc_regulateur: bool,
pub nom_fichier: String,
pub date_reception: Value,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Details {
pub date: String,
#[serde(rename = "content_type")]
pub content_type: String,
pub language: String,
pub title: String,
#[serde(rename = "content_length")]
pub content_length: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Societe {
pub role: String,
pub raison_sociale: String,
pub jeton: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Aggregations {
pub types_information: TypesInformation,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TypesInformation {
#[serde(rename = "doc_count_error_upper_bound")]
pub doc_count_error_upper_bound: i64,
#[serde(rename = "sum_other_doc_count")]
pub sum_other_doc_count: i64,
pub buckets: Vec<Bucket>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bucket {
pub key: String,
#[serde(rename = "doc_count")]
pub doc_count: i64,
pub types_operation: TypesOperation,
pub types_document: TypesDocument,
pub instrument_financier: InstrumentFinancier,
pub marche: Marche,
pub annee_comptable: AnneeComptable,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TypesOperation {
#[serde(rename = "doc_count_error_upper_bound")]
pub doc_count_error_upper_bound: i64,
#[serde(rename = "sum_other_doc_count")]
pub sum_other_doc_count: i64,
pub buckets: Vec<Value>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TypesDocument {
#[serde(rename = "doc_count_error_upper_bound")]
pub doc_count_error_upper_bound: i64,
#[serde(rename = "sum_other_doc_count")]
pub sum_other_doc_count: i64,
pub buckets: Vec<Bucket2>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bucket2 {
pub key: String,
#[serde(rename = "doc_count")]
pub doc_count: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstrumentFinancier {
#[serde(rename = "doc_count_error_upper_bound")]
pub doc_count_error_upper_bound: i64,
#[serde(rename = "sum_other_doc_count")]
pub sum_other_doc_count: i64,
pub buckets: Vec<Value>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Marche {
#[serde(rename = "doc_count_error_upper_bound")]
pub doc_count_error_upper_bound: i64,
#[serde(rename = "sum_other_doc_count")]
pub sum_other_doc_count: i64,
pub buckets: Vec<Value>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnneeComptable {
#[serde(rename = "doc_count_error_upper_bound")]
pub doc_count_error_upper_bound: i64,
#[serde(rename = "sum_other_doc_count")]
pub sum_other_doc_count: i64,
pub buckets: Vec<Value>,
} }

@ -1,7 +0,0 @@
extern crate pretty_env_logger;
use log::SetLoggerError;
/// For this function to do anything the RUST_LOG environment variable should be set.
pub fn init_log() -> Result<(), SetLoggerError> {
pretty_env_logger::try_init_timed()
}

@ -3,7 +3,6 @@
// Macros // Macros
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
extern crate pretty_env_logger;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
@ -30,7 +29,6 @@ mod amf;
mod db; mod db;
mod env; mod env;
mod error; mod error;
mod logger;
mod model; mod model;
mod repo; mod repo;
mod route; mod route;
@ -82,6 +80,10 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
"/transaction/aggregated", "/transaction/aggregated",
get(transaction::get_aggregated_transactions), get(transaction::get_aggregated_transactions),
) )
.route(
"/transaction/major",
get(transaction::get_recent_major_transactions),
)
.route( .route(
"/in_process_transaction", "/in_process_transaction",
get(in_process_transaction::get_all), get(in_process_transaction::get_all),

@ -181,6 +181,71 @@ pub async fn get_aggregated_transactions(
Ok(Json(res)) Ok(Json(res))
} }
#[derive(FromQueryResult, Serialize)]
pub struct MajorTransactions {
company_name: String,
slug: String,
date_published: NaiveDate,
date_executed: NaiveDate,
nature: String,
instrument: String,
volume: i32,
unit_price: f32,
total: f32,
}
pub async fn get_recent_major_transactions(
state: State<AppState>,
pagination: Query<Pagination>,
) -> Result<Json<PaginatedResponse<MajorTransactions>>, AppError> {
let db = &state.db;
let s = pagination.size.unwrap_or(20).min(50);
let query_raw = "SELECT
b.name as company_name,
b.slug,
date_published,
date_executed,
nature,
instrument,
volume,
unit_price,
volume * unit_price as total
FROM transaction a
JOIN company b
ON a.company_id = b.id
WHERE unit_price*volume > 1000000
AND created_at_utc > DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
ORDER BY a.nature, total DESC"
.to_string();
let query = model::transaction::Entity::find()
.from_raw_sql(Statement::from_string(
DbBackend::MySql,
query_raw.to_string(),
))
.into_model::<MajorTransactions>();
let pages = query.paginate(db, s);
let ItemsAndPagesNumber {
number_of_items: count,
number_of_pages: num_pages,
} = pages.num_items_and_pages().await?;
let p = pagination.page.unwrap_or(0).min(num_pages);
let list = pages.fetch_page(p).await?;
let res = PaginatedResponse {
count,
num_pages,
list,
};
Ok(Json(res))
}
#[derive(Serialize, FromQueryResult, Debug)] #[derive(Serialize, FromQueryResult, Debug)]
pub struct TransactionsAggregated { pub struct TransactionsAggregated {
id: i32, id: i32,

@ -64,7 +64,7 @@ impl GetAMFTransactions {
let list = resp.get_hits(); let list = resp.get_hits();
for hit in list.iter() { for hit in list.iter() {
let number = &hit.source.numero; let number = &hit.numero;
if transaction::Entity::find() if transaction::Entity::find()
.filter(transaction::Column::ForeignId.eq(number.to_owned())) .filter(transaction::Column::ForeignId.eq(number.to_owned()))

@ -21,7 +21,7 @@ pub async fn run_tasks(tasks_pool: &DatabaseConnection) -> Result<(), sea_orm::D
loop { loop {
inter.tick().await; inter.tick().await;
info!("Running task: getamftransactions"); info!("Running task: getamftransactions");
match GetAMFTransactions::new(1000).run(&tasks_pool).await { match GetAMFTransactions::new(1000).run(tasks_pool).await {
Ok(_) => (), Ok(_) => (),
Err(e) => error!("Task failed: {}", e), Err(e) => error!("Task failed: {}", e),
}; };

Loading…
Cancel
Save