Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,602 changes: 2,163 additions & 439 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 4 additions & 8 deletions apps/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,6 @@ sword = { git = "https://github.com/sword-web/sword.git", features = [

sword-layers = { git = "https://github.com/sword-web/sword.git", package = "sword-layers", features = ["cors"] }

sqlx = { version = "0.8.6", features = [
"chrono",
"macros",
"postgres",
"runtime-tokio",
"uuid",
] }

time = "0.3.47"
csv = "1.3"
tracing = "0.1.44"
Expand All @@ -61,3 +53,7 @@ html-escape = "0.2.14"
lettre = { version = "0.11.19", features = ["tokio1", "tokio1-native-tls"] }
zip = "2"
reqwest = { version = "0.12.23", features = ["stream"] }
toasty = { version = "0.9.0", features = ["postgresql", "jiff", "serde"] }
jiff = { version = "0.2.34", features = ["serde"] }
parking_lot = "0.12.5"
o2o = "0.5.5"
61 changes: 31 additions & 30 deletions apps/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
# ==============================================================================
# Base Stage
# ==============================================================================

ARG RUST_VERSION=1.97
FROM rust:${RUST_VERSION}-slim AS base

RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config \
libssl-dev \
curl \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

# ==============================================================================
# Development Stage
# ==============================================================================

ARG RUST_VERSION=1.97
FROM rust:${RUST_VERSION}-alpine AS dev
FROM base AS dev

RUN apk add --no-cache \
openssl-dev \
openssl-libs-static \
unzip \
RUN apt-get update && apt-get install -y --no-install-recommends \
netcat-openbsd \
xz \
build-base \
pkgconf \
bash \
curl \
&& rm -rf /var/cache/apk/*

RUN wget https://github.com/watchexec/cargo-watch/releases/download/v8.5.3/cargo-watch-v8.5.3-x86_64-unknown-linux-musl.tar.xz && \
tar -xf cargo-watch-v8.5.3-x86_64-unknown-linux-musl.tar.xz && \
mv cargo-watch-v8.5.3-x86_64-unknown-linux-musl/cargo-watch /usr/local/bin/ && \
rm -rf cargo-watch-v8.5.3-x86_64-unknown-linux-musl.tar.xz cargo-watch-v8.5.3-x86_64-unknown-linux-musl
xz-utils \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
ADD --chmod=755 \
https://github.com/watchexec/cargo-watch/releases/download/v8.5.3/cargo-watch-v8.5.3-x86_64-unknown-linux-gnu.tar.xz \
/tmp/cargo-watch.tar.xz

RUN tar -xf /tmp/cargo-watch.tar.xz -C /usr/local/bin --strip-components=1 \
&& rm /tmp/cargo-watch.tar.xz

CMD ["/bin/bash", "/app/apps/server/config/scripts/entrypoint.sh"]

Expand All @@ -31,17 +39,8 @@ CMD ["/bin/bash", "/app/apps/server/config/scripts/entrypoint.sh"]
# ==============================================================================

ARG RUST_VERSION=1.97
FROM rust:${RUST_VERSION}-alpine AS builder

RUN apk add --no-cache \
build-base \
pkgconf \
openssl-dev \
openssl-libs-static \
curl \
&& rm -rf /var/cache/apk/*
FROM base AS builder

WORKDIR /app
COPY . .

RUN cargo build -p acad-mgr-server --release
Expand All @@ -50,13 +49,15 @@ RUN cargo build -p acad-mgr-server --release
# Production Stage
# ==============================================================================

FROM alpine:latest AS prod
FROM debian:trixie-slim AS prod

RUN apk add --no-cache ca-certificates openssl ripgrep tree && rm -rf /var/cache/apk/*
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

RUN adduser -D appuser
RUN adduser --disabled-password appuser
USER appuser

COPY --from=builder /app/target/release/orcid-acad-mgr-server ./server
Expand Down
6 changes: 3 additions & 3 deletions apps/server/bin/issn-seeder/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
let database_url = std::env::var("LOCAL_POSTGRES_DATABASE_URL")
.or_else(|_| std::env::var("POSTGRES_DATABASE_URL"))?;

let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(5)
let mut db = toasty::Db::builder()
.max_pool_size(5)
.connect(&database_url)
.await?;

Expand Down Expand Up @@ -55,7 +55,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
let records = reader::read_csv(&path)?;
eprintln!(" {} records loaded, inserting...", records.len());

let affected = seeder::seed_records(&pool, &records, kind).await?;
let affected = seeder::seed_records(&mut db, &records, kind).await?;
eprintln!(" Done — {} rows affected", affected);
}

Expand Down
199 changes: 15 additions & 184 deletions apps/server/bin/issn-seeder/seeder.rs
Original file line number Diff line number Diff line change
@@ -1,204 +1,35 @@
use crate::reader::IssnRecord;
use sqlx::PgPool;

const CHUNK_SIZE: usize = 500;

pub async fn seed_records(
pool: &PgPool,
db: &mut toasty::Db,
records: &[IssnRecord],
kind: &str,
) -> Result<u64, Box<dyn std::error::Error>> {
let mut total_affected = 0u64;
let mut total = 0u64;

for chunk in records.chunks(CHUNK_SIZE) {
let mut tx = pool.begin().await?;
let mut tx = db.transaction().await?;
for r in chunk {
total_affected += upsert_record(&mut tx, r, kind).await?;
}
tx.commit().await?;
}
let issn = r.issn.as_deref().or(r.eissn.as_deref());

Ok(total_affected)
}

async fn upsert_record(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
r: &IssnRecord,
kind: &str,
) -> Result<u64, Box<dyn std::error::Error>> {
if kind == "wos" {
if let Some(ref issn) = r.issn {
return upsert_issn_wos(tx, issn, r.eissn.as_deref()).await;
}
if let Some(ref eissn) = r.eissn {
return upsert_eissn_wos(tx, eissn).await;
}
} else {
if let Some(ref issn) = r.issn {
return upsert_issn_scopus(tx, issn, r.eissn.as_deref()).await;
}
if let Some(ref eissn) = r.eissn {
return upsert_eissn_scopus(tx, eissn).await;
}
}
Ok(0)
}

async fn upsert_issn_wos(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
issn: &str,
eissn: Option<&str>,
) -> Result<u64, Box<dyn std::error::Error>> {
sqlx::query("SAVEPOINT wos_issn_sp")
.execute(&mut **tx)
.await?;

let result = sqlx::query(
r#"INSERT INTO journal_issn (issn, eissn, kind)
VALUES ($1, $2, 'wos'::journal_kind)
ON CONFLICT (issn) DO UPDATE SET
eissn = COALESCE(journal_issn.eissn, EXCLUDED.eissn),
kind = EXCLUDED.kind"#,
)
.bind(issn)
.bind(eissn)
.execute(&mut **tx)
.await;

match result {
Ok(r) => {
sqlx::query("RELEASE SAVEPOINT wos_issn_sp")
.execute(&mut **tx)
.await?;
Ok(r.rows_affected())
}
Err(sqlx::Error::Database(ref e)) if e.is_unique_violation() => {
sqlx::query("ROLLBACK TO SAVEPOINT wos_issn_sp")
.execute(&mut **tx)
.await?;
if let Some(eissn_val) = eissn {
let r = sqlx::query(
r#"UPDATE journal_issn SET
issn = COALESCE(issn, $1),
kind = 'wos'::journal_kind
WHERE eissn = $2"#,
if let Some(v) = issn {
toasty::sql::statement(
"INSERT INTO journal_issn (issn, kind) VALUES ($1, $2::journal_kind)
ON CONFLICT (issn) DO UPDATE SET kind = $2::journal_kind",
)
.bind(issn)
.bind(eissn_val)
.execute(&mut **tx)
.bind(v)
.bind(kind)
.exec(&mut tx)
.await?;
Ok(r.rows_affected())
} else {
Ok(0)
}
}
Err(e) => Err(Box::new(e)),
}
}

async fn upsert_eissn_wos(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
eissn: &str,
) -> Result<u64, Box<dyn std::error::Error>> {
let result = sqlx::query(
r#"INSERT INTO journal_issn (eissn, kind)
VALUES ($1, 'wos'::journal_kind)
ON CONFLICT (eissn) DO UPDATE SET kind = EXCLUDED.kind"#,
)
.bind(eissn)
.execute(&mut **tx)
.await?;
Ok(result.rows_affected())
}

async fn upsert_issn_scopus(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
issn: &str,
eissn: Option<&str>,
) -> Result<u64, Box<dyn std::error::Error>> {
if let Some(eissn_val) = eissn
&& let Some(id) = find_id_by_eissn(tx, eissn_val).await?
{
return update_issin_by_id(tx, id, issn).await;
}

if let Some(id) = find_id_by_issn(tx, issn).await? {
if let Some(eissn_val) = eissn {
return update_eissn_by_id(tx, id, eissn_val).await;
total += 1;
}
}
return Ok(0);
}

let result = sqlx::query(
"INSERT INTO journal_issn (issn, eissn, kind) VALUES ($1, $2, 'scopus'::journal_kind)",
)
.bind(issn)
.bind(eissn)
.execute(&mut **tx)
.await?;
Ok(result.rows_affected())
}

async fn upsert_eissn_scopus(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
eissn: &str,
) -> Result<u64, Box<dyn std::error::Error>> {
if find_id_by_eissn(tx, eissn).await?.is_some() {
return Ok(0);
tx.commit().await?;
}

let result =
sqlx::query("INSERT INTO journal_issn (eissn, kind) VALUES ($1, 'scopus'::journal_kind)")
.bind(eissn)
.execute(&mut **tx)
.await?;
Ok(result.rows_affected())
}

async fn find_id_by_eissn(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
value: &str,
) -> Result<Option<i32>, Box<dyn std::error::Error>> {
sqlx::query_scalar::<_, i32>("SELECT id FROM journal_issn WHERE eissn = $1")
.bind(value)
.fetch_optional(&mut **tx)
.await
.map_err(Into::into)
}

async fn find_id_by_issn(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
value: &str,
) -> Result<Option<i32>, Box<dyn std::error::Error>> {
sqlx::query_scalar::<_, i32>("SELECT id FROM journal_issn WHERE issn = $1")
.bind(value)
.fetch_optional(&mut **tx)
.await
.map_err(Into::into)
}

async fn update_issin_by_id(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
id: i32,
issn: &str,
) -> Result<u64, Box<dyn std::error::Error>> {
let result = sqlx::query("UPDATE journal_issn SET issn = COALESCE(issn, $1) WHERE id = $2")
.bind(issn)
.bind(id)
.execute(&mut **tx)
.await?;
Ok(result.rows_affected())
}

async fn update_eissn_by_id(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
id: i32,
eissn: &str,
) -> Result<u64, Box<dyn std::error::Error>> {
let result = sqlx::query("UPDATE journal_issn SET eissn = COALESCE(eissn, $1) WHERE id = $2")
.bind(eissn)
.bind(id)
.execute(&mut **tx)
.await?;
Ok(result.rows_affected())
Ok(total)
}
8 changes: 4 additions & 4 deletions apps/server/src/academic/academics/dtos/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::{ORCID_ID_REGEX, RUT_REGEX};
use crate::academic::{Academic, AcademicCategoryOptionId, Sex};
use crate::university::{AcademicWorkPositionId, CareerId, DepartmentId};

use chrono::NaiveDate;
use jiff::civil::Date;
use serde::{Deserialize, Serialize};
use validator::Validate;

Expand Down Expand Up @@ -47,10 +47,10 @@ pub struct CreateAcademicDto {
pub sex: Sex,

#[validate(custom(function = "super::validate_birth_date"))]
pub birth_date: NaiveDate,
pub birth_date: Date,

#[validate(custom(function = "super::validate_joined_at"))]
pub joined_at: NaiveDate,
pub joined_at: Date,
pub work_position_id: AcademicWorkPositionId,
pub department_id: DepartmentId,
pub career_id: Option<CareerId>,
Expand Down Expand Up @@ -100,7 +100,7 @@ impl From<CreateAcademicDto> for Academic {
.work_position_id(input.work_position_id)
.department_id(input.department_id)
.maybe_career_id(input.career_id)
.acad_category_options_id(input.acad_category_options_id)
.category_option_id(input.acad_category_options_id)
.annual_discount_hours(input.annual_discount_hours)
.nationality_code(input.nationality_code)
.city(input.city)
Expand Down
Loading
Loading