From 5a4c31f967d7505aa4f0fad0e5e9645bf34f1081 Mon Sep 17 00:00:00 2001 From: MrRevillod Date: Fri, 24 Jul 2026 15:24:15 -0400 Subject: [PATCH 1/7] chore: improve server's docker image build time --- apps/server/Dockerfile | 60 +++++++++++++++++++++--------------------- compose.yml | 6 ++++- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index 53b7534..91e22dd 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -1,28 +1,35 @@ +# ============================================================================== +# 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 \ + && 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"] @@ -31,17 +38,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 @@ -50,13 +48,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 diff --git a/compose.yml b/compose.yml index 328d7b4..b2341d9 100644 --- a/compose.yml +++ b/compose.yml @@ -11,7 +11,11 @@ services: postgres: condition: service_healthy healthcheck: - test: ["CMD-SHELL", "nc -z localhost 8000 || exit 1"] + test: + [ + "CMD-SHELL", + "nc -z localhost 8000 || exit 1", + ] interval: 10s timeout: 5s retries: 5 From a8ca0ccae8fed185671a4e94035b8cf71cbdd08f Mon Sep 17 00:00:00 2001 From: MrRevillod Date: Sat, 25 Jul 2026 12:51:06 -0400 Subject: [PATCH 2/7] refactor: migrate from sqlx -> toasty on auth module --- Cargo.lock | 2595 ++++++++++++++++++---- apps/server/Cargo.toml | 10 +- apps/server/Dockerfile | 1 + apps/server/src/auth/controller.rs | 7 +- apps/server/src/auth/dtos.rs | 9 +- apps/server/src/auth/entity.rs | 30 +- apps/server/src/auth/repository.rs | 87 +- apps/server/src/auth/services/cookies.rs | 14 +- apps/server/src/auth/services/mod.rs | 19 +- apps/server/src/auth/users/dtos.rs | 3 +- apps/server/src/auth/users/entity.rs | 28 +- apps/server/src/auth/users/repository.rs | 121 +- apps/server/src/auth/users/service.rs | 34 +- apps/server/src/shared/database.rs | 38 +- apps/server/src/shared/errors.rs | 8 +- apps/server/src/shared/id.rs | 225 +- apps/server/src/shared/mod.rs | 2 +- apps/server/src/shared/seeder.rs | 32 +- 18 files changed, 2412 insertions(+), 851 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5557db3..e9080e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,7 @@ dependencies = [ "chrono", "csv", "html-escape", + "jiff", "jsonwebtoken", "lettre", "papers-openalex", @@ -19,11 +20,11 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", - "sqlx", "sword", "sword-layers", "thiserror", "time", + "toasty", "tokio", "tower-http", "tracing", @@ -44,10 +45,20 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common 0.1.6", + "crypto-common 0.1.7", "generic-array", ] +[[package]] +name = "aegis" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58541132f980da31e9aa99f7bdee69bc84bf1e168b9b91ef2dbe8abb7b4ce5dd" +dependencies = [ + "cc", + "softaes", +] + [[package]] name = "aes" version = "0.8.4" @@ -99,6 +110,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "allocator-api2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -108,6 +125,28 @@ dependencies = [ "libc", ] +[[package]] +name = "antithesis_sdk" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08410fcac93669a476c006cd6c4512ac1e2b30fd117231a5d55d8a2c76599b82" +dependencies = [ + "libc", + "libloading", + "linkme", + "once_cell", + "rand 0.8.7", + "rustc_version_runtime", + "serde", + "serde_json", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "arbitrary" version = "1.4.2" @@ -117,12 +156,53 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "aristo" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdeefa800110050103e2459a2f8dbdbd560ad046e30f1f87e24ce19c2cb8bde" +dependencies = [ + "aristo-macros", +] + +[[package]] +name = "aristo-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64a66d21a80182b35b1741997a6d2456911f54b7eb1918aa4e2382fc205268d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + [[package]] name = "arrayvec" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "assoc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8" + [[package]] name = "async-trait" version = "0.1.91" @@ -131,16 +211,7 @@ checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", -] - -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", + "syn 3.0.3", ] [[package]] @@ -289,13 +360,80 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.13.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex 1.3.0", + "syn 2.0.119", + "which", +] + +[[package]] +name = "bit-set" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitpacking" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019" dependencies = [ - "serde_core", + "crunchy", ] [[package]] @@ -387,12 +525,36 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "branches" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fba76cd916045514e3064707df9ed5282b94419444c2753a6c62ecaf458e56" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "btoi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5ab9db53bcda568284df0fd39f6eac24ad6f7ba7ff1168b9e76eba6576b976" +dependencies = [ + "num-traits", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "byte-unit" version = "5.2.5" @@ -427,6 +589,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -463,14 +645,29 @@ dependencies = [ [[package]] name = "cc" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", +] + +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", ] [[package]] @@ -485,6 +682,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "cfg_block" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18758054972164c3264f7c8386f5fc6da6114cb46b619fd365d4e3b2dc3ae487" + [[package]] name = "chacha20" version = "0.10.1" @@ -516,7 +719,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common 0.1.6", + "crypto-common 0.1.7", "inout 0.1.4", ] @@ -530,6 +733,17 @@ dependencies = [ "inout 0.2.2", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "cmake" version = "0.1.58" @@ -539,6 +753,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "combine" version = "4.6.7" @@ -585,7 +805,7 @@ dependencies = [ "aes-gcm", "base64", "hkdf", - "hmac", + "hmac 0.12.1", "percent-encoding", "rand 0.8.7", "sha2 0.10.9", @@ -653,6 +873,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -662,6 +891,34 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.13" @@ -677,6 +934,12 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -691,9 +954,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -739,6 +1002,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -806,12 +1078,69 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "datasketches" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c286de4e81ea2590afc24d754e0f83810c566f50a1388fa75ebd57928c0d9745" + +[[package]] +name = "deadpool" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883466cb8db62725aee5f4a6011e8a5d42912b42632df32aad57fc91127c6e04" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae" +dependencies = [ + "tokio", +] + [[package]] name = "deflate64" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + [[package]] name = "der" version = "0.7.10" @@ -819,15 +1148,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", + "der_derive", + "flagset", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] [[package]] name = "derive_arbitrary" @@ -848,7 +1193,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid 0.9.6", - "crypto-common 0.1.6", + "crypto-common 0.1.7", "subtle", ] @@ -861,6 +1206,7 @@ dependencies = [ "block-buffer 0.12.1", "const-oid 0.10.2", "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -896,10 +1242,10 @@ dependencies = [ ] [[package]] -name = "dotenvy" -version = "0.15.7" +name = "downcast-rs" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "dunce" @@ -967,12 +1313,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" -dependencies = [ - "serde", -] +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "elliptic-curve" @@ -1067,12 +1410,43 @@ dependencies = [ ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "env_filter" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "env_filter", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1083,27 +1457,40 @@ dependencies = [ ] [[package]] -name = "etcetera" -version = "0.8.0" +name = "fallible-iterator" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] -name = "event-listener" -version = "5.4.1" +name = "fastbloom" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", + "getrandom 0.3.4", + "libm", + "siphasher", ] +[[package]] +name = "fastdivide" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" + [[package]] name = "fastrand" version = "2.5.0" @@ -1132,6 +1519,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.9" @@ -1139,21 +1532,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", + "libz-sys", "miniz_oxide", "zlib-rs", ] -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1162,9 +1545,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "foreign-types" @@ -1190,6 +1573,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix 1.1.4", + "windows-sys 0.59.0", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -1218,28 +1611,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" -[[package]] -name = "futures-executor" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - [[package]] name = "futures-io" version = "0.3.33" @@ -1298,11 +1669,41 @@ dependencies = [ "slab", ] +[[package]] +name = "genawaiter" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" +dependencies = [ + "genawaiter-macro", +] + +[[package]] +name = "genawaiter-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1318,7 +1719,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1360,6 +1761,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "group" version = "0.13.0" @@ -1401,11 +1808,11 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", "equivalent", "foldhash", ] @@ -1415,14 +1822,19 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2 0.2.21", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" -version = "0.10.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.17.1", ] [[package]] @@ -1431,6 +1843,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1443,7 +1861,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -1455,6 +1873,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "home" version = "0.5.12" @@ -1481,6 +1908,12 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c1ff2d1cbf39efe5af0900ced8a069b5e61557a17544eb0c4a50239937389e" +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "1.4.2" @@ -1637,6 +2070,31 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bbdf98e5e0aa827770acee171ebb568aaab975a36b56afdb5fd0e2f525750bb" +dependencies = [ + "icu_collator_data", + "icu_collections", + "icu_locale", + "icu_locale_core", + "icu_normalizer", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_collator_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038ed8e5817f2059c2f3efb0945ba78d060d3d25e8f1a1bea5139f821a21a2f0" + [[package]] name = "icu_collections" version = "2.2.0" @@ -1651,6 +2109,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locale" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + [[package]] name = "icu_locale_core" version = "2.2.0" @@ -1659,11 +2132,18 @@ checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993" + [[package]] name = "icu_normalizer" version = "2.2.0" @@ -1675,6 +2155,9 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] @@ -1712,6 +2195,8 @@ checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1746,6 +2231,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "index_vec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44faf5bb8861a9c72e20d3fb0fdbd59233e43056e2b80475ab0aacdc2e781355" + [[package]] name = "indexmap" version = "2.14.0" @@ -1776,6 +2267,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "intrusive-collections" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" +dependencies = [ + "memoffset", +] + [[package]] name = "inventory" version = "0.3.24" @@ -1785,18 +2285,100 @@ dependencies = [ "rustversion", ] +[[package]] +name = "io-uring" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9080b15e63775b9a2ac7dca720f7050a8b955e092ea0f6020a4a80f69998cdc0" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.22.4" @@ -1876,7 +2458,7 @@ dependencies = [ "base64", "ed25519-dalek", "getrandom 0.2.17", - "hmac", + "hmac 0.12.1", "js-sys", "p256", "p384", @@ -1891,6 +2473,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "keyed_priority_queue" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" +dependencies = [ + "indexmap", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1900,6 +2491,12 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lettre" version = "0.11.22" @@ -1918,7 +2515,7 @@ dependencies = [ "idna", "mime", "native-tls", - "nom", + "nom 8.0.0", "percent-encoding", "quoted_printable", "socket2", @@ -1927,11 +2524,27 @@ dependencies = [ "url", ] +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] [[package]] name = "libm" @@ -1939,28 +2552,72 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", +] + [[package]] name = "libredox" version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags", "libc", - "plain", - "redox_syscall 0.9.0", ] [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ + "cc", "pkg-config", "vcpkg", ] +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1988,12 +2645,49 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lz4_flex" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" + [[package]] name = "lzma-rs" version = "0.3.0" @@ -2038,12 +2732,21 @@ checksum = "8863b587001c1b9a8a4e36008cebc6b3612cb1226fe2de94858e06092687b608" [[package]] name = "md-5" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest 0.10.7", + "digest 0.11.3", +] + +[[package]] +name = "measure_time" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" +dependencies = [ + "log", ] [[package]] @@ -2052,6 +2755,55 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" @@ -2068,6 +2820,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2085,7 +2843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -2106,6 +2864,68 @@ dependencies = [ "version_check", ] +[[package]] +name = "murmurhash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" + +[[package]] +name = "mysql_async" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3519e91b0d254ac1ffa495bc42053286cb2172ad7241d5b3b1b9f8a891f21ee2" +dependencies = [ + "bytes", + "crossbeam-queue", + "crossbeam-utils", + "flate2", + "futures-core", + "futures-sink", + "futures-util", + "keyed_priority_queue", + "lru 0.18.1", + "mysql_common", + "native-tls", + "pem", + "percent-encoding", + "rand 0.10.2", + "serde", + "socket2", + "thiserror", + "tokio", + "tokio-native-tls", + "tokio-util", + "twox-hash", + "url", +] + +[[package]] +name = "mysql_common" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f27695f286b461da077b8c2f72f47feaa04ce3c3f9c0976257410e90e21208a" +dependencies = [ + "base64", + "bitflags 2.13.1", + "btoi", + "byteorder", + "bytes", + "crc32fast", + "flate2", + "getrandom 0.3.4", + "num-bigint", + "num-traits", + "regex", + "saturating", + "serde", + "serde_json", + "sha1", + "sha2 0.10.9", + "thiserror", + "uuid", +] + [[package]] name = "native-tls" version = "0.2.18" @@ -2123,6 +2943,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -2202,12 +3032,46 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "oneshot" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -2220,7 +3084,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags", + "bitflags 2.13.1", "cfg-if", "foreign-types", "libc", @@ -2263,6 +3127,30 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ownedbytes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "p256" version = "0.13.2" @@ -2287,6 +3175,15 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "pack1" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b7bb0ecf2e447b1f20ee94ee79ef6eed1e9d4b3c36ce1903b9dea3bf205523" +dependencies = [ + "bytemuck", +] + [[package]] name = "papers-openalex" version = "0.3.1" @@ -2325,11 +3222,17 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -2337,7 +3240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac", + "hmac 0.12.1", ] [[package]] @@ -2365,6 +3268,25 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared", + "serde", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -2399,10 +3321,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] -name = "plain" -version = "0.2.3" +name = "pluralizer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "polling" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] [[package]] name = "polyval" @@ -2417,38 +3357,87 @@ dependencies = [ ] [[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" +name = "portable-atomic" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "portable-atomic-util" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ - "zerocopy", + "portable-atomic", ] [[package]] -name = "prettyplease" -version = "0.2.37" +name = "postgres-protocol" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" dependencies = [ - "proc-macro2", - "syn 2.0.119", -] + "base64", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "hmac 0.13.0", + "md-5", + "memchr", + "rand 0.10.2", + "sha2 0.11.0", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" +dependencies = [ + "array-init", + "bytes", + "fallible-iterator 0.2.0", + "jiff", + "postgres-protocol", + "uuid", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] [[package]] name = "primeorder" @@ -2470,9 +3459,9 @@ dependencies = [ [[package]] name = "proc-macro-error-attr3" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e4dd828515431dd6c4a030d26f7eaed7dd4778226e9d2bb968d65ca4ec3d4d" +checksum = "be5bfc63c4dc85083c9daaf7112d0261701d4058677c3bff7f2afc44e30ef3e1" dependencies = [ "proc-macro2", "quote", @@ -2480,14 +3469,14 @@ dependencies = [ [[package]] name = "proc-macro-error3" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee475e440453418ff1335189eddf7101ba502cd818ab7ae04209bc83aa925aa" +checksum = "dd0d42490f6b7b143eef32b9e3522e42bf25dadc02c69ed72236f80adb949b5c" dependencies = [ "proc-macro-error-attr3", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -2499,6 +3488,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -2530,7 +3542,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "socket2", "thiserror", @@ -2550,9 +3562,9 @@ dependencies = [ "getrandom 0.4.3", "lru-slab", "rand 0.10.2", - "rand_pcg", + "rand_pcg 0.10.2", "ring", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", @@ -2685,6 +3697,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand_pcg" version = "0.10.2" @@ -2695,21 +3716,41 @@ dependencies = [ ] [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "rapidhash" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ - "bitflags", + "crossbeam-deque", + "crossbeam-utils", ] [[package]] name = "redox_syscall" -version = "0.9.0" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -2740,7 +3781,7 @@ checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -2881,7 +3922,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -2947,6 +3988,16 @@ dependencies = [ "serde", ] +[[package]] +name = "roaring" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" +dependencies = [ + "bytemuck", + "byteorder", +] + [[package]] name = "rsa" version = "0.9.10" @@ -2967,6 +4018,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" +dependencies = [ + "bitflags 2.13.1", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + [[package]] name = "rust-embed" version = "8.12.0" @@ -3002,6 +4078,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "rust_decimal" version = "1.42.1" @@ -3019,6 +4105,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -3034,16 +4126,39 @@ dependencies = [ "semver", ] +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -3055,6 +4170,7 @@ checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -3073,11 +4189,20 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -3143,6 +4268,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "saturating" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" + [[package]] name = "schannel" version = "0.1.29" @@ -3164,6 +4295,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3196,7 +4333,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3246,7 +4383,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -3305,6 +4442,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -3336,12 +4479,38 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "shuttle" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab17edba38d63047f46780cf7360acf7467fec2c048928689a5c1dd1c2b4e31" +dependencies = [ + "assoc", + "bitvec", + "cfg-if", + "generator", + "hex", + "owo-colors", + "rand 0.8.7", + "rand_core 0.6.4", + "rand_pcg 0.3.1", + "scoped-tls", + "smallvec", + "tracing", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3396,6 +4565,30 @@ dependencies = [ "time", ] +[[package]] +name = "simsimd" +version = "6.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fb3bc3cdce07a7d7d4caa4c54f8aa967f6be41690482b54b24100a2253fa70" +dependencies = [ + "cc", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "sketches-ddsketch" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e40b6cf54d988dc1a2223531b969c9a9e30906ad90ef64890c27b4bfbb46ea" +dependencies = [ + "serde", +] + [[package]] name = "slab" version = "0.4.12" @@ -3487,14 +4680,17 @@ dependencies = [ "socketioxide-core", ] +[[package]] +name = "softaes" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e14297decde697ddf377c25752aead0927d5cfc89c2684d2af96901a4ceeea" + [[package]] name = "spin" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" -dependencies = [ - "lock_api", -] [[package]] name = "spki" @@ -3507,224 +4703,62 @@ dependencies = [ ] [[package]] -name = "sqlx" -version = "0.8.6" +name = "sqlite-wasm-rs" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", ] [[package]] -name = "sqlx-core" -version = "0.8.6" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" -dependencies = [ - "base64", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.5", - "hashlink", - "indexmap", - "log", - "memchr", - "once_cell", - "percent-encoding", - "serde", - "serde_json", - "sha2 0.10.9", - "smallvec", - "thiserror", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", -] +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "sqlx-macros" -version = "0.8.6" +name = "stringprep" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 2.0.119", + "unicode-bidi", + "unicode-normalization", + "unicode-properties", ] [[package]] -name = "sqlx-macros-core" -version = "0.8.6" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" -dependencies = [ - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2 0.10.9", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 2.0.119", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" -dependencies = [ - "atoi", - "base64", - "bitflags", - "byteorder", - "bytes", - "chrono", - "crc", - "digest 0.10.7", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand 0.8.7", - "rsa", - "serde", - "sha1", - "sha2 0.10.9", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "uuid", - "whoami", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "sqlx-postgres" -version = "0.8.6" +name = "strum" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "atoi", - "base64", - "bitflags", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "rand 0.8.7", - "serde", - "serde_json", - "sha2 0.10.9", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "uuid", - "whoami", + "strum_macros", ] [[package]] -name = "sqlx-sqlite" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core", - "thiserror", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stringprep" -version = "0.1.5" +name = "strum_macros" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "subtle" version = "2.6.1" @@ -3734,7 +4768,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sword" version = "0.2.2" -source = "git+https://github.com/sword-web/sword.git#28e2655967478609c53b6130e7ba3eaf27e1fe32" +source = "git+https://github.com/sword-web/sword.git#5f2a73ce96565c669a3760a854e3872f71924f9e" dependencies = [ "futures-lite", "inventory", @@ -3752,7 +4786,7 @@ dependencies = [ [[package]] name = "sword-core" version = "0.2.2" -source = "git+https://github.com/sword-web/sword.git#28e2655967478609c53b6130e7ba3eaf27e1fe32" +source = "git+https://github.com/sword-web/sword.git#5f2a73ce96565c669a3760a854e3872f71924f9e" dependencies = [ "axum", "inventory", @@ -3767,7 +4801,7 @@ dependencies = [ [[package]] name = "sword-events" version = "0.2.2" -source = "git+https://github.com/sword-web/sword.git#28e2655967478609c53b6130e7ba3eaf27e1fe32" +source = "git+https://github.com/sword-web/sword.git#5f2a73ce96565c669a3760a854e3872f71924f9e" dependencies = [ "inventory", "serde", @@ -3779,7 +4813,7 @@ dependencies = [ [[package]] name = "sword-layers" version = "0.2.2" -source = "git+https://github.com/sword-web/sword.git#28e2655967478609c53b6130e7ba3eaf27e1fe32" +source = "git+https://github.com/sword-web/sword.git#5f2a73ce96565c669a3760a854e3872f71924f9e" dependencies = [ "axum", "axum_responses", @@ -3801,7 +4835,7 @@ dependencies = [ [[package]] name = "sword-macros" version = "0.2.2" -source = "git+https://github.com/sword-web/sword.git#28e2655967478609c53b6130e7ba3eaf27e1fe32" +source = "git+https://github.com/sword-web/sword.git#5f2a73ce96565c669a3760a854e3872f71924f9e" dependencies = [ "axum", "proc-macro2", @@ -3812,7 +4846,7 @@ dependencies = [ [[package]] name = "sword-socketio" version = "0.2.2" -source = "git+https://github.com/sword-web/sword.git#28e2655967478609c53b6130e7ba3eaf27e1fe32" +source = "git+https://github.com/sword-web/sword.git#5f2a73ce96565c669a3760a854e3872f71924f9e" dependencies = [ "axum", "bytes", @@ -3837,7 +4871,7 @@ dependencies = [ [[package]] name = "sword-web" version = "0.2.2" -source = "git+https://github.com/sword-web/sword.git#28e2655967478609c53b6130e7ba3eaf27e1fe32" +source = "git+https://github.com/sword-web/sword.git#5f2a73ce96565c669a3760a854e3872f71924f9e" dependencies = [ "axum", "bytes", @@ -3860,6 +4894,12 @@ dependencies = [ "validator", ] +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "1.0.109" @@ -3884,9 +4924,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -3919,7 +4959,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags", + "bitflags 2.13.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3934,6 +4974,154 @@ dependencies = [ "libc", ] +[[package]] +name = "tantivy" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edde6a10743fff00a4e1a8c9ef020bf5f3cbad301b7d2d39f2b07f123c4eac07" +dependencies = [ + "aho-corasick", + "arc-swap", + "base64", + "bitpacking", + "bon", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "datasketches", + "downcast-rs", + "fastdivide", + "fnv", + "fs4", + "htmlescape", + "itertools 0.14.0", + "levenshtein_automata", + "log", + "lru 0.16.4", + "lz4_flex", + "measure_time", + "memmap2", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash 2.1.3", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror", + "time", + "typetag", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fed3d674429bcd2de5d0a6d1aa5495fed8afd9c5ecce993019caf7615f53fa4" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57166f5bcfd478f370ab8445afb4678dce44801fa5ce5c451aaf8595583c5dc" +dependencies = [ + "downcast-rs", + "fastdivide", + "itertools 0.14.0", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf10915aa75da3c3b0d58b58853d2e889efbaf32d4982a4c3715dde6bba23e5" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time", +] + +[[package]] +name = "tantivy-fst" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" +dependencies = [ + "byteorder", + "regex-syntax", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfadb8526b6da90704feb293b0701a6aae62ea14983143344be2dc5ce30f1d82" +dependencies = [ + "fnv", + "nom 7.1.3", + "ordered-float", + "serde", + "serde_json", +] + +[[package]] +name = "tantivy-sstable" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2cfc3ac5164cbadc28965ffb145a8f47582a60ae5897859ad8d4316596c606" +dependencies = [ + "futures-util", + "itertools 0.14.0", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-fst", + "zstd", +] + +[[package]] +name = "tantivy-stacker" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbb051742da9d53ca9e8fff43a9b10e319338b24e2c0e15d0372df19ffeb951" +dependencies = [ + "murmurhash32", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac258c2c6390673f2685813afeeafcb8c4e0ee7de8dd3fc46838dcc37263f98" +dependencies = [ + "serde", +] + [[package]] name = "tap" version = "1.0.1" @@ -3949,7 +5137,7 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -3994,7 +5182,7 @@ checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.3", ] [[package]] @@ -4043,6 +5231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -4061,6 +5250,181 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "toasty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2abf272caf351f58e03758745b6185576fcfe7a513b007683c2f8eec698eed2" +dependencies = [ + "async-trait", + "bit-set", + "by_address", + "deadpool", + "hashbrown 0.17.1", + "index_vec", + "indexmap", + "inventory", + "jiff", + "serde_core", + "serde_json", + "toasty-core", + "toasty-driver-mysql", + "toasty-driver-postgresql", + "toasty-driver-sqlite", + "toasty-driver-turso", + "toasty-macros", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "toasty-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1650657e3784097859a1c31cf91b48e337f85684757e0c6ad44e887f0e009f" +dependencies = [ + "async-trait", + "bit-set", + "hashbrown 0.17.1", + "heck", + "indexmap", + "jiff", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "toasty-driver-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0347a79ded078fd43a53a07cd6ee41312a8f0b44a404ea370667a2aa7023791d" +dependencies = [ + "async-trait", + "jiff", + "mysql_async", + "toasty-core", + "toasty-sql", + "tokio", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "toasty-driver-postgresql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc49e29cd2521b4835aa342adb99fe362a0b38083dda31112218c2762dfe09b" +dependencies = [ + "async-trait", + "fallible-iterator 0.2.0", + "hashbrown 0.17.1", + "jiff", + "lru 0.18.1", + "percent-encoding", + "postgres-protocol", + "postgres-types", + "rustls", + "rustls-pemfile", + "rustls-platform-verifier", + "rustls-webpki", + "toasty-core", + "toasty-sql", + "tokio", + "tokio-postgres", + "tokio-postgres-rustls", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "toasty-driver-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce0b90d030bd83e135b96259d630482efe6b1fa937670c75d825ae2992d6ea91" +dependencies = [ + "async-trait", + "percent-encoding", + "rusqlite", + "serde", + "toasty-core", + "toasty-sql", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "toasty-driver-turso" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cc0682b65eeab9b7d6e0bac3f309591df5b9b6df32f868a3a6ee5ca3636e24" +dependencies = [ + "async-trait", + "percent-encoding", + "serde", + "toasty-core", + "toasty-sql", + "tokio", + "tracing", + "turso", + "url", + "uuid", +] + +[[package]] +name = "toasty-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16966e54a422be54959b0ad12bdbdcffc101c77aeca7d49fefff33142c709a29" +dependencies = [ + "hashbrown 0.17.1", + "heck", + "pluralizer", + "proc-macro2", + "quote", + "syn 3.0.3", + "toasty-core", +] + +[[package]] +name = "toasty-sql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b19948205b682d1750a8cf331b0b7fa666baffe2a3a2c0a05d5e94103c0fcc08" +dependencies = [ + "jiff", + "serde", + "serde_json", + "toasty-core", +] + [[package]] name = "tokio" version = "1.53.1" @@ -4093,10 +5457,50 @@ dependencies = [ name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.10.2", + "socket2", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-postgres-rustls" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2ad44aa0ae96db89c4742212ed41645b2f597311ff6e1945542a4d9fadc2fb" dependencies = [ - "native-tls", + "rustls", + "sha2 0.11.0", "tokio", + "tokio-postgres", + "tokio-rustls", + "x509-cert", ] [[package]] @@ -4111,9 +5515,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -4134,13 +5538,14 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -4243,7 +5648,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -4283,6 +5688,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -4369,12 +5787,253 @@ dependencies = [ "thiserror", ] +[[package]] +name = "turso" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b6c49aecd4abae3ffa88ab1e28b3de7a7497a9732be828e3a6f1855b6ea80fd" +dependencies = [ + "mimalloc", + "thiserror", + "tracing", + "tracing-subscriber", + "turso_core", + "turso_sdk_kit", + "turso_sync_sdk_kit", +] + +[[package]] +name = "turso_core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d19014707ed1ad4eadba5bf27394983b92697fe22b93e679e4ce9157cf8493" +dependencies = [ + "aegis", + "aes", + "aes-gcm", + "allocator-api2 0.4.0", + "antithesis_sdk", + "arc-swap", + "aristo", + "bigdecimal", + "bitflags 2.13.1", + "branches", + "bumpalo", + "bytemuck", + "cfg_aliases", + "cfg_block", + "chrono", + "crc32c", + "crossbeam-epoch", + "crossbeam-utils", + "either", + "fallible-iterator 0.3.0", + "fastbloom", + "hex", + "icu_collator", + "icu_locale", + "intrusive-collections", + "io-uring", + "libc", + "libloading", + "libm", + "loom", + "miette", + "num-bigint", + "num-traits", + "pack1", + "parking_lot", + "pastey", + "polling", + "rand 0.9.5", + "rapidhash", + "regex", + "regex-syntax", + "roaring", + "rustc-hash 2.1.3", + "rustix 1.1.4", + "ryu", + "serde_json", + "shuttle", + "simsimd", + "smallvec", + "strum", + "strum_macros", + "tantivy", + "tempfile", + "thiserror", + "tracing", + "tracing-subscriber", + "turso_ext", + "turso_macros", + "turso_parser", + "twox-hash", + "uncased", + "uuid", + "windows-sys 0.61.2", +] + +[[package]] +name = "turso_ext" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f075537ef0eef76bb25fb297cb5698e6582b42f4f4c082b88cd0dc6248d0ac53" +dependencies = [ + "chrono", + "getrandom 0.4.3", + "turso_macros", +] + +[[package]] +name = "turso_macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cf1ef8779d07a7bd8dc3d34a3e8cc6f59e929ef74aedf2bf8b2dfceda59d7a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "turso_parser" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae794301cc7490b2fa0af5ef2f175aaba2b69e2abbc9bbaac2c91a68a04d075f" +dependencies = [ + "bitflags 2.13.1", + "memchr", + "miette", + "strum", + "strum_macros", + "thiserror", + "turso_macros", +] + +[[package]] +name = "turso_sdk_kit" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c4060696c394c0e38382f1757262c2e75bd7c65a139fddf8331d49f874cfe5d" +dependencies = [ + "bindgen", + "env_logger", + "parking_lot", + "tracing", + "tracing-appender", + "tracing-subscriber", + "turso_core", + "turso_ext", + "turso_sdk_kit_macros", +] + +[[package]] +name = "turso_sdk_kit_macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3478f8f4e06133fd218ba93039765c096a7126636bca9b0893f79bcfd41adaf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "turso_sync_engine" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341090cbeeb2866073b0a29c5279b138c8b8a67a85fbf636b26e9a11170988db" +dependencies = [ + "base64", + "bytes", + "crc32c", + "genawaiter", + "http", + "libc", + "prost", + "roaring", + "serde", + "serde_json", + "thiserror", + "tracing", + "turso_core", + "turso_parser", + "uuid", +] + +[[package]] +name = "turso_sync_sdk_kit" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f51333f96fbef3c1c9bd7831b365d21e4078785d1c7f8dbd30dd338a30eb8b" +dependencies = [ + "bindgen", + "env_logger", + "genawaiter", + "parking_lot", + "tracing", + "tracing-appender", + "tracing-subscriber", + "turso_core", + "turso_sdk_kit", + "turso_sdk_kit_macros", + "turso_sync_engine", +] + +[[package]] +name = "twox-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9" +dependencies = [ + "rand 0.10.2", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "typetag" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c90e86058a30d42a1a928dfb4b49bb33c98c3a2b4909492e6b0881cd94798ec2" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f153acc4e99a5f2a5aefa09fb078be54e26271b2813f6041200b224c098d8328" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unicase" version = "2.9.0" @@ -4408,13 +6067,19 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common 0.1.6", + "crypto-common 0.1.7", "subtle", ] @@ -4436,6 +6101,18 @@ dependencies = [ "serde", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + [[package]] name = "utf8-width" version = "0.1.9" @@ -4497,7 +6174,9 @@ checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "getrandom 0.4.3", "js-sys", + "rand 0.10.2", "serde_core", + "sha1_smol", "wasm-bindgen", ] @@ -4573,6 +6252,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" @@ -4584,9 +6272,12 @@ dependencies = [ [[package]] name = "wasite" -version = "0.1.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] [[package]] name = "wasm-bindgen" @@ -4698,16 +6389,47 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "whoami" -version = "1.6.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" dependencies = [ + "libc", "libredox", + "objc2-system-configuration", "wasite", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -4717,6 +6439,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -4789,20 +6517,20 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -4814,67 +6542,34 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4887,48 +6582,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4959,6 +6630,12 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" version = "0.6.3" @@ -4974,6 +6651,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der", + "spki", + "tls_codec", +] + [[package]] name = "xz2" version = "0.1.7" @@ -5008,18 +6697,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", @@ -5076,6 +6765,7 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] @@ -5084,6 +6774,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -5116,7 +6807,7 @@ dependencies = [ "displaydoc", "flate2", "getrandom 0.3.4", - "hmac", + "hmac 0.12.1", "indexmap", "lzma-rs", "memchr", diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index e56e515..0354084 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -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" @@ -61,3 +53,5 @@ 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"] } diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index 91e22dd..f763aa9 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -8,6 +8,7 @@ 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 diff --git a/apps/server/src/auth/controller.rs b/apps/server/src/auth/controller.rs index 59eb7e1..6c421e9 100644 --- a/apps/server/src/auth/controller.rs +++ b/apps/server/src/auth/controller.rs @@ -1,7 +1,8 @@ use crate::auth::*; use crate::shared::RequestExt; -use chrono::{Duration, Utc}; +use jiff::Timestamp; +use jiff::ToSpan; use std::sync::Arc; use sword::prelude::*; use sword::web::*; @@ -72,11 +73,11 @@ impl AuthController { let access_cookie = self .cookie_manager - .build_access_cookie(String::new(), Utc::now() - Duration::days(1))?; + .build_access_cookie(String::new(), Timestamp::now().checked_sub(1.day())?)?; let refresh_cookie = self .cookie_manager - .build_refresh_cookie(String::new(), Utc::now() - Duration::days(1))?; + .build_refresh_cookie(String::new(), Timestamp::now().checked_sub(1.day())?)?; req.cookies()?.remove(access_cookie); req.cookies()?.remove(refresh_cookie); diff --git a/apps/server/src/auth/dtos.rs b/apps/server/src/auth/dtos.rs index 570b82c..7ac7398 100644 --- a/apps/server/src/auth/dtos.rs +++ b/apps/server/src/auth/dtos.rs @@ -1,6 +1,5 @@ use crate::auth::UserView; - -use chrono::{DateTime, Utc}; +use jiff::Timestamp; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -26,14 +25,14 @@ pub struct LoginDto { pub struct LoginResponse { pub user: UserView, pub access_token: String, - pub access_token_exp: DateTime, + pub access_token_exp: Timestamp, + pub refresh_token_exp: Timestamp, pub refresh_token: String, - pub refresh_token_exp: DateTime, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct RefreshResponse { pub access_token: String, - pub access_token_exp: DateTime, + pub access_token_exp: Timestamp, } diff --git a/apps/server/src/auth/entity.rs b/apps/server/src/auth/entity.rs index 0558558..51044f5 100644 --- a/apps/server/src/auth/entity.rs +++ b/apps/server/src/auth/entity.rs @@ -1,29 +1,23 @@ -use crate::{ - auth::UserId, - shared::{Entity, Id}, -}; +use crate::{auth::UserId, model_id}; -use chrono::{DateTime, Utc}; +use jiff::Timestamp; use serde::{Deserialize, Serialize}; -use sqlx::prelude::FromRow; +use toasty::Model; -pub type SessionId = Id; +model_id! { + struct SessionId, key: "session" +} -#[derive(Debug, Serialize, Deserialize, FromRow)] +#[derive(Debug, Serialize, Deserialize, Model)] pub struct Session { + #[key] pub id: SessionId, pub user_id: UserId, pub refresh_token_hash: String, - pub created_at: DateTime, - pub expires_at: DateTime, - pub refresh_expires_at: DateTime, - pub revoked_at: Option>, -} - -impl Entity for Session { - fn key_name() -> &'static str { - "session" - } + pub created_at: Timestamp, + pub expires_at: Timestamp, + pub refresh_expires_at: Timestamp, + pub revoked_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/apps/server/src/auth/repository.rs b/apps/server/src/auth/repository.rs index 05dce5d..dedfa82 100644 --- a/apps/server/src/auth/repository.rs +++ b/apps/server/src/auth/repository.rs @@ -3,7 +3,7 @@ use crate::{ shared::{AppResult, Database}, }; -use chrono::{DateTime, Utc}; +use jiff::Timestamp; use std::sync::Arc; use sword::prelude::*; @@ -14,79 +14,56 @@ pub struct SessionRepository { impl SessionRepository { pub async fn save(&self, session: &Session) -> AppResult { - let session = sqlx::query_as::<_, Session>( - "INSERT INTO sessions ( - id, user_id, refresh_token_hash, created_at, - expires_at, refresh_expires_at, revoked_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (id) - DO UPDATE SET - user_id = EXCLUDED.user_id, - refresh_token_hash = EXCLUDED.refresh_token_hash, - created_at = EXCLUDED.created_at, - expires_at = EXCLUDED.expires_at, - refresh_expires_at = EXCLUDED.refresh_expires_at, - revoked_at = EXCLUDED.revoked_at - RETURNING *", - ) - .bind(session.id) - .bind(session.user_id) - .bind(session.refresh_token_hash.clone()) - .bind(session.created_at) - .bind(session.expires_at) - .bind(session.refresh_expires_at) - .bind(session.revoked_at) - .fetch_one(self.database.pool()) - .await?; + let session = Session::upsert_by_id(session.id) + .user_id(session.id) + .refresh_token_hash(session.refresh_token_hash.clone()) + .created_at(session.created_at) + .expires_at(session.expires_at) + .refresh_expires_at(session.refresh_expires_at) + .revoked_at(session.revoked_at) + .exec(&mut self.database.pool()) + .await?; Ok(session) } pub async fn is_active(&self, id: &SessionId) -> AppResult { - let res = sqlx::query_as::<_, Session>( - "SELECT * FROM sessions - WHERE id = $1 AND revoked_at IS NULL AND expires_at > NOW()", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await?; + let res = Session::filter_by_id(id) + .filter(Session::fields().revoked_at().is_none()) + .filter(Session::fields().expires_at().gt(Timestamp::now())) + .first() + .exec(&mut self.database.pool()) + .await?; Ok(res.is_some()) } pub async fn find_active_by_id(&self, id: &SessionId) -> AppResult> { - let res = sqlx::query_as::<_, Session>( - "SELECT * FROM sessions - WHERE id = $1 AND revoked_at IS NULL AND expires_at > NOW()", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await?; + let res = Session::filter_by_id(id) + .filter(Session::fields().revoked_at().is_none()) + .filter(Session::fields().expires_at().gt(Timestamp::now())) + .first() + .exec(&mut self.database.pool()) + .await?; Ok(res) } pub async fn find_active_by_refresh_id(&self, id: &SessionId) -> AppResult> { - let res = sqlx::query_as::<_, Session>( - "SELECT * FROM sessions - WHERE id = $1 AND revoked_at IS NULL AND refresh_expires_at > NOW()", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await?; + let res = Session::filter_by_id(id) + .filter(Session::fields().revoked_at().is_none()) + .filter(Session::fields().refresh_expires_at().gt(Timestamp::now())) + .first() + .exec(&mut self.database.pool()) + .await?; Ok(res) } - pub async fn update_expires_at( - &self, - id: &SessionId, - expires_at: DateTime, - ) -> AppResult<()> { - sqlx::query("UPDATE sessions SET expires_at = $1 WHERE id = $2") - .bind(expires_at) - .bind(id) - .execute(self.database.pool()) + pub async fn update_expires_at(&self, id: &SessionId, expires_at: Timestamp) -> AppResult<()> { + Session::update_by_id(id) + .expires_at(expires_at) + .exec(&mut self.database.pool()) .await?; Ok(()) diff --git a/apps/server/src/auth/services/cookies.rs b/apps/server/src/auth/services/cookies.rs index 99dd68d..356eb57 100644 --- a/apps/server/src/auth/services/cookies.rs +++ b/apps/server/src/auth/services/cookies.rs @@ -1,6 +1,6 @@ use crate::shared::{AppError, AppResult}; -use chrono::{DateTime, Utc}; +use jiff::Timestamp; use serde::Deserialize; use sword::prelude::*; use sword::web::{Cookie, CookieBuilder, CookiesExpiration, SameSite}; @@ -22,11 +22,7 @@ pub struct CookieManager { } impl CookieManager { - pub fn build_access_cookie( - &self, - value: String, - exp: DateTime, - ) -> AppResult> { + pub fn build_access_cookie(&self, value: String, exp: Timestamp) -> AppResult> { let expiration = self.format_expiration(exp)?; let cookie = CookieBuilder::new(self.config.access_cookie_name.clone(), value) @@ -43,7 +39,7 @@ impl CookieManager { pub fn build_refresh_cookie( &self, value: String, - exp: DateTime, + exp: Timestamp, ) -> AppResult> { let expiration = self.format_expiration(exp)?; @@ -58,8 +54,8 @@ impl CookieManager { Ok(cookie) } - pub fn format_expiration(&self, expires: DateTime) -> AppResult { - let Ok(exp_dt) = OffsetDateTime::from_unix_timestamp(expires.timestamp()) else { + pub fn format_expiration(&self, expires: Timestamp) -> AppResult { + let Ok(exp_dt) = OffsetDateTime::from_unix_timestamp(expires.as_second()) else { tracing::error!("Cookie expiration Datetime convertion error on CookieBuilding"); return Err(AppError::InternalError); }; diff --git a/apps/server/src/auth/services/mod.rs b/apps/server/src/auth/services/mod.rs index fd2b04f..00ab3b6 100644 --- a/apps/server/src/auth/services/mod.rs +++ b/apps/server/src/auth/services/mod.rs @@ -5,8 +5,7 @@ pub use cookies::*; pub use hasher::*; use crate::{auth::*, shared::*}; - -use chrono::{DateTime, Duration, Utc}; +use jiff::{Timestamp, ToSpan}; use sha2::{Digest, Sha256}; use std::sync::Arc; use sword::prelude::*; @@ -38,7 +37,7 @@ impl AuthService { let (refresh_token, refresh_token_exp) = self.generate_refresh_token(&session_id, &user.id)?; - let now = Utc::now(); + let now = Timestamp::now(); let session = Session { id: session_id, @@ -91,7 +90,7 @@ impl AuthService { pub async fn logout(&self, session_id: &SessionId) -> AppResult<()> { if let Some(mut session) = self.sessions.find_active_by_id(session_id).await? { - session.revoked_at = Some(Utc::now()); + session.revoked_at = Some(Timestamp::now()); self.sessions.save(&session).await?; } @@ -102,13 +101,13 @@ impl AuthService { &self, session_id: &SessionId, user_id: &UserId, - ) -> AppResult<(String, DateTime)> { - let expiration = Utc::now() + Duration::minutes(self.config.access_exp_minutes); + ) -> AppResult<(String, Timestamp)> { + let expiration = Timestamp::now().checked_add(self.config.access_exp_minutes.minutes())?; let claims = SessionClaims { session_id: *session_id, user_id: *user_id, - exp: expiration.timestamp(), + exp: expiration.as_second(), typ: "access".to_string(), }; @@ -123,13 +122,13 @@ impl AuthService { &self, session_id: &SessionId, user_id: &UserId, - ) -> AppResult<(String, DateTime)> { - let expiration = Utc::now() + Duration::days(self.config.refresh_exp_days); + ) -> AppResult<(String, Timestamp)> { + let expiration = Timestamp::now().checked_add(self.config.refresh_exp_minutes.minutes())?; let claims = SessionClaims { session_id: *session_id, user_id: *user_id, - exp: expiration.timestamp(), + exp: expiration.as_second(), typ: "refresh".to_string(), }; diff --git a/apps/server/src/auth/users/dtos.rs b/apps/server/src/auth/users/dtos.rs index 6f301a5..0eae91b 100644 --- a/apps/server/src/auth/users/dtos.rs +++ b/apps/server/src/auth/users/dtos.rs @@ -1,7 +1,6 @@ use crate::auth::{User, UserId, UserRole}; use serde::{Deserialize, Serialize}; -use sqlx::FromRow; use validator::{Validate, ValidationError}; #[derive(Debug, Default, Validate, Deserialize)] @@ -63,7 +62,7 @@ fn validate_password(password: &str) -> Result<(), ValidationError> { } } -#[derive(Debug, Clone, Serialize, FromRow)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct UserView { pub id: UserId, diff --git a/apps/server/src/auth/users/entity.rs b/apps/server/src/auth/users/entity.rs index 22c6d68..cf04f2f 100644 --- a/apps/server/src/auth/users/entity.rs +++ b/apps/server/src/auth/users/entity.rs @@ -1,21 +1,27 @@ -use crate::shared::{Entity, Id}; +use crate::model_id; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; +use toasty::{Embed, Model}; -#[derive(Debug, Clone, Serialize, Deserialize, Type, Copy)] +model_id! { + struct UserId, key: "user" +} + +#[derive(Debug, Clone, Serialize, Deserialize, Copy, Embed)] #[serde(rename_all = "lowercase")] -#[sqlx(type_name = "user_role", rename_all = "lowercase")] +#[column(rename_all = "lowercase")] pub enum UserRole { Admin, } -pub type UserId = Id; - -#[derive(Debug, Clone, FromRow, Serialize)] +#[derive(Debug, Clone, Model)] pub struct User { + #[key] pub id: UserId, - pub name: String, + + #[unique] pub email: String, + + pub name: String, pub role: UserRole, pub password_hash: String, } @@ -25,9 +31,3 @@ pub struct UserFilter { pub search: Option, pub role: Option, } - -impl Entity for User { - fn key_name() -> &'static str { - "user" - } -} diff --git a/apps/server/src/auth/users/repository.rs b/apps/server/src/auth/users/repository.rs index 5b1f9f3..03989aa 100644 --- a/apps/server/src/auth/users/repository.rs +++ b/apps/server/src/auth/users/repository.rs @@ -1,7 +1,6 @@ -use crate::auth::{User, UserFilter, UserId, UserView}; -use crate::shared::{AppResult, Database}; +use crate::auth::*; +use crate::shared::{AppError, AppResult, Database}; -use sqlx::QueryBuilder; use std::sync::Arc; use sword::prelude::*; @@ -12,87 +11,89 @@ pub struct UsersRepository { impl UsersRepository { pub async fn list(&self, filter: UserFilter) -> AppResult> { - let mut query = QueryBuilder::new("SELECT id, name, email, role FROM users WHERE 1=1"); + let mut query = User::all(); if let Some(q) = filter.search { let pattern = format!("%{}%", q.trim()); - query - .push(" AND (email ILIKE ") - .push_bind(pattern.clone()) - .push(" OR name ILIKE ") - .push_bind(pattern) - .push(")"); + let name_pattern = User::fields().name().ilike(pattern); + let email_pattern = User::fields().email().ilike(pattern); + + query = query.filter(email_pattern.or(name_pattern)); } if let Some(role) = filter.role { - query.push(" AND role IN ("); - - let mut separated = query.separated(", "); - - separated.push_bind(role); - separated.push_unseparated(")"); + query = query.filter(User::fields().role().eq(role)); } - query.push(" ORDER BY name ASC LIMIT 200"); + query = query.order_by(User::fields().name().asc()).limit(200); let users = query - .build_query_as::() - .fetch_all(self.database.pool()) - .await?; + .exec(&mut self.database.pool()) + .await? + .iter() + .map(UserView::from) + .collect(); Ok(users) } pub async fn find_by_email(&self, email: &str) -> AppResult> { - let user = sqlx::query_as::<_, User>( - "SELECT id, name, email, role, password_hash FROM users WHERE email = $1", - ) - .bind(email) - .fetch_optional(self.database.pool()) - .await?; - - Ok(user) + User::filter_by_email(email) + .first() + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from) } pub async fn find_by_id(&self, id: &UserId) -> AppResult> { - let user = sqlx::query_as::<_, User>( - "SELECT id, name, email, role, password_hash FROM users WHERE id = $1", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await?; - - Ok(user) + User::filter_by_id(id) + .first() + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from) } pub async fn delete(&self, id: &UserId) -> AppResult<()> { - sqlx::query("DELETE FROM users WHERE id = $1") - .bind(id) - .execute(self.database.pool()) - .await?; + User::delete_by_id(&mut self.database.pool(), id) + .await + .map_err(AppError::from) + } - Ok(()) + pub async fn create(&self, data: &CreateUserDto) -> AppResult { + User::create() + .id(UserId::new()) + .name(data.name) + .email(data.email) + .password_hash(data.password) + .role(UserRole::Admin) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } - pub async fn save(&self, user: &User) -> AppResult { - let user = sqlx::query_as::<_, User>( - "INSERT INTO users (id, name, email, role, password_hash) VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (id) DO UPDATE SET - name = EXCLUDED.name, - email = EXCLUDED.email, - role = EXCLUDED.role, - password_hash = EXCLUDED.password_hash - RETURNING id, name, email, role, password_hash", - ) - .bind(user.id) - .bind(&user.name) - .bind(&user.email) - .bind(user.role) - .bind(&user.password_hash) - .fetch_one(self.database.pool()) - .await?; - - Ok(user) + pub async fn update(&self, id: &UserId, data: &UpdateUserDto) -> AppResult<()> { + let mut builder = User::update_by_id(id); + + if let Some(name) = &data.name { + builder = builder.name(name); + } + + if let Some(email) = &data.email { + builder = builder.email(email); + } + + if let Some(password_hash) = &data.password { + builder = builder.password_hash(password_hash); + } + + if let Some(role) = &data.role { + builder = builder.role(role); + } + + builder + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from) } } diff --git a/apps/server/src/auth/users/service.rs b/apps/server/src/auth/users/service.rs index 05f3983..a5f1b53 100644 --- a/apps/server/src/auth/users/service.rs +++ b/apps/server/src/auth/users/service.rs @@ -1,7 +1,4 @@ -use crate::auth::{ - AuthError, CreateUserDto, GetUsersQuery, Hasher, UpdateUserDto, User, UserFilter, UserId, - UserView, UsersRepository, -}; +use crate::auth::*; use crate::shared::AppResult; use std::sync::Arc; @@ -31,25 +28,19 @@ impl UsersService { Ok(UserView::from(user)) } - pub async fn create(&self, dto: CreateUserDto) -> AppResult { + pub async fn create(&self, mut dto: CreateUserDto) -> AppResult { if self.users.find_by_email(&dto.email).await?.is_some() { Err(AuthError::EmailAlreadyExists)?; } - let user = User { - id: UserId::new(), - name: dto.name, - email: dto.email, - role: dto.role, - password_hash: self.hasher.hash(&dto.password)?, - }; + dto.password = self.hasher.hash(&dto.password)?; - let user = self.users.save(&user).await?; + let user = self.users.create(&dto).await?; Ok(UserView::from(user)) } - pub async fn update(&self, id: &UserId, dto: UpdateUserDto) -> AppResult { + pub async fn update(&self, id: &UserId, mut dto: UpdateUserDto) -> AppResult { let Some(user) = self.users.find_by_id(id).await? else { return Err(AuthError::UserNotFound)?; }; @@ -61,18 +52,11 @@ impl UsersService { Err(AuthError::EmailAlreadyExists)?; } - let updated = User { - name: dto.name.unwrap_or(user.name), - email: dto.email.unwrap_or(user.email), - role: dto.role.unwrap_or(user.role), - password_hash: match dto.password { - Some(p) => self.hasher.hash(&p)?, - None => user.password_hash, - }, - ..user - }; + if let Some(ref password) = dto.password { + dto.password = Some(self.hasher.hash(password)?); + } - let user = self.users.save(&updated).await?; + let user = self.users.update(&user.id, &dto).await?; Ok(UserView::from(user)) } diff --git a/apps/server/src/shared/database.rs b/apps/server/src/shared/database.rs index d2aacb1..ead432a 100644 --- a/apps/server/src/shared/database.rs +++ b/apps/server/src/shared/database.rs @@ -1,15 +1,15 @@ use crate::shared::AppResult; use serde::Deserialize; -use sqlx::{PgPool, migrate::Migrator, postgres::PgPoolOptions}; -use std::{path::Path, sync::Arc, time::Duration}; +use std::sync::Arc; use sword::prelude::*; +use toasty::{Db as Pool, models}; -pub type Tx<'a> = sqlx::Transaction<'a, sqlx::Postgres>; +pub use toasty::Transaction as Tx; #[injectable(provider)] pub struct Database { - pool: Arc, + pool: Arc, } #[config(key = "postgres-db")] @@ -28,29 +28,17 @@ pub struct DatabaseConfig { impl Database { pub async fn new(db_conf: DatabaseConfig) -> Self { - let pool = PgPoolOptions::new() - .min_connections(db_conf.min_connections.into()) - .max_connections(db_conf.max_connections.into()) - .acquire_timeout(Duration::from_millis(db_conf.acquire_timeout_ms)) + let mut db = Pool::builder() + .max_pool_size(db_conf.max_connections as usize) + .models(models!(crate::*)) .connect(&Self::create_uri(&db_conf)) - .await - .inspect_err(|err| { - tracing::error!("Failed to connect to PostgreSQL database: {}", err); - }) - .expect("Failed to create database connection pool"); + .await?; - let migrator = Migrator::new(Path::new(&db_conf.migrations_path)) - .await - .expect("Failed to initialize migrator"); + db.push_schema().expect("Failed to migrate database schema"); - migrator - .run(&pool) - .await - .expect("Failed to run database migrations"); + let a = db.transaction().await?; - Self { - pool: Arc::new(pool), - } + Self { pool: Arc::new(db) } } fn create_uri(db_conf: &DatabaseConfig) -> String { @@ -60,12 +48,12 @@ impl Database { ) } - pub fn pool(&self) -> &PgPool { + pub fn pool(&self) -> &Pool { &self.pool } pub async fn tx(&self) -> AppResult> { - Ok(self.pool.begin().await?) + Ok(self.pool.transaction().await?) } } diff --git a/apps/server/src/shared/errors.rs b/apps/server/src/shared/errors.rs index 50418b0..b801897 100644 --- a/apps/server/src/shared/errors.rs +++ b/apps/server/src/shared/errors.rs @@ -2,10 +2,10 @@ use crate::{ academic::AcademicError, auth::AuthError, research::WorksError, university::UniversityError, }; -use sqlx::Error as SqlxError; use std::io::Error as IoError; use sword::web::*; use thiserror::Error; +use toasty::Error as DatabaseError; pub type AppResult = Result; @@ -30,7 +30,7 @@ pub enum AppError { #[http(code = 500)] #[tracing(error)] #[error("Database error: {0}")] - Database(#[from] SqlxError), + Database(#[from] DatabaseError), #[http(code = 500)] #[tracing(error)] @@ -45,4 +45,8 @@ pub enum AppError { #[http(code = 500, message = "Internal Server Error")] #[error("Internal Error")] InternalError, + + #[http(code = 500, message = "Internal Server Error")] + #[error("Jiff time error: {0}")] + Time(#[from] jiff::Error), } diff --git a/apps/server/src/shared/id.rs b/apps/server/src/shared/id.rs index 4786636..1b76ae2 100644 --- a/apps/server/src/shared/id.rs +++ b/apps/server/src/shared/id.rs @@ -1,162 +1,109 @@ -use std::cmp::Ordering; -use std::fmt; -use std::hash::{Hash, Hasher}; -use std::marker::PhantomData; -use std::ops::Deref; -use std::str::FromStr; - -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use sqlx::encode::IsNull; -use sqlx::error::BoxDynError; -use sqlx::postgres::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef}; -use sqlx::{Decode, Encode, Postgres, Type}; use thiserror::Error; -use uuid::Uuid; -pub trait Entity { - fn key_name() -> &'static str; -} - -#[derive(Debug, Default)] -pub struct Id { - value: Uuid, - _marker: PhantomData, -} - -impl PartialEq for Id { - fn eq(&self, other: &Self) -> bool { - self.value == other.value - } -} +#[macro_export] +macro_rules! model_id { + (struct $name:ident, key: $entity_name:literal) => { + #[derive(::std::fmt::Debug, ::std::default::Default, ::toasty::Embed)] + pub struct $name(::uuid::Uuid); + + impl $name { + pub fn new() -> Self { + Self(::uuid::Uuid::new_v4()) + } + + pub fn from_uuid(uuid: ::uuid::Uuid) -> Self { + Self(uuid) + } + + pub fn parse(input: &str) -> ::std::result::Result { + <::uuid::Uuid as ::std::str::FromStr>::from_str(input) + .map(Self::from_uuid) + .map_err(|_| $crate::shared::IdError::Invalid { + entity: $entity_name, + value: input.to_string(), + }) + } + } -impl Eq for Id {} + impl ::std::str::FromStr for $name { + type Err = $crate::shared::IdError; -impl PartialOrd for Id { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} + fn from_str(s: &str) -> ::std::result::Result { + Self::parse(s) + } + } -impl Ord for Id { - fn cmp(&self, other: &Self) -> Ordering { - self.value.cmp(&other.value) - } -} + impl ::std::fmt::Display for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::std::write!(f, "{}", self.0) + } + } -impl Hash for Id { - fn hash(&self, state: &mut H) { - self.value.hash(state); - } -} + impl ::serde::Serialize for $name { + fn serialize(&self, serializer: S) -> ::std::result::Result + where + S: ::serde::Serializer, + { + ::serde::Serialize::serialize(&self.0, serializer) + } + } -impl Clone for Id { - fn clone(&self) -> Self { - *self - } -} + impl<'de> ::serde::Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> ::std::result::Result + where + D: ::serde::Deserializer<'de>, + { + let value = <::uuid::Uuid as ::serde::Deserialize>::deserialize(deserializer)?; + Ok(Self::from_uuid(value)) + } + } -impl Copy for Id {} + impl ::std::ops::Deref for $name { + type Target = ::uuid::Uuid; -impl Id { - pub fn new() -> Self { - Self { - value: Uuid::new_v4(), - _marker: PhantomData, + fn deref(&self) -> &Self::Target { + &self.0 + } } - } - pub fn from_uuid(uuid: Uuid) -> Self { - Self { - value: uuid, - _marker: PhantomData, + impl ::std::cmp::PartialEq for $name { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } } - } - - pub fn parse(input: &str) -> Result> { - Uuid::from_str(input) - .map(Self::from_uuid) - .map_err(|_| IdError::Invalid { - entity: T::key_name(), - value: input.to_string(), - _marker: PhantomData, - }) - } -} -impl FromStr for Id { - type Err = IdError; + impl ::std::cmp::Eq for $name {} - fn from_str(s: &str) -> Result { - Self::parse(s) - } -} + impl ::std::cmp::PartialOrd for $name { + fn partial_cmp(&self, other: &Self) -> ::std::option::Option<::std::cmp::Ordering> { + ::std::option::Option::Some(::std::cmp::Ord::cmp(self, other)) + } + } -impl fmt::Display for Id { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.value) - } -} + impl ::std::cmp::Ord for $name { + fn cmp(&self, other: &Self) -> ::std::cmp::Ordering { + ::std::cmp::Ord::cmp(&self.0, &other.0) + } + } -impl Serialize for Id { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - self.value.serialize(serializer) - } -} + impl ::std::hash::Hash for $name { + fn hash(&self, state: &mut H) { + ::std::hash::Hash::hash(&self.0, state); + } + } -impl<'de, T: Entity> Deserialize<'de> for Id { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = Uuid::deserialize(deserializer)?; - Ok(Self::from_uuid(value)) - } -} + impl ::std::clone::Clone for $name { + fn clone(&self) -> Self { + Self(self.0) + } + } -impl PgHasArrayType for Id { - fn array_type_info() -> PgTypeInfo { - ::array_type_info() - } + impl ::std::marker::Copy for $name {} + }; } #[derive(Debug, Error)] -pub enum IdError { +pub enum IdError { #[error("Invalid id for '{entity}': '{value}'")] - Invalid { - entity: &'static str, - value: String, - _marker: PhantomData, - }, -} - -impl Type for Id { - fn type_info() -> PgTypeInfo { - >::type_info() - } -} - -impl Encode<'_, Postgres> for Id { - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - >::encode_by_ref(&self.value, buf) - } -} - -impl<'r, T: Entity> Decode<'r, Postgres> for Id { - fn decode(value: PgValueRef<'r>) -> Result { - Ok(Self { - value: >::decode(value)?, - _marker: PhantomData, - }) - } -} - -impl Deref for Id { - type Target = Uuid; - - fn deref(&self) -> &Self::Target { - &self.value - } + Invalid { entity: &'static str, value: String }, } diff --git a/apps/server/src/shared/mod.rs b/apps/server/src/shared/mod.rs index 437c9a8..4c74428 100644 --- a/apps/server/src/shared/mod.rs +++ b/apps/server/src/shared/mod.rs @@ -22,7 +22,7 @@ use sword::prelude::*; pub use database::{Database, TransactionManager, Tx}; pub use errors::*; pub use extensions::*; -pub use id::{Entity, Id}; +pub use id::*; pub use jsonwebtoken::JsonWebTokenService; pub use logger::LoggerLayer; pub use mailer::*; diff --git a/apps/server/src/shared/seeder.rs b/apps/server/src/shared/seeder.rs index 8e8a8f1..afed6b2 100644 --- a/apps/server/src/shared/seeder.rs +++ b/apps/server/src/shared/seeder.rs @@ -29,29 +29,15 @@ impl DatabaseSeeder { } pub async fn seed(&self) { - let admin = User { - id: UserId::new(), - name: "ADMINISTRACIÓN".to_string(), - email: self.config.admin_email.clone(), - password_hash: self.config.admin_password_hash.clone(), - role: UserRole::Admin, - }; - - sqlx::query( - r" - INSERT INTO users (id, name, email, password_hash, role) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (email) DO NOTHING - ", - ) - .bind(admin.id) - .bind(admin.name) - .bind(admin.email) - .bind(admin.password_hash) - .bind(admin.role) - .execute(self.database.pool()) - .await - .expect("Failed to seed admin user"); + User::create() + .id(UserId::new()) + .name("ADMINISTRACIÓN".to_string()) + .email(self.config.admin_email.clone()) + .password_hash(self.config.admin_password_hash.clone()) + .role(UserRole::Admin) + .exec(&mut self.database.pool()) + .await + .expect("Failed to seed admin user"); tracing::info!("Database seeding completed successfully."); } From 8668ad2eb7c2e742fa9b5935fc9f9a2ffb55eaff Mon Sep 17 00:00:00 2001 From: MrRevillod Date: Sat, 25 Jul 2026 14:22:32 -0400 Subject: [PATCH 3/7] refactor: migrate from sqlx -> toasty on university module --- apps/server/src/auth/entity.rs | 13 +++- apps/server/src/university/careers/entity.rs | 28 +++++--- .../src/university/careers/repository.rs | 72 +++++-------------- apps/server/src/university/careers/service.rs | 2 +- apps/server/src/university/countries/mod.rs | 10 ++- .../src/university/countries/repository.rs | 17 +++-- .../src/university/departments/entity.rs | 31 +++++--- .../src/university/departments/repository.rs | 62 +++++----------- .../server/src/university/faculties/entity.rs | 21 +++--- .../src/university/faculties/repository.rs | 49 +++++-------- .../university/work_positions/controller.rs | 5 +- .../src/university/work_positions/dtos.rs | 10 --- .../src/university/work_positions/entity.rs | 34 +++------ .../university/work_positions/repository.rs | 64 +++++------------ .../src/university/work_positions/service.rs | 6 +- 15 files changed, 160 insertions(+), 264 deletions(-) diff --git a/apps/server/src/auth/entity.rs b/apps/server/src/auth/entity.rs index 51044f5..212dc6e 100644 --- a/apps/server/src/auth/entity.rs +++ b/apps/server/src/auth/entity.rs @@ -1,8 +1,11 @@ -use crate::{auth::UserId, model_id}; +use crate::{ + auth::{User, UserId}, + model_id, +}; use jiff::Timestamp; use serde::{Deserialize, Serialize}; -use toasty::Model; +use toasty::{Deferred, Model}; model_id! { struct SessionId, key: "session" @@ -12,12 +15,16 @@ model_id! { pub struct Session { #[key] pub id: SessionId, + pub user_id: UserId, pub refresh_token_hash: String, pub created_at: Timestamp, pub expires_at: Timestamp, - pub refresh_expires_at: Timestamp, pub revoked_at: Option, + pub refresh_expires_at: Timestamp, + + #[belongs_to] + user: Deferred, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/apps/server/src/university/careers/entity.rs b/apps/server/src/university/careers/entity.rs index 86bd2b0..91be171 100644 --- a/apps/server/src/university/careers/entity.rs +++ b/apps/server/src/university/careers/entity.rs @@ -1,18 +1,30 @@ -use crate::shared::{Entity, Id}; -use crate::university::DepartmentId; +use crate::{ + model_id, + university::{Department, DepartmentId}, +}; + use bon::Builder; use serde::Serialize; -use sqlx::FromRow; +use toasty::{Deferred, Model}; -pub type CareerId = Id; +model_id! { + struct CareerId, + key: "career" +} -#[derive(Debug, Clone, FromRow, Serialize, Builder)] +#[derive(Debug, Clone, Serialize, Builder, Model)] #[serde(rename_all = "camelCase")] pub struct Career { + #[key] #[builder(default = CareerId::new())] pub id: CareerId, pub name: String, + + #[index] pub department_id: DepartmentId, + + #[belongs_to] + department: Deferred, } #[derive(Debug)] @@ -20,9 +32,3 @@ pub struct CareerFilter { pub name: Option, pub department_id: Option, } - -impl Entity for Career { - fn key_name() -> &'static str { - "career" - } -} diff --git a/apps/server/src/university/careers/repository.rs b/apps/server/src/university/careers/repository.rs index 25c0988..3a88554 100644 --- a/apps/server/src/university/careers/repository.rs +++ b/apps/server/src/university/careers/repository.rs @@ -1,9 +1,9 @@ -use crate::shared::{AppResult, Database, Tx}; +use crate::shared::{AppError, AppResult, Database}; use crate::university::{Career, CareerFilter, CareerId}; -use sqlx::Postgres; use std::sync::Arc; use sword::prelude::*; +use toasty::schema::Model; #[injectable] pub struct CareersRepository { @@ -12,71 +12,33 @@ pub struct CareersRepository { impl CareersRepository { pub async fn list(&self, filter: CareerFilter) -> AppResult> { - let mut query = sqlx::QueryBuilder::::new( - "SELECT id, name, department_id FROM careers WHERE 1=1", - ); + let mut query = Career::all(); if let Some(n) = filter.name { - let pattern = format!("%{}%", n.trim()); - - query - .push(" AND (name ILIKE ") - .push_bind(pattern.clone()) - .push(")"); + query = query.filter(Career::fields().name().ilike(format!("%{}%", n.trim()))); } if let Some(dept_id) = filter.department_id { - query.push(" AND department_id = ").push_bind(dept_id); + query = query.filter(Career::fields().department_id().eq(dept_id)); } - let careers = query - .build_query_as::() - .fetch_all(self.database.pool()) - .await?; - - Ok(careers) - } - - pub async fn find_by_name(&self, name: &str) -> AppResult> { - let item = sqlx::query_as::<_, Career>( - "SELECT id, name, department_id FROM careers WHERE name ILIKE $1", - ) - .bind(name) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) + query.exec(&mut self.database.pool()).await.into() } pub async fn find_by_id(&self, id: &CareerId) -> AppResult> { - let item = sqlx::query_as::<_, Career>( - "SELECT id, name, department_id FROM careers WHERE id = $1", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) - } - - pub async fn save(&self, career: &Career) -> AppResult<()> { - sqlx::query("INSERT INTO careers (id, name, department_id) VALUES ($1, $2, $3)") - .bind(career.id) - .bind(&career.name) - .bind(career.department_id) - .execute(self.database.pool()) - .await?; - - Ok(()) + Career::get_by_id(&mut self.database.pool(), id) + .await? + .map_err(AppError::from) } - pub async fn _save_with_tx(&self, tx: &mut Tx<'_>, career: &Career) -> AppResult<()> { - sqlx::query("INSERT INTO careers (id, name, department_id) VALUES ($1, $2, $3)") - .bind(career.id) - .bind(&career.name) - .bind(career.department_id) - .execute(&mut **tx) - .await?; + pub async fn create(&self, career: &Career) -> AppResult<()> { + Career::create() + .id(career.id.clone()) + .name(career.name.clone()) + .department_id(career.department_id.clone()) + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from)?; Ok(()) } diff --git a/apps/server/src/university/careers/service.rs b/apps/server/src/university/careers/service.rs index 019d57c..2649175 100644 --- a/apps/server/src/university/careers/service.rs +++ b/apps/server/src/university/careers/service.rs @@ -32,7 +32,7 @@ impl CareersService { .department_id(input.department_id) .build(); - self.careers.save(&career).await?; + self.careers.create(&career).await?; Ok(career) } diff --git a/apps/server/src/university/countries/mod.rs b/apps/server/src/university/countries/mod.rs index 5b40278..61e3755 100644 --- a/apps/server/src/university/countries/mod.rs +++ b/apps/server/src/university/countries/mod.rs @@ -1,3 +1,11 @@ mod repository; - pub use repository::*; + +use toasty::Model; + +#[derive(Model)] +pub struct Country { + #[key] + code: String, + name: String, +} diff --git a/apps/server/src/university/countries/repository.rs b/apps/server/src/university/countries/repository.rs index 81cc7e2..6690bdf 100644 --- a/apps/server/src/university/countries/repository.rs +++ b/apps/server/src/university/countries/repository.rs @@ -1,4 +1,7 @@ -use crate::shared::{AppResult, Database}; +use crate::{ + shared::{AppError, AppResult, Database}, + university::Country, +}; use std::sync::Arc; use sword::prelude::*; @@ -10,11 +13,11 @@ pub struct CountriesRepository { impl CountriesRepository { pub async fn find_by_code(&self, code: &str) -> AppResult> { - let item = sqlx::query_scalar::<_, String>("SELECT code FROM countries WHERE code = $1") - .bind(code) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) + Country::filter_by_code(code) + .select(Country::fields().code()) + .first() + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from) } } diff --git a/apps/server/src/university/departments/entity.rs b/apps/server/src/university/departments/entity.rs index 286d527..52d1860 100644 --- a/apps/server/src/university/departments/entity.rs +++ b/apps/server/src/university/departments/entity.rs @@ -1,18 +1,33 @@ -use crate::shared::{Entity, Id}; -use crate::university::FacultyId; +use crate::{ + model_id, + university::{Career, Faculty, FacultyId}, +}; + use bon::Builder; use serde::Serialize; -use sqlx::FromRow; +use toasty::{Deferred, Model}; -pub type DepartmentId = Id; +model_id! { + struct DepartmentId, + key: "department" +} -#[derive(Debug, Clone, Serialize, FromRow, Builder)] +#[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] pub struct Department { + #[key] #[builder(default = DepartmentId::new())] pub id: DepartmentId, pub name: String, + + #[index] pub faculty_id: FacultyId, + + #[belongs_to] + pub faculty: Deferred, + + #[has_many] + careers: Deferred>, } #[derive(Debug)] @@ -20,9 +35,3 @@ pub struct DepartmentFilter { pub name: Option, pub faculty_id: Option, } - -impl Entity for Department { - fn key_name() -> &'static str { - "department" - } -} diff --git a/apps/server/src/university/departments/repository.rs b/apps/server/src/university/departments/repository.rs index 53c19a7..4e90539 100644 --- a/apps/server/src/university/departments/repository.rs +++ b/apps/server/src/university/departments/repository.rs @@ -1,7 +1,6 @@ -use crate::shared::{AppResult, Database}; +use crate::shared::{AppError, AppResult, Database}; use crate::university::{Department, DepartmentFilter, DepartmentId}; -use sqlx::{Postgres, QueryBuilder}; use std::sync::Arc; use sword::prelude::*; @@ -12,60 +11,35 @@ pub struct DepartmentsRepository { impl DepartmentsRepository { pub async fn list(&self, filter: DepartmentFilter) -> AppResult> { - let mut query = - QueryBuilder::::new("SELECT id, name, faculty_id FROM departments WHERE 1=1"); + let mut query = Department::all(); if let Some(n) = filter.name { - let pattern = format!("%{}%", n.trim()); - - query - .push(" AND (name ILIKE ") - .push_bind(pattern.clone()) - .push(")"); + query = query.filter(Department::fields().name().ilike(format!("%{}%", n.trim()))); } if let Some(faculty_id) = filter.faculty_id { - query.push(" AND faculty_id = ").push_bind(faculty_id); + query = query.filter(Department::fields().faculty_id().eq(faculty_id)); } - let departments = query - .build_query_as::() - .fetch_all(self.database.pool()) - .await?; - - Ok(departments) - } - - pub async fn find_by_name(&self, name: &str) -> AppResult> { - let item = sqlx::query_as::<_, Department>( - "SELECT id, name, faculty_id FROM departments WHERE name ILIKE $1", - ) - .bind(name) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) + query + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from) } pub async fn find_by_id(&self, id: &DepartmentId) -> AppResult> { - let item = sqlx::query_as::<_, Department>( - "SELECT id, name, faculty_id FROM departments WHERE id = $1", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) + Department::get_by_id(&mut self.database.pool(), id) + .await? + .map_err(AppError::from) } pub async fn save(&self, department: &Department) -> AppResult<()> { - sqlx::query("INSERT INTO departments (id, name, faculty_id) VALUES ($1, $2, $3)") - .bind(department.id) - .bind(&department.name) - .bind(department.faculty_id) - .execute(self.database.pool()) - .await?; - - Ok(()) + Department::create() + .id(department.id) + .name(&department.name) + .faculty_id(department.faculty_id) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } } diff --git a/apps/server/src/university/faculties/entity.rs b/apps/server/src/university/faculties/entity.rs index a1dc267..5dd29be 100644 --- a/apps/server/src/university/faculties/entity.rs +++ b/apps/server/src/university/faculties/entity.rs @@ -1,23 +1,24 @@ -use crate::shared::{Entity, Id}; +use crate::{model_id, university::Department}; use bon::Builder; use serde::Serialize; -use sqlx::FromRow; +use toasty::{Deferred, Model}; -pub type FacultyId = Id; +model_id! { + struct FacultyId, + key: "faculty" +} -#[derive(Debug, Clone, Serialize, FromRow, Builder)] +#[derive(Debug, Clone, Serialize, Builder, Model)] pub struct Faculty { + #[key] #[builder(default = FacultyId::new())] pub id: FacultyId, pub name: String, + + #[has_many] + departments: Deferred>, } pub struct FacultyFilter { pub name: Option, } - -impl Entity for Faculty { - fn key_name() -> &'static str { - "faculty" - } -} diff --git a/apps/server/src/university/faculties/repository.rs b/apps/server/src/university/faculties/repository.rs index feb5383..532df63 100644 --- a/apps/server/src/university/faculties/repository.rs +++ b/apps/server/src/university/faculties/repository.rs @@ -1,9 +1,7 @@ -use std::sync::Arc; +use crate::shared::{AppError, AppResult, Database}; +use crate::university::{Faculty, FacultyFilter, FacultyId}; -use crate::shared::{AppResult, Database}; -use crate::university::faculties::Faculty; -use crate::university::{FacultyFilter, FacultyId}; -use sqlx::{Postgres, QueryBuilder}; +use std::sync::Arc; use sword::prelude::*; #[injectable] @@ -13,41 +11,30 @@ pub struct FacultiesRepository { impl FacultiesRepository { pub async fn list(&self, filter: FacultyFilter) -> AppResult> { - let mut query = QueryBuilder::::new("SELECT id, name FROM faculties WHERE 1=1"); + let query = Faculty::all(); if let Some(n) = filter.name { - let pattern = format!("%{}%", n.trim()); - - query - .push(" AND (name ILIKE ") - .push_bind(pattern.clone()) - .push(")"); + query = query.filter(Faculty::fields().name().ilike(format!("%{}%", n.trim()))) } - let faculties = query - .build_query_as::() - .fetch_all(self.database.pool()) - .await?; - - Ok(faculties) + query + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } pub async fn find_by_id(&self, id: &FacultyId) -> AppResult> { - let item = sqlx::query_as::<_, Faculty>("SELECT id, name FROM faculties WHERE id = $1") - .bind(id) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) + Faculty::get_by_id(&mut self.database.pool(), id) + .await? + .map_err(AppError::from) } pub async fn save(&self, faculty: &Faculty) -> AppResult<()> { - sqlx::query("INSERT INTO faculties (id, name) VALUES ($1, $2)") - .bind(faculty.id) - .bind(&faculty.name) - .execute(self.database.pool()) - .await?; - - Ok(()) + Faculty::create() + .id(faculty.id) + .name(faculty.name.clone()) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } } diff --git a/apps/server/src/university/work_positions/controller.rs b/apps/server/src/university/work_positions/controller.rs index 82d6a3e..7095207 100644 --- a/apps/server/src/university/work_positions/controller.rs +++ b/apps/server/src/university/work_positions/controller.rs @@ -14,10 +14,7 @@ pub struct WorkPositionsController { impl WorkPositionsController { #[get("/")] pub async fn get_positions(&self, req: Request) -> WebResult> { - let query = req.query_validator::()?; - let positions = self.positions.find(query.unwrap_or_default()).await?; - - Ok(positions) + Ok(self.positions.find().await?) } #[post("/")] diff --git a/apps/server/src/university/work_positions/dtos.rs b/apps/server/src/university/work_positions/dtos.rs index 8bb26bf..f0ec9a1 100644 --- a/apps/server/src/university/work_positions/dtos.rs +++ b/apps/server/src/university/work_positions/dtos.rs @@ -10,13 +10,3 @@ pub struct CreateAcademicWorkPositionDto { ))] pub name: String, } - -#[derive(Debug, Serialize, Deserialize, Validate, Default)] -pub struct GetWorkPositionsQuery { - #[validate(length( - min = 1, - max = 255, - message = "El nombre debe tener entre 1 y 255 caracteres" - ))] - pub name: Option, -} diff --git a/apps/server/src/university/work_positions/entity.rs b/apps/server/src/university/work_positions/entity.rs index 628f8b7..97f2c75 100644 --- a/apps/server/src/university/work_positions/entity.rs +++ b/apps/server/src/university/work_positions/entity.rs @@ -1,31 +1,17 @@ -use crate::shared::{Entity, Id}; - +use crate::model_id; +use bon::Builder; use serde::Serialize; -use sqlx::FromRow; +use toasty::Model; -pub type AcademicWorkPositionId = Id; +model_id! { + struct AcademicWorkPositionId, + key: "academic_work_position" +} -#[derive(Debug, Clone, Serialize, FromRow)] +#[derive(Debug, Clone, Serialize, Builder, Model)] pub struct AcademicWorkPosition { + #[key] + #[builder(default = AcademicWorkPositionId::new())] pub id: AcademicWorkPositionId, pub name: String, } - -pub struct WorkPositionFilter { - pub name: Option, -} - -impl Entity for AcademicWorkPosition { - fn key_name() -> &'static str { - "academic_work_position" - } -} - -impl AcademicWorkPosition { - pub fn new(name: String) -> Self { - Self { - id: AcademicWorkPositionId::new(), - name, - } - } -} diff --git a/apps/server/src/university/work_positions/repository.rs b/apps/server/src/university/work_positions/repository.rs index 72c00e8..8a7f6df 100644 --- a/apps/server/src/university/work_positions/repository.rs +++ b/apps/server/src/university/work_positions/repository.rs @@ -1,7 +1,6 @@ -use crate::shared::{AppResult, Database}; -use crate::university::{AcademicWorkPosition, AcademicWorkPositionId, WorkPositionFilter}; +use crate::shared::{AppError, AppResult, Database}; +use crate::university::{AcademicWorkPosition, AcademicWorkPositionId}; -use sqlx::{Postgres, QueryBuilder}; use std::sync::Arc; use sword::prelude::*; @@ -11,59 +10,28 @@ pub struct AcademicWorkPositionsRepository { } impl AcademicWorkPositionsRepository { - pub async fn list(&self, filter: WorkPositionFilter) -> AppResult> { - let mut query = - QueryBuilder::::new("SELECT id, name FROM academic_work_positions WHERE 1=1"); - - if let Some(n) = filter.name { - let pattern = format!("%{}%", n.trim()); - - query - .push(" AND (name ILIKE ") - .push_bind(pattern.clone()) - .push(")"); - } - - let positions = query - .build_query_as::() - .fetch_all(self.database.pool()) - .await?; - - Ok(positions) + pub async fn list(&self) -> AppResult> { + AcademicWorkPosition::all() + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } pub async fn find_by_id( &self, id: &AcademicWorkPositionId, ) -> AppResult> { - let item = sqlx::query_as::<_, AcademicWorkPosition>( - "SELECT id, name FROM academic_work_positions WHERE id = $1", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) - } - - pub async fn find_by_name(&self, name: &str) -> AppResult> { - let item = sqlx::query_as::<_, AcademicWorkPosition>( - "SELECT id, name FROM academic_work_positions WHERE name = $1", - ) - .bind(name) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) + AcademicWorkPosition::get_by_id(&mut self.database.pool(), id) + .await? + .map_err(AppError::from) } pub async fn save(&self, position: &AcademicWorkPosition) -> AppResult<()> { - sqlx::query("INSERT INTO academic_work_positions (id, name) VALUES ($1, $2)") - .bind(position.id) - .bind(&position.name) - .execute(self.database.pool()) - .await?; - - Ok(()) + AcademicWorkPosition::create() + .id(position.id) + .name(position.name.clone()) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } } diff --git a/apps/server/src/university/work_positions/service.rs b/apps/server/src/university/work_positions/service.rs index f7ac03a..220f799 100644 --- a/apps/server/src/university/work_positions/service.rs +++ b/apps/server/src/university/work_positions/service.rs @@ -9,10 +9,8 @@ pub struct AcademicWorkPositionsService { } impl AcademicWorkPositionsService { - pub async fn find(&self, query: GetWorkPositionsQuery) -> AppResult> { - let filter = WorkPositionFilter { name: query.name }; - - self.positions.list(filter).await + pub async fn find(&self) -> AppResult> { + self.positions.list().await } pub async fn create( From 269d73763d68cd925deba81329a74a20ff732474 Mon Sep 17 00:00:00 2001 From: MrRevillod Date: Sat, 25 Jul 2026 18:47:50 -0400 Subject: [PATCH 4/7] refactor: migrate from sqlx -> toasty on academic module --- apps/server/src/academic/academics/entity.rs | 92 +++-- .../src/academic/academics/repository.rs | 344 ++++++------------ apps/server/src/academic/academics/views.rs | 42 ++- apps/server/src/academic/categories/entity.rs | 26 +- .../src/academic/categories/repository.rs | 54 +-- apps/server/src/academic/degrees/entity.rs | 45 ++- .../server/src/academic/degrees/repository.rs | 97 ++--- apps/server/src/academic/options/entity.rs | 35 +- .../server/src/academic/options/repository.rs | 80 ++-- apps/server/src/auth/entity.rs | 2 +- apps/server/src/auth/users/entity.rs | 2 +- apps/server/src/shared/id.rs | 3 +- apps/server/src/shared/mod.rs | 2 +- apps/server/src/university/careers/entity.rs | 2 +- .../src/university/departments/entity.rs | 2 +- .../server/src/university/faculties/entity.rs | 2 +- .../src/university/work_positions/entity.rs | 2 +- 17 files changed, 363 insertions(+), 469 deletions(-) diff --git a/apps/server/src/academic/academics/entity.rs b/apps/server/src/academic/academics/entity.rs index 65ae140..873a2fc 100644 --- a/apps/server/src/academic/academics/entity.rs +++ b/apps/server/src/academic/academics/entity.rs @@ -1,55 +1,91 @@ -use bon::Builder; - -use crate::academic::{ - AcademicCategoryId, AcademicCategoryOptionId, AcademicOption, AcademicPlanta, AcademicSortField, -}; -use crate::shared::{Entity, Id}; -use crate::university::{AcademicWorkPositionId, CareerId, DepartmentId}; +use crate::academic::*; +use crate::shared::model_id; +use crate::university::*; -use chrono::{DateTime, NaiveDate, Utc}; +use bon::Builder; +use jiff::{Timestamp, civil::Date}; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; +use toasty::{Deferred, Embed, Model}; -pub type AcademicId = Id; +model_id! { + struct AcademicId, + key: "academic_id" +} -#[derive(Debug, Clone, Copy, Type, Serialize, Deserialize)] -#[sqlx(type_name = "sex", rename_all = "UPPERCASE")] +#[derive(Debug, Clone, Copy, Embed, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] +#[column(rename_all = "UPPERCASE")] pub enum Sex { H, M, O, } -#[derive(Debug, Clone, Serialize, FromRow, Builder)] +#[derive(Debug, Clone, Serialize, Model, Builder)] pub struct Academic { + #[key] #[builder(default = AcademicId::new())] pub id: AcademicId, + + #[unique] pub rut: String, + + #[unique] + pub email: String, + + #[unique] + pub orcid: Option, + pub names: String, pub paternal_surname: String, pub maternal_surname: String, - pub email: String, - pub orcid: Option, pub sex: Sex, - pub birth_date: NaiveDate, - pub joined_at: NaiveDate, - pub work_position_id: AcademicWorkPositionId, - pub department_id: DepartmentId, - pub career_id: Option, + pub joined_at: Date, + pub birth_date: Date, pub jce: f64, - pub acad_category_options_id: AcademicCategoryOptionId, + pub city: String, pub annual_discount_hours: f64, + + #[builder(default = Timestamp::now())] + pub updated_at: Timestamp, + + #[index] pub nationality_code: String, - pub city: String, - #[builder(default = Utc::now())] - pub updated_at: DateTime, + + #[index] + pub department_id: DepartmentId, + + #[index] + pub career_id: Option, + + #[index] + pub category_option_id: AcademicCategoryOptionId, + + #[index] + pub work_position_id: AcademicWorkPositionId, + + #[has_many] + pub degrees: Deferred>, + + #[belongs_to(key = nationality_code, references = code)] + pub nationality: Deferred, + + #[belongs_to] + pub department: Deferred, + + #[belongs_to] + pub career: Deferred>, + + #[belongs_to] + pub category_option: Deferred, + + #[belongs_to] + pub work_position: Deferred, } #[derive(Debug)] pub struct AcademicListFilter { pub search: Option, - pub sort: Option, pub career_id: Option, pub department_id: Option, pub category_id: Option, @@ -65,9 +101,3 @@ impl Academic { ) } } - -impl Entity for Academic { - fn key_name() -> &'static str { - "academic" - } -} diff --git a/apps/server/src/academic/academics/repository.rs b/apps/server/src/academic/academics/repository.rs index e964b3b..29f90e3 100644 --- a/apps/server/src/academic/academics/repository.rs +++ b/apps/server/src/academic/academics/repository.rs @@ -1,11 +1,10 @@ -use crate::academic::{Academic, AcademicId, AcademicListFilter, AcademicSortField, AcademicView}; -use crate::shared::{AppResult, Database, Tx}; +use crate::academic::*; +use crate::auth::User; +use crate::shared::{AppError, AppResult, Database, Tx}; -use chrono::{DateTime, Utc}; -use sqlx::QueryBuilder; +use jiff::Timestamp; use std::sync::Arc; use sword::prelude::*; -use uuid::Uuid; #[injectable] pub struct AcademicsRepository { @@ -14,274 +13,173 @@ pub struct AcademicsRepository { impl AcademicsRepository { pub async fn list(&self, filter: AcademicListFilter) -> AppResult> { - let mut query = QueryBuilder::new( - r" - SELECT - a.id, a.names, a.paternal_surname, a.maternal_surname, - a.email, a.orcid, a.sex, a.birth_date, a.joined_at, - wp.name AS work_position, - d.name AS department, - c.name AS career, - a.jce, - ac.name AS category, - ac.planta, - aco.option, - aco.hours AS acad_category_hours, a.annual_discount_hours, - a.nationality_code AS nationality, - a.city - FROM academics a - LEFT JOIN academic_work_positions wp ON a.work_position_id = wp.id - JOIN departments d ON a.department_id = d.id - LEFT JOIN careers c ON a.career_id = c.id - JOIN academic_category_options aco ON a.acad_category_options_id = aco.id - JOIN academic_categories ac ON aco.category_id = ac.id - WHERE 1=1 - ", - ); + let mut query = Academic::all() + .include(( + Academic::fields().degrees(), + Academic::fields().department(), + Academic::fields().career(), + Academic::fields().work_position(), + Academic::fields().category_option().category(), + )) + .exec(&mut self.database.pool()) + .await?; if let Some(q) = filter.search { let pattern = format!("%{}%", q.trim()); - query - .push(" AND (a.names ILIKE ") - .push_bind(pattern.clone()) - .push(" OR a.paternal_surname ILIKE ") - .push_bind(pattern.clone()) - .push(" OR a.maternal_surname ILIKE ") - .push_bind(pattern.clone()) - .push(" OR a.email ILIKE ") - .push_bind(pattern) - .push(")"); + let pattern_chain = User::fields() + .name() + .ilike(&pattern) + .or(User::fields().paternal_surname().ilike(&pattern)) + .or(User::fields().maternal_surname().ilike(&pattern)) + .or(User::fields().email().ilike(&pattern)); + + query = query.filter(pattern_chain); } if let Some(id) = filter.department_id { - query.push(" AND a.department_id = ").push_bind(id); + query = query.filter(Academic::fields().department_id().eq(id)); } if let Some(id) = filter.career_id { - query.push(" AND a.career_id = ").push_bind(id); + query = query.filter(Academic::fields().career_id().eq(id)); } if let Some(id) = filter.category_id { - query.push(" AND aco.category_id = ").push_bind(id); + query = query.filter(Academic::fields().category_option().category_id().eq(id)); } if let Some(planta) = filter.planta { - query.push(" AND ac.planta = ").push_bind(planta); + query = query.filter( + Academic::fields() + .category_option() + .category() + .planta() + .eq(planta), + ); } if let Some(option) = filter.option { - query.push(" AND aco.option = ").push_bind(option); + query = query.filter(Academic::fields().category_option().option().eq(option)); } - match filter.sort { - Some(AcademicSortField::Names) => { - query.push(" ORDER BY a.names ASC"); - } - Some(AcademicSortField::MaternalSurname) => { - query.push(" ORDER BY a.maternal_surname, a.paternal_surname, a.names ASC"); - } - Some(AcademicSortField::JoinedAt) => { - query.push(" ORDER BY a.joined_at ASC"); - } - Some(AcademicSortField::BirthDate) => { - query.push(" ORDER BY a.birth_date ASC"); - } - Some(AcademicSortField::PaternalSurname) | None => { - query.push(" ORDER BY a.paternal_surname, a.maternal_surname, a.names ASC"); - } - } - - let items = query - .build_query_as::() - .fetch_all(self.database.pool()) - .await?; + let academics = query + .exec(&mut self.database.pool()) + .await? + .iter() + .map(AcademicView::from) + .collect(); - Ok(items) + Ok(academics) } pub async fn find_view_by_id(&self, id: &AcademicId) -> AppResult> { - let item = sqlx::query_as::<_, AcademicView>( - r" - SELECT - a.id, a.names, a.paternal_surname, a.maternal_surname, - a.email, a.orcid, a.sex, a.birth_date, a.joined_at, - wp.name AS work_position, - d.name AS department, - c.name AS career, - a.jce, - ac.name AS category, - ac.planta, - aco.option, - aco.hours AS acad_category_hours, a.annual_discount_hours, - a.nationality_code AS nationality, - a.city - FROM academics a - LEFT JOIN academic_work_positions wp ON a.work_position_id = wp.id - JOIN departments d ON a.department_id = d.id - LEFT JOIN careers c ON a.career_id = c.id - JOIN academic_category_options aco ON a.acad_category_options_id = aco.id - JOIN academic_categories ac ON aco.category_id = ac.id - WHERE a.id = $1 - ", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) + Ok(self.find_by_id(id).await?.map(AcademicView::from)) } pub async fn find_by_id(&self, id: &AcademicId) -> AppResult> { - let item = sqlx::query_as::<_, Academic>("SELECT * FROM academics WHERE id = $1") - .bind(id) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) - } - - #[allow(dead_code)] - pub async fn find_by_email(&self, email: &str) -> AppResult> { - let item = sqlx::query_as::<_, Academic>("SELECT * FROM academics WHERE email = $1") - .bind(email) - .fetch_optional(self.database.pool()) + let academic = Academic::all() + .include(( + Academic::fields().degrees(), + Academic::fields().department(), + Academic::fields().career(), + Academic::fields().work_position(), + Academic::fields().category_option().category(), + )) + .first() + .exec(&mut self.database.pool()) .await?; - Ok(item) + Ok(academic) } pub async fn find_by_rut(&self, rut: &str) -> AppResult> { - let item = sqlx::query_as::<_, Academic>("SELECT * FROM academics WHERE rut = $1") - .bind(rut) - .fetch_optional(self.database.pool()) + let academic = Academic::all() + .filter(Academic::fields().rut().eq(rut)) + .first() + .exec(&mut self.database.pool()) .await?; - Ok(item) + Ok(academic) } pub async fn find_by_orcid(&self, orcid: &str) -> AppResult> { - let item = sqlx::query_as::<_, Academic>("SELECT * FROM academics WHERE orcid = $1") - .bind(orcid) - .fetch_optional(self.database.pool()) + let academic = Academic::all() + .filter(Academic::fields().orcid().eq(orcid)) + .first() + .exec(&mut self.database.pool()) .await?; - Ok(item) + Ok(academic) } - pub async fn update_updated_at(&self, id: &AcademicId) -> AppResult> { - let updated_at = sqlx::query_scalar::<_, DateTime>( - "UPDATE academics SET updated_at = NOW() WHERE id = $1 RETURNING updated_at", - ) - .bind(id) - .fetch_one(self.database.pool()) - .await?; + pub async fn update_updated_at(&self, id: &AcademicId) -> AppResult { + let now = Timestamp::now(); - Ok(updated_at) + Academic::update_by_id(id) + .updated_at(now) + .exec(&mut self.database.pool()) + .await?; + + Ok(now) } pub async fn save(&self, academic: &Academic) -> AppResult<()> { - let query = r" - INSERT INTO academics ( - id, rut, names, paternal_surname, maternal_surname, email, orcid, sex, - birth_date, joined_at, work_position_id, - department_id, career_id, jce, acad_category_options_id, - annual_discount_hours, nationality_code, city, updated_at - ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, - $11, $12, $13, $14, $15, $16, $17, $18, $19 - ) - ON CONFLICT (id) DO UPDATE SET - names = EXCLUDED.names, - paternal_surname = EXCLUDED.paternal_surname, - maternal_surname = EXCLUDED.maternal_surname, - email = EXCLUDED.email, - orcid = EXCLUDED.orcid, - sex = EXCLUDED.sex, - birth_date = EXCLUDED.birth_date, - joined_at = EXCLUDED.joined_at, - work_position_id = EXCLUDED.work_position_id, - department_id = EXCLUDED.department_id, - career_id = EXCLUDED.career_id, - jce = EXCLUDED.jce, - acad_category_options_id = EXCLUDED.acad_category_options_id, - annual_discount_hours = EXCLUDED.annual_discount_hours, - nationality_code = EXCLUDED.nationality_code, - city = EXCLUDED.city, - updated_at = NOW() - "; - - sqlx::query(query) - .bind(academic.id) - .bind(&academic.rut) - .bind(&academic.names) - .bind(&academic.paternal_surname) - .bind(&academic.maternal_surname) - .bind(&academic.email) - .bind(&academic.orcid) - .bind(academic.sex) - .bind(academic.birth_date) - .bind(academic.joined_at) - .bind(academic.work_position_id) - .bind(academic.department_id) - .bind(academic.career_id) - .bind(academic.jce) - .bind(academic.acad_category_options_id) - .bind(academic.annual_discount_hours) - .bind(&academic.nationality_code) - .bind(&academic.city) - .bind(academic.updated_at) - .execute(self.database.pool()) - .await?; - - Ok(()) + Academic::upsert_by_id(academic.id) + .rut(&academic.rut) + .names(&academic.names) + .paternal_surname(&academic.paternal_surname) + .maternal_surname(&academic.maternal_surname) + .email(&academic.email) + .orcid(&academic.orcid) + .sex(academic.sex) + .birth_date(academic.birth_date) + .joined_at(academic.joined_at) + .work_position_id(academic.work_position_id) + .department_id(academic.department_id) + .career_id(academic.career_id) + .jce(academic.jce) + .acad_category_options_id(academic.acad_category_options_id) + .annual_discount_hours(academic.annual_discount_hours) + .nationality_code(&academic.nationality_code) + .city(&academic.city) + .updated_at(academic.updated_at) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from)? } pub async fn save_tx(&self, tx: &mut Tx<'_>, academic: &Academic) -> AppResult<()> { - let query = r" - INSERT INTO academics ( - id, rut, names, paternal_surname, maternal_surname, email, orcid, sex, - birth_date, joined_at, work_position_id, - department_id, career_id, jce, acad_category_options_id, - annual_discount_hours, nationality_code, city, updated_at - ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, - $11, $12, $13, $14, $15, $16, $17, $18, $19 - )"; - - sqlx::query(query) - .bind(academic.id) - .bind(&academic.rut) - .bind(&academic.names) - .bind(&academic.paternal_surname) - .bind(&academic.maternal_surname) - .bind(&academic.email) - .bind(&academic.orcid) - .bind(academic.sex) - .bind(academic.birth_date) - .bind(academic.joined_at) - .bind(academic.work_position_id) - .bind(academic.department_id) - .bind(academic.career_id) - .bind(academic.jce) - .bind(academic.acad_category_options_id) - .bind(academic.annual_discount_hours) - .bind(&academic.nationality_code) - .bind(&academic.city) - .bind(academic.updated_at) - .execute(&mut **tx) - .await?; - - Ok(()) + Academic::upsert_by_id(academic.id) + .rut(&academic.rut) + .names(&academic.names) + .paternal_surname(&academic.paternal_surname) + .maternal_surname(&academic.maternal_surname) + .email(&academic.email) + .orcid(&academic.orcid) + .sex(academic.sex) + .birth_date(academic.birth_date) + .joined_at(academic.joined_at) + .work_position_id(academic.work_position_id) + .department_id(academic.department_id) + .career_id(academic.career_id) + .jce(academic.jce) + .acad_category_options_id(academic.acad_category_options_id) + .annual_discount_hours(academic.annual_discount_hours) + .nationality_code(&academic.nationality_code) + .city(&academic.city) + .updated_at(academic.updated_at) + .exec(tx) + .await? + .map_err(AppError::from)? } - pub async fn list_orcids(&self) -> AppResult> { - let rows = sqlx::query_as::<_, (Uuid, String)>( - "SELECT id, orcid FROM academics WHERE orcid IS NOT NULL", - ) - .fetch_all(self.database.pool()) - .await?; - - Ok(rows) + pub async fn list_orcids(&self) -> AppResult> { + Academic::all() + .filter(Academic::fields().orcid().is_not_null()) + .select((Academic::fields().id(), Academic::fields().orcid())) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } } diff --git a/apps/server/src/academic/academics/views.rs b/apps/server/src/academic/academics/views.rs index 348ec74..3e12208 100644 --- a/apps/server/src/academic/academics/views.rs +++ b/apps/server/src/academic/academics/views.rs @@ -1,10 +1,9 @@ -use crate::academic::{AcademicId, AcademicOption, AcademicPlanta, Sex}; +use crate::academic::{Academic, AcademicId, AcademicOption, AcademicPlanta, Sex}; -use chrono::NaiveDate; +use jiff::civil::Date; use serde::Serialize; -use sqlx::FromRow; -#[derive(Debug, Clone, Serialize, FromRow)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AcademicView { pub id: AcademicId, @@ -14,8 +13,8 @@ pub struct AcademicView { pub email: String, pub orcid: Option, pub sex: Sex, - pub birth_date: NaiveDate, - pub joined_at: NaiveDate, + pub birth_date: Date, + pub joined_at: Date, pub work_position: Option, pub department: String, pub career: Option, @@ -39,8 +38,8 @@ pub struct AcademicPublicView { pub email: String, pub orcid: Option, pub sex: Sex, - pub birth_date: NaiveDate, - pub joined_at: NaiveDate, + pub birth_date: Date, + pub joined_at: Date, pub department: String, pub career: Option, pub nationality: String, @@ -66,3 +65,30 @@ impl From for AcademicPublicView { } } } + +impl From for AcademicView { + fn from(a: Academic) -> Self { + AcademicView { + id: a.id, + names: a.names, + paternal_surname: a.paternal_surname, + maternal_surname: a.maternal_surname, + email: a.email, + orcid: a.orcid, + sex: a.sex, + birth_date: a.birth_date, + joined_at: a.joined_at, + work_position: Some(a.work_position.get().name), + department: a.department.get().name, + career: a.career.get().name, + jce: a.jce, + category: a.category_option.get().category.get().name, + planta: a.category_option.get().category.get().planta, + option: a.category_option.get().option, + acad_category_hours: a.category_option.get().hours, + annual_discount_hours: a.annual_discount_hours, + nationality: a.nationality_code, + city: a.city, + } + } +} diff --git a/apps/server/src/academic/categories/entity.rs b/apps/server/src/academic/categories/entity.rs index c5bdb3b..1bb1417 100644 --- a/apps/server/src/academic/categories/entity.rs +++ b/apps/server/src/academic/categories/entity.rs @@ -1,29 +1,33 @@ +use crate::shared::model_id; use bon::Builder; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; +use toasty::{Embed, Model}; -use crate::shared::{Entity, Id}; +model_id! { + struct AcademicCategoryId, + key: "academic_category" +} -#[derive(Debug, Clone, Type, Serialize, Deserialize, PartialEq, Eq)] -#[sqlx(type_name = "academic_planta", rename_all = "lowercase")] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Embed)] #[serde(rename_all = "lowercase")] +#[column(rename_all = "lowercase")] pub enum AcademicPlanta { Adjunta, Permanente, } -pub type AcademicCategoryId = Id; - -#[derive(Debug, Clone, Serialize, Deserialize, FromRow, Builder)] +#[derive(Debug, Clone, Serialize, Deserialize, Builder, Model)] pub struct AcademicCategory { + #[key] #[builder(default = AcademicCategoryId::new())] pub id: AcademicCategoryId, + pub name: String, pub planta: AcademicPlanta, } -impl Entity for AcademicCategory { - fn key_name() -> &'static str { - "academic_category" - } +#[derive(Debug)] +pub struct AcademicCategoryFilter { + pub name: Option, + pub planta: Option, } diff --git a/apps/server/src/academic/categories/repository.rs b/apps/server/src/academic/categories/repository.rs index 31114de..e637fdf 100644 --- a/apps/server/src/academic/categories/repository.rs +++ b/apps/server/src/academic/categories/repository.rs @@ -1,16 +1,9 @@ -use crate::academic::{AcademicCategory, AcademicCategoryId, AcademicPlanta}; -use crate::shared::{AppResult, Database}; +use crate::academic::{AcademicCategory, AcademicCategoryFilter, AcademicCategoryId}; +use crate::shared::{AppError, AppResult, Database}; -use sqlx::QueryBuilder; use std::sync::Arc; use sword::prelude::*; -#[derive(Debug)] -pub struct AcademicCategoryFilter { - pub name: Option, - pub planta: Option, -} - #[injectable] pub struct AcademicCategoriesRepository { database: Arc, @@ -18,45 +11,36 @@ pub struct AcademicCategoriesRepository { impl AcademicCategoriesRepository { pub async fn list(&self, filter: AcademicCategoryFilter) -> AppResult> { - let mut query = - QueryBuilder::new("SELECT id, name, planta FROM academic_categories WHERE 1=1"); + let mut categories = AcademicCategory::all(); if let Some(n) = filter.name { let pattern = format!("%{}%", n.trim()); - query.push(" AND name ILIKE ").push_bind(pattern); + categories = categories.filter(AcademicCategory::fields().name.ilike(pattern)); } if let Some(planta) = filter.planta { - query.push(" AND planta = ").push_bind(planta); + categories = categories.filter(AcademicCategory::fields().planta.eq(planta)); } - let items = query - .build_query_as::() - .fetch_all(self.database.pool()) - .await?; - - Ok(items) + categories + .execute(&mut self.database.pool()) + .await? + .map_err(AppError::from) } pub async fn find_by_id(&self, id: &AcademicCategoryId) -> AppResult> { - let item = sqlx::query_as::<_, AcademicCategory>( - "SELECT id, name, planta FROM academic_categories WHERE id = $1", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) + AcademicCategory::get_by_id(&mut self.database.pool(), id) + .await? + .map_err(AppError::from) } pub async fn save(&self, category: &AcademicCategory) -> AppResult<()> { - sqlx::query("INSERT INTO academic_categories (id, name, planta) VALUES ($1, $2, $3)") - .bind(category.id) - .bind(&category.name) - .bind(&category.planta) - .execute(self.database.pool()) - .await?; - - Ok(()) + AcademicCategory::create() + .id(&category.id) + .name(&category.name) + .planta(category.planta) + .execute(&mut self.database.pool()) + .await? + .map_err(AppError::from) } } diff --git a/apps/server/src/academic/degrees/entity.rs b/apps/server/src/academic/degrees/entity.rs index 19c5395..cf7d5af 100644 --- a/apps/server/src/academic/degrees/entity.rs +++ b/apps/server/src/academic/degrees/entity.rs @@ -1,37 +1,46 @@ -use bon::Builder; - -use crate::academic::academics::AcademicId; -use crate::shared::{Entity, Id}; +use crate::{ + academic::{Academic, AcademicId}, + shared::model_id, + university::Country, +}; -use chrono::NaiveDate; +use bon::Builder; +use jiff::civil::Date; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; +use toasty::{Deferred, Embed, Model}; -#[derive(Debug, Clone, Type, Serialize, Deserialize)] -#[sqlx(type_name = "degree_kind", rename_all = "lowercase")] +#[derive(Debug, Clone, Embed, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] +#[column(rename_all = "lowercase")] pub enum DegreeKind { Base, Advanced, } -pub type DegreeId = Id; +model_id! { + struct DegreeId, + key: "degree" +} -#[derive(Debug, Clone, Serialize, FromRow, Builder)] +#[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] pub struct Degree { - #[builder(default = DegreeId::new())] + #[key] pub id: DegreeId, - pub academic_id: AcademicId, pub name: String, pub university: String, - pub obtained_at: NaiveDate, + pub obtained_at: Date, pub kind: DegreeKind, + + #[index] + pub academic_id: AcademicId, + + #[index] pub country_code: String, -} -impl Entity for Degree { - fn key_name() -> &'static str { - "degree" - } + #[belongs_to] + pub academic: Deferred, + + #[belongs_to(key = country_code, references = code)] + pub country: Deferred, } diff --git a/apps/server/src/academic/degrees/repository.rs b/apps/server/src/academic/degrees/repository.rs index ad2e308..2eb8a54 100644 --- a/apps/server/src/academic/degrees/repository.rs +++ b/apps/server/src/academic/degrees/repository.rs @@ -1,6 +1,5 @@ -use crate::academic::degrees::Degree; -use crate::academic::{AcademicId, DegreeId}; -use crate::shared::{AppResult, Database, Tx}; +use crate::academic::{AcademicId, Degree, DegreeId}; +use crate::shared::{AppError, AppResult, Database, Tx}; use std::sync::Arc; use sword::prelude::*; @@ -12,76 +11,44 @@ pub struct DegreesRepository { impl DegreesRepository { pub async fn list(&self, academic_id: &AcademicId) -> AppResult> { - let items = sqlx::query_as::<_, Degree>( - "SELECT id, academic_id, name, university, obtained_at, kind, country_code - FROM degrees WHERE academic_id = $1 ORDER BY obtained_at DESC", - ) - .bind(academic_id) - .fetch_all(self.database.pool()) - .await?; - - Ok(items) - } - - pub async fn find_by_id(&self, degree_id: &DegreeId) -> AppResult> { - let item = sqlx::query_as::<_, Degree>("SELECT * FROM degrees WHERE id = $1") - .bind(degree_id) - .fetch_optional(self.database.pool()) + let degrees = Degree::all() + .filter(Degree::academic_id().eq(academic_id)) + .order_by(Degree::obtained_at().desc()) + .exec(&mut self.database.pool()) .await?; - Ok(item) + Ok(degrees) } - pub async fn create(&self, degree: &Degree) -> AppResult<()> { - sqlx::query( - "INSERT INTO degrees (id, academic_id, name, university, obtained_at, kind, country_code) - VALUES ($1, $2, $3, $4, $5, $6, $7)", - ) - .bind(degree.id) - .bind(degree.academic_id) - .bind(°ree.name) - .bind(°ree.university) - .bind(degree.obtained_at) - .bind(°ree.kind) - .bind(°ree.country_code) - .execute(self.database.pool()) - .await?; - - Ok(()) + pub async fn find_by_id(&self, degree_id: &DegreeId) -> AppResult> { + Degree::get_by_id(&mut self.database.pool(), degree_id) + .await? + .map_err(AppError::from) } - pub async fn update(&self, degree: &Degree) -> AppResult<()> { - sqlx::query( - "UPDATE degrees - SET name = $1, university = $2, obtained_at = $3, country_code = $4 - WHERE id = $5", - ) - .bind(°ree.name) - .bind(°ree.university) - .bind(degree.obtained_at) - .bind(°ree.country_code) - .bind(degree.id) - .execute(self.database.pool()) - .await?; - - Ok(()) + pub async fn save(&self, degree: &Degree) -> AppResult<()> { + Degree::upsert_by_id(degree.id) + .academic_id(degree.academic_id) + .name(°ree.name) + .university(°ree.university) + .obtained_at(degree.obtained_at) + .kind(°ree.kind) + .country_code(°ree.country_code) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } pub async fn save_tx(&self, tx: &mut Tx<'_>, degree: &Degree) -> AppResult<()> { - sqlx::query( - "INSERT INTO degrees (id, academic_id, name, university, obtained_at, kind, country_code) - VALUES ($1, $2, $3, $4, $5, $6, $7)", - ) - .bind(degree.id) - .bind(degree.academic_id) - .bind(°ree.name) - .bind(°ree.university) - .bind(degree.obtained_at) - .bind(°ree.kind) - .bind(°ree.country_code) - .execute(&mut **tx) - .await?; - - Ok(()) + Degree::upsert_by_id(degree.id) + .academic_id(degree.academic_id) + .name(°ree.name) + .university(°ree.university) + .obtained_at(degree.obtained_at) + .kind(°ree.kind) + .country_code(°ree.country_code) + .exec(tx) + .await? + .map_err(AppError::from) } } diff --git a/apps/server/src/academic/options/entity.rs b/apps/server/src/academic/options/entity.rs index b6ec1a4..47bede9 100644 --- a/apps/server/src/academic/options/entity.rs +++ b/apps/server/src/academic/options/entity.rs @@ -1,41 +1,44 @@ -use bon::Builder; - use crate::{ - academic::AcademicCategoryId, - shared::{Entity, Id}, + academic::{AcademicCategory, AcademicCategoryId}, + shared::model_id, }; + +use bon::Builder; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; +use toasty::{Deferred, Embed, Model}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Type, Serialize, Deserialize)] -#[sqlx(type_name = "academic_option", rename_all = "lowercase")] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Embed, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] +#[column(rename_all = "lowercase")] pub enum AcademicOption { Teaching, Research, } -pub type AcademicCategoryOptionId = Id; +model_id! { + struct AcademicCategoryOptionId, + key: "academic_category_option" +} -#[derive(Debug, Clone, Serialize, FromRow, Builder)] +#[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] pub struct AcademicCategoryOption { + #[key] #[builder(default = AcademicCategoryOptionId::new())] pub id: AcademicCategoryOptionId, + + #[index] pub category_id: AcademicCategoryId, + pub hours: Option, pub option: AcademicOption, -} -impl Entity for AcademicCategoryOption { - fn key_name() -> &'static str { - "academic_category_option" - } + #[belongs_to] + pub category: Deferred, } #[derive(Debug, Default)] pub struct AcademicCategoryOptionFilter { - pub category_id: Option, pub option: Option, - pub category_name: Option, + pub category_id: Option, } diff --git a/apps/server/src/academic/options/repository.rs b/apps/server/src/academic/options/repository.rs index fe47cb7..b54cfc1 100644 --- a/apps/server/src/academic/options/repository.rs +++ b/apps/server/src/academic/options/repository.rs @@ -1,7 +1,6 @@ use crate::academic::*; -use crate::shared::{AppResult, Database}; +use crate::shared::{AppError, AppResult, Database}; -use sqlx::{Postgres, QueryBuilder}; use std::sync::Arc; use sword::prelude::*; @@ -15,83 +14,56 @@ impl AcademicCategoryOptionsRepository { &self, filter: AcademicCategoryOptionFilter, ) -> AppResult> { - let mut query = QueryBuilder::::new( - "SELECT id, category_id, option, hours FROM academic_category_options WHERE 1=1", - ); + let mut options = AcademicCategoryOption::all(); if let Some(cid) = filter.category_id { - query.push(" AND category_id = ").push_bind(cid); + options = options.filter(AcademicCategoryOption::fields().category_id.eq(cid)); } - let items = query - .build_query_as::() - .fetch_all(self.database.pool()) - .await?; - - Ok(items) + options + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } pub async fn find_one( &self, filter: AcademicCategoryOptionFilter, ) -> AppResult> { - let base = if filter.category_name.is_some() { - "SELECT aco.id, aco.category_id, aco.option, aco.hours \ - FROM academic_category_options aco \ - JOIN academic_categories ac ON ac.id = aco.category_id \ - WHERE 1=1" - } else { - "SELECT aco.id, aco.category_id, aco.option, aco.hours \ - FROM academic_category_options aco WHERE 1=1" - }; - - let mut query = QueryBuilder::::new(base); + let mut option = AcademicCategoryOption::all(); if let Some(cid) = filter.category_id { - query.push(" AND aco.category_id = ").push_bind(cid); + option = option.filter(AcademicCategoryOption::fields().category_id.eq(cid)); } if let Some(option) = filter.option { - query.push(" AND aco.option = ").push_bind(option); - } - - if let Some(name) = filter.category_name { - query.push(" AND ac.name = ").push_bind(name); + option = option.filter(AcademicCategoryOption::fields().option.eq(option)); } - let item = query - .build_query_as::() - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) + option + .first() + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } pub async fn find_by_id( &self, id: &AcademicCategoryOptionId, ) -> AppResult> { - let item = sqlx::query_as::<_, AcademicCategoryOption>( - "SELECT id, category_id, option, hours FROM academic_category_options WHERE id = $1", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await?; - - Ok(item) + AcademicCategoryOption::get_by_id(&mut self.database.pool(), id) + .await? + .map_err(AppError::from) } pub async fn save(&self, option: &AcademicCategoryOption) -> AppResult<()> { - sqlx::query( - "INSERT INTO academic_category_options (id, category_id, option, hours) VALUES ($1, $2, $3, $4)", - ) - .bind(option.id) - .bind(option.category_id) - .bind(option.option) - .bind(option.hours) - .execute(self.database.pool()) - .await?; - - Ok(()) + AcademicCategoryOption::create() + .id(&option.id) + .category_id(&option.category_id) + .option(option.option) + .hours(option.hours) + .execute(&mut self.database.pool()) + .await? + .map_err(AppError::from) } } diff --git a/apps/server/src/auth/entity.rs b/apps/server/src/auth/entity.rs index 212dc6e..148a572 100644 --- a/apps/server/src/auth/entity.rs +++ b/apps/server/src/auth/entity.rs @@ -1,6 +1,6 @@ use crate::{ auth::{User, UserId}, - model_id, + shared::model_id, }; use jiff::Timestamp; diff --git a/apps/server/src/auth/users/entity.rs b/apps/server/src/auth/users/entity.rs index cf04f2f..3ce8291 100644 --- a/apps/server/src/auth/users/entity.rs +++ b/apps/server/src/auth/users/entity.rs @@ -1,4 +1,4 @@ -use crate::model_id; +use crate::shared::model_id; use serde::{Deserialize, Serialize}; use toasty::{Embed, Model}; diff --git a/apps/server/src/shared/id.rs b/apps/server/src/shared/id.rs index 1b76ae2..4ef8851 100644 --- a/apps/server/src/shared/id.rs +++ b/apps/server/src/shared/id.rs @@ -1,6 +1,5 @@ use thiserror::Error; -#[macro_export] macro_rules! model_id { (struct $name:ident, key: $entity_name:literal) => { #[derive(::std::fmt::Debug, ::std::default::Default, ::toasty::Embed)] @@ -107,3 +106,5 @@ pub enum IdError { #[error("Invalid id for '{entity}': '{value}'")] Invalid { entity: &'static str, value: String }, } + +pub(crate) use model_id; diff --git a/apps/server/src/shared/mod.rs b/apps/server/src/shared/mod.rs index 4c74428..9b2163d 100644 --- a/apps/server/src/shared/mod.rs +++ b/apps/server/src/shared/mod.rs @@ -22,7 +22,7 @@ use sword::prelude::*; pub use database::{Database, TransactionManager, Tx}; pub use errors::*; pub use extensions::*; -pub use id::*; +pub(crate) use id::*; pub use jsonwebtoken::JsonWebTokenService; pub use logger::LoggerLayer; pub use mailer::*; diff --git a/apps/server/src/university/careers/entity.rs b/apps/server/src/university/careers/entity.rs index 91be171..b279251 100644 --- a/apps/server/src/university/careers/entity.rs +++ b/apps/server/src/university/careers/entity.rs @@ -1,5 +1,5 @@ use crate::{ - model_id, + shared::model_id, university::{Department, DepartmentId}, }; diff --git a/apps/server/src/university/departments/entity.rs b/apps/server/src/university/departments/entity.rs index 52d1860..125f626 100644 --- a/apps/server/src/university/departments/entity.rs +++ b/apps/server/src/university/departments/entity.rs @@ -1,5 +1,5 @@ use crate::{ - model_id, + shared::model_id, university::{Career, Faculty, FacultyId}, }; diff --git a/apps/server/src/university/faculties/entity.rs b/apps/server/src/university/faculties/entity.rs index 5dd29be..847a446 100644 --- a/apps/server/src/university/faculties/entity.rs +++ b/apps/server/src/university/faculties/entity.rs @@ -1,4 +1,4 @@ -use crate::{model_id, university::Department}; +use crate::{shared::model_id, university::Department}; use bon::Builder; use serde::Serialize; use toasty::{Deferred, Model}; diff --git a/apps/server/src/university/work_positions/entity.rs b/apps/server/src/university/work_positions/entity.rs index 97f2c75..9de81f1 100644 --- a/apps/server/src/university/work_positions/entity.rs +++ b/apps/server/src/university/work_positions/entity.rs @@ -1,4 +1,4 @@ -use crate::model_id; +use crate::shared::model_id; use bon::Builder; use serde::Serialize; use toasty::Model; From db40eaf4fe25c66a41a9b0915913dfe108cc1298 Mon Sep 17 00:00:00 2001 From: MrRevillod Date: Sun, 26 Jul 2026 22:20:07 -0400 Subject: [PATCH 5/7] fix: multiple toasty migration errors --- Cargo.lock | 1 + apps/server/Cargo.toml | 1 + apps/server/bin/issn-seeder/main.rs | 6 +- apps/server/bin/issn-seeder/seeder.rs | 199 +---------- .../src/academic/academics/dtos/create.rs | 8 +- .../src/academic/academics/dtos/imports.rs | 9 +- .../server/src/academic/academics/dtos/mod.rs | 19 +- .../academic/academics/dtos/self_update.rs | 4 +- .../src/academic/academics/dtos/update.rs | 6 +- apps/server/src/academic/academics/entity.rs | 6 + .../src/academic/academics/repository.rs | 81 +++-- .../academic/academics/services/imports.rs | 14 +- .../src/academic/academics/services/mod.rs | 16 +- apps/server/src/academic/academics/views.rs | 17 +- apps/server/src/academic/categories/entity.rs | 2 +- .../src/academic/categories/repository.rs | 21 +- apps/server/src/academic/degrees/dtos.rs | 7 +- apps/server/src/academic/degrees/entity.rs | 3 + .../server/src/academic/degrees/repository.rs | 29 +- apps/server/src/academic/degrees/service.rs | 4 +- apps/server/src/academic/options/entity.rs | 2 + .../server/src/academic/options/repository.rs | 31 +- apps/server/src/auth/controller.rs | 12 +- apps/server/src/auth/entity.rs | 7 +- apps/server/src/auth/repository.rs | 2 +- apps/server/src/auth/services/cookies.rs | 12 +- apps/server/src/auth/services/mod.rs | 20 +- apps/server/src/auth/users/dtos.rs | 55 ++-- apps/server/src/auth/users/repository.rs | 12 +- apps/server/src/auth/users/service.rs | 2 +- .../src/research/classification/controller.rs | 45 ++- .../src/research/classification/dtos.rs | 39 +-- .../src/research/classification/entity.rs | 134 +++++--- .../src/research/classification/repository.rs | 310 ++++++------------ apps/server/src/research/sources/entity.rs | 44 --- apps/server/src/research/sources/mod.rs | 68 +++- .../server/src/research/sources/repository.rs | 66 ++-- apps/server/src/research/sources/views.rs | 16 - apps/server/src/research/stats/repository.rs | 17 +- apps/server/src/research/works/entity.rs | 123 ++----- apps/server/src/research/works/repository.rs | 12 +- .../server/src/research/works/services/mod.rs | 38 +-- apps/server/src/shared/database.rs | 32 +- apps/server/src/shared/mod.rs | 3 +- apps/server/src/shared/seeder.rs | 5 +- apps/server/src/university/careers/entity.rs | 1 + .../src/university/careers/repository.rs | 21 +- apps/server/src/university/countries/mod.rs | 3 +- .../src/university/departments/entity.rs | 7 +- .../src/university/departments/repository.rs | 20 +- .../server/src/university/faculties/entity.rs | 6 +- .../src/university/faculties/repository.rs | 15 +- .../university/work_positions/controller.rs | 2 +- .../university/work_positions/repository.rs | 22 +- .../src/university/work_positions/service.rs | 2 +- 55 files changed, 707 insertions(+), 952 deletions(-) delete mode 100644 apps/server/src/research/sources/entity.rs delete mode 100644 apps/server/src/research/sources/views.rs diff --git a/Cargo.lock b/Cargo.lock index e9080e8..e805848 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,7 @@ dependencies = [ "jsonwebtoken", "lettre", "papers-openalex", + "parking_lot", "regex", "reqwest 0.12.28", "serde", diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index 0354084..fcdf014 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -55,3 +55,4 @@ 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" diff --git a/apps/server/bin/issn-seeder/main.rs b/apps/server/bin/issn-seeder/main.rs index a8adec7..6466c72 100644 --- a/apps/server/bin/issn-seeder/main.rs +++ b/apps/server/bin/issn-seeder/main.rs @@ -9,8 +9,8 @@ async fn main() -> Result<(), Box> { 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?; @@ -55,7 +55,7 @@ async fn main() -> Result<(), Box> { 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); } diff --git a/apps/server/bin/issn-seeder/seeder.rs b/apps/server/bin/issn-seeder/seeder.rs index dfe1a07..864200c 100644 --- a/apps/server/bin/issn-seeder/seeder.rs +++ b/apps/server/bin/issn-seeder/seeder.rs @@ -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> { - 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> { - 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> { - 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> { - 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> { - 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> { - 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, Box> { - 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, Box> { - 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> { - 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> { - 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) } diff --git a/apps/server/src/academic/academics/dtos/create.rs b/apps/server/src/academic/academics/dtos/create.rs index 6f5f62e..f6ebe5e 100644 --- a/apps/server/src/academic/academics/dtos/create.rs +++ b/apps/server/src/academic/academics/dtos/create.rs @@ -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; @@ -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, @@ -100,7 +100,7 @@ impl From 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) diff --git a/apps/server/src/academic/academics/dtos/imports.rs b/apps/server/src/academic/academics/dtos/imports.rs index a422178..4f1da20 100644 --- a/apps/server/src/academic/academics/dtos/imports.rs +++ b/apps/server/src/academic/academics/dtos/imports.rs @@ -4,7 +4,6 @@ use crate::{ shared::{CLf64, Country}, }; -use chrono::NaiveDate; use serde::Deserialize; use validator::{Validate, ValidationErrors}; @@ -53,11 +52,11 @@ pub struct AcademicImportRowDto { #[validate(custom(function = "validate_birth_date"))] #[serde(rename = "FECHA DE NACIMIENTO")] - pub birth_date: NaiveDate, + pub birth_date: Date, #[validate(custom(function = "validate_joined_at"))] #[serde(rename = "FECHA DE INGRESO")] - pub joined_at: NaiveDate, + pub joined_at: Date, #[serde(rename = "CARGO")] pub work_position_name: String, @@ -108,7 +107,7 @@ pub struct AcademicImportRowDto { #[serde(rename = "FECHA (I)")] #[serde(default)] - pub degree_1_date: Option, + pub degree_1_date: Option, #[serde(rename = "PAIS (I)")] #[serde(default)] @@ -124,7 +123,7 @@ pub struct AcademicImportRowDto { #[serde(rename = "FECHA (II)")] #[serde(default)] - pub degree_2_date: Option, + pub degree_2_date: Option, #[serde(rename = "PAIS (II)")] #[serde(default)] diff --git a/apps/server/src/academic/academics/dtos/mod.rs b/apps/server/src/academic/academics/dtos/mod.rs index f973996..824ddde 100644 --- a/apps/server/src/academic/academics/dtos/mod.rs +++ b/apps/server/src/academic/academics/dtos/mod.rs @@ -6,6 +6,7 @@ mod update; pub use create::*; pub use imports::*; +use jiff::{Timestamp, civil::Date}; pub use self_update::*; pub use token::*; pub use update::*; @@ -15,7 +16,6 @@ use crate::{ university::{CareerId, DepartmentId}, }; -use chrono::{NaiveDate, Utc}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; @@ -28,7 +28,7 @@ static ORCID_ID_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^https://orcid\.org/\d{4}-\d{4}-\d{4}-\d{3}[\dX]$").expect("regex inválida") }); -static UCT_FOUNDATION_DATE: NaiveDate = NaiveDate::from_ymd_opt(1959, 9, 8).unwrap(); +static UCT_FOUNDATION_DATE: LazyLock = LazyLock::new(|| Date::new(1959, 9, 8).unwrap()); #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -57,14 +57,14 @@ pub struct GetAcademicsQuery { pub option: Option, } -fn validate_birth_date(date: &NaiveDate) -> Result<(), ValidationError> { +fn validate_birth_date(date: &Date) -> Result<(), ValidationError> { validate_future_date(date) } -fn validate_joined_at(joined_at: &NaiveDate) -> Result<(), ValidationError> { +fn validate_joined_at(joined_at: &Date) -> Result<(), ValidationError> { validate_future_date(joined_at)?; - if *joined_at < UCT_FOUNDATION_DATE { + if *joined_at < *UCT_FOUNDATION_DATE { return Err(ValidationError::new( "La fecha de ingreso no puede ser anterior al año de fundación de la universidad (1959)", )); @@ -73,8 +73,13 @@ fn validate_joined_at(joined_at: &NaiveDate) -> Result<(), ValidationError> { Ok(()) } -fn validate_future_date(date: &NaiveDate) -> Result<(), ValidationError> { - if *date > Utc::now().naive_utc().date() { +fn validate_future_date(date: &Date) -> Result<(), ValidationError> { + let today = Timestamp::now() + .in_tz("UTC") + .map_err(|_| ValidationError::new("Error al obtener la fecha actual"))? + .date(); + + if *date > today { Err(ValidationError::new( "La fecha de nacimiento no puede ser en el futuro", )) diff --git a/apps/server/src/academic/academics/dtos/self_update.rs b/apps/server/src/academic/academics/dtos/self_update.rs index ef306b7..f768970 100644 --- a/apps/server/src/academic/academics/dtos/self_update.rs +++ b/apps/server/src/academic/academics/dtos/self_update.rs @@ -1,7 +1,7 @@ use super::ORCID_ID_REGEX; use crate::academic::Sex; -use chrono::NaiveDate; +use jiff::civil::Date; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -37,7 +37,7 @@ pub struct SelfUpdateAcademicDto { pub sex: Option, #[validate(custom(function = "super::validate_birth_date"))] - pub birth_date: Option, + pub birth_date: Option, #[validate(length( min = 2, diff --git a/apps/server/src/academic/academics/dtos/update.rs b/apps/server/src/academic/academics/dtos/update.rs index 98c89df..d71962d 100644 --- a/apps/server/src/academic/academics/dtos/update.rs +++ b/apps/server/src/academic/academics/dtos/update.rs @@ -2,7 +2,7 @@ use super::ORCID_ID_REGEX; use crate::academic::{AcademicCategoryOptionId, Sex}; use crate::university::{AcademicWorkPositionId, CareerId, DepartmentId}; -use chrono::NaiveDate; +use jiff::civil::Date; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -38,10 +38,10 @@ pub struct UpdateAcademicDto { pub sex: Option, #[validate(custom(function = "super::validate_birth_date"))] - pub birth_date: Option, + pub birth_date: Option, #[validate(custom(function = "super::validate_joined_at"))] - pub joined_at: Option, + pub joined_at: Option, pub work_position_id: Option, pub department_id: Option, pub career_id: Option, diff --git a/apps/server/src/academic/academics/entity.rs b/apps/server/src/academic/academics/entity.rs index 873a2fc..911e2c2 100644 --- a/apps/server/src/academic/academics/entity.rs +++ b/apps/server/src/academic/academics/entity.rs @@ -65,21 +65,27 @@ pub struct Academic { pub work_position_id: AcademicWorkPositionId, #[has_many] + #[builder(default)] pub degrees: Deferred>, #[belongs_to(key = nationality_code, references = code)] + #[builder(default)] pub nationality: Deferred, #[belongs_to] + #[builder(default)] pub department: Deferred, #[belongs_to] + #[builder(default)] pub career: Deferred>, #[belongs_to] + #[builder(default)] pub category_option: Deferred, #[belongs_to] + #[builder(default)] pub work_position: Deferred, } diff --git a/apps/server/src/academic/academics/repository.rs b/apps/server/src/academic/academics/repository.rs index 29f90e3..8e50d07 100644 --- a/apps/server/src/academic/academics/repository.rs +++ b/apps/server/src/academic/academics/repository.rs @@ -1,5 +1,4 @@ use crate::academic::*; -use crate::auth::User; use crate::shared::{AppError, AppResult, Database, Tx}; use jiff::Timestamp; @@ -14,25 +13,21 @@ pub struct AcademicsRepository { impl AcademicsRepository { pub async fn list(&self, filter: AcademicListFilter) -> AppResult> { let mut query = Academic::all() - .include(( - Academic::fields().degrees(), - Academic::fields().department(), - Academic::fields().career(), - Academic::fields().work_position(), - Academic::fields().category_option().category(), - )) - .exec(&mut self.database.pool()) - .await?; + .include(Academic::fields().degrees()) + .include(Academic::fields().department()) + .include(Academic::fields().career()) + .include(Academic::fields().work_position()) + .include(Academic::fields().category_option().category()); if let Some(q) = filter.search { let pattern = format!("%{}%", q.trim()); - let pattern_chain = User::fields() - .name() + let pattern_chain = Academic::fields() + .names() .ilike(&pattern) - .or(User::fields().paternal_surname().ilike(&pattern)) - .or(User::fields().maternal_surname().ilike(&pattern)) - .or(User::fields().email().ilike(&pattern)); + .or(Academic::fields().paternal_surname().ilike(&pattern)) + .or(Academic::fields().maternal_surname().ilike(&pattern)) + .or(Academic::fields().email().ilike(&pattern)); query = query.filter(pattern_chain); } @@ -50,13 +45,13 @@ impl AcademicsRepository { } if let Some(planta) = filter.planta { - query = query.filter( - Academic::fields() - .category_option() - .category() - .planta() - .eq(planta), - ); + let chain = Academic::fields() + .category_option() + .category() + .planta() + .eq(planta); + + query = query.filter(chain); } if let Some(option) = filter.option { @@ -66,7 +61,7 @@ impl AcademicsRepository { let academics = query .exec(&mut self.database.pool()) .await? - .iter() + .into_iter() .map(AcademicView::from) .collect(); @@ -78,14 +73,12 @@ impl AcademicsRepository { } pub async fn find_by_id(&self, id: &AcademicId) -> AppResult> { - let academic = Academic::all() - .include(( - Academic::fields().degrees(), - Academic::fields().department(), - Academic::fields().career(), - Academic::fields().work_position(), - Academic::fields().category_option().category(), - )) + let academic = Academic::filter_by_id(id) + .include(Academic::fields().degrees()) + .include(Academic::fields().department()) + .include(Academic::fields().career()) + .include(Academic::fields().work_position()) + .include(Academic::fields().category_option().category()) .first() .exec(&mut self.database.pool()) .await?; @@ -94,8 +87,7 @@ impl AcademicsRepository { } pub async fn find_by_rut(&self, rut: &str) -> AppResult> { - let academic = Academic::all() - .filter(Academic::fields().rut().eq(rut)) + let academic = Academic::filter_by_rut(rut) .first() .exec(&mut self.database.pool()) .await?; @@ -104,8 +96,7 @@ impl AcademicsRepository { } pub async fn find_by_orcid(&self, orcid: &str) -> AppResult> { - let academic = Academic::all() - .filter(Academic::fields().orcid().eq(orcid)) + let academic = Academic::filter_by_orcid(orcid) .first() .exec(&mut self.database.pool()) .await?; @@ -139,14 +130,15 @@ impl AcademicsRepository { .department_id(academic.department_id) .career_id(academic.career_id) .jce(academic.jce) - .acad_category_options_id(academic.acad_category_options_id) + .category_option_id(academic.category_option_id) .annual_discount_hours(academic.annual_discount_hours) .nationality_code(&academic.nationality_code) .city(&academic.city) .updated_at(academic.updated_at) .exec(&mut self.database.pool()) - .await? - .map_err(AppError::from)? + .await?; + + Ok(()) } pub async fn save_tx(&self, tx: &mut Tx<'_>, academic: &Academic) -> AppResult<()> { @@ -164,22 +156,23 @@ impl AcademicsRepository { .department_id(academic.department_id) .career_id(academic.career_id) .jce(academic.jce) - .acad_category_options_id(academic.acad_category_options_id) + .category_option_id(academic.category_option_id) .annual_discount_hours(academic.annual_discount_hours) .nationality_code(&academic.nationality_code) .city(&academic.city) .updated_at(academic.updated_at) .exec(tx) - .await? - .map_err(AppError::from)? + .await?; + + Ok(()) } - pub async fn list_orcids(&self) -> AppResult> { + pub async fn list_orcids(&self) -> AppResult)>> { Academic::all() - .filter(Academic::fields().orcid().is_not_null()) + .filter(Academic::fields().orcid().is_some()) .select((Academic::fields().id(), Academic::fields().orcid())) .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } } diff --git a/apps/server/src/academic/academics/services/imports.rs b/apps/server/src/academic/academics/services/imports.rs index cde6154..bdda188 100644 --- a/apps/server/src/academic/academics/services/imports.rs +++ b/apps/server/src/academic/academics/services/imports.rs @@ -2,7 +2,7 @@ use crate::academic::*; use crate::shared::{AppError, AppResult, TransactionManager, Tx}; use crate::university::*; -use chrono::NaiveDate; +use jiff::civil::Date; use std::path::PathBuf; use std::sync::Arc; use sword::prelude::*; @@ -10,7 +10,7 @@ use validator::Validate; #[injectable] pub struct ImportsService { - tx: Arc, + tx: TransactionManager, academics: Arc, degrees: Arc, departments: Arc, @@ -71,7 +71,8 @@ impl ImportsService { continue; } - let mut tx = self.tx.begin().await?; + let mut db = self.tx.database().await?; + let mut tx = db.transaction().await?; match self.process_row(&input, &mut tx).await { Ok(()) => { @@ -80,6 +81,7 @@ impl ImportsService { } Err(e) => { tx.rollback().await?; + errors.push(ImportRowError { row: row_num, reasons: vec![e.to_string()], @@ -181,7 +183,7 @@ impl ImportsService { .department_id(department.id) .maybe_career_id(career_id) .jce(*input.jce) - .acad_category_options_id(category_option_id) + .category_option_id(category_option_id) .annual_discount_hours(*input.annual_discount_hours) .nationality_code(nationality_code) .city(input.city.clone()) @@ -224,16 +226,18 @@ impl ImportsService { academic_id: &AcademicId, name: &Option, university: &Option, - obtained_at: Option, + obtained_at: Option, country_code: Option<&str>, kind: DegreeKind, ) -> AppResult<()> { let name = name.as_deref().map(|s| s.trim()).unwrap_or(""); + if name.is_empty() { return Ok(()); } let university = university.as_deref().map(|s| s.trim()).unwrap_or(""); + if university.is_empty() { return Ok(()); } diff --git a/apps/server/src/academic/academics/services/mod.rs b/apps/server/src/academic/academics/services/mod.rs index a9c9677..0bd0466 100644 --- a/apps/server/src/academic/academics/services/mod.rs +++ b/apps/server/src/academic/academics/services/mod.rs @@ -1,7 +1,6 @@ mod imports; pub use imports::*; -use serde_json::{Value, json}; use crate::{ academic::*, @@ -10,8 +9,11 @@ use crate::{ shared::{AppResult, JsonWebTokenService}, university::*, }; +use jiff::{Timestamp, ToSpan}; +use serde_json::{Value, json}; use std::sync::Arc; -use sword::{events::EventPublisher, prelude::*}; +use sword::events::EventPublisher; +use sword::prelude::*; #[injectable] pub struct AcademicsService { @@ -33,7 +35,6 @@ impl AcademicsService { pub async fn find(&self, query: GetAcademicsQuery) -> AppResult> { let filter = AcademicListFilter { search: query.search, - sort: query.sort, career_id: query.career_id, category_id: query.category_id, department_id: query.department_id, @@ -176,7 +177,7 @@ impl AcademicsService { Err(AcademicError::CategoryOptionNotFound)?; } - academic.acad_category_options_id = cat_opt_id; + academic.category_option_id = cat_opt_id; } if let Some(jce) = input.jce { @@ -209,11 +210,12 @@ impl AcademicsService { }; let updated_at = self.academics.update_updated_at(id).await?; + let exp = Timestamp::now().checked_add(7.days())?; let claims = json!({ "academic_id": academic.id.to_string(), - "updated_at": updated_at.timestamp(), - "exp": (updated_at + chrono::Duration::days(7)).timestamp(), + "updated_at": &updated_at.as_second(), + "exp": exp.as_second(), }); let one_time_token = self @@ -259,7 +261,7 @@ impl AcademicsService { return Err(AcademicError::AcademicNotFound)?; }; - if token_updated_at != academic.updated_at.timestamp() { + if token_updated_at != academic.updated_at.as_second() { Err(AcademicError::InvalidOneTimeToken)?; } diff --git a/apps/server/src/academic/academics/views.rs b/apps/server/src/academic/academics/views.rs index 3e12208..3fbde32 100644 --- a/apps/server/src/academic/academics/views.rs +++ b/apps/server/src/academic/academics/views.rs @@ -78,17 +78,24 @@ impl From for AcademicView { sex: a.sex, birth_date: a.birth_date, joined_at: a.joined_at, - work_position: Some(a.work_position.get().name), - department: a.department.get().name, - career: a.career.get().name, + work_position: Some(a.work_position.get().name.clone()), + department: a.department.get().name.clone(), + career: a.career.get().clone().map(|c| c.name), jce: a.jce, - category: a.category_option.get().category.get().name, - planta: a.category_option.get().category.get().planta, + category: a.category_option.get().clone().category.get().clone().name, option: a.category_option.get().option, acad_category_hours: a.category_option.get().hours, annual_discount_hours: a.annual_discount_hours, nationality: a.nationality_code, city: a.city, + planta: a + .category_option + .get() + .clone() + .category + .get() + .clone() + .planta, } } } diff --git a/apps/server/src/academic/categories/entity.rs b/apps/server/src/academic/categories/entity.rs index 1bb1417..440ed2e 100644 --- a/apps/server/src/academic/categories/entity.rs +++ b/apps/server/src/academic/categories/entity.rs @@ -8,7 +8,7 @@ model_id! { key: "academic_category" } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Embed)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Embed, Copy)] #[serde(rename_all = "lowercase")] #[column(rename_all = "lowercase")] pub enum AcademicPlanta { diff --git a/apps/server/src/academic/categories/repository.rs b/apps/server/src/academic/categories/repository.rs index e637fdf..6e8846d 100644 --- a/apps/server/src/academic/categories/repository.rs +++ b/apps/server/src/academic/categories/repository.rs @@ -15,22 +15,24 @@ impl AcademicCategoriesRepository { if let Some(n) = filter.name { let pattern = format!("%{}%", n.trim()); - categories = categories.filter(AcademicCategory::fields().name.ilike(pattern)); + categories = categories.filter(AcademicCategory::fields().name().ilike(pattern)); } if let Some(planta) = filter.planta { - categories = categories.filter(AcademicCategory::fields().planta.eq(planta)); + categories = categories.filter(AcademicCategory::fields().planta().eq(planta)); } categories - .execute(&mut self.database.pool()) - .await? + .exec(&mut self.database.pool()) + .await .map_err(AppError::from) } pub async fn find_by_id(&self, id: &AcademicCategoryId) -> AppResult> { - AcademicCategory::get_by_id(&mut self.database.pool(), id) - .await? + AcademicCategory::filter_by_id(id) + .first() + .exec(&mut self.database.pool()) + .await .map_err(AppError::from) } @@ -39,8 +41,9 @@ impl AcademicCategoriesRepository { .id(&category.id) .name(&category.name) .planta(category.planta) - .execute(&mut self.database.pool()) - .await? - .map_err(AppError::from) + .exec(&mut self.database.pool()) + .await?; + + Ok(()) } } diff --git a/apps/server/src/academic/degrees/dtos.rs b/apps/server/src/academic/degrees/dtos.rs index c93dc10..5fc8a5c 100644 --- a/apps/server/src/academic/degrees/dtos.rs +++ b/apps/server/src/academic/degrees/dtos.rs @@ -1,6 +1,5 @@ use crate::academic::{AcademicId, DegreeKind}; - -use chrono::NaiveDate; +use jiff::civil::Date; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -23,7 +22,7 @@ pub struct CreateDegreeDto { ))] pub university: String, - pub obtained_at: NaiveDate, + pub obtained_at: Date, pub kind: DegreeKind, #[validate(length( @@ -50,7 +49,7 @@ pub struct UpdateDegreeDto { message = "La universidad debe tener entre 1 y 255 caracteres" ))] pub university: Option, - pub obtained_at: Option, + pub obtained_at: Option, #[validate(length( min = 2, diff --git a/apps/server/src/academic/degrees/entity.rs b/apps/server/src/academic/degrees/entity.rs index cf7d5af..811abef 100644 --- a/apps/server/src/academic/degrees/entity.rs +++ b/apps/server/src/academic/degrees/entity.rs @@ -26,6 +26,7 @@ model_id! { #[serde(rename_all = "camelCase")] pub struct Degree { #[key] + #[builder(default = DegreeId::new())] pub id: DegreeId, pub name: String, pub university: String, @@ -39,8 +40,10 @@ pub struct Degree { pub country_code: String, #[belongs_to] + #[builder(default)] pub academic: Deferred, #[belongs_to(key = country_code, references = code)] + #[builder(default)] pub country: Deferred, } diff --git a/apps/server/src/academic/degrees/repository.rs b/apps/server/src/academic/degrees/repository.rs index 2eb8a54..d249077 100644 --- a/apps/server/src/academic/degrees/repository.rs +++ b/apps/server/src/academic/degrees/repository.rs @@ -11,18 +11,19 @@ pub struct DegreesRepository { impl DegreesRepository { pub async fn list(&self, academic_id: &AcademicId) -> AppResult> { - let degrees = Degree::all() - .filter(Degree::academic_id().eq(academic_id)) - .order_by(Degree::obtained_at().desc()) + Degree::all() + .filter(Degree::fields().academic_id().eq(academic_id)) + .order_by(Degree::fields().obtained_at().desc()) .exec(&mut self.database.pool()) - .await?; - - Ok(degrees) + .await + .map_err(AppError::from) } - pub async fn find_by_id(&self, degree_id: &DegreeId) -> AppResult> { - Degree::get_by_id(&mut self.database.pool(), degree_id) - .await? + pub async fn find_by_id(&self, id: &DegreeId) -> AppResult> { + Degree::filter_by_id(id) + .first() + .exec(&mut self.database.pool()) + .await .map_err(AppError::from) } @@ -35,8 +36,9 @@ impl DegreesRepository { .kind(°ree.kind) .country_code(°ree.country_code) .exec(&mut self.database.pool()) - .await? - .map_err(AppError::from) + .await?; + + Ok(()) } pub async fn save_tx(&self, tx: &mut Tx<'_>, degree: &Degree) -> AppResult<()> { @@ -48,7 +50,8 @@ impl DegreesRepository { .kind(°ree.kind) .country_code(°ree.country_code) .exec(tx) - .await? - .map_err(AppError::from) + .await?; + + Ok(()) } } diff --git a/apps/server/src/academic/degrees/service.rs b/apps/server/src/academic/degrees/service.rs index 5dec25d..c323aed 100644 --- a/apps/server/src/academic/degrees/service.rs +++ b/apps/server/src/academic/degrees/service.rs @@ -22,7 +22,7 @@ impl DegreesService { .country_code(input.country_code) .build(); - self.degrees.create(°ree).await?; + self.degrees.save(°ree).await?; Ok(degree) } @@ -48,7 +48,7 @@ impl DegreesService { degree.country_code = country_code; } - self.degrees.update(°ree).await?; + self.degrees.save(°ree).await?; Ok(degree) } diff --git a/apps/server/src/academic/options/entity.rs b/apps/server/src/academic/options/entity.rs index 47bede9..3453a90 100644 --- a/apps/server/src/academic/options/entity.rs +++ b/apps/server/src/academic/options/entity.rs @@ -34,6 +34,7 @@ pub struct AcademicCategoryOption { pub option: AcademicOption, #[belongs_to] + #[builder(default)] pub category: Deferred, } @@ -41,4 +42,5 @@ pub struct AcademicCategoryOption { pub struct AcademicCategoryOptionFilter { pub option: Option, pub category_id: Option, + pub category_name: Option, } diff --git a/apps/server/src/academic/options/repository.rs b/apps/server/src/academic/options/repository.rs index b54cfc1..082faee 100644 --- a/apps/server/src/academic/options/repository.rs +++ b/apps/server/src/academic/options/repository.rs @@ -17,12 +17,12 @@ impl AcademicCategoryOptionsRepository { let mut options = AcademicCategoryOption::all(); if let Some(cid) = filter.category_id { - options = options.filter(AcademicCategoryOption::fields().category_id.eq(cid)); + options = options.filter(AcademicCategoryOption::fields().category_id().eq(cid)); } options .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } @@ -30,20 +30,24 @@ impl AcademicCategoryOptionsRepository { &self, filter: AcademicCategoryOptionFilter, ) -> AppResult> { - let mut option = AcademicCategoryOption::all(); + let mut query = AcademicCategoryOption::all(); if let Some(cid) = filter.category_id { - option = option.filter(AcademicCategoryOption::fields().category_id.eq(cid)); + query = query.filter(AcademicCategoryOption::fields().category_id().eq(cid)); } if let Some(option) = filter.option { - option = option.filter(AcademicCategoryOption::fields().option.eq(option)); + query = query.filter(AcademicCategoryOption::fields().option().eq(option)); } - option + if let Some(name) = filter.category_name { + query = query.filter(AcademicCategoryOption::fields().category().name().eq(name)); + } + + query .first() .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } @@ -51,8 +55,10 @@ impl AcademicCategoryOptionsRepository { &self, id: &AcademicCategoryOptionId, ) -> AppResult> { - AcademicCategoryOption::get_by_id(&mut self.database.pool(), id) - .await? + AcademicCategoryOption::filter_by_id(id) + .first() + .exec(&mut self.database.pool()) + .await .map_err(AppError::from) } @@ -62,8 +68,9 @@ impl AcademicCategoryOptionsRepository { .category_id(&option.category_id) .option(option.option) .hours(option.hours) - .execute(&mut self.database.pool()) - .await? - .map_err(AppError::from) + .exec(&mut self.database.pool()) + .await?; + + Ok(()) } } diff --git a/apps/server/src/auth/controller.rs b/apps/server/src/auth/controller.rs index 6c421e9..5bb9b2d 100644 --- a/apps/server/src/auth/controller.rs +++ b/apps/server/src/auth/controller.rs @@ -1,8 +1,6 @@ use crate::auth::*; use crate::shared::RequestExt; -use jiff::Timestamp; -use jiff::ToSpan; use std::sync::Arc; use sword::prelude::*; use sword::web::*; @@ -71,17 +69,11 @@ impl AuthController { self.auth_service.logout(&session_claims.session_id).await?; - let access_cookie = self - .cookie_manager - .build_access_cookie(String::new(), Timestamp::now().checked_sub(1.day())?)?; - - let refresh_cookie = self - .cookie_manager - .build_refresh_cookie(String::new(), Timestamp::now().checked_sub(1.day())?)?; + let (access_cookie, refresh_cookie) = self.cookie_manager.build_logout_cookies()?; req.cookies()?.remove(access_cookie); req.cookies()?.remove(refresh_cookie); - Ok(JsonResponse::Ok().message("Sesión cerrada correctamente")) + Ok(JsonResponse::Ok()) } } diff --git a/apps/server/src/auth/entity.rs b/apps/server/src/auth/entity.rs index 148a572..90f9b33 100644 --- a/apps/server/src/auth/entity.rs +++ b/apps/server/src/auth/entity.rs @@ -3,6 +3,7 @@ use crate::{ shared::model_id, }; +use bon::Builder; use jiff::Timestamp; use serde::{Deserialize, Serialize}; use toasty::{Deferred, Model}; @@ -11,11 +12,12 @@ model_id! { struct SessionId, key: "session" } -#[derive(Debug, Serialize, Deserialize, Model)] +#[derive(Debug, Model, Builder)] pub struct Session { #[key] pub id: SessionId, + #[index] pub user_id: UserId, pub refresh_token_hash: String, pub created_at: Timestamp, @@ -24,7 +26,8 @@ pub struct Session { pub refresh_expires_at: Timestamp, #[belongs_to] - user: Deferred, + #[builder(default)] + pub user: Deferred, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/apps/server/src/auth/repository.rs b/apps/server/src/auth/repository.rs index dedfa82..c150393 100644 --- a/apps/server/src/auth/repository.rs +++ b/apps/server/src/auth/repository.rs @@ -15,7 +15,7 @@ pub struct SessionRepository { impl SessionRepository { pub async fn save(&self, session: &Session) -> AppResult { let session = Session::upsert_by_id(session.id) - .user_id(session.id) + .user_id(session.user_id) .refresh_token_hash(session.refresh_token_hash.clone()) .created_at(session.created_at) .expires_at(session.expires_at) diff --git a/apps/server/src/auth/services/cookies.rs b/apps/server/src/auth/services/cookies.rs index 356eb57..deaa27b 100644 --- a/apps/server/src/auth/services/cookies.rs +++ b/apps/server/src/auth/services/cookies.rs @@ -1,6 +1,6 @@ use crate::shared::{AppError, AppResult}; -use jiff::Timestamp; +use jiff::{Timestamp, ToSpan}; use serde::Deserialize; use sword::prelude::*; use sword::web::{Cookie, CookieBuilder, CookiesExpiration, SameSite}; @@ -62,4 +62,14 @@ impl CookieManager { Ok(CookiesExpiration::DateTime(exp_dt)) } + + pub fn build_logout_cookies(&self) -> AppResult<(Cookie<'static>, Cookie<'static>)> { + let access_cookie = + self.build_access_cookie("".to_string(), Timestamp::now().checked_sub(1.day())?)?; + + let refresh_cookie = + self.build_refresh_cookie("".to_string(), Timestamp::now().checked_sub(1.day())?)?; + + Ok((access_cookie, refresh_cookie)) + } } diff --git a/apps/server/src/auth/services/mod.rs b/apps/server/src/auth/services/mod.rs index 00ab3b6..da6338c 100644 --- a/apps/server/src/auth/services/mod.rs +++ b/apps/server/src/auth/services/mod.rs @@ -39,15 +39,15 @@ impl AuthService { let now = Timestamp::now(); - let session = Session { - id: session_id, - user_id: user.id, - refresh_token_hash: Self::hash_token(&refresh_token), - created_at: now, - expires_at: access_token_exp, - refresh_expires_at: refresh_token_exp, - revoked_at: None, - }; + let session = Session::builder() + .id(session_id) + .user_id(user.id) + .refresh_token_hash(Self::hash_token(&refresh_token)) + .created_at(now) + .expires_at(access_token_exp) + .refresh_expires_at(refresh_token_exp) + .maybe_revoked_at(None) + .build(); self.sessions.save(&session).await?; @@ -123,7 +123,7 @@ impl AuthService { session_id: &SessionId, user_id: &UserId, ) -> AppResult<(String, Timestamp)> { - let expiration = Timestamp::now().checked_add(self.config.refresh_exp_minutes.minutes())?; + let expiration = Timestamp::now().checked_add(self.config.refresh_exp_days.minutes())?; let claims = SessionClaims { session_id: *session_id, diff --git a/apps/server/src/auth/users/dtos.rs b/apps/server/src/auth/users/dtos.rs index 0eae91b..5f8edda 100644 --- a/apps/server/src/auth/users/dtos.rs +++ b/apps/server/src/auth/users/dtos.rs @@ -3,6 +3,15 @@ use crate::auth::{User, UserId, UserRole}; use serde::{Deserialize, Serialize}; use validator::{Validate, ValidationError}; +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserView { + pub id: UserId, + pub name: String, + pub email: String, + pub role: UserRole, +} + #[derive(Debug, Default, Validate, Deserialize)] pub struct GetUsersQuery { #[validate(length( @@ -30,6 +39,24 @@ pub struct CreateUserDto { pub role: UserRole, } +#[derive(Debug, Validate, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateUserDto { + #[validate(length( + min = 1, + max = 255, + message = "El atributo 'name' no puede tener más de 255 caracteres" + ))] + pub name: Option, + + #[validate(email(message = "El atributo 'email' debe ser un correo electrónico válido"))] + pub email: Option, + pub role: Option, + + #[validate(custom(function = "validate_password"))] + pub password: Option, +} + fn validate_password(password: &str) -> Result<(), ValidationError> { let mut missing = Vec::new(); @@ -62,34 +89,6 @@ fn validate_password(password: &str) -> Result<(), ValidationError> { } } -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UserView { - pub id: UserId, - pub name: String, - pub email: String, - pub role: UserRole, -} - -#[derive(Debug, Validate, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdateUserDto { - #[validate(length( - min = 1, - max = 255, - message = "El atributo 'name' no puede tener más de 255 caracteres" - ))] - pub name: Option, - - #[validate(email(message = "El atributo 'email' debe ser un correo electrónico válido"))] - pub email: Option, - - pub role: Option, - - #[validate(custom(function = "validate_password"))] - pub password: Option, -} - impl From for UserView { fn from(user: User) -> Self { Self { diff --git a/apps/server/src/auth/users/repository.rs b/apps/server/src/auth/users/repository.rs index 03989aa..88d2c3e 100644 --- a/apps/server/src/auth/users/repository.rs +++ b/apps/server/src/auth/users/repository.rs @@ -16,7 +16,7 @@ impl UsersRepository { if let Some(q) = filter.search { let pattern = format!("%{}%", q.trim()); - let name_pattern = User::fields().name().ilike(pattern); + let name_pattern = User::fields().name().ilike(pattern.clone()); let email_pattern = User::fields().email().ilike(pattern); query = query.filter(email_pattern.or(name_pattern)); @@ -31,7 +31,7 @@ impl UsersRepository { let users = query .exec(&mut self.database.pool()) .await? - .iter() + .into_iter() .map(UserView::from) .collect(); @@ -63,12 +63,12 @@ impl UsersRepository { pub async fn create(&self, data: &CreateUserDto) -> AppResult { User::create() .id(UserId::new()) - .name(data.name) - .email(data.email) - .password_hash(data.password) + .name(&data.name) + .email(&data.email) + .password_hash(&data.password) .role(UserRole::Admin) .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } diff --git a/apps/server/src/auth/users/service.rs b/apps/server/src/auth/users/service.rs index a5f1b53..df5c093 100644 --- a/apps/server/src/auth/users/service.rs +++ b/apps/server/src/auth/users/service.rs @@ -56,7 +56,7 @@ impl UsersService { dto.password = Some(self.hasher.hash(password)?); } - let user = self.users.update(&user.id, &dto).await?; + self.users.update(&user.id, &dto).await?; Ok(UserView::from(user)) } diff --git a/apps/server/src/research/classification/controller.rs b/apps/server/src/research/classification/controller.rs index 18ae6f3..0a0356d 100644 --- a/apps/server/src/research/classification/controller.rs +++ b/apps/server/src/research/classification/controller.rs @@ -50,38 +50,33 @@ impl WorksClassificationController { } #[get("/research-lines")] - pub async fn get_research_lines(&self) -> WebResult> { + pub async fn get_research_lines(&self) -> WebResult> { Ok(self.work_classifications.list_research_lines().await?) } - #[get("/research-lines/detail")] - pub async fn get_research_lines_detail(&self) -> WebResult { - let lines = self - .work_classifications - .list_research_lines_with_subfields() - .await?; - - Ok(ResearchLinesDetailResponse { lines }) - } - #[put("/research-line-mappings")] pub async fn update_research_line_mapping(&self, req: Request) -> WebResult<()> { let dto = req.body::()?; - self.work_classifications - .update_mapping(&dto.subfield_openalex_id, dto.research_line_id) - .await?; - - Ok(()) - } - - #[delete("/research-line-mappings/{subfield_openalex_id}")] - pub async fn delete_research_line_mapping(&self, req: Request) -> WebResult<()> { - let subfield_openalex_id = req.param::("subfield_openalex_id")?; - - self.work_classifications - .delete_mapping(&subfield_openalex_id) - .await?; + if self + .work_classifications + .find_research_line_by_id(dto.research_line_id) + .await? + .is_none() + { + return Err(JsonResponse::NotFound())?; + }; + + let Some(mut subfield) = self + .work_classifications + .find_subfield_by_openalex_id(&dto.subfield_openalex_id) + .await? + else { + return Err(JsonResponse::NotFound())?; + }; + + subfield.research_line_id = Some(dto.research_line_id); + self.work_classifications.save_subfield(&subfield).await?; Ok(()) } diff --git a/apps/server/src/research/classification/dtos.rs b/apps/server/src/research/classification/dtos.rs index 6d56971..0a852e7 100644 --- a/apps/server/src/research/classification/dtos.rs +++ b/apps/server/src/research/classification/dtos.rs @@ -1,17 +1,8 @@ use crate::research::classification::*; use serde::{Deserialize, Serialize}; -use sqlx::FromRow; use uuid::Uuid; use validator::Validate; -#[derive(Debug, Serialize, FromRow)] -#[serde(rename_all = "camelCase")] -pub struct ResearchLineView { - pub id: Uuid, - pub name: String, - pub slug: String, -} - #[derive(Debug, Clone, Default, Deserialize, Validate)] pub struct WorkClassificationQueryDto { pub domain_id: Option, @@ -26,7 +17,7 @@ pub struct WorkClassificationQueryDto { pub search: Option, } -#[derive(Debug, Serialize, FromRow)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ResearchTopicView { pub topic_id: ResearchTopicId, @@ -40,7 +31,7 @@ pub struct ResearchTopicView { pub domain_name: String, } -#[derive(Debug, Serialize, FromRow)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct ResearchKeywordView { pub keyword_id: Uuid, @@ -61,33 +52,11 @@ impl From for ClassificationFilter { } } -#[derive(Debug, Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct SubfieldMapping { - pub subfield_openalex_id: String, - pub subfield_name: String, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ResearchLineDetail { - pub id: Uuid, - pub name: String, - pub slug: String, - pub subfields: Vec, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ResearchLinesDetailResponse { - pub lines: Vec, -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateMappingBody { - pub subfield_openalex_id: String, - pub research_line_id: Uuid, + pub subfield_id: ResearchSubfieldId, + pub research_line_id: ResearchLineId, } impl diff --git a/apps/server/src/research/classification/entity.rs b/apps/server/src/research/classification/entity.rs index 81df965..23402c8 100644 --- a/apps/server/src/research/classification/entity.rs +++ b/apps/server/src/research/classification/entity.rs @@ -1,95 +1,110 @@ -use crate::shared::{Entity, Id}; - +use crate::shared::model_id; use bon::Builder; use serde::Serialize; -use sqlx::FromRow; - -pub type ResearchDomainId = Id; +use toasty::{Deferred, Model}; -#[derive(Debug, Clone, Serialize, FromRow, Builder)] +#[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] pub struct ResearchDomain { + #[key] #[builder(default = ResearchDomainId::new())] pub id: ResearchDomainId, - pub openalex_id: String, pub name: String, -} -impl Entity for ResearchDomain { - fn key_name() -> &'static str { - "research_domain" - } -} + #[unique] + pub openalex_id: String, -pub type ResearchFieldId = Id; + #[has_many] + pub fields: Deferred>, +} -#[derive(Debug, Clone, Serialize, FromRow, Builder)] +#[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] pub struct ResearchField { + #[key] #[builder(default = ResearchFieldId::new())] pub id: ResearchFieldId, - pub openalex_id: String, pub name: String, + + #[unique] + pub openalex_id: String, + + #[index] pub domain_id: ResearchDomainId, -} -impl Entity for ResearchField { - fn key_name() -> &'static str { - "research_field" - } -} + #[belongs_to] + pub domain: Deferred, -pub type ResearchSubfieldId = Id; + #[has_many] + pub subfields: Deferred>, +} -#[derive(Debug, Clone, Serialize, FromRow, Builder)] +#[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] pub struct ResearchSubfield { + #[key] #[builder(default = ResearchSubfieldId::new())] pub id: ResearchSubfieldId, - pub openalex_id: String, pub name: String, + + #[unique] + pub openalex_id: String, + + #[index] pub field_id: ResearchFieldId, -} -impl Entity for ResearchSubfield { - fn key_name() -> &'static str { - "research_subfield" - } -} + #[index] + pub research_line_id: Option, + + #[belongs_to] + pub field: Deferred, -pub type ResearchTopicId = Id; + #[belongs_to] + pub research_line: Option>, -#[derive(Debug, Clone, Serialize, FromRow, Builder)] + #[has_many] + pub topics: Deferred>, +} + +#[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] pub struct ResearchTopic { + #[key] #[builder(default = ResearchTopicId::new())] pub id: ResearchTopicId, + + #[unique] pub openalex_id: String, pub name: String, + + #[index] pub subfield_id: ResearchSubfieldId, -} -impl Entity for ResearchTopic { - fn key_name() -> &'static str { - "research_topic" - } + #[belongs_to] + pub subfield: Deferred, } -pub type ResearchKeywordId = Id; - -#[derive(Debug, Clone, Serialize, FromRow, Builder)] +#[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] pub struct ResearchKeyword { + #[key] #[builder(default = ResearchKeywordId::new())] pub id: ResearchKeywordId, + + #[unique] pub openalex_id: String, pub name: String, } -impl Entity for ResearchKeyword { - fn key_name() -> &'static str { - "research_keyword" - } +#[derive(Debug, Clone, Serialize, Model, Builder)] +pub struct ResearchLine { + #[key] + pub id: ResearchLineId, + pub name: String, + pub slug: String, + + #[has_many] + pub subfields: Deferred>, } #[allow(dead_code)] @@ -101,3 +116,32 @@ pub struct ClassificationFilter { pub openalex_id: Option, pub search: Option, } + +model_id! { + struct ResearchLineId, + key: "research_line" +} + +model_id! { + struct ResearchDomainId, + key: "research_domain" +} + +model_id! { + struct ResearchFieldId, + key: "research_field" +} + +model_id! { + struct ResearchSubfieldId, + key: "research_subfield" +} + +model_id! { + struct ResearchTopicId, + key: "research_topic" +} +model_id! { + struct ResearchKeywordId, + key: "research_keyword" +} diff --git a/apps/server/src/research/classification/repository.rs b/apps/server/src/research/classification/repository.rs index 42a119e..1cdba50 100644 --- a/apps/server/src/research/classification/repository.rs +++ b/apps/server/src/research/classification/repository.rs @@ -1,10 +1,8 @@ use crate::research::*; -use crate::shared::{AppResult, Database}; +use crate::shared::{AppError, AppResult, Database}; -use sqlx::{Postgres, QueryBuilder, Row}; use std::sync::Arc; use sword::prelude::*; -use uuid::Uuid; #[injectable] pub struct WorkClassificationRepository { @@ -13,262 +11,172 @@ pub struct WorkClassificationRepository { impl WorkClassificationRepository { pub async fn list_domains(&self, f: ClassificationFilter) -> AppResult> { - let mut query = QueryBuilder::::new("SELECT * FROM research_domains WHERE 1=1"); + let mut query = ResearchDomain::all(); - if let Some(search) = f.search { - let pattern = format!("%{}%", search.trim()); - query.push(" AND name ILIKE ").push_bind(pattern); + if let Some(s) = f.search { + let pattern = format!("%{}%", s.trim()); + query = query.filter(ResearchDomain::fields().name().ilike(pattern)) } - query.push(" ORDER BY name"); + query = query.order_by(ResearchDomain::fields().name().asc()); query - .build_query_as::() - .fetch_all(self.database.pool()) - .await - .map_err(Into::into) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } pub async fn list_fields(&self, f: ClassificationFilter) -> AppResult> { - let mut query = QueryBuilder::::new("SELECT * FROM research_fields WHERE 1=1"); + let mut fields = ResearchField::all(); if let Some(domain_id) = f.domain_id { - query.push(" AND domain_id = ").push_bind(domain_id); + fields = fields.filter(ResearchField::fields().domain_id().eq(domain_id)); } - if let Some(search) = f.search { - let pattern = format!("%{}%", search.trim()); - query.push(" AND name ILIKE ").push_bind(pattern); + if let Some(s) = f.search { + let pattern = format!("%{}%", s.trim()); + fields = fields.filter(ResearchField::fields().name().ilike(pattern)) } - query.push(" ORDER BY name"); - - query - .build_query_as::() - .fetch_all(self.database.pool()) - .await - .map_err(Into::into) + fields + .order_by(ResearchField::fields().name()) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } pub async fn list_subfields( &self, f: ClassificationFilter, ) -> AppResult> { - let mut query = QueryBuilder::::new("SELECT * FROM research_subfields WHERE 1=1"); + let mut subfields = ResearchSubfield::all(); if let Some(field_id) = f.field_id { - query.push(" AND field_id = ").push_bind(field_id); + subfields = subfields.filter(ResearchSubfield::fields().field_id().eq(field_id)); } - if let Some(search) = f.search { - let pattern = format!("%{}%", search.trim()); - query.push(" AND name ILIKE ").push_bind(pattern); + if let Some(s) = f.search { + let pattern = format!("%{}%", s.trim()); + subfields = subfields.filter(ResearchSubfield::fields().name().ilike(pattern)) } - query.push(" ORDER BY name"); - query.push(" LIMIT 50"); - - query - .build_query_as::() - .fetch_all(self.database.pool()) - .await - .map_err(Into::into) + subfields + .order_by(ResearchSubfield::fields().name()) + .limit(50) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } pub async fn list_topics(&self, f: ClassificationFilter) -> AppResult> { - let mut query = QueryBuilder::::new("SELECT * FROM research_topics WHERE 1=1"); + let mut topics = ResearchTopic::all(); if let Some(subfield_id) = f.subfield_id { - query.push(" AND subfield_id = ").push_bind(subfield_id); + topics = topics.filter(ResearchTopic::fields().subfield_id().eq(subfield_id)); } - if let Some(search) = f.search { - let pattern = format!("%{}%", search.trim()); - query.push(" AND name ILIKE ").push_bind(pattern); + if let Some(s) = f.search { + let pattern = format!("%{}%", s.trim()); + let pattern = topics = topics.filter(ResearchTopic::fields().name().ilike(pattern)); } - query.push(" ORDER BY name"); - query.push(" LIMIT 50"); - - query - .build_query_as::() - .fetch_all(self.database.pool()) - .await - .map_err(Into::into) - } - - pub async fn unknown_topic_id(&self) -> AppResult> { - let topic = sqlx::query_as::<_, ResearchTopic>( - "SELECT * FROM research_topics WHERE openalex_id = 'unknown'", - ) - .fetch_optional(self.database.pool()) - .await?; - - Ok(topic) - } - - pub async fn find_topic_by_openalex_id( - &self, - openalex_id: &str, - ) -> AppResult> { - sqlx::query_as::<_, ResearchTopic>("SELECT * FROM research_topics WHERE openalex_id = $1") - .bind(openalex_id) - .fetch_optional(self.database.pool()) - .await - .map_err(Into::into) - } - - pub async fn unknown_keyword_id(&self) -> AppResult> { - sqlx::query_as::<_, ResearchKeyword>("SELECT * FROM keywords WHERE openalex_id = 'unknown'") - .fetch_optional(self.database.pool()) - .await - .map_err(Into::into) - } - - pub async fn upsert_keyword( - &self, - openalex_id: &str, - name: &str, - ) -> AppResult { - let row = sqlx::query( - "INSERT INTO keywords (openalex_id, name) - VALUES ($1, $2) ON CONFLICT (openalex_id) - DO UPDATE SET name = EXCLUDED.name RETURNING id", - ) - .bind(openalex_id) - .bind(name) - .fetch_one(self.database.pool()) - .await?; - Ok(ResearchKeywordId::from_uuid(row.get("id"))) + topics + .order_by(ResearchTopic::fields().name()) + .limit(50) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } pub async fn list_keywords(&self, f: ClassificationFilter) -> AppResult> { - let mut query = QueryBuilder::::new("SELECT * FROM keywords WHERE 1=1"); + let mut query = ResearchKeyword::all(); if let Some(search) = f.search { let pattern = format!("%{}%", search.trim()); - query.push(" AND name ILIKE ").push_bind(pattern); + query = query.filter(ResearchKeyword::fields().name().ilike(pattern)); } - query.push(" ORDER BY name"); - query.push(" LIMIT 50"); - query - .build_query_as::() - .fetch_all(self.database.pool()) - .await - .map_err(Into::into) + .order_by(ResearchKeyword::fields().name()) + .limit(50) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } - pub async fn list_research_lines(&self) -> AppResult> { - sqlx::query_as::<_, ResearchLineView>( - "SELECT id, name, slug FROM research_lines ORDER BY name", - ) - .fetch_all(self.database.pool()) - .await - .map_err(Into::into) + pub async fn list_research_lines(&self) -> AppResult> { + ResearchLine::all() + .include(ResearchLine::fields().subfields()) + .order_by(ResearchLine::fields().name()) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } - pub async fn list_research_lines_with_subfields(&self) -> AppResult> { - let rows = sqlx::query( - "SELECT rl.id, rl.name, rl.slug, - COALESCE(jsonb_agg( - jsonb_build_object( - 'subfieldOpenalexId', rlm.subfield_openalex_id, - 'subfieldName', rs.name - ) - ORDER BY rs.name - ) FILTER (WHERE rlm.subfield_openalex_id IS NOT NULL), '[]'::jsonb) AS subfields - FROM research_lines rl - LEFT JOIN research_line_mappings rlm ON rlm.research_line_id = rl.id - LEFT JOIN research_subfields rs ON rs.openalex_id = rlm.subfield_openalex_id - GROUP BY rl.id, rl.name, rl.slug - ORDER BY rl.name", - ) - .fetch_all(self.database.pool()) - .await?; - - let lines = rows - .into_iter() - .map(|row| { - let id: Uuid = row.get("id"); - let name: String = row.get("name"); - let slug: String = row.get("slug"); - let subfields: serde_json::Value = row.get("subfields"); - let subfields: Vec = - serde_json::from_value(subfields).unwrap_or_default(); - ResearchLineDetail { - id, - name, - slug, - subfields, - } - }) - .collect(); - - Ok(lines) + pub async fn find_subfield_by_id( + &self, + id: ResearchSubfieldId, + ) -> AppResult> { + ResearchSubfield::filter_by_id(id) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } - pub async fn update_mapping( + pub async fn find_research_line_by_id( &self, - subfield_openalex_id: &str, - research_line_id: Uuid, - ) -> AppResult<()> { - sqlx::query( - "UPDATE research_line_mappings SET research_line_id = $1 WHERE subfield_openalex_id = $2", - ) - .bind(research_line_id) - .bind(subfield_openalex_id) - .execute(self.database.pool()) - .await?; + id: ResearchLineId, + ) -> AppResult> { + ResearchLine::filter_by_id(id) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) + } - Ok(()) + pub async fn find_topic_by_openalex_id( + &self, + openalex_id: &str, + ) -> AppResult> { + ResearchTopic::all() + .filter(ResearchTopic::fields().openalex_id().eq(openalex_id)) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } - pub async fn delete_mapping(&self, subfield_openalex_id: &str) -> AppResult<()> { - sqlx::query("DELETE FROM research_line_mappings WHERE subfield_openalex_id = $1") - .bind(subfield_openalex_id) - .execute(self.database.pool()) - .await?; + pub async fn save_keyword(&self, keyword: &ResearchKeyword) -> AppResult<()> { + ResearchKeyword::upsert_by_openalex_id(keyword.openalex_id) + .id(&keyword.id) + .name(&keyword.name) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) + } - Ok(()) + pub async fn save_subfield(&self, subfield: &ResearchSubfield) -> AppResult<()> { + ResearchSubfield::upsert_by_openalex_id(subfield.openalex_id) + .id(&subfield.id) + .name(&subfield.name) + .field_id(&subfield.field_id) + .research_line_id(subfield.research_line_id) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } - pub async fn list_topics_by_work(&self, work_id: &WorkId) -> AppResult> { - sqlx::query_as::<_, ResearchTopicView>( - "SELECT - wt.topic_id, t.name, wt.score, - s.id AS subfield_id, s.name AS subfield_name, - f.id AS field_id, f.name AS field_name, - d.id AS domain_id, d.name AS domain_name - FROM work_topics wt - JOIN research_topics t ON t.id = wt.topic_id - JOIN research_subfields s ON s.id = t.subfield_id - JOIN research_fields f ON f.id = s.field_id - JOIN research_domains d ON d.id = f.domain_id - WHERE wt.work_id = $1 - ORDER BY wt.score DESC", - ) - .bind(work_id) - .fetch_all(self.database.pool()) - .await - .map_err(Into::into) + pub async fn unknown_keyword_id(&self) -> AppResult> { + ResearchKeyword::all() + .filter(ResearchKeyword::fields().openalex_id().eq("unknown")) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } - pub async fn list_keywords_by_work( - &self, - work_id: &WorkId, - ) -> AppResult> { - sqlx::query_as::<_, ResearchKeywordView>( - "SELECT wk.keyword_id, k.name, wk.score - FROM work_keywords wk - JOIN keywords k ON k.id = wk.keyword_id - WHERE wk.work_id = $1 - ORDER BY wk.score DESC", - ) - .bind(work_id) - .fetch_all(self.database.pool()) - .await - .map_err(Into::into) + pub async fn unknown_topic_id(&self) -> AppResult> { + ResearchTopic::all() + .filter(ResearchTopic::fields().openalex_id().eq("unknown")) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } } diff --git a/apps/server/src/research/sources/entity.rs b/apps/server/src/research/sources/entity.rs deleted file mode 100644 index 5a69577..0000000 --- a/apps/server/src/research/sources/entity.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::shared::{Entity, Id}; -use bon::Builder; -use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; - -pub type SourceId = Id; - -#[derive(Debug, Clone, Serialize, FromRow, Builder)] -#[serde(rename_all = "camelCase")] -pub struct Source { - #[builder(default = SourceId::new())] - pub id: SourceId, - pub openalex_id: String, - pub display_name: String, - pub ty: String, - pub issn_l: Option, - pub issn: Option>, -} - -impl Source { - pub fn normalize_issn(issn: &str) -> Option { - let normalized = issn.replace("-", "").to_uppercase(); - - if normalized.is_empty() { - None - } else { - Some(normalized) - } - } -} - -impl Entity for Source { - fn key_name() -> &'static str { - "source" - } -} - -#[derive(Debug, Clone, Copy, Type, Serialize, Deserialize, Eq, PartialEq)] -#[sqlx(type_name = "journal_kind", rename_all = "lowercase")] -#[serde(rename_all = "lowercase")] -pub enum JournalKind { - Wos, - Scopus, -} diff --git a/apps/server/src/research/sources/mod.rs b/apps/server/src/research/sources/mod.rs index 49a59d0..47587c3 100644 --- a/apps/server/src/research/sources/mod.rs +++ b/apps/server/src/research/sources/mod.rs @@ -1,7 +1,67 @@ -mod entity; mod repository; -mod views; -pub use entity::{JournalKind, Source, SourceId}; pub use repository::SourcesRepository; -pub use views::SourceView; + +use crate::shared::model_id; +use bon::Builder; +use serde::{Deserialize, Serialize}; +use toasty::{Deferred, Embed, Model}; + +model_id! { + struct SourceId, + key: "source" +} + +#[derive(Debug, Clone, Serialize, Model, Builder)] +#[serde(rename_all = "camelCase")] +pub struct Source { + #[key] + #[builder(default = SourceId::new())] + pub id: SourceId, + + #[unique] + pub openalex_id: String, + pub display_name: String, + pub ty: String, + pub issn: Option>, + + #[has_one] + pub journal_issn: Deferred>, +} + +#[derive(Debug, Clone, Serialize, Model)] +pub struct JournalIssn { + #[key] + #[auto] + pub id: i64, + + #[unique] + pub issn: String, + pub kind: JournalKind, + + #[index] + pub source_id: Option, + + #[belongs_to] + pub source: Deferred>, +} + +#[derive(Debug, Clone, Copy, Embed, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +#[column(rename_all = "lowercase")] +pub enum JournalKind { + Wos, + Scopus, +} + +impl Source { + pub fn normalize_issn(issn: &str) -> Option { + let normalized = issn.replace("-", "").to_uppercase(); + + if normalized.is_empty() { + None + } else { + Some(normalized) + } + } +} diff --git a/apps/server/src/research/sources/repository.rs b/apps/server/src/research/sources/repository.rs index 1b024ff..03dceef 100644 --- a/apps/server/src/research/sources/repository.rs +++ b/apps/server/src/research/sources/repository.rs @@ -1,8 +1,6 @@ -use crate::research::sources::views::SourceView; use crate::research::*; -use crate::shared::{AppResult, Database}; +use crate::shared::{AppError, AppResult, Database}; -use sqlx::Row; use std::sync::Arc; use sword::prelude::*; @@ -12,43 +10,35 @@ pub struct SourcesRepository { } impl SourcesRepository { - pub async fn find_source_view_by_id(&self, id: &SourceId) -> AppResult> { - sqlx::query_as::<_, SourceView>( - r#"SELECT s.id, s.openalex_id, s.display_name, s.ty, s.issn_l, s.issn, - (SELECT ji.kind FROM journal_issn ji - WHERE ji.issn = s.issn_l - OR ji.eissn = s.issn_l - OR ji.issn = ANY(s.issn) - OR ji.eissn = ANY(s.issn) - LIMIT 1 - ) AS kind - FROM sources s - WHERE s.id = $1"#, - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await - .map_err(Into::into) + pub async fn find_by_id(&self, id: &SourceId) -> AppResult> { + Source::filter_by_id(id) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from) } - pub async fn save(&self, source: &Source) -> AppResult { - let row = sqlx::query( - "INSERT INTO sources (openalex_id, display_name, ty, issn_l, issn) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (openalex_id) DO UPDATE SET - display_name = EXCLUDED.display_name, - ty = EXCLUDED.ty, - issn_l = EXCLUDED.issn_l, - issn = EXCLUDED.issn RETURNING id", - ) - .bind(&source.openalex_id) - .bind(&source.display_name) - .bind(&source.ty) - .bind(&source.issn_l) - .bind(&source.issn) - .fetch_one(self.database.pool()) - .await?; + pub async fn save(&self, source: &Source) -> AppResult<()> { + Source::upsert_by_id(source.id) + .openalex_id(&source.openalex_id) + .display_name(&source.display_name) + .ty(&source.ty) + .issn(source.issn.clone()) + .exec(&mut self.database.pool()) + .await? + .map_err(AppError::from)?; - Ok(SourceId::from_uuid(row.get("id"))) + if let Some(ref issns) = source.issn { + toasty::sql::statement( + "UPDATE sources SET journal_issn_id = ( + SELECT id FROM journal_issn WHERE issn = ANY($1) LIMIT 1 + ) WHERE id = $2", + ) + .bind(issns) + .bind(&source.id) + .exec(&mut self.database.pool()) + .await?; + } + + Ok(()) } } diff --git a/apps/server/src/research/sources/views.rs b/apps/server/src/research/sources/views.rs deleted file mode 100644 index 4408c29..0000000 --- a/apps/server/src/research/sources/views.rs +++ /dev/null @@ -1,16 +0,0 @@ -use super::{JournalKind, SourceId}; - -use serde::Serialize; -use sqlx::FromRow; - -#[derive(Debug, Serialize, FromRow)] -#[serde(rename_all = "camelCase")] -pub struct SourceView { - pub id: SourceId, - pub openalex_id: String, - pub display_name: String, - pub ty: String, - pub issn_l: Option, - pub issn: Option>, - pub kind: Option, -} diff --git a/apps/server/src/research/stats/repository.rs b/apps/server/src/research/stats/repository.rs index 3d353db..3fefc7f 100644 --- a/apps/server/src/research/stats/repository.rs +++ b/apps/server/src/research/stats/repository.rs @@ -59,12 +59,7 @@ fn base_from() -> &'static str { JOIN departments d ON a.department_id = d.id JOIN academic_category_options aco ON a.acad_category_options_id = aco.id LEFT JOIN sources src ON w.primary_source_id = src.id - LEFT JOIN LATERAL ( - SELECT kind FROM journal_issn - WHERE issn = src.issn_l OR eissn = src.issn_l - OR issn = ANY(src.issn) OR eissn = ANY(src.issn) - LIMIT 1 - ) ji ON TRUE + LEFT JOIN journal_issn ji ON ji.id = src.journal_issn_id WHERE w.publication_year >= $1 AND ($2::smallint IS NULL OR w.publication_year <= $2) AND ($3::uuid IS NULL OR a.department_id = $3) @@ -237,10 +232,7 @@ impl StatsRepository { JOIN departments d ON a.department_id = d.id JOIN academic_category_options aco ON a.acad_category_options_id = aco.id LEFT JOIN sources src ON w.primary_source_id = src.id - LEFT JOIN LATERAL ( - SELECT kind FROM journal_issn WHERE issn = src.issn_l OR eissn = src.issn_l - OR issn = ANY(src.issn) OR eissn = ANY(src.issn) LIMIT 1 - ) ji ON TRUE + LEFT JOIN journal_issn ji ON ji.id = src.journal_issn_id WHERE d.id = $1 AND w.publication_year >= $2 AND ($3::smallint IS NULL OR w.publication_year <= $3) AND ($4::academic_option IS NULL OR aco.option = $4) @@ -270,10 +262,7 @@ impl StatsRepository { JOIN departments d ON a.department_id = d.id JOIN academic_category_options aco ON a.acad_category_options_id = aco.id LEFT JOIN sources src ON w.primary_source_id = src.id - LEFT JOIN LATERAL ( - SELECT kind FROM journal_issn WHERE issn = src.issn_l OR eissn = src.issn_l - OR issn = ANY(src.issn) OR eissn = ANY(src.issn) LIMIT 1 - ) ji ON TRUE + LEFT JOIN journal_issn ji ON ji.id = src.journal_issn_id WHERE d.id = $1 AND w.publication_year >= $2 AND ($3::smallint IS NULL OR w.publication_year <= $3) AND ($4::academic_option IS NULL OR aco.option = $4) diff --git a/apps/server/src/research/works/entity.rs b/apps/server/src/research/works/entity.rs index 0d25047..bfad53e 100644 --- a/apps/server/src/research/works/entity.rs +++ b/apps/server/src/research/works/entity.rs @@ -1,54 +1,51 @@ -use crate::research::sources::SourceId; -use crate::shared::{Entity, Id}; +use crate::{ + research::{Source, SourceId}, + shared::model_id, +}; use bon::Builder; -use chrono::NaiveDate; +use jiff::civil::Date; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; use std::str::FromStr; -use uuid::Uuid; +use toasty::{Deferred, Embed, Model}; -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[serde(default)] -pub struct WorkOverrides { - pub title: Option, +#[derive(Debug, Clone, Serialize, Model, Builder)] +#[serde(rename_all = "camelCase")] +pub struct Work { + #[key] + #[builder(default = WorkId::new())] + pub id: WorkId, + + #[unique] + pub openalex_id: String, + pub title: String, pub r#abstract: Option, pub doi: Option, + pub publication_date: Option, pub publication_year: Option, - pub is_accepted: Option, - pub is_published: Option, -} + pub ty: WorkType, + pub lang: String, + pub is_accepted: bool, + pub is_published: bool, -impl WorkOverrides { - pub fn non_null_fields(&self) -> Vec { - let mut fields = Vec::new(); - if self.title.is_some() { - fields.push("title".into()); - } - if self.r#abstract.is_some() { - fields.push("abstract".into()); - } - if self.doi.is_some() { - fields.push("doi".into()); - } - if self.publication_year.is_some() { - fields.push("publicationYear".into()); - } - if self.is_accepted.is_some() { - fields.push("isAccepted".into()); - } - if self.is_published.is_some() { - fields.push("isPublished".into()); - } - fields - } + #[index] + pub primary_source_id: Option, + + #[column(type = "jsonb")] + pub overrides: serde_json::Value, + + #[has_one] + pub primary_source: Deferred>, } -pub type WorkId = Id; +model_id! { + struct WorkId, + key: "work" +} -#[derive(Debug, Clone, Copy, Type, Serialize, Deserialize, Eq, PartialEq)] -#[sqlx(type_name = "work_type", rename_all = "kebab-case")] +#[derive(Debug, Clone, Copy, Embed, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "kebab-case")] +#[column(rename_all = "kebab-case")] pub enum WorkType { Article, Book, @@ -109,53 +106,3 @@ impl FromStr for WorkType { } } } - -#[derive(Debug, Clone, Serialize, FromRow, Builder)] -#[serde(rename_all = "camelCase")] -pub struct Work { - #[builder(default = WorkId::new())] - pub id: WorkId, - pub openalex_id: String, - pub title: String, - pub r#abstract: Option, - pub doi: Option, - pub publication_date: Option, - pub publication_year: Option, - pub ty: WorkType, - pub lang: String, - pub is_accepted: bool, - pub is_published: bool, - pub primary_source_id: Option, - pub journal_kind: Option, - pub research_line_id: Option, - pub research_line_name: Option, - pub research_line_slug: Option, - #[serde(skip)] - pub overrides: serde_json::Value, - #[serde(rename = "overriddenFields")] - #[sqlx(default)] - pub overridden_fields: Vec, -} - -impl Work { - pub fn resolve(&self) -> Self { - let o: WorkOverrides = serde_json::from_value(self.overrides.clone()).unwrap_or_default(); - - Self { - title: o.title.clone().unwrap_or_else(|| self.title.clone()), - r#abstract: o.r#abstract.clone().or(self.r#abstract.clone()), - doi: o.doi.clone().or(self.doi.clone()), - publication_year: o.publication_year.or(self.publication_year), - is_accepted: o.is_accepted.unwrap_or(self.is_accepted), - is_published: o.is_published.unwrap_or(self.is_published), - overridden_fields: o.non_null_fields(), - ..self.clone() - } - } -} - -impl Entity for Work { - fn key_name() -> &'static str { - "work" - } -} diff --git a/apps/server/src/research/works/repository.rs b/apps/server/src/research/works/repository.rs index eea14c4..ab26150 100644 --- a/apps/server/src/research/works/repository.rs +++ b/apps/server/src/research/works/repository.rs @@ -18,9 +18,9 @@ impl WorksRepository { w.publication_date, w.publication_year, w.ty, w.lang, w.is_accepted, w.is_published, w.primary_source_id, w.overrides, ji.kind::text AS journal_kind, rl.id AS research_line_id, rl.name AS research_line_name, rl.slug AS research_line_slug - FROM works w LEFT JOIN sources src ON w.primary_source_id = src.id - LEFT JOIN LATERAL (SELECT kind FROM journal_issn WHERE issn = src.issn_l - OR eissn = src.issn_l OR issn = ANY(src.issn) OR eissn = ANY(src.issn) LIMIT 1) ji ON TRUE + FROM works w + LEFT JOIN sources src ON w.primary_source_id = src.id + LEFT JOIN journal_issn ji ON ji.id = src.journal_issn_id LEFT JOIN LATERAL ( SELECT r.id, r.name, r.slug FROM work_topics wt @@ -45,13 +45,13 @@ impl WorksRepository { let mut qb = QueryBuilder::new( "SELECT DISTINCT w.id, w.openalex_id, w.title, w.abstract, w.doi, w.publication_date, w.publication_year, w.ty, w.lang, w.is_accepted, - w.is_published, w.primary_source_id, w.overrides, ji.kind::text AS journal_kind, + w.is_published, w.primary_source_id, w.overrides, + ji.kind::text AS journal_kind, rl.id AS research_line_id, rl.name AS research_line_name, rl.slug AS research_line_slug FROM works w LEFT JOIN work_authorships wa ON w.id = wa.work_id LEFT JOIN sources src ON w.primary_source_id = src.id - LEFT JOIN LATERAL ( - SELECT kind FROM journal_issn WHERE issn = src.issn_l OR eissn = src.issn_l OR issn = ANY(src.issn) OR eissn = ANY(src.issn) LIMIT 1) ji ON TRUE + LEFT JOIN journal_issn ji ON ji.id = src.journal_issn_id LEFT JOIN LATERAL ( SELECT r.id, r.name, r.slug FROM work_topics wt diff --git a/apps/server/src/research/works/services/mod.rs b/apps/server/src/research/works/services/mod.rs index f7187e7..dbc1f0b 100644 --- a/apps/server/src/research/works/services/mod.rs +++ b/apps/server/src/research/works/services/mod.rs @@ -223,28 +223,28 @@ impl WorksService { .and_then(|l| l.source.as_ref()) { let source_ty = s.r#type.clone().unwrap_or_else(|| "unknown".to_string()); - let issn_l = s.issn_l.as_deref().and_then(Source::normalize_issn); - let issn: Option> = s.issn.as_ref().and_then(|vec| { - let normalized: Vec = vec - .iter() - .filter_map(|v| Source::normalize_issn(v)) - .collect(); - if normalized.is_empty() { - None - } else { - Some(normalized) - } + let mut normalized_issns: Vec = s.issn.as_ref().map_or_else(Vec::new, |vec| { + vec.iter().filter_map(|v| Source::normalize_issn(v)).collect() }); + if let Some(issn_l) = s.issn_l.as_deref().and_then(Source::normalize_issn) { + if !normalized_issns.contains(&issn_l) { + normalized_issns.push(issn_l); + } + } + let issn = if normalized_issns.is_empty() { + None + } else { + Some(normalized_issns) + }; Some( self.sources - .save(&Source { - id: SourceId::new(), - openalex_id: s.id.clone().unwrap_or_default(), - display_name: s.display_name.clone().unwrap_or_default(), - ty: source_ty, - issn_l, - issn, - }) + .save(&Source::builder() + .id(SourceId::new()) + .openalex_id(s.id.clone().unwrap_or_default()) + .display_name(s.display_name.clone().unwrap_or_default()) + .ty(source_ty) + .issn(issn) + .build()) .await?, ) } else { diff --git a/apps/server/src/shared/database.rs b/apps/server/src/shared/database.rs index ead432a..20a844d 100644 --- a/apps/server/src/shared/database.rs +++ b/apps/server/src/shared/database.rs @@ -1,5 +1,6 @@ use crate::shared::AppResult; +use parking_lot::RwLock; use serde::Deserialize; use std::sync::Arc; use sword::prelude::*; @@ -7,9 +8,11 @@ use toasty::{Db as Pool, models}; pub use toasty::Transaction as Tx; +pub type DatabasePool = RwLock; + #[injectable(provider)] pub struct Database { - pool: Arc, + pool: Arc, } #[config(key = "postgres-db")] @@ -28,17 +31,20 @@ pub struct DatabaseConfig { impl Database { pub async fn new(db_conf: DatabaseConfig) -> Self { - let mut db = Pool::builder() + let pool = Pool::builder() .max_pool_size(db_conf.max_connections as usize) .models(models!(crate::*)) .connect(&Self::create_uri(&db_conf)) - .await?; - - db.push_schema().expect("Failed to migrate database schema"); + .await + .expect("Failed to connect to database"); - let a = db.transaction().await?; + pool.push_schema() + .await + .expect("Failed to push schema to database"); - Self { pool: Arc::new(db) } + Self { + pool: Arc::new(RwLock::new(pool)), + } } fn create_uri(db_conf: &DatabaseConfig) -> String { @@ -48,12 +54,8 @@ impl Database { ) } - pub fn pool(&self) -> &Pool { - &self.pool - } - - pub async fn tx(&self) -> AppResult> { - Ok(self.pool.transaction().await?) + pub fn pool(&self) -> Pool { + self.pool.read().clone() } } @@ -63,7 +65,7 @@ pub struct TransactionManager { } impl TransactionManager { - pub async fn begin(&self) -> AppResult> { - self.db.tx().await + pub async fn database(&self) -> AppResult { + Ok(self.db.pool()) } } diff --git a/apps/server/src/shared/mod.rs b/apps/server/src/shared/mod.rs index 9b2163d..41bab41 100644 --- a/apps/server/src/shared/mod.rs +++ b/apps/server/src/shared/mod.rs @@ -16,7 +16,6 @@ mod value_objects { } use database::DatabaseConfig; -use std::sync::Arc; use sword::prelude::*; pub use database::{Database, TransactionManager, Tx}; @@ -40,7 +39,7 @@ impl Module for SharedModule { let database = Database::new(db_config).await; let seeder_data = config.expect::(); - let seeder = DatabaseSeeder::new(Arc::new(database.clone()), seeder_data); + let seeder = DatabaseSeeder::new(database.clone(), seeder_data); seeder.seed().await; diff --git a/apps/server/src/shared/seeder.rs b/apps/server/src/shared/seeder.rs index afed6b2..c175dcd 100644 --- a/apps/server/src/shared/seeder.rs +++ b/apps/server/src/shared/seeder.rs @@ -4,7 +4,6 @@ use crate::{ }; use serde::Deserialize; -use std::sync::Arc; use sword::prelude::*; #[derive(Debug, Clone, Deserialize)] @@ -16,12 +15,12 @@ pub struct SeederData { #[injectable(provider)] pub struct DatabaseSeeder { - database: Arc, + database: Database, config: SeederData, } impl DatabaseSeeder { - pub fn new(db_ref: Arc, config: SeederData) -> Self { + pub fn new(db_ref: Database, config: SeederData) -> Self { Self { database: db_ref, config, diff --git a/apps/server/src/university/careers/entity.rs b/apps/server/src/university/careers/entity.rs index b279251..da8d074 100644 --- a/apps/server/src/university/careers/entity.rs +++ b/apps/server/src/university/careers/entity.rs @@ -24,6 +24,7 @@ pub struct Career { pub department_id: DepartmentId, #[belongs_to] + #[builder(default)] department: Deferred, } diff --git a/apps/server/src/university/careers/repository.rs b/apps/server/src/university/careers/repository.rs index 3a88554..163f848 100644 --- a/apps/server/src/university/careers/repository.rs +++ b/apps/server/src/university/careers/repository.rs @@ -3,7 +3,6 @@ use crate::university::{Career, CareerFilter, CareerId}; use std::sync::Arc; use sword::prelude::*; -use toasty::schema::Model; #[injectable] pub struct CareersRepository { @@ -22,12 +21,26 @@ impl CareersRepository { query = query.filter(Career::fields().department_id().eq(dept_id)); } - query.exec(&mut self.database.pool()).await.into() + query + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from) } pub async fn find_by_id(&self, id: &CareerId) -> AppResult> { - Career::get_by_id(&mut self.database.pool(), id) - .await? + Career::filter_by_id(id) + .first() + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from) + } + + pub async fn find_by_name(&self, name: &str) -> AppResult> { + Career::all() + .filter(Career::fields().name().eq(name)) + .first() + .exec(&mut self.database.pool()) + .await .map_err(AppError::from) } diff --git a/apps/server/src/university/countries/mod.rs b/apps/server/src/university/countries/mod.rs index 61e3755..06d8d9f 100644 --- a/apps/server/src/university/countries/mod.rs +++ b/apps/server/src/university/countries/mod.rs @@ -1,9 +1,10 @@ mod repository; pub use repository::*; +use serde::Serialize; use toasty::Model; -#[derive(Model)] +#[derive(Debug, Clone, Serialize, Model)] pub struct Country { #[key] code: String, diff --git a/apps/server/src/university/departments/entity.rs b/apps/server/src/university/departments/entity.rs index 125f626..1f80431 100644 --- a/apps/server/src/university/departments/entity.rs +++ b/apps/server/src/university/departments/entity.rs @@ -8,8 +8,7 @@ use serde::Serialize; use toasty::{Deferred, Model}; model_id! { - struct DepartmentId, - key: "department" + struct DepartmentId, key: "department" } #[derive(Debug, Clone, Serialize, Model, Builder)] @@ -24,10 +23,12 @@ pub struct Department { pub faculty_id: FacultyId, #[belongs_to] + #[builder(default)] pub faculty: Deferred, #[has_many] - careers: Deferred>, + #[builder(default)] + pub careers: Deferred>, } #[derive(Debug)] diff --git a/apps/server/src/university/departments/repository.rs b/apps/server/src/university/departments/repository.rs index 4e90539..dddee8b 100644 --- a/apps/server/src/university/departments/repository.rs +++ b/apps/server/src/university/departments/repository.rs @@ -28,8 +28,19 @@ impl DepartmentsRepository { } pub async fn find_by_id(&self, id: &DepartmentId) -> AppResult> { - Department::get_by_id(&mut self.database.pool(), id) - .await? + Department::filter_by_id(id) + .first() + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from) + } + + pub async fn find_by_name(&self, name: &str) -> AppResult> { + Department::all() + .filter(Department::fields().name().eq(name)) + .first() + .exec(&mut self.database.pool()) + .await .map_err(AppError::from) } @@ -39,7 +50,8 @@ impl DepartmentsRepository { .name(&department.name) .faculty_id(department.faculty_id) .exec(&mut self.database.pool()) - .await? - .map_err(AppError::from) + .await?; + + Ok(()) } } diff --git a/apps/server/src/university/faculties/entity.rs b/apps/server/src/university/faculties/entity.rs index 847a446..2ac0825 100644 --- a/apps/server/src/university/faculties/entity.rs +++ b/apps/server/src/university/faculties/entity.rs @@ -4,8 +4,7 @@ use serde::Serialize; use toasty::{Deferred, Model}; model_id! { - struct FacultyId, - key: "faculty" + struct FacultyId, key: "faculty" } #[derive(Debug, Clone, Serialize, Builder, Model)] @@ -16,7 +15,8 @@ pub struct Faculty { pub name: String, #[has_many] - departments: Deferred>, + #[builder(default)] + pub departments: Deferred>, } pub struct FacultyFilter { diff --git a/apps/server/src/university/faculties/repository.rs b/apps/server/src/university/faculties/repository.rs index 532df63..401078c 100644 --- a/apps/server/src/university/faculties/repository.rs +++ b/apps/server/src/university/faculties/repository.rs @@ -11,7 +11,7 @@ pub struct FacultiesRepository { impl FacultiesRepository { pub async fn list(&self, filter: FacultyFilter) -> AppResult> { - let query = Faculty::all(); + let mut query = Faculty::all(); if let Some(n) = filter.name { query = query.filter(Faculty::fields().name().ilike(format!("%{}%", n.trim()))) @@ -19,13 +19,15 @@ impl FacultiesRepository { query .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } pub async fn find_by_id(&self, id: &FacultyId) -> AppResult> { - Faculty::get_by_id(&mut self.database.pool(), id) - .await? + Faculty::filter_by_id(id) + .first() + .exec(&mut self.database.pool()) + .await .map_err(AppError::from) } @@ -34,7 +36,8 @@ impl FacultiesRepository { .id(faculty.id) .name(faculty.name.clone()) .exec(&mut self.database.pool()) - .await? - .map_err(AppError::from) + .await?; + + Ok(()) } } diff --git a/apps/server/src/university/work_positions/controller.rs b/apps/server/src/university/work_positions/controller.rs index 7095207..2be363c 100644 --- a/apps/server/src/university/work_positions/controller.rs +++ b/apps/server/src/university/work_positions/controller.rs @@ -13,7 +13,7 @@ pub struct WorkPositionsController { impl WorkPositionsController { #[get("/")] - pub async fn get_positions(&self, req: Request) -> WebResult> { + pub async fn get_positions(&self, _: Request) -> WebResult> { Ok(self.positions.find().await?) } diff --git a/apps/server/src/university/work_positions/repository.rs b/apps/server/src/university/work_positions/repository.rs index 8a7f6df..c70f271 100644 --- a/apps/server/src/university/work_positions/repository.rs +++ b/apps/server/src/university/work_positions/repository.rs @@ -13,7 +13,7 @@ impl AcademicWorkPositionsRepository { pub async fn list(&self) -> AppResult> { AcademicWorkPosition::all() .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } @@ -21,8 +21,19 @@ impl AcademicWorkPositionsRepository { &self, id: &AcademicWorkPositionId, ) -> AppResult> { - AcademicWorkPosition::get_by_id(&mut self.database.pool(), id) - .await? + AcademicWorkPosition::filter_by_id(id) + .first() + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from) + } + + pub async fn find_by_name(&self, name: &str) -> AppResult> { + AcademicWorkPosition::all() + .filter(AcademicWorkPosition::fields().name().eq(name)) + .first() + .exec(&mut self.database.pool()) + .await .map_err(AppError::from) } @@ -31,7 +42,8 @@ impl AcademicWorkPositionsRepository { .id(position.id) .name(position.name.clone()) .exec(&mut self.database.pool()) - .await? - .map_err(AppError::from) + .await?; + + Ok(()) } } diff --git a/apps/server/src/university/work_positions/service.rs b/apps/server/src/university/work_positions/service.rs index 220f799..6be1209 100644 --- a/apps/server/src/university/work_positions/service.rs +++ b/apps/server/src/university/work_positions/service.rs @@ -17,7 +17,7 @@ impl AcademicWorkPositionsService { &self, input: CreateAcademicWorkPositionDto, ) -> AppResult { - let position = AcademicWorkPosition::new(input.name); + let position = AcademicWorkPosition::builder().name(input.name).build(); self.positions.save(&position).await?; From aa766669ac043f7806059c98bfa5869e173a4799 Mon Sep 17 00:00:00 2001 From: MrRevillod Date: Mon, 27 Jul 2026 00:10:02 -0400 Subject: [PATCH 6/7] feat: half migration of research domain --- apps/server/src/research/authorships/dto.rs | 12 ---- .../server/src/research/authorships/entity.rs | 26 -------- apps/server/src/research/authorships/mod.rs | 8 --- .../src/research/authorships/repository.rs | 47 ------------- .../src/research/classification/controller.rs | 4 +- .../src/research/classification/entity.rs | 46 +++++-------- .../src/research/classification/repository.rs | 66 ++++++++++--------- apps/server/src/research/mod.rs | 3 - apps/server/src/research/sources/mod.rs | 7 +- .../server/src/research/sources/repository.rs | 17 +---- apps/server/src/research/works/dtos.rs | 12 +++- apps/server/src/research/works/entity.rs | 41 ++++++++++-- 12 files changed, 108 insertions(+), 181 deletions(-) delete mode 100644 apps/server/src/research/authorships/dto.rs delete mode 100644 apps/server/src/research/authorships/entity.rs delete mode 100644 apps/server/src/research/authorships/mod.rs delete mode 100644 apps/server/src/research/authorships/repository.rs diff --git a/apps/server/src/research/authorships/dto.rs b/apps/server/src/research/authorships/dto.rs deleted file mode 100644 index 98919b4..0000000 --- a/apps/server/src/research/authorships/dto.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::research::authorships::entity::AuthorshipPosition; -use crate::research::works::WorkId; - -pub struct NewAuthorship { - pub work_id: WorkId, - pub orcid: String, - pub name: String, - pub is_external: bool, - pub is_corresponding: bool, - pub affiliations: Vec, - pub position: AuthorshipPosition, -} diff --git a/apps/server/src/research/authorships/entity.rs b/apps/server/src/research/authorships/entity.rs deleted file mode 100644 index 9e94df3..0000000 --- a/apps/server/src/research/authorships/entity.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::research::works::WorkId; -use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; -use uuid::Uuid; - -#[derive(Debug, Clone, Copy, Type, Serialize, Deserialize, Eq, PartialEq)] -#[sqlx(type_name = "authorship_position", rename_all = "lowercase")] -#[serde(rename_all = "lowercase")] -pub enum AuthorshipPosition { - First, - Middle, - Last, -} - -#[derive(Debug, Serialize, FromRow)] -#[serde(rename_all = "camelCase")] -pub struct Authorship { - pub work_id: WorkId, - pub orcid: String, - pub name: String, - pub is_external: bool, - pub is_corresponding: bool, - pub affiliations: Vec, - pub position: AuthorshipPosition, - pub academic_id: Option, -} diff --git a/apps/server/src/research/authorships/mod.rs b/apps/server/src/research/authorships/mod.rs deleted file mode 100644 index 6b5a8fc..0000000 --- a/apps/server/src/research/authorships/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod dto; -mod entity; -mod repository; - -pub use dto::NewAuthorship; -pub use entity::AuthorshipPosition; -pub use entity::*; -pub use repository::AuthorshipsRepository; diff --git a/apps/server/src/research/authorships/repository.rs b/apps/server/src/research/authorships/repository.rs deleted file mode 100644 index 74a1746..0000000 --- a/apps/server/src/research/authorships/repository.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::research::*; -use crate::shared::{AppResult, Database}; - -use std::sync::Arc; -use sword::prelude::*; - -#[injectable] -pub struct AuthorshipsRepository { - database: Arc, -} - -impl AuthorshipsRepository { - pub async fn list(&self, work_id: &WorkId) -> AppResult> { - sqlx::query_as::<_, Authorship>( - r#"SELECT wa.*, a.id as academic_id - FROM work_authorships wa - LEFT JOIN academics a ON a.orcid = wa.orcid - WHERE wa.work_id = $1 - ORDER BY CASE wa.position WHEN 'first' THEN 0 WHEN 'middle' THEN 1 WHEN 'last' THEN 2 END"#, - ) - .bind(work_id) - .fetch_all(self.database.pool()) - .await - .map_err(Into::into) - } - - pub async fn insert(&self, authorship: &NewAuthorship) -> AppResult<()> { - sqlx::query( - "INSERT INTO work_authorships ( - work_id, orcid, name, is_external, - is_corresponding, affiliations, position - ) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (work_id, orcid) DO NOTHING - ", - ) - .bind(authorship.work_id) - .bind(&authorship.orcid) - .bind(&authorship.name) - .bind(authorship.is_external) - .bind(authorship.is_corresponding) - .bind(&authorship.affiliations) - .bind(authorship.position) - .execute(self.database.pool()) - .await?; - - Ok(()) - } -} diff --git a/apps/server/src/research/classification/controller.rs b/apps/server/src/research/classification/controller.rs index 0a0356d..eaa697e 100644 --- a/apps/server/src/research/classification/controller.rs +++ b/apps/server/src/research/classification/controller.rs @@ -60,7 +60,7 @@ impl WorksClassificationController { if self .work_classifications - .find_research_line_by_id(dto.research_line_id) + .find_research_line_by_id(&dto.research_line_id) .await? .is_none() { @@ -69,7 +69,7 @@ impl WorksClassificationController { let Some(mut subfield) = self .work_classifications - .find_subfield_by_openalex_id(&dto.subfield_openalex_id) + .find_subfield_by_id(&dto.subfield_id) .await? else { return Err(JsonResponse::NotFound())?; diff --git a/apps/server/src/research/classification/entity.rs b/apps/server/src/research/classification/entity.rs index 23402c8..06c3b4b 100644 --- a/apps/server/src/research/classification/entity.rs +++ b/apps/server/src/research/classification/entity.rs @@ -13,9 +13,6 @@ pub struct ResearchDomain { #[unique] pub openalex_id: String, - - #[has_many] - pub fields: Deferred>, } #[derive(Debug, Clone, Serialize, Model, Builder)] @@ -34,6 +31,15 @@ pub struct ResearchField { #[belongs_to] pub domain: Deferred, +} + +#[derive(Debug, Clone, Serialize, Model, Builder)] +pub struct ResearchLine { + #[key] + #[builder(default = ResearchLineId::new())] + pub id: ResearchLineId, + pub name: String, + pub slug: String, #[has_many] pub subfields: Deferred>, @@ -60,10 +66,7 @@ pub struct ResearchSubfield { pub field: Deferred, #[belongs_to] - pub research_line: Option>, - - #[has_many] - pub topics: Deferred>, + pub research_line: Deferred>, } #[derive(Debug, Clone, Serialize, Model, Builder)] @@ -96,17 +99,6 @@ pub struct ResearchKeyword { pub name: String, } -#[derive(Debug, Clone, Serialize, Model, Builder)] -pub struct ResearchLine { - #[key] - pub id: ResearchLineId, - pub name: String, - pub slug: String, - - #[has_many] - pub subfields: Deferred>, -} - #[allow(dead_code)] pub struct ClassificationFilter { pub domain_id: Option, @@ -118,30 +110,24 @@ pub struct ClassificationFilter { } model_id! { - struct ResearchLineId, - key: "research_line" + struct ResearchLineId, key: "research_line" } model_id! { - struct ResearchDomainId, - key: "research_domain" + struct ResearchDomainId, key: "research_domain" } model_id! { - struct ResearchFieldId, - key: "research_field" + struct ResearchFieldId, key: "research_field" } model_id! { - struct ResearchSubfieldId, - key: "research_subfield" + struct ResearchSubfieldId, key: "research_subfield" } model_id! { - struct ResearchTopicId, - key: "research_topic" + struct ResearchTopicId, key: "research_topic" } model_id! { - struct ResearchKeywordId, - key: "research_keyword" + struct ResearchKeywordId, key: "research_keyword" } diff --git a/apps/server/src/research/classification/repository.rs b/apps/server/src/research/classification/repository.rs index 1cdba50..08c25c5 100644 --- a/apps/server/src/research/classification/repository.rs +++ b/apps/server/src/research/classification/repository.rs @@ -22,7 +22,7 @@ impl WorkClassificationRepository { query .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } @@ -39,9 +39,9 @@ impl WorkClassificationRepository { } fields - .order_by(ResearchField::fields().name()) + .order_by(ResearchField::fields().name().asc()) .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } @@ -61,10 +61,10 @@ impl WorkClassificationRepository { } subfields - .order_by(ResearchSubfield::fields().name()) + .order_by(ResearchSubfield::fields().name().asc()) .limit(50) .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } @@ -77,14 +77,14 @@ impl WorkClassificationRepository { if let Some(s) = f.search { let pattern = format!("%{}%", s.trim()); - let pattern = topics = topics.filter(ResearchTopic::fields().name().ilike(pattern)); + topics = topics.filter(ResearchTopic::fields().name().ilike(pattern)); } topics - .order_by(ResearchTopic::fields().name()) + .order_by(ResearchTopic::fields().name().asc()) .limit(50) .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } @@ -97,39 +97,41 @@ impl WorkClassificationRepository { } query - .order_by(ResearchKeyword::fields().name()) + .order_by(ResearchKeyword::fields().name().asc()) .limit(50) .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } pub async fn list_research_lines(&self) -> AppResult> { ResearchLine::all() .include(ResearchLine::fields().subfields()) - .order_by(ResearchLine::fields().name()) + .order_by(ResearchLine::fields().name().asc()) .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } pub async fn find_subfield_by_id( &self, - id: ResearchSubfieldId, + id: &ResearchSubfieldId, ) -> AppResult> { ResearchSubfield::filter_by_id(id) + .first() .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } pub async fn find_research_line_by_id( &self, - id: ResearchLineId, + id: &ResearchLineId, ) -> AppResult> { ResearchLine::filter_by_id(id) + .first() .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } @@ -137,46 +139,48 @@ impl WorkClassificationRepository { &self, openalex_id: &str, ) -> AppResult> { - ResearchTopic::all() - .filter(ResearchTopic::fields().openalex_id().eq(openalex_id)) + ResearchTopic::filter(ResearchTopic::fields().openalex_id().eq(openalex_id)) + .first() .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } pub async fn save_keyword(&self, keyword: &ResearchKeyword) -> AppResult<()> { - ResearchKeyword::upsert_by_openalex_id(keyword.openalex_id) + ResearchKeyword::upsert_by_openalex_id(&keyword.openalex_id) .id(&keyword.id) .name(&keyword.name) .exec(&mut self.database.pool()) - .await? - .map_err(AppError::from) + .await?; + + Ok(()) } pub async fn save_subfield(&self, subfield: &ResearchSubfield) -> AppResult<()> { - ResearchSubfield::upsert_by_openalex_id(subfield.openalex_id) + ResearchSubfield::upsert_by_openalex_id(&subfield.openalex_id) .id(&subfield.id) .name(&subfield.name) .field_id(&subfield.field_id) .research_line_id(subfield.research_line_id) .exec(&mut self.database.pool()) - .await? - .map_err(AppError::from) + .await?; + + Ok(()) } pub async fn unknown_keyword_id(&self) -> AppResult> { - ResearchKeyword::all() - .filter(ResearchKeyword::fields().openalex_id().eq("unknown")) + ResearchKeyword::filter(ResearchKeyword::fields().openalex_id().eq("unknown")) + .first() .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } pub async fn unknown_topic_id(&self) -> AppResult> { - ResearchTopic::all() - .filter(ResearchTopic::fields().openalex_id().eq("unknown")) + ResearchTopic::filter(ResearchTopic::fields().openalex_id().eq("unknown")) + .first() .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } } diff --git a/apps/server/src/research/mod.rs b/apps/server/src/research/mod.rs index 6b2a8b8..7bb4db5 100644 --- a/apps/server/src/research/mod.rs +++ b/apps/server/src/research/mod.rs @@ -1,10 +1,8 @@ -mod authorships; mod classification; mod sources; mod stats; mod works; -pub use authorships::*; pub use classification::*; pub use sources::*; pub use stats::*; @@ -30,7 +28,6 @@ impl Module for ResearchModule { components.register::(); components.register::(); components.register::(); - components.register::(); } async fn register_providers(config: &Config, providers: &ProviderRegistry) { diff --git a/apps/server/src/research/sources/mod.rs b/apps/server/src/research/sources/mod.rs index 47587c3..f98a569 100644 --- a/apps/server/src/research/sources/mod.rs +++ b/apps/server/src/research/sources/mod.rs @@ -2,7 +2,7 @@ mod repository; pub use repository::SourcesRepository; -use crate::shared::model_id; +use crate::{research::Work, shared::model_id}; use bon::Builder; use serde::{Deserialize, Serialize}; use toasty::{Deferred, Embed, Model}; @@ -23,10 +23,13 @@ pub struct Source { pub openalex_id: String, pub display_name: String, pub ty: String, - pub issn: Option>, + pub issn: Vec, #[has_one] pub journal_issn: Deferred>, + + #[has_many] + pub works: Deferred>, } #[derive(Debug, Clone, Serialize, Model)] diff --git a/apps/server/src/research/sources/repository.rs b/apps/server/src/research/sources/repository.rs index 03dceef..6383d73 100644 --- a/apps/server/src/research/sources/repository.rs +++ b/apps/server/src/research/sources/repository.rs @@ -12,8 +12,9 @@ pub struct SourcesRepository { impl SourcesRepository { pub async fn find_by_id(&self, id: &SourceId) -> AppResult> { Source::filter_by_id(id) + .first() .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from) } @@ -24,21 +25,9 @@ impl SourcesRepository { .ty(&source.ty) .issn(source.issn.clone()) .exec(&mut self.database.pool()) - .await? + .await .map_err(AppError::from)?; - if let Some(ref issns) = source.issn { - toasty::sql::statement( - "UPDATE sources SET journal_issn_id = ( - SELECT id FROM journal_issn WHERE issn = ANY($1) LIMIT 1 - ) WHERE id = $2", - ) - .bind(issns) - .bind(&source.id) - .exec(&mut self.database.pool()) - .await?; - } - Ok(()) } } diff --git a/apps/server/src/research/works/dtos.rs b/apps/server/src/research/works/dtos.rs index 87acb01..f1c2ee4 100644 --- a/apps/server/src/research/works/dtos.rs +++ b/apps/server/src/research/works/dtos.rs @@ -1,9 +1,19 @@ -use crate::research::{JournalKind, SourceId, WorkType}; +use crate::research::*; use chrono::NaiveDate; use serde::Deserialize; use uuid::Uuid; use validator::Validate; +pub struct NewAuthorship { + pub work_id: WorkId, + pub orcid: String, + pub name: String, + pub is_external: bool, + pub is_corresponding: bool, + pub affiliations: Vec, + pub position: AuthorshipPosition, +} + pub struct NewWork { pub openalex_id: String, pub title: String, diff --git a/apps/server/src/research/works/entity.rs b/apps/server/src/research/works/entity.rs index bfad53e..b5b77c0 100644 --- a/apps/server/src/research/works/entity.rs +++ b/apps/server/src/research/works/entity.rs @@ -1,4 +1,5 @@ use crate::{ + academic::AcademicId, research::{Source, SourceId}, shared::model_id, }; @@ -9,6 +10,34 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; use toasty::{Deferred, Embed, Model}; +#[derive(Debug, Clone, Copy, Embed, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +#[column(rename_all = "lowercase")] +pub enum AuthorshipPosition { + First, + Middle, + Last, +} + +#[derive(Debug, Clone, Serialize, Model)] +#[key(work_id, orcid)] +#[serde(rename_all = "camelCase")] +pub struct Authorship { + pub work_id: WorkId, + pub orcid: String, + pub name: String, + pub is_external: bool, + pub is_corresponding: bool, + pub affiliations: Vec, + pub position: AuthorshipPosition, + + #[index] + pub academic_id: Option, + + #[belongs_to] + pub work: Deferred, +} + #[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] pub struct Work { @@ -29,18 +58,20 @@ pub struct Work { pub is_published: bool, #[index] - pub primary_source_id: Option, + pub source_id: Option, #[column(type = "jsonb")] pub overrides: serde_json::Value, - #[has_one] - pub primary_source: Deferred>, + #[belongs_to] + pub source: Deferred>, + + #[has_many] + pub authorships: Deferred>, } model_id! { - struct WorkId, - key: "work" + struct WorkId, key: "work" } #[derive(Debug, Clone, Copy, Embed, Serialize, Deserialize, Eq, PartialEq)] From 0d3cb563d01aa5bb99aa5565063e335d123f86e7 Mon Sep 17 00:00:00 2001 From: MrRevillod Date: Tue, 28 Jul 2026 00:09:54 -0400 Subject: [PATCH 7/7] feat: migrate work repository to orm based queries --- Cargo.lock | 32 +++ apps/server/Cargo.toml | 1 + .../src/research/classification/controller.rs | 11 +- .../src/research/classification/dtos.rs | 40 +-- .../src/research/classification/entity.rs | 84 +++--- .../src/research/classification/repository.rs | 86 +++---- apps/server/src/research/sources/mod.rs | 6 +- apps/server/src/research/works/dtos.rs | 6 +- apps/server/src/research/works/entity.rs | 101 +++++++- apps/server/src/research/works/repository.rs | 240 +++++------------- .../server/src/research/works/services/mod.rs | 65 ++--- apps/server/src/research/works/views.rs | 51 +++- 12 files changed, 387 insertions(+), 336 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e805848..5b7a103 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "jiff", "jsonwebtoken", "lettre", + "o2o", "papers-openalex", "parking_lot", "regex", @@ -3043,6 +3044,37 @@ dependencies = [ "libc", ] +[[package]] +name = "o2o" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a0d8296bd43ca3f7f8331de606fd191a673c7fdee3ac391a0d493d3d9e11c7" +dependencies = [ + "o2o-impl", + "o2o-macros", +] + +[[package]] +name = "o2o-impl" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6113fd04ff48f6cedcb359b1a148c02372581931eaae0c81f86f31c22770486" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "o2o-macros" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69c3374e5d85ab5641278683e366e5a786f00cf387ccf1328d4067f72b170d" +dependencies = [ + "o2o-impl", + "syn 1.0.109", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index fcdf014..66cdabe 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -56,3 +56,4 @@ 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" diff --git a/apps/server/src/research/classification/controller.rs b/apps/server/src/research/classification/controller.rs index eaa697e..77afff8 100644 --- a/apps/server/src/research/classification/controller.rs +++ b/apps/server/src/research/classification/controller.rs @@ -1,3 +1,4 @@ +use crate::research; use crate::research::classification::*; use std::sync::Arc; use sword::prelude::*; @@ -10,7 +11,7 @@ pub struct WorksClassificationController { impl WorksClassificationController { #[get("/domains")] - pub async fn get_domains(&self, req: Request) -> WebResult> { + pub async fn get_domains(&self, req: Request) -> WebResult> { let query = req.query_validator::()?; let filter = ClassificationFilter::from(query.unwrap_or_default()); @@ -18,7 +19,7 @@ impl WorksClassificationController { } #[get("/fields")] - pub async fn get_fields(&self, req: Request) -> WebResult> { + pub async fn get_fields(&self, req: Request) -> WebResult> { let query = req.query_validator::()?; let filter = ClassificationFilter::from(query.unwrap_or_default()); @@ -26,7 +27,7 @@ impl WorksClassificationController { } #[get("/subfields")] - pub async fn get_subfields(&self, req: Request) -> WebResult> { + pub async fn get_subfields(&self, req: Request) -> WebResult> { let query = req.query_validator::()?; let filter = ClassificationFilter::from(query.unwrap_or_default()); @@ -34,7 +35,7 @@ impl WorksClassificationController { } #[get("/topics")] - pub async fn get_topics(&self, req: Request) -> WebResult> { + pub async fn get_topics(&self, req: Request) -> WebResult> { let query = req.query_validator::()?; let filter = ClassificationFilter::from(query.unwrap_or_default()); @@ -42,7 +43,7 @@ impl WorksClassificationController { } #[get("/keywords")] - pub async fn get_keywords(&self, req: Request) -> WebResult> { + pub async fn get_keywords(&self, req: Request) -> WebResult> { let query = req.query_validator::()?; let filter = ClassificationFilter::from(query.unwrap_or_default()); diff --git a/apps/server/src/research/classification/dtos.rs b/apps/server/src/research/classification/dtos.rs index 0a852e7..4025836 100644 --- a/apps/server/src/research/classification/dtos.rs +++ b/apps/server/src/research/classification/dtos.rs @@ -5,10 +5,10 @@ use validator::Validate; #[derive(Debug, Clone, Default, Deserialize, Validate)] pub struct WorkClassificationQueryDto { - pub domain_id: Option, - pub field_id: Option, - pub subfield_id: Option, - pub topic_id: Option, + pub domain_id: Option, + pub field_id: Option, + pub subfield_id: Option, + pub topic_id: Option, #[validate(length(min = 1, max = 255))] pub openalex_id: Option, @@ -19,21 +19,21 @@ pub struct WorkClassificationQueryDto { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ResearchTopicView { - pub topic_id: ResearchTopicId, +pub struct TopicView { + pub topic_id: TopicId, pub name: String, pub score: f64, - pub subfield_id: ResearchSubfieldId, + pub subfield_id: SubfieldId, pub subfield_name: String, - pub field_id: ResearchFieldId, + pub field_id: FieldId, pub field_name: String, - pub domain_id: ResearchDomainId, + pub domain_id: DomainId, pub domain_name: String, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ResearchKeywordView { +pub struct KeywordView { pub keyword_id: Uuid, pub name: String, pub score: f64, @@ -55,27 +55,13 @@ impl From for ClassificationFilter { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateMappingBody { - pub subfield_id: ResearchSubfieldId, + pub subfield_id: SubfieldId, pub research_line_id: ResearchLineId, } -impl - From<( - ResearchTopic, - ResearchSubfield, - ResearchField, - ResearchDomain, - f64, - )> for ResearchTopicView -{ +impl From<(Topic, Subfield, Field, Domain, f64)> for TopicView { fn from( - (topic, subfield, field, domain, score): ( - ResearchTopic, - ResearchSubfield, - ResearchField, - ResearchDomain, - f64, - ), + (topic, subfield, field, domain, score): (Topic, Subfield, Field, Domain, f64), ) -> Self { Self { topic_id: topic.id, diff --git a/apps/server/src/research/classification/entity.rs b/apps/server/src/research/classification/entity.rs index 06c3b4b..f0efcdf 100644 --- a/apps/server/src/research/classification/entity.rs +++ b/apps/server/src/research/classification/entity.rs @@ -1,14 +1,17 @@ -use crate::shared::model_id; +use crate::{ + research::{Work, WorkKeyWordScore, WorkTopicScore}, + shared::model_id, +}; use bon::Builder; use serde::Serialize; use toasty::{Deferred, Model}; #[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] -pub struct ResearchDomain { +pub struct Domain { #[key] - #[builder(default = ResearchDomainId::new())] - pub id: ResearchDomainId, + #[builder(default)] + pub id: DomainId, pub name: String, #[unique] @@ -17,94 +20,107 @@ pub struct ResearchDomain { #[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] -pub struct ResearchField { +pub struct Field { #[key] - #[builder(default = ResearchFieldId::new())] - pub id: ResearchFieldId, + #[builder(default)] + pub id: FieldId, pub name: String, #[unique] pub openalex_id: String, #[index] - pub domain_id: ResearchDomainId, + pub domain_id: DomainId, #[belongs_to] - pub domain: Deferred, + pub domain: Domain, } #[derive(Debug, Clone, Serialize, Model, Builder)] pub struct ResearchLine { #[key] - #[builder(default = ResearchLineId::new())] + #[builder(default)] pub id: ResearchLineId, pub name: String, pub slug: String, #[has_many] - pub subfields: Deferred>, + #[serde(skip)] + pub subfields: Deferred>, } #[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] -pub struct ResearchSubfield { +pub struct Subfield { #[key] - #[builder(default = ResearchSubfieldId::new())] - pub id: ResearchSubfieldId, + #[builder(default)] + pub id: SubfieldId, pub name: String, #[unique] pub openalex_id: String, #[index] - pub field_id: ResearchFieldId, + pub field_id: FieldId, #[index] pub research_line_id: Option, #[belongs_to] - pub field: Deferred, + pub field: Field, #[belongs_to] - pub research_line: Deferred>, + pub research_line: Option, } #[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] -pub struct ResearchTopic { +pub struct Topic { #[key] - #[builder(default = ResearchTopicId::new())] - pub id: ResearchTopicId, + #[builder(default)] + pub id: TopicId, #[unique] pub openalex_id: String, pub name: String, #[index] - pub subfield_id: ResearchSubfieldId, + pub subfield_id: SubfieldId, #[belongs_to] - pub subfield: Deferred, + pub subfield: Subfield, + + #[has_many] + pub work_scores: Deferred>, + + #[has_many(via = work_scores.work)] + pub works: Deferred>, } #[derive(Debug, Clone, Serialize, Model, Builder)] #[serde(rename_all = "camelCase")] -pub struct ResearchKeyword { +pub struct Keyword { #[key] - #[builder(default = ResearchKeywordId::new())] - pub id: ResearchKeywordId, + #[builder(default)] + pub id: KeywordId, #[unique] pub openalex_id: String, pub name: String, + + #[has_many] + pub work_scores: Deferred>, + + #[has_many(via = work_scores.work)] + pub works: Deferred>, } #[allow(dead_code)] pub struct ClassificationFilter { - pub domain_id: Option, - pub field_id: Option, - pub subfield_id: Option, - pub topic_id: Option, + pub domain_id: Option, + pub field_id: Option, + pub subfield_id: Option, + pub topic_id: Option, pub openalex_id: Option, pub search: Option, } @@ -114,20 +130,20 @@ model_id! { } model_id! { - struct ResearchDomainId, key: "research_domain" + struct DomainId, key: "domain" } model_id! { - struct ResearchFieldId, key: "research_field" + struct FieldId, key: "field" } model_id! { - struct ResearchSubfieldId, key: "research_subfield" + struct SubfieldId, key: "subfield" } model_id! { - struct ResearchTopicId, key: "research_topic" + struct TopicId, key: "topic" } model_id! { - struct ResearchKeywordId, key: "research_keyword" + struct KeywordId, key: "keyword" } diff --git a/apps/server/src/research/classification/repository.rs b/apps/server/src/research/classification/repository.rs index 08c25c5..5f370b9 100644 --- a/apps/server/src/research/classification/repository.rs +++ b/apps/server/src/research/classification/repository.rs @@ -10,15 +10,15 @@ pub struct WorkClassificationRepository { } impl WorkClassificationRepository { - pub async fn list_domains(&self, f: ClassificationFilter) -> AppResult> { - let mut query = ResearchDomain::all(); + pub async fn list_domains(&self, f: ClassificationFilter) -> AppResult> { + let mut query = Domain::all(); if let Some(s) = f.search { let pattern = format!("%{}%", s.trim()); - query = query.filter(ResearchDomain::fields().name().ilike(pattern)) + query = query.filter(Domain::fields().name().ilike(pattern)) } - query = query.order_by(ResearchDomain::fields().name().asc()); + query = query.order_by(Domain::fields().name().asc()); query .exec(&mut self.database.pool()) @@ -26,78 +26,76 @@ impl WorkClassificationRepository { .map_err(AppError::from) } - pub async fn list_fields(&self, f: ClassificationFilter) -> AppResult> { - let mut fields = ResearchField::all(); + pub async fn list_fields(&self, f: ClassificationFilter) -> AppResult> { + let mut fields = Field::all(); if let Some(domain_id) = f.domain_id { - fields = fields.filter(ResearchField::fields().domain_id().eq(domain_id)); + fields = fields.filter(Field::fields().domain_id().eq(domain_id)); } if let Some(s) = f.search { let pattern = format!("%{}%", s.trim()); - fields = fields.filter(ResearchField::fields().name().ilike(pattern)) + fields = fields.filter(Field::fields().name().ilike(pattern)) } fields - .order_by(ResearchField::fields().name().asc()) + .order_by(Field::fields().name().asc()) .exec(&mut self.database.pool()) .await .map_err(AppError::from) } - pub async fn list_subfields( - &self, - f: ClassificationFilter, - ) -> AppResult> { - let mut subfields = ResearchSubfield::all(); + pub async fn list_subfields(&self, f: ClassificationFilter) -> AppResult> { + let mut subfields = Subfield::all(); if let Some(field_id) = f.field_id { - subfields = subfields.filter(ResearchSubfield::fields().field_id().eq(field_id)); + subfields = subfields.filter(Subfield::fields().field_id().eq(field_id)); } if let Some(s) = f.search { - let pattern = format!("%{}%", s.trim()); - subfields = subfields.filter(ResearchSubfield::fields().name().ilike(pattern)) + subfields = subfields.filter(Subfield::fields().name().ilike(format!("%{}%", s.trim()))) } subfields - .order_by(ResearchSubfield::fields().name().asc()) + .order_by(Subfield::fields().name().asc()) .limit(50) .exec(&mut self.database.pool()) .await .map_err(AppError::from) } - pub async fn list_topics(&self, f: ClassificationFilter) -> AppResult> { - let mut topics = ResearchTopic::all(); + pub async fn list_topics(&self, f: ClassificationFilter) -> AppResult> { + let mut topics = Topic::all(); if let Some(subfield_id) = f.subfield_id { - topics = topics.filter(ResearchTopic::fields().subfield_id().eq(subfield_id)); + topics = topics.filter(Topic::fields().subfield_id().eq(subfield_id)); } if let Some(s) = f.search { - let pattern = format!("%{}%", s.trim()); - topics = topics.filter(ResearchTopic::fields().name().ilike(pattern)); + topics = topics.filter(Topic::fields().name().ilike(format!("%{}%", s.trim()))); } topics - .order_by(ResearchTopic::fields().name().asc()) + .order_by(Topic::fields().name().asc()) .limit(50) .exec(&mut self.database.pool()) .await .map_err(AppError::from) } - pub async fn list_keywords(&self, f: ClassificationFilter) -> AppResult> { - let mut query = ResearchKeyword::all(); + pub async fn list_keywords(&self, f: ClassificationFilter) -> AppResult> { + let mut query = Keyword::all(); if let Some(search) = f.search { - let pattern = format!("%{}%", search.trim()); - query = query.filter(ResearchKeyword::fields().name().ilike(pattern)); + query = query.filter( + Keyword::fields() + .name() + .ilike(format!("%{}%", search.trim())), + ); } query - .order_by(ResearchKeyword::fields().name().asc()) + .order_by(Keyword::fields().name().asc()) .limit(50) .exec(&mut self.database.pool()) .await @@ -113,11 +111,8 @@ impl WorkClassificationRepository { .map_err(AppError::from) } - pub async fn find_subfield_by_id( - &self, - id: &ResearchSubfieldId, - ) -> AppResult> { - ResearchSubfield::filter_by_id(id) + pub async fn find_subfield_by_id(&self, id: &SubfieldId) -> AppResult> { + Subfield::filter_by_id(id) .first() .exec(&mut self.database.pool()) .await @@ -135,19 +130,16 @@ impl WorkClassificationRepository { .map_err(AppError::from) } - pub async fn find_topic_by_openalex_id( - &self, - openalex_id: &str, - ) -> AppResult> { - ResearchTopic::filter(ResearchTopic::fields().openalex_id().eq(openalex_id)) + pub async fn find_topic_by_openalex_id(&self, openalex_id: &str) -> AppResult> { + Topic::filter(Topic::fields().openalex_id().eq(openalex_id)) .first() .exec(&mut self.database.pool()) .await .map_err(AppError::from) } - pub async fn save_keyword(&self, keyword: &ResearchKeyword) -> AppResult<()> { - ResearchKeyword::upsert_by_openalex_id(&keyword.openalex_id) + pub async fn save_keyword(&self, keyword: &Keyword) -> AppResult<()> { + Keyword::upsert_by_openalex_id(&keyword.openalex_id) .id(&keyword.id) .name(&keyword.name) .exec(&mut self.database.pool()) @@ -156,8 +148,8 @@ impl WorkClassificationRepository { Ok(()) } - pub async fn save_subfield(&self, subfield: &ResearchSubfield) -> AppResult<()> { - ResearchSubfield::upsert_by_openalex_id(&subfield.openalex_id) + pub async fn save_subfield(&self, subfield: &Subfield) -> AppResult<()> { + Subfield::upsert_by_openalex_id(&subfield.openalex_id) .id(&subfield.id) .name(&subfield.name) .field_id(&subfield.field_id) @@ -168,16 +160,16 @@ impl WorkClassificationRepository { Ok(()) } - pub async fn unknown_keyword_id(&self) -> AppResult> { - ResearchKeyword::filter(ResearchKeyword::fields().openalex_id().eq("unknown")) + pub async fn unknown_keyword_id(&self) -> AppResult> { + Keyword::filter(Keyword::fields().openalex_id().eq("unknown")) .first() .exec(&mut self.database.pool()) .await .map_err(AppError::from) } - pub async fn unknown_topic_id(&self) -> AppResult> { - ResearchTopic::filter(ResearchTopic::fields().openalex_id().eq("unknown")) + pub async fn unknown_topic_id(&self) -> AppResult> { + Topic::filter(Topic::fields().openalex_id().eq("unknown")) .first() .exec(&mut self.database.pool()) .await diff --git a/apps/server/src/research/sources/mod.rs b/apps/server/src/research/sources/mod.rs index f98a569..a328102 100644 --- a/apps/server/src/research/sources/mod.rs +++ b/apps/server/src/research/sources/mod.rs @@ -16,7 +16,7 @@ model_id! { #[serde(rename_all = "camelCase")] pub struct Source { #[key] - #[builder(default = SourceId::new())] + #[builder(default)] pub id: SourceId, #[unique] @@ -26,9 +26,11 @@ pub struct Source { pub issn: Vec, #[has_one] - pub journal_issn: Deferred>, + pub journal_info: Option, #[has_many] + #[serde(skip)] + #[builder(default)] pub works: Deferred>, } diff --git a/apps/server/src/research/works/dtos.rs b/apps/server/src/research/works/dtos.rs index f1c2ee4..da72843 100644 --- a/apps/server/src/research/works/dtos.rs +++ b/apps/server/src/research/works/dtos.rs @@ -1,4 +1,4 @@ -use crate::research::*; +use crate::{academic::AcademicId, research::*}; use chrono::NaiveDate; use serde::Deserialize; use uuid::Uuid; @@ -31,7 +31,7 @@ pub struct NewWork { #[derive(Debug, Deserialize, Validate, Default)] #[serde(rename_all = "camelCase")] pub struct GetWorksQuery { - pub academic_id: Option, + pub academic_id: Option, pub search: Option, #[validate(range(min = 1900, max = 2100))] @@ -45,7 +45,7 @@ pub struct GetWorksQuery { pub department_id: Option, pub career_id: Option, pub journal_kind: Option, - pub research_line_id: Option, + pub research_line_id: Option, #[validate(range(min = 1, max = 1000))] pub size: Option, diff --git a/apps/server/src/research/works/entity.rs b/apps/server/src/research/works/entity.rs index b5b77c0..48ff611 100644 --- a/apps/server/src/research/works/entity.rs +++ b/apps/server/src/research/works/entity.rs @@ -1,9 +1,4 @@ -use crate::{ - academic::AcademicId, - research::{Source, SourceId}, - shared::model_id, -}; - +use crate::{academic::AcademicId, research::*, shared::model_id}; use bon::Builder; use jiff::civil::Date; use serde::{Deserialize, Serialize}; @@ -35,6 +30,7 @@ pub struct Authorship { pub academic_id: Option, #[belongs_to] + #[serde(skip)] pub work: Deferred, } @@ -48,7 +44,7 @@ pub struct Work { #[unique] pub openalex_id: String, pub title: String, - pub r#abstract: Option, + pub abstract_text: Option, pub doi: Option, pub publication_date: Option, pub publication_year: Option, @@ -64,10 +60,99 @@ pub struct Work { pub overrides: serde_json::Value, #[belongs_to] - pub source: Deferred>, + pub source: Option, #[has_many] pub authorships: Deferred>, + + #[has_many] + #[serde(skip)] + pub topic_scores: Deferred>, + + #[serde(skip)] + #[has_many(via = topic_scores.topic)] + pub topics: Deferred>, + + #[has_many] + #[serde(skip)] + pub keyword_scores: Deferred>, + + #[serde(skip)] + #[has_many(via = keyword_scores.keyword)] + pub keywords: Deferred>, +} + +#[derive(Debug, Clone, Serialize, Model)] +#[key(work_id, topic_id)] +pub struct WorkTopicScore { + #[index] + pub work_id: WorkId, + + #[index] + pub topic_id: TopicId, + pub score: f64, + + #[belongs_to(key = work_id)] + pub work: Deferred, + + #[belongs_to(key = topic_id)] + pub topic: Topic, +} + +#[derive(Debug, Clone, Serialize, Model)] +#[key(work_id, keyword_id)] +pub struct WorkKeyWordScore { + #[index] + pub work_id: WorkId, + + #[index] + pub keyword_id: KeywordId, + pub score: f64, + + #[belongs_to(key = work_id)] + pub work: Deferred, + + #[belongs_to(key = keyword_id)] + pub keyword: Keyword, +} + +impl Work { + pub fn resolve_overrides(&mut self) { + // pub title: Option>, + // pub r#abstract: Option>, + // pub doi: Option>, + // pub publication_year: Option>, + // pub is_accepted: Option>, + // pub is_published: Option>, + + if let Some(title) = self.overrides.get("title").and_then(|v| v.as_str()) { + self.title = title.to_string(); + } + + if let Some(abstract_text) = self.overrides.get("abstract").and_then(|v| v.as_str()) { + self.abstract_text = Some(abstract_text.to_string()); + } + + if let Some(doi) = self.overrides.get("doi").and_then(|v| v.as_str()) { + self.doi = Some(doi.to_string()); + } + + if let Some(publication_year) = self + .overrides + .get("publication_year") + .and_then(|v| v.as_i64()) + { + self.publication_year = Some(publication_year as i16); + } + + if let Some(is_accepted) = self.overrides.get("is_accepted").and_then(|v| v.as_bool()) { + self.is_accepted = is_accepted; + } + + if let Some(is_published) = self.overrides.get("is_published").and_then(|v| v.as_bool()) { + self.is_published = is_published; + } + } } model_id! { diff --git a/apps/server/src/research/works/repository.rs b/apps/server/src/research/works/repository.rs index ab26150..2e25bd4 100644 --- a/apps/server/src/research/works/repository.rs +++ b/apps/server/src/research/works/repository.rs @@ -1,10 +1,8 @@ use crate::research::*; -use crate::shared::{AppResult, Database}; +use crate::shared::{AppError, AppResult, Database}; -use sqlx::{QueryBuilder, Row}; use std::sync::Arc; use sword::prelude::*; -use uuid::Uuid; #[injectable] pub struct WorksRepository { @@ -13,218 +11,108 @@ pub struct WorksRepository { impl WorksRepository { pub async fn find_by_id(&self, id: &WorkId) -> AppResult> { - sqlx::query_as::<_, Work>( - "SELECT w.id, w.openalex_id, w.title, w.abstract, w.doi, - w.publication_date, w.publication_year, w.ty, w.lang, w.is_accepted, - w.is_published, w.primary_source_id, w.overrides, ji.kind::text AS journal_kind, - rl.id AS research_line_id, rl.name AS research_line_name, rl.slug AS research_line_slug - FROM works w - LEFT JOIN sources src ON w.primary_source_id = src.id - LEFT JOIN journal_issn ji ON ji.id = src.journal_issn_id - LEFT JOIN LATERAL ( - SELECT r.id, r.name, r.slug - FROM work_topics wt - JOIN research_topics rt ON rt.id = wt.topic_id - JOIN research_subfields rs ON rs.id = rt.subfield_id - LEFT JOIN research_line_mappings rlm ON rlm.subfield_openalex_id = rs.openalex_id - LEFT JOIN work_research_line_overrides o ON o.work_id = w.id - JOIN research_lines r ON r.id = COALESCE(o.research_line_id, rlm.research_line_id) - WHERE wt.work_id = w.id - ORDER BY wt.score DESC - LIMIT 1 - ) rl ON TRUE - WHERE w.id = $1", - ) - .bind(id) - .fetch_optional(self.database.pool()) - .await - .map_err(Into::into) + Work::filter_by_id(id) + .include(Work::fields().authorships()) + .include(Work::fields().topic_scores()) + .include(Work::fields().topics()) + .include(Work::fields().keyword_scores()) + .include(Work::fields().keywords()) + .first() + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from) } pub async fn list(&self, query: &GetWorksQuery) -> AppResult> { - let mut qb = QueryBuilder::new( - "SELECT DISTINCT w.id, w.openalex_id, w.title, w.abstract, - w.doi, w.publication_date, w.publication_year, w.ty, w.lang, w.is_accepted, - w.is_published, w.primary_source_id, w.overrides, - ji.kind::text AS journal_kind, - rl.id AS research_line_id, rl.name AS research_line_name, rl.slug AS research_line_slug - FROM works w - LEFT JOIN work_authorships wa ON w.id = wa.work_id - LEFT JOIN sources src ON w.primary_source_id = src.id - LEFT JOIN journal_issn ji ON ji.id = src.journal_issn_id - LEFT JOIN LATERAL ( - SELECT r.id, r.name, r.slug - FROM work_topics wt - JOIN research_topics rt ON rt.id = wt.topic_id - JOIN research_subfields rs ON rs.id = rt.subfield_id - LEFT JOIN research_line_mappings rlm ON rlm.subfield_openalex_id = rs.openalex_id - LEFT JOIN work_research_line_overrides o ON o.work_id = w.id - JOIN research_lines r ON r.id = COALESCE(o.research_line_id, rlm.research_line_id) - WHERE wt.work_id = w.id - ORDER BY wt.score DESC - LIMIT 1 - ) rl ON TRUE - WHERE TRUE", - ); + let mut works = Work::all() + .include(Work::fields().topic_scores()) + .include(Work::fields().keyword_scores()) + .order_by(Work::fields().publication_year().desc()); if let Some(academic_id) = query.academic_id { - qb.push(" AND wa.work_id IN (SELECT wa2.work_id FROM work_authorships wa2 JOIN academics a ON a.orcid = wa2.orcid WHERE wa2.is_external = false AND a.orcid != 'https://orcid.org/0000-0000-0000-0000' AND a.id = "); - qb.push_bind(academic_id); - qb.push(")"); - } - - if let Some(department_id) = query.department_id { - qb.push(" AND wa.work_id IN (SELECT wa2.work_id FROM work_authorships wa2 JOIN academics a ON a.orcid = wa2.orcid WHERE wa2.is_external = false AND a.orcid != 'https://orcid.org/0000-0000-0000-0000' AND a.department_id = "); - qb.push_bind(department_id); - qb.push(")"); - } - - if let Some(career_id) = query.career_id { - qb.push(" AND wa.work_id IN (SELECT wa2.work_id FROM work_authorships wa2 JOIN academics a ON a.orcid = wa2.orcid WHERE wa2.is_external = false AND a.orcid != 'https://orcid.org/0000-0000-0000-0000' AND a.career_id = "); - qb.push_bind(career_id); - qb.push(")"); + works = works.filter( + Work::fields().authorships().any( + Authorship::fields() + .academic_id() + .eq(academic_id) + .and(Authorship::fields().is_external().eq(false)), + ), + ); } if let Some(ref search) = query.search { - qb.push(" AND w.title ILIKE "); - qb.push_bind(format!("%{}%", search)); + works = works.filter(Work::fields().title().like(format!("%{}%", search))); } if let Some(year_from) = query.year_from { - qb.push(" AND w.publication_year >= "); - qb.push_bind(year_from); + works = works.filter(Work::fields().publication_year().ge(year_from)); } if let Some(year_to) = query.year_to { - qb.push(" AND w.publication_year <= "); - qb.push_bind(year_to); + works = works.filter(Work::fields().publication_year().le(year_to)); } if let Some(is_accepted) = query.is_accepted { - qb.push(" AND w.is_accepted = "); - qb.push_bind(is_accepted); + works = works.filter(Work::fields().is_accepted().eq(is_accepted)); } if let Some(is_published) = query.is_published { - qb.push(" AND w.is_published = "); - qb.push_bind(is_published); + works = works.filter(Work::fields().is_published().eq(is_published)); } - if let Some(ref journal_kind) = query.journal_kind { - qb.push(" AND (ji.kind = "); - qb.push_bind(journal_kind); - qb.push("::journal_kind)"); - } + works = works + .order_by(Work::fields().publication_year().desc()) + .order_by(Work::fields().publication_date().desc()); - if let Some(research_line_id) = query.research_line_id { - qb.push(" AND rl.id = "); - qb.push_bind(research_line_id); + if let Some(size) = query.size { + works = works.limit(size as usize); } - qb.push( - " ORDER BY w.publication_year DESC NULLS LAST, w.publication_date DESC NULLS LAST, w.id LIMIT ", - ); - - qb.push_bind(query.size.unwrap_or(100) as i64); - - qb.build_query_as() - .fetch_all(self.database.pool()) + works + .exec(&mut self.database.pool()) .await - .map_err(Into::into) - } - - pub async fn upsert_work(&self, work: &NewWork) -> AppResult<(WorkId, bool)> { - let row = sqlx::query( - "INSERT INTO works (openalex_id, title, abstract, doi, publication_date, publication_year, ty, lang, is_accepted, is_published, primary_source_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ON CONFLICT (openalex_id) DO UPDATE SET title = EXCLUDED.title, abstract = EXCLUDED.abstract, doi = EXCLUDED.doi, publication_date = EXCLUDED.publication_date, publication_year = EXCLUDED.publication_year, ty = EXCLUDED.ty, lang = EXCLUDED.lang, is_accepted = EXCLUDED.is_accepted, is_published = EXCLUDED.is_published, primary_source_id = EXCLUDED.primary_source_id RETURNING id, (xmax = 0) AS was_inserted", - ) - .bind(&work.openalex_id) - .bind(&work.title) - .bind(&work.abstract_text) - .bind(&work.doi) - .bind(work.publication_date) - .bind(work.publication_year) - .bind(work.ty) - .bind(&work.lang) - .bind(work.is_accepted) - .bind(work.is_published) - .bind(work.primary_source_id) - .fetch_one(self.database.pool()) - .await?; - - let id = WorkId::from_uuid(row.get::("id")); - let was_inserted: bool = row.get("was_inserted"); - Ok((id, was_inserted)) + .map_err(AppError::from) } - pub async fn apply_overrides_sync(&self, work_id: &WorkId) -> AppResult<()> { - sqlx::query( - "UPDATE works SET - title = COALESCE(overrides->>'title', title), - doi = COALESCE(overrides->>'doi', doi), - is_accepted = COALESCE((overrides->>'is_accepted')::boolean, is_accepted), - is_published = COALESCE((overrides->>'is_published')::boolean, is_published), - publication_year = COALESCE((overrides->>'publication_year')::smallint, publication_year) - WHERE id = $1 AND overrides != '{}'::jsonb", - ) - .bind(work_id) - .execute(self.database.pool()) - .await?; + pub async fn save(&self, work: &Work) -> AppResult<()> { + Work::upsert_by_id(work.id) + .title(&work.title) + .abstract_text(&work.abstract_text) + .doi(&work.doi) + .publication_date(work.publication_date) + .publication_year(work.publication_year) + .ty(work.ty) + .lang(&work.lang) + .is_accepted(work.is_accepted) + .is_published(work.is_published) + .source_id(work.source_id) + .overrides(&work.overrides) + .exec(&mut self.database.pool()) + .await + .map_err(AppError::from); Ok(()) } - pub async fn update_overrides( - &self, - work_id: &WorkId, - overrides: &serde_json::Value, - ) -> AppResult<()> { - sqlx::query("UPDATE works SET overrides = $1 WHERE id = $2") - .bind(overrides) - .bind(work_id) - .execute(self.database.pool()) + pub async fn link_topic(&self, work_topic_score: &WorkTopicScore) -> AppResult<()> { + WorkTopicScore::create() + .work_id(work_topic_score.work_id) + .topic_id(work_topic_score.topic_id) + .score(work_topic_score.score) + .exec(&mut self.database.pool()) .await?; Ok(()) } - pub async fn clear_overrides(&self, work_id: &WorkId) -> AppResult<()> { - sqlx::query("UPDATE works SET overrides = '{}'::jsonb WHERE id = $1") - .bind(work_id) - .execute(self.database.pool()) + pub async fn link_keyword(&self, work_keyword_score: &WorkKeyWordScore) -> AppResult<()> { + WorkKeyWordScore::create() + .work_id(work_keyword_score.work_id) + .keyword_id(work_keyword_score.keyword_id) + .score(work_keyword_score.score) + .exec(&mut self.database.pool()) .await?; Ok(()) } - - pub async fn link_topic(&self, work_id: &WorkId, topic_id: Uuid, score: f64) -> AppResult<()> { - sqlx::query( - "INSERT INTO work_topics (work_id, topic_id, score) VALUES ($1, $2, $3) ON CONFLICT (work_id, topic_id) DO NOTHING", - ) - .bind(work_id) - .bind(topic_id) - .bind(score) - .execute(self.database.pool()) - .await?; - - Ok(()) - } - - pub async fn link_keyword( - &self, - work_id: &WorkId, - keyword_id: Uuid, - score: f64, - ) -> AppResult<()> { - sqlx::query( - "INSERT INTO work_keywords (work_id, keyword_id, score) - VALUES ($1, $2, $3) ON CONFLICT (work_id, keyword_id) DO NOTHING", - ) - .bind(work_id) - .bind(keyword_id) - .bind(score) - .execute(self.database.pool()) - .await?; - - Ok(()) - } } diff --git a/apps/server/src/research/works/services/mod.rs b/apps/server/src/research/works/services/mod.rs index dbc1f0b..6df8c7b 100644 --- a/apps/server/src/research/works/services/mod.rs +++ b/apps/server/src/research/works/services/mod.rs @@ -8,6 +8,7 @@ pub use openalex::*; use html_escape::decode_html_entities; use papers_openalex::Work as OpenAlexWork; +use std::cmp::Ordering; use std::str::FromStr; use std::sync::Arc; use sword::prelude::*; @@ -17,7 +18,6 @@ use uuid::Uuid; pub struct WorksService { works: Arc, sources: Arc, - authorships: Arc, classification: Arc, academics: Arc, openalex: Arc, @@ -28,31 +28,34 @@ impl WorksService { self.works.list(query).await } - pub async fn find_by_id(&self, id: WorkId) -> AppResult { - let Some(work) = self.works.find_by_id(&id).await? else { + pub async fn find_by_id(&self, id: WorkId) -> AppResult { + let Some(mut work) = self.works.find_by_id(&id).await? else { return Err(WorksError::NotFound)?; }; - let resolved = work.resolve(); - let source = match resolved.primary_source_id { - Some(sid) => self.sources.find_source_view_by_id(&sid).await?, - None => None, - }; + let topics = work.topic_scores.get().clone(); - let authorships = self.authorships.list(&resolved.id).await?; - let topics = self - .classification - .list_topics_by_work(&resolved.id) - .await?; - let keywords = self - .classification - .list_keywords_by_work(&resolved.id) - .await?; + let max_score_research_line = topics + .iter() + .max_by(|a, b| a.score.partial_cmp(&b.score).unwrap_or(Ordering::Equal)) + .map(|score| score.topic.subfield.research_line.clone()) + .flatten(); - Ok(WorkDetailView { - work: resolved, - source, - authorships, + let topics = topics.into_iter().map(WorkTopicView::from).collect(); + + let keywords = work + .keyword_scores + .get() + .clone() + .into_iter() + .map(WorkKeywordView::from) + .collect(); + + work.resolve_overrides(); + + Ok(WorkView { + work, + research_line: max_score_research_line, topics, keywords, }) @@ -224,7 +227,9 @@ impl WorksService { { let source_ty = s.r#type.clone().unwrap_or_else(|| "unknown".to_string()); let mut normalized_issns: Vec = s.issn.as_ref().map_or_else(Vec::new, |vec| { - vec.iter().filter_map(|v| Source::normalize_issn(v)).collect() + vec.iter() + .filter_map(|v| Source::normalize_issn(v)) + .collect() }); if let Some(issn_l) = s.issn_l.as_deref().and_then(Source::normalize_issn) { if !normalized_issns.contains(&issn_l) { @@ -238,13 +243,15 @@ impl WorksService { }; Some( self.sources - .save(&Source::builder() - .id(SourceId::new()) - .openalex_id(s.id.clone().unwrap_or_default()) - .display_name(s.display_name.clone().unwrap_or_default()) - .ty(source_ty) - .issn(issn) - .build()) + .save( + &Source::builder() + .id(SourceId::new()) + .openalex_id(s.id.clone().unwrap_or_default()) + .display_name(s.display_name.clone().unwrap_or_default()) + .ty(source_ty) + .issn(issn) + .build(), + ) .await?, ) } else { diff --git a/apps/server/src/research/works/views.rs b/apps/server/src/research/works/views.rs index 09cd31f..81b7b03 100644 --- a/apps/server/src/research/works/views.rs +++ b/apps/server/src/research/works/views.rs @@ -1,15 +1,56 @@ use crate::research::*; +use o2o::o2o as FromImpl; use serde::Serialize; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct WorkDetailView { +pub struct WorkView { #[serde(flatten)] pub work: Work, - pub source: Option, - pub authorships: Vec, - pub topics: Vec, - pub keywords: Vec, + pub research_line: Option, + pub topics: Vec, + pub keywords: Vec, +} + +#[derive(Debug, Serialize, FromImpl)] +#[from_owned(WorkTopicScore)] +#[serde(rename_all = "camelCase")] +pub struct WorkTopicView { + pub topic_id: TopicId, + + #[from(@.topic.name)] + pub name: String, + pub score: f64, + + #[from(@.topic.subfield_id)] + pub subfield_id: SubfieldId, + + #[from(@.topic.subfield.name)] + pub subfield_name: String, + + #[from(@.topic.subfield.field_id)] + pub field_id: FieldId, + + #[from(@.topic.subfield.field.name)] + pub field_name: String, + + #[from(@.topic.subfield.field.domain_id)] + pub domain_id: DomainId, + + #[from(@.topic.subfield.field.domain.name)] + pub domain_name: String, +} + +#[derive(Debug, Serialize, FromImpl)] +#[from_owned(WorkKeyWordScore)] +#[serde(rename_all = "camelCase")] +pub struct WorkKeywordView { + #[from(@.keyword_id)] + pub keyword_id: KeywordId, + + #[from(@.keyword.name)] + pub name: String, + pub score: f64, } #[derive(Debug, Serialize)]