add native calls

This commit is contained in:
Namekuji 2023-06-02 11:55:14 -04:00
parent 3af4a86254
commit af85304578
12 changed files with 151 additions and 74 deletions

View File

@ -9,7 +9,7 @@ members = ["migration/Cargo.toml"]
[features] [features]
default = ["napi"] default = ["napi"]
noarray = [] noarray = []
napi = ["dep:napi", "dep:napi-derive"] napi = ["dep:napi", "dep:napi-derive", "dep:radix_fmt"]
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]
@ -33,8 +33,9 @@ tokio = { version = "1.28.1", features = ["full"] }
utoipa = "3.3.0" utoipa = "3.3.0"
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix # 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 } napi-derive = { version = "2.12.0", optional = true }
radix_fmt = { version = "1.0.0", optional = true }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"

View File

@ -34,13 +34,13 @@
}, },
"scripts": { "scripts": {
"artifacts": "napi artifacts", "artifacts": "napi artifacts",
"build": "napi build --platform --release ./built/", "build": "napi build --features napi --platform --release ./built/",
"build:debug": "napi build --platform", "build:debug": "napi build --platform",
"prepublishOnly": "napi prepublish -t npm", "prepublishOnly": "napi prepublish -t npm",
"test": "ava", "test": "ava",
"universal": "napi universal", "universal": "napi universal",
"version": "napi version", "version": "napi version",
"cargo:unit": "cargo test unit_test", "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"
} }
} }

View File

@ -1,5 +1,7 @@
use sea_orm::error::DbErr; use sea_orm::error::DbErr;
use crate::impl_into_napi_error;
#[derive(thiserror::Error, Debug, PartialEq, Eq)] #[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum Error { pub enum Error {
#[error("The database connections have not been initialized yet")] #[error("The database connections have not been initialized yet")]
@ -7,3 +9,5 @@ pub enum Error {
#[error("ORM error: {0}")] #[error("ORM error: {0}")]
OrmError(#[from] DbErr), OrmError(#[from] DbErr),
} }
impl_into_napi_error!(Error);

View File

@ -1,12 +1,13 @@
pub mod error; pub mod error;
use cfg_if::cfg_if;
use error::Error; use error::Error;
use sea_orm::{Database, DbConn}; use sea_orm::{Database, DbConn};
static DB_CONN: once_cell::sync::OnceCell<DbConn> = once_cell::sync::OnceCell::new(); static DB_CONN: once_cell::sync::OnceCell<DbConn> = once_cell::sync::OnceCell::new();
pub async fn init_database(connection_uri: impl Into<String>) -> Result<(), Error> { pub async fn init_database(conn_uri: impl Into<String>) -> Result<(), Error> {
let conn = Database::connect(connection_uri.into()).await?; let conn = Database::connect(conn_uri.into()).await?;
DB_CONN.get_or_init(move || conn); DB_CONN.get_or_init(move || conn);
Ok(()) Ok(())
} }
@ -15,6 +16,17 @@ pub fn get_database() -> Result<&'static DbConn, Error> {
DB_CONN.get().ok_or(Error::Uninitialized) 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)] #[cfg(test)]
mod unit_test { mod unit_test {
use super::{error::Error, get_database}; use super::{error::Error, get_database};

View File

@ -1,4 +1,5 @@
pub mod database; pub mod database;
pub mod macros;
pub mod model; pub mod model;
pub mod util; pub mod util;

View File

@ -0,0 +1,11 @@
#[macro_export]
macro_rules! impl_into_napi_error {
($a:ty) => {
#[cfg(feature = "napi")]
impl Into<napi::Error> for $a {
fn into(self) -> napi::Error {
napi::Error::from_reason(self.to_string())
}
}
};
}

View File

@ -1,8 +1,10 @@
use crate::impl_into_napi_error;
#[derive(thiserror::Error, Debug, PartialEq, Eq)] #[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum Error { pub enum Error {
#[error("Failed to parse string")] #[error("Failed to parse string: {0}")]
ParseError(#[from] parse_display::ParseError), ParseError(#[from] parse_display::ParseError),
#[error("Failed to get database connection")] #[error("Failed to get database connection: {0}")]
DbConnError(#[from] crate::database::error::Error), DbConnError(#[from] crate::database::error::Error),
#[error("Database operation error: {0}")] #[error("Database operation error: {0}")]
DbOperationError(#[from] sea_orm::DbErr), DbOperationError(#[from] sea_orm::DbErr),
@ -10,9 +12,4 @@ pub enum Error {
NotFound, NotFound,
} }
#[cfg(feature = "napi")] impl_into_napi_error!(Error);
impl Into<napi::Error> for Error {
fn into(self) -> napi::Error {
napi::Error::from_reason(self.to_string())
}
}

View File

@ -5,13 +5,18 @@ use schemars::JsonSchema;
use super::error::Error; use super::error::Error;
/// Repositories have a packer that converts a database model to its
/// corresponding API schema.
#[async_trait] #[async_trait]
pub trait Repository<T: JsonSchema> { pub trait Repository<T: JsonSchema> {
async fn pack(self) -> Result<T, Error>; async fn pack(self) -> Result<T, Error>;
/// Retrieves one model by its id and pack it.
async fn pack_by_id(id: String) -> Result<T, Error>; async fn pack_by_id(id: String) -> Result<T, Error>;
} }
mod macros { mod macros {
/// Provides the default implementation of
/// [crate::model::repository::Repository::pack_by_id].
macro_rules! impl_pack_by_id { macro_rules! impl_pack_by_id {
($a:ty, $b:ident) => { ($a:ty, $b:ident) => {
match <$a>::find_by_id($b) match <$a>::find_by_id($b)

View File

@ -23,8 +23,9 @@ pub trait Schema<T: JsonSchema> {
cfg_if! { cfg_if! {
if #[cfg(feature = "napi")] { if #[cfg(feature = "napi")] {
pub use antenna::napi::AntennaSchema as Antenna; // Will be disabled once we completely migrate to rust
pub use antenna::napi::AntennaSrc; pub use antenna::NativeAntennaSchema as Antenna;
pub use antenna::NativeAntennaSrc as AntennaSrc;
} else { } else {
pub use antenna::Antenna; pub use antenna::Antenna;
pub use antenna::AntennaSrc; pub use antenna::AntennaSrc;

View File

@ -1,3 +1,4 @@
use cfg_if::cfg_if;
use jsonschema::JSONSchema; use jsonschema::JSONSchema;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use parse_display::FromStr; use parse_display::FromStr;
@ -60,62 +61,62 @@ impl Schema<Self> for super::Antenna {}
pub static VALIDATOR: Lazy<JSONSchema> = Lazy::new(|| super::Antenna::validator()); pub static VALIDATOR: Lazy<JSONSchema> = Lazy::new(|| super::Antenna::validator());
// ---- // ----
#[cfg(feature = "napi")] cfg_if! {
pub mod napi { if #[cfg(feature = "napi")] {
use napi::bindgen_prelude::*; use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
use napi_derive::napi; use napi_derive::napi;
use parse_display::FromStr;
use schemars::JsonSchema;
use utoipa::ToSchema;
use crate::model::{entity::antenna, repository::Repository}; use crate::model::entity::antenna;
use crate::model::repository::Repository;
#[napi] /// For NAPI because [chrono] is not supported.
#[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<Vec<String>>,
pub exclude_keywords: Vec<Vec<String>>,
#[schema(inline)]
pub src: AntennaSrc,
pub user_list_id: Option<String>,
pub user_group_id: Option<String>,
pub users: Vec<String>,
pub instances: Vec<String>,
#[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 {
#[napi] #[napi]
pub async fn pack_by_id(id: String) -> napi::Result<AntennaSchema> { #[derive(Clone, Debug, PartialEq, Eq, JsonSchema, ToSchema)]
antenna::Model::pack_by_id(id).await.map_err(Into::into) #[serde(rename_all = "camelCase")]
pub struct NativeAntennaSchema {
pub id: String,
pub created_at: String,
pub name: String,
pub keywords: Vec<Vec<String>>,
pub exclude_keywords: Vec<Vec<String>>,
#[schema(inline)]
pub src: NativeAntennaSrc,
pub user_list_id: Option<String>,
pub user_group_id: Option<String>,
pub users: Vec<String>,
pub instances: Vec<String>,
#[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<NativeAntennaSchema> {
antenna::Model::pack_by_id(id).await.map_err(Into::into)
}
} }
} }
} }

View File

@ -1,18 +1,31 @@
//! ID generation utility based on [cuid2] //! ID generation utility based on [cuid2]
use cuid2::CuidConstructor; use cfg_if::cfg_if;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use crate::impl_into_napi_error;
#[derive(thiserror::Error, Debug, PartialEq, Eq)] #[derive(thiserror::Error, Debug, PartialEq, Eq)]
#[error("ID generator has not been initialized yet")] #[error("ID generator has not been initialized yet")]
pub struct ErrorUninitialized; pub struct ErrorUninitialized;
static GENERATOR: OnceCell<CuidConstructor> = OnceCell::new(); impl_into_napi_error!(ErrorUninitialized);
pub fn init_id(length: u16) { static FINGERPRINT: OnceCell<String> = OnceCell::new();
GENERATOR.get_or_init(move || CuidConstructor::new().with_length(length)); static GENERATOR: OnceCell<cuid2::CuidConstructor> = OnceCell::new();
/// Initializes Cuid2 generator. Must be called before any [create_id].
pub fn init_id(length: u16, fingerprint: impl Into<String>) {
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<String, ErrorUninitialized> { pub fn create_id() -> Result<String, ErrorUninitialized> {
match GENERATOR.get() { match GENERATOR.get() {
None => Err(ErrorUninitialized), None => Err(ErrorUninitialized),
@ -20,6 +33,30 @@ pub fn create_id() -> Result<String, ErrorUninitialized> {
} }
} }
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)] #[cfg(test)]
mod unit_test { mod unit_test {
use pretty_assertions::{assert_eq, assert_ne}; use pretty_assertions::{assert_eq, assert_ne};
@ -30,7 +67,7 @@ mod unit_test {
#[test] #[test]
fn can_generate_unique_ids() { fn can_generate_unique_ids() {
assert_eq!(id::create_id(), Err(id::ErrorUninitialized)); 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_eq!(id::create_id().unwrap().len(), 12);
assert_ne!(id::create_id().unwrap(), id::create_id().unwrap()); assert_ne!(id::create_id().unwrap(), id::create_id().unwrap());
let id1 = thread::spawn(|| id::create_id().unwrap()); let id1 = thread::spawn(|| id::create_id().unwrap());

View File

@ -1,5 +1,6 @@
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::{distributions::Alphanumeric, thread_rng, Rng};
/// Generate random string based on [thread_rng] and [Alphanumeric].
pub fn gen_string(length: u16) -> String { pub fn gen_string(length: u16) -> String {
thread_rng() thread_rng()
.sample_iter(Alphanumeric) .sample_iter(Alphanumeric)
@ -8,6 +9,12 @@ pub fn gen_string(length: u16) -> String {
.collect() .collect()
} }
#[cfg(feature = "napi")]
#[napi_derive::napi]
pub fn native_random_str(length: u16) -> String {
gen_string(length)
}
#[cfg(test)] #[cfg(test)]
mod unit_test { mod unit_test {
use pretty_assertions::{assert_eq, assert_ne}; use pretty_assertions::{assert_eq, assert_ne};