diff --git a/packages/backend/native-utils/Cargo.toml b/packages/backend/native-utils/Cargo.toml index 3c70a0209b..ba7a48b8d3 100644 --- a/packages/backend/native-utils/Cargo.toml +++ b/packages/backend/native-utils/Cargo.toml @@ -9,7 +9,7 @@ members = ["migration/Cargo.toml"] [features] default = ["napi"] noarray = [] -napi = ["dep:napi", "dep:napi-derive"] +napi = ["dep:napi", "dep:napi-derive", "dep:radix_fmt"] [lib] crate-type = ["cdylib", "lib"] @@ -33,8 +33,9 @@ tokio = { version = "1.28.1", features = ["full"] } utoipa = "3.3.0" # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix -napi = { version = "2.12.0", default-features = false, features = ["napi4", "tokio_rt"], optional = true } +napi = { version = "2.12.0", default-features = false, features = ["napi6", "tokio_rt"], optional = true } napi-derive = { version = "2.12.0", optional = true } +radix_fmt = { version = "1.0.0", optional = true } [dev-dependencies] pretty_assertions = "1.3.0" diff --git a/packages/backend/native-utils/package.json b/packages/backend/native-utils/package.json index f1ea2c9b41..8423f569b1 100644 --- a/packages/backend/native-utils/package.json +++ b/packages/backend/native-utils/package.json @@ -34,13 +34,13 @@ }, "scripts": { "artifacts": "napi artifacts", - "build": "napi build --platform --release ./built/", + "build": "napi build --features napi --platform --release ./built/", "build:debug": "napi build --platform", "prepublishOnly": "napi prepublish -t npm", "test": "ava", "universal": "napi universal", "version": "napi version", "cargo:unit": "cargo test unit_test", - "cargo:integration": "cargo test --no-default-features int_test -- --test-threads=1" + "cargo:integration": "cargo test --no-default-features -F noarray int_test -- --test-threads=1" } } diff --git a/packages/backend/native-utils/src/database/error.rs b/packages/backend/native-utils/src/database/error.rs index babdd68318..68e959e0af 100644 --- a/packages/backend/native-utils/src/database/error.rs +++ b/packages/backend/native-utils/src/database/error.rs @@ -1,5 +1,7 @@ use sea_orm::error::DbErr; +use crate::impl_into_napi_error; + #[derive(thiserror::Error, Debug, PartialEq, Eq)] pub enum Error { #[error("The database connections have not been initialized yet")] @@ -7,3 +9,5 @@ pub enum Error { #[error("ORM error: {0}")] OrmError(#[from] DbErr), } + +impl_into_napi_error!(Error); diff --git a/packages/backend/native-utils/src/database/mod.rs b/packages/backend/native-utils/src/database/mod.rs index c3cdea7278..80189a8135 100644 --- a/packages/backend/native-utils/src/database/mod.rs +++ b/packages/backend/native-utils/src/database/mod.rs @@ -1,12 +1,13 @@ pub mod error; +use cfg_if::cfg_if; use error::Error; use sea_orm::{Database, DbConn}; static DB_CONN: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); -pub async fn init_database(connection_uri: impl Into) -> Result<(), Error> { - let conn = Database::connect(connection_uri.into()).await?; +pub async fn init_database(conn_uri: impl Into) -> Result<(), Error> { + let conn = Database::connect(conn_uri.into()).await?; DB_CONN.get_or_init(move || conn); Ok(()) } @@ -15,6 +16,17 @@ pub fn get_database() -> Result<&'static DbConn, Error> { DB_CONN.get().ok_or(Error::Uninitialized) } +cfg_if! { + if #[cfg(feature = "napi")] { + use napi_derive::napi; + + #[napi] + pub async fn native_init_database(conn_uri: String) -> napi::Result<()> { + init_database(conn_uri).await.map_err(Into::into) + } + } +} + #[cfg(test)] mod unit_test { use super::{error::Error, get_database}; diff --git a/packages/backend/native-utils/src/lib.rs b/packages/backend/native-utils/src/lib.rs index 6a5e3f7f2c..f18e69a48f 100644 --- a/packages/backend/native-utils/src/lib.rs +++ b/packages/backend/native-utils/src/lib.rs @@ -1,4 +1,5 @@ pub mod database; +pub mod macros; pub mod model; pub mod util; diff --git a/packages/backend/native-utils/src/macros.rs b/packages/backend/native-utils/src/macros.rs new file mode 100644 index 0000000000..49ab826329 --- /dev/null +++ b/packages/backend/native-utils/src/macros.rs @@ -0,0 +1,11 @@ +#[macro_export] +macro_rules! impl_into_napi_error { + ($a:ty) => { + #[cfg(feature = "napi")] + impl Into for $a { + fn into(self) -> napi::Error { + napi::Error::from_reason(self.to_string()) + } + } + }; +} diff --git a/packages/backend/native-utils/src/model/error.rs b/packages/backend/native-utils/src/model/error.rs index 2292246aed..8e9213066b 100644 --- a/packages/backend/native-utils/src/model/error.rs +++ b/packages/backend/native-utils/src/model/error.rs @@ -1,8 +1,10 @@ +use crate::impl_into_napi_error; + #[derive(thiserror::Error, Debug, PartialEq, Eq)] pub enum Error { - #[error("Failed to parse string")] + #[error("Failed to parse string: {0}")] ParseError(#[from] parse_display::ParseError), - #[error("Failed to get database connection")] + #[error("Failed to get database connection: {0}")] DbConnError(#[from] crate::database::error::Error), #[error("Database operation error: {0}")] DbOperationError(#[from] sea_orm::DbErr), @@ -10,9 +12,4 @@ pub enum Error { NotFound, } -#[cfg(feature = "napi")] -impl Into for Error { - fn into(self) -> napi::Error { - napi::Error::from_reason(self.to_string()) - } -} +impl_into_napi_error!(Error); diff --git a/packages/backend/native-utils/src/model/repository.rs b/packages/backend/native-utils/src/model/repository.rs index 0f9f7de329..5abf7907fe 100644 --- a/packages/backend/native-utils/src/model/repository.rs +++ b/packages/backend/native-utils/src/model/repository.rs @@ -5,13 +5,18 @@ use schemars::JsonSchema; use super::error::Error; +/// Repositories have a packer that converts a database model to its +/// corresponding API schema. #[async_trait] pub trait Repository { async fn pack(self) -> Result; + /// Retrieves one model by its id and pack it. async fn pack_by_id(id: String) -> Result; } mod macros { + /// Provides the default implementation of + /// [crate::model::repository::Repository::pack_by_id]. macro_rules! impl_pack_by_id { ($a:ty, $b:ident) => { match <$a>::find_by_id($b) diff --git a/packages/backend/native-utils/src/model/schema.rs b/packages/backend/native-utils/src/model/schema.rs index ef4368dda1..4c0ca7941c 100644 --- a/packages/backend/native-utils/src/model/schema.rs +++ b/packages/backend/native-utils/src/model/schema.rs @@ -23,8 +23,9 @@ pub trait Schema { cfg_if! { if #[cfg(feature = "napi")] { - pub use antenna::napi::AntennaSchema as Antenna; - pub use antenna::napi::AntennaSrc; + // Will be disabled once we completely migrate to rust + pub use antenna::NativeAntennaSchema as Antenna; + pub use antenna::NativeAntennaSrc as AntennaSrc; } else { pub use antenna::Antenna; pub use antenna::AntennaSrc; diff --git a/packages/backend/native-utils/src/model/schema/antenna.rs b/packages/backend/native-utils/src/model/schema/antenna.rs index 8b553c0820..99521a98b0 100644 --- a/packages/backend/native-utils/src/model/schema/antenna.rs +++ b/packages/backend/native-utils/src/model/schema/antenna.rs @@ -1,3 +1,4 @@ +use cfg_if::cfg_if; use jsonschema::JSONSchema; use once_cell::sync::Lazy; use parse_display::FromStr; @@ -60,62 +61,62 @@ impl Schema for super::Antenna {} pub static VALIDATOR: Lazy = Lazy::new(|| super::Antenna::validator()); // ---- -#[cfg(feature = "napi")] -pub mod napi { - use napi::bindgen_prelude::*; - use napi_derive::napi; - use parse_display::FromStr; - use schemars::JsonSchema; - use utoipa::ToSchema; +cfg_if! { + if #[cfg(feature = "napi")] { + use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; + use napi_derive::napi; - use crate::model::{entity::antenna, repository::Repository}; + use crate::model::entity::antenna; + use crate::model::repository::Repository; - #[napi] - #[derive(Clone, Debug, PartialEq, Eq, JsonSchema, ToSchema)] - #[serde(rename_all = "camelCase")] - pub struct AntennaSchema { - pub id: String, - pub created_at: String, - pub name: String, - pub keywords: Vec>, - pub exclude_keywords: Vec>, - #[schema(inline)] - pub src: AntennaSrc, - pub user_list_id: Option, - pub user_group_id: Option, - pub users: Vec, - pub instances: Vec, - #[serde(default)] - pub case_sensitive: bool, - #[serde(default)] - pub notify: bool, - #[serde(default)] - pub with_replies: bool, - #[serde(default)] - pub with_file: bool, - #[serde(default)] - pub has_unread_note: bool, - } - - #[napi] - #[derive(Debug, FromStr, PartialEq, Eq, JsonSchema, ToSchema)] - #[serde(rename_all = "camelCase")] - #[display(style = "camelCase")] - #[display("'{}'")] - pub enum AntennaSrc { - Home, - All, - Users, - List, - Group, - Instances, - } - - #[napi] - impl AntennaSchema { + /// For NAPI because [chrono] is not supported. #[napi] - pub async fn pack_by_id(id: String) -> napi::Result { - antenna::Model::pack_by_id(id).await.map_err(Into::into) + #[derive(Clone, Debug, PartialEq, Eq, JsonSchema, ToSchema)] + #[serde(rename_all = "camelCase")] + pub struct NativeAntennaSchema { + pub id: String, + pub created_at: String, + pub name: String, + pub keywords: Vec>, + pub exclude_keywords: Vec>, + #[schema(inline)] + pub src: NativeAntennaSrc, + pub user_list_id: Option, + pub user_group_id: Option, + pub users: Vec, + pub instances: Vec, + #[serde(default)] + pub case_sensitive: bool, + #[serde(default)] + pub notify: bool, + #[serde(default)] + pub with_replies: bool, + #[serde(default)] + pub with_file: bool, + #[serde(default)] + pub has_unread_note: bool, + } + + #[napi] + #[derive(Debug, FromStr, PartialEq, Eq, JsonSchema, ToSchema)] + #[serde(rename_all = "camelCase")] + #[display(style = "camelCase")] + #[display("'{}'")] + pub enum NativeAntennaSrc { + Home, + All, + Users, + List, + Group, + Instances, + } + + #[napi] + impl NativeAntennaSchema { + #[napi] + pub async fn pack_by_id(id: String) -> napi::Result { + antenna::Model::pack_by_id(id).await.map_err(Into::into) + } } } } diff --git a/packages/backend/native-utils/src/util/id.rs b/packages/backend/native-utils/src/util/id.rs index 98dd63c4e5..1d28b80c05 100644 --- a/packages/backend/native-utils/src/util/id.rs +++ b/packages/backend/native-utils/src/util/id.rs @@ -1,18 +1,31 @@ //! ID generation utility based on [cuid2] -use cuid2::CuidConstructor; +use cfg_if::cfg_if; use once_cell::sync::OnceCell; +use crate::impl_into_napi_error; + #[derive(thiserror::Error, Debug, PartialEq, Eq)] #[error("ID generator has not been initialized yet")] pub struct ErrorUninitialized; -static GENERATOR: OnceCell = OnceCell::new(); +impl_into_napi_error!(ErrorUninitialized); -pub fn init_id(length: u16) { - GENERATOR.get_or_init(move || CuidConstructor::new().with_length(length)); +static FINGERPRINT: OnceCell = OnceCell::new(); +static GENERATOR: OnceCell = OnceCell::new(); + +/// Initializes Cuid2 generator. Must be called before any [create_id]. +pub fn init_id(length: u16, fingerprint: impl Into) { + FINGERPRINT.get_or_init(move || format!("{}{}", fingerprint.into(), cuid2::create_id())); + GENERATOR.get_or_init(move || { + cuid2::CuidConstructor::new() + .with_length(length) + .with_fingerprinter(|| FINGERPRINT.get().unwrap().clone()) + }); } +/// Returns Cuid2 with the length specified by [init_id]. Must be called after +/// [init_id], otherwise returns [ErrorUninitialized]. pub fn create_id() -> Result { match GENERATOR.get() { None => Err(ErrorUninitialized), @@ -20,6 +33,30 @@ pub fn create_id() -> Result { } } +cfg_if! { + if #[cfg(feature = "napi")] { + use radix_fmt::radix_36; + use std::cmp; + use napi::bindgen_prelude::BigInt; + use napi_derive::napi; + + const TIME_2000: u64 = 946_684_800_000; + + /// Calls [init_id] inside. Must be called before [native_create_id]. + #[napi] + pub fn native_init_id_generator(length: u16, fingerprint: String) { + init_id(length, fingerprint); + } + + /// Generates + #[napi] + pub fn native_create_id(date_num: BigInt) -> String { + let time = cmp::max(date_num.get_u64().1 - TIME_2000, 0); + format!("{:0>8}{}", radix_36(time).to_string(), create_id().unwrap()) + } + } +} + #[cfg(test)] mod unit_test { use pretty_assertions::{assert_eq, assert_ne}; @@ -30,7 +67,7 @@ mod unit_test { #[test] fn can_generate_unique_ids() { assert_eq!(id::create_id(), Err(id::ErrorUninitialized)); - id::init_id(12); + id::init_id(12, ""); assert_eq!(id::create_id().unwrap().len(), 12); assert_ne!(id::create_id().unwrap(), id::create_id().unwrap()); let id1 = thread::spawn(|| id::create_id().unwrap()); diff --git a/packages/backend/native-utils/src/util/random.rs b/packages/backend/native-utils/src/util/random.rs index 5ee06d1282..ffcbca980f 100644 --- a/packages/backend/native-utils/src/util/random.rs +++ b/packages/backend/native-utils/src/util/random.rs @@ -1,5 +1,6 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng}; +/// Generate random string based on [thread_rng] and [Alphanumeric]. pub fn gen_string(length: u16) -> String { thread_rng() .sample_iter(Alphanumeric) @@ -8,6 +9,12 @@ pub fn gen_string(length: u16) -> String { .collect() } +#[cfg(feature = "napi")] +#[napi_derive::napi] +pub fn native_random_str(length: u16) -> String { + gen_string(length) +} + #[cfg(test)] mod unit_test { use pretty_assertions::{assert_eq, assert_ne};