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/
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
[dependencies]
chrono = { workspace = true, features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
perseus = { version = "0.4", features = ["hydrate"] }
chrono = { version = "0.4.23", features = ["serde"] }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91"
perseus = { version = "0.4.1", features = ["hydrate"] }
sycamore = { version = "^0.8.1", features = [
"ssr",
"serde",
"suspense",
"hydrate",
] }
lazy_static = "1"
[target.'cfg(engine)'.dev-dependencies]
fantoccini = "^0.19.3"

@ -7,7 +7,7 @@ RUN apt-get update \
build-essential curl wget pkg-config
# vars
ENV PERSEUS_VERSION=0.4.0 \
ENV PERSEUS_VERSION=0.4.1 \
PERSEUS_SIZE_OPT_VERSION=0.1.9 \
ESBUILD_VERSION=0.15.18 \
BINARYEN_VERSION=112
@ -21,43 +21,22 @@ RUN rustup target add wasm32-unknown-unknown;\
# retrieve the src dir
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
RUN cargo install perseus-cli --version $PERSEUS_VERSION;\
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
RUN perseus deploy
# go back to app dir
WORKDIR /app
# download and unpack esbuild
RUN curl -O https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-${ESBUILD_VERSION}.tgz \
&& tar xf esbuild-linux-64-${ESBUILD_VERSION}.tgz \
&& ./package/bin/esbuild --version
# 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 \
&& ls -lha ./client/pkg/dist/pkg
RUN ./package/bin/esbuild ./pkg/dist/pkg/perseus_engine.js --minify --target=es6 --outfile=./pkg/dist/pkg/perseus_engine.js --allow-overwrite \
&& ls -lha ./pkg/dist/pkg
# 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 \
@ -65,14 +44,14 @@ RUN wget -nv https://github.com/WebAssembly/binaryen/releases/download/version_$
&& ./binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt --version
# 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
FROM debian:stable-slim
WORKDIR /app
COPY --from=build /app/client/pkg /app/
COPY --from=build /app/pkg /app/
ENV HOST=0.0.0.0

@ -1,7 +1,9 @@
use crate::api::{
types::{
paginated_response::PaginatedResponse,
transaction::{LatestTransaction, TransactionCompany, TransactionsAggregated},
transaction::{
LatestTransaction, MajorTransactions, TransactionCompany, TransactionsAggregated,
},
},
FastInsidersApi,
};
@ -105,4 +107,32 @@ impl FastInsidersApi {
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,7 +3,7 @@ use sycamore::prelude::*;
use crate::components::base_table::TableContent;
use super::transaction::{TransactionCompany, TransactionsAggregated, LatestTransaction};
use super::transaction::{TransactionCompany, TransactionsAggregated, LatestTransaction, MajorTransactions};
pub trait IntoTableData<G>
where
@ -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 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,30 +1,10 @@
use perseus::prelude::*;
use sycamore::prelude::*;
use crate::global_state::AppStateRx;
use crate::capsules::dark_mode_btn::DARK_MODE_BTN;
#[component]
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() {
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,
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") {
@ -39,8 +19,7 @@ pub fn TheHeader<G: Html>(cx: Scope) -> View<G> {
}
}
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")
{ "Toggle dark mode" }
(DARK_MODE_BTN.widget(cx,"",()))
}
}
}

@ -2,6 +2,7 @@ use perseus::prelude::*;
use sycamore::view;
mod api;
pub mod capsules;
mod components;
mod env;
pub mod error_pages;
@ -13,6 +14,7 @@ pub fn main<G: Html>() -> PerseusApp<G> {
PerseusApp::new()
.template(crate::templates::index::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())
.error_views(crate::error_pages::get_error_views())
.index_view(|cx| {

@ -2,7 +2,7 @@ use perseus::prelude::*;
use sycamore::prelude::*;
use crate::{
api::types::transaction::{LatestTransaction, TransactionsAggregated},
api::types::transaction::{LatestTransaction, MajorTransactions, TransactionsAggregated},
components::{
main_content_container::MainContentContainer,
paginated_data_table::{PaginatedTable, PaginatedTableStateRx},
@ -42,6 +42,17 @@ fn index_page<G: Html>(cx: BoundedScope) -> View<G> {
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, || {
if *global_state.dark_mode.get() {
"dark"
@ -68,6 +79,12 @@ fn index_page<G: Html>(cx: BoundedScope) -> View<G> {
}
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)
}
}
}
}

File diff suppressed because one or more lines are too long

@ -3,8 +3,7 @@ services:
client:
container_name: fast-insiders-client
image: docker.albv.org/root/fast-insiders-client:latest
build:
dockerfile: ./client/Dockerfile
build: ./client
restart: always
ports:
- 8080:8080
@ -14,8 +13,7 @@ services:
server:
container_name: fast-insiders-server
image: docker.albv.org/root/fast-insiders-server:latest
build:
dockerfile: ./server/Dockerfile
build: ./server
restart: always
ports:
- 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
[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" }
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"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
axum = "0.6.12"

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

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

@ -1,7 +1,7 @@
use crate::{
amf::{
types::{
amf_response::{Document, Hit},
amf_response::{Document, Source},
AMFResponse, TransactionData,
},
TransactionDataTrait,
@ -12,16 +12,16 @@ use crate::{
use super::pdf::AMFPdf;
impl AMFResponse {
pub fn get_hits(&self) -> &Vec<Hit> {
&self.hits.hits
pub fn get_hits(&self) -> &Vec<Source> {
&self.hits
}
}
#[async_trait::async_trait]
impl TransactionDataTrait for Hit {
impl TransactionDataTrait for Source {
type Err = GetAMFTransactionsError;
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();
if docs.len() > 1 {
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> {
&self.source.documents
&self.documents
}
pub fn get_foreign_id(&self) -> String {
self.source.numero.to_owned()
self.numero.to_owned()
}
}

@ -4,32 +4,8 @@ use serde_json::Value;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AMFResponse {
pub hits: Hits,
pub aggregations: Aggregations,
}
#[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>,
#[serde(rename = "result")]
pub hits: Vec<Source>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -60,128 +36,12 @@ pub struct Source {
pub version: i64,
pub regulateur: String,
pub relations: Vec<Value>,
pub societes: Vec<Societe>,
pub annee_comptable: Value,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Document {
pub accessible: bool,
pub issuer_id: Option<String>,
pub path: String,
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>,
}

@ -80,6 +80,10 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
"/transaction/aggregated",
get(transaction::get_aggregated_transactions),
)
.route(
"/transaction/major",
get(transaction::get_recent_major_transactions),
)
.route(
"/in_process_transaction",
get(in_process_transaction::get_all),

@ -181,6 +181,71 @@ pub async fn get_aggregated_transactions(
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)]
pub struct TransactionsAggregated {
id: i32,

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

Loading…
Cancel
Save