feat: Cluster API

This commit is contained in:
LynithDev 2025-04-03 22:12:03 +02:00
parent 11d2ff43ed
commit 2f7c26229a
25 changed files with 660 additions and 48 deletions

1
Cargo.lock generated
View file

@ -2993,6 +2993,7 @@ dependencies = [
"serde",
"serde_json",
"specta",
"url",
"uuid 1.10.0",
]

View file

@ -30,7 +30,7 @@
"lint": "pnpm lint:es && pnpm lint:rs",
"migrate": "sea-orm-cli migrate -d ./packages/migration",
"entity:gen": "sea-orm-cli generate entity -o packages/entity/src/entities --with-serde both --model-extra-attributes 'onelauncher_macro::specta'",
"entity:gen": "sea-orm-cli generate entity -o packages/entity/src/entities --with-serde both --model-extra-attributes 'onelauncher_macro::specta' -t",
"frontend": "pnpm --filter @onelauncher/frontend -- ",
"client": "pnpm --filter @onelauncher/client -- ",

View file

@ -0,0 +1,26 @@
use onelauncher_core::{api::{self, proxy::proxy_empty::ProxyEmpty}, error::LauncherResult, initialize_core, store::CoreOptions};
use onelauncher_entity::loader::GameLoader;
#[tokio::main]
async fn main() -> LauncherResult<()> {
initialize_core(CoreOptions::default(), ProxyEmpty::new()).await?;
println!("syncing clusters");
let missing = api::cluster::sync_clusters().await?;
if !missing.is_empty() {
println!("missing clusters: {missing:#?}");
}
println!("creating cluster");
let cluster = api::cluster::create_cluster(
"Test Cluster",
"1.8.9",
GameLoader::Vanilla,
None,
None,
).await?;
println!("cluster created: {cluster:#?}");
Ok(())
}

View file

@ -12,6 +12,7 @@
"sideEffects": false,
"scripts": {
"cargo": "cargo",
"binary": "cargo run --bin"
"binary": "cargo run --bin",
"example": "cargo run --example"
}
}

View file

@ -0,0 +1,120 @@
use std::path::Path;
use onelauncher_entity::{cluster_groups, clusters, icon::Icon, loader::GameLoader};
use sea_orm::{ActiveValue::Set, IntoActiveModel, prelude::*};
use crate::{error::{DaoError, LauncherResult}, store::State};
/// Inserts a cluster in the database.
pub async fn insert_cluster(
name: &str,
path: &str,
mc_version: &str,
mc_loader: GameLoader,
mc_loader_version: Option<&str>,
icon_url: Option<Icon>,
) -> LauncherResult<clusters::Model> {
let state = State::get().await?;
let db = &state.db;
let model = clusters::ActiveModel {
name: Set(name.to_string()),
path: Set(path.to_string()),
mc_version: Set(mc_version.to_string()),
mc_loader: Set(mc_loader),
mc_loader_version: Set(mc_loader_version.map(String::from)),
icon_url: Set(icon_url),
..Default::default()
};
let model = model.insert(db).await?;
Ok(model)
}
/// Updates an existing cluster in the database.
pub async fn update_cluster_by_id<B>(
id: i32,
block: B,
) -> LauncherResult<clusters::Model>
where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel> {
let state = State::get().await?;
let db = &state.db;
let model = get_cluster_by_id(id).await?.ok_or(DaoError::NotFound)?;
let model = block(model.into_active_model()).await?;
let model = model.update(db).await?;
Ok(model)
}
/// Deletes a cluster by its ID from the **database**.
pub async fn delete_cluster_by_id(id: i32) -> LauncherResult<()> {
let state = State::get().await?;
let db = &state.db;
let model = get_cluster_by_id(id).await?.ok_or(DaoError::NotFound)?;
model.delete(db).await?;
Ok(())
}
/// Gets a cluster by its ID from the database.
pub async fn get_cluster_by_id(id: i32) -> LauncherResult<Option<clusters::Model>> {
let state = State::get().await?;
let db = &state.db;
let model = clusters::Entity::find_by_id(id)
.one(db)
.await?;
Ok(model)
}
/// Gets a cluster by its path from the database.
pub async fn get_cluster_by_path(path: &Path) -> LauncherResult<Option<clusters::Model>> {
let state = State::get().await?;
let db = &state.db;
let model = clusters::Entity::find()
.filter(clusters::Column::Path.eq(path.to_string_lossy().to_string()))
.one(db)
.await?;
Ok(model)
}
/// Gets all clusters from the database.
pub async fn get_all_clusters() -> LauncherResult<Vec<clusters::Model>> {
let state = State::get().await?;
let db = &state.db;
let clusters = clusters::Entity::find()
.all(db)
.await?;
Ok(clusters)
}
/// Gets all clusters grouped by their group from the database.
pub async fn get_clusters_grouped() -> LauncherResult<Vec<(cluster_groups::Model, Vec<clusters::Model>)>> {
let state = State::get().await?;
let db = &state.db;
let groups = cluster_groups::Entity::find()
.all(db)
.await?;
let clusters = groups.load_many(clusters::Entity, db)
.await?;
let mut grouped = Vec::new();
for (index, group) in groups.iter().enumerate() {
let group_clusters: Vec<clusters::Model> = clusters.get(index).cloned().unwrap_or_else(Vec::new);
grouped.push((group.clone(), group_clusters));
}
Ok(grouped)
}

View file

@ -0,0 +1,189 @@
use std::collections::HashSet;
use std::path::PathBuf;
use onelauncher_entity::clusters;
use onelauncher_entity::icon::Icon;
use onelauncher_entity::loader::GameLoader;
use serde::Serialize;
use crate::error::{DaoError, LauncherResult};
use crate::send_error;
use crate::store::Dirs;
use crate::utils::io;
pub mod dao;
#[must_use]
pub fn sanitize_name(name: &str) -> String {
let mut name = name.to_string();
name.retain(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | ' ' | '.' | '(' | ')'));
name
}
#[tracing::instrument]
pub async fn create_cluster(
name: &str,
mc_version: &str,
mc_loader: GameLoader,
mc_loader_version: Option<&str>,
icon_url: Option<Icon>,
) -> LauncherResult<clusters::Model> {
let name = sanitize_name(name);
let cluster_dir = Dirs::get().await?.clusters_dir();
// Get the directory for the cluster
let mut path = cluster_dir.join(&name);
// Folder name conflict resolution
if path.exists() {
let mut which = 1;
loop {
let new_name = format!("{name} ({which})");
path = cluster_dir.join(&new_name);
if !path.exists() {
break;
}
which += 1;
}
tracing::warn!(
"collision while creating new cluster: {}, renaming to {}",
cluster_dir.display(),
path.display()
);
}
let result = async {
io::create_dir_all(&path).await?;
tracing::info!("creating cluster at path {}", path.display());
// Finally add the cluster to the database
dao::insert_cluster(
name.as_str(),
path.to_string_lossy().into_owned().as_str(),
mc_version,
mc_loader,
mc_loader_version,
icon_url,
)
.await
}
.await;
match result {
Ok(result) => Ok(result),
Err(err) => {
tracing::error!("failed to create cluster: {}", err);
let _ = io::remove_dir_all(&path).await;
Err(err)
}
}
}
#[onelauncher_macro::specta]
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub enum SyncAction {
Created,
Updated,
MissingDb,
MissingFs,
NoAction,
}
impl SyncAction {
#[must_use]
pub const fn is_missing(self) -> bool {
matches!(self, Self::MissingDb | Self::MissingFs)
}
}
/// Syncs all clusters in the database. Returns a list of cluster IDs that are missing.
#[tracing::instrument]
pub async fn sync_clusters() -> LauncherResult<Vec<i32>> {
let clusters = dao::get_all_clusters().await?;
let mut missing_ids = Vec::new();
let mut checked_paths = HashSet::new();
for cluster in clusters {
if sync_cluster(&cluster)
.await
.map(SyncAction::is_missing)
.unwrap_or(false)
{
missing_ids.push(cluster.id);
let path = cluster.path.clone();
send_error!(
"cluster {} is missing from the filesystem",
path.clone()
);
}
checked_paths.insert(PathBuf::from(cluster.path));
}
let cluster_dir = Dirs::get().await?.clusters_dir();
let mut stream = io::read_dir(cluster_dir).await?;
while let Ok(Some(entry)) = stream.next_entry().await {
let path = entry.path();
if !path.is_dir() || checked_paths.contains(&path) {
// Skip if it's not a directory or if we've already checked it
continue;
}
// We have a directory that is not in the database
if let Err(err) = sync_from_fs_to_db(&path).await {
tracing::error!("failed to sync cluster from fs to db: {}", err);
};
}
Ok(missing_ids)
}
/// Syncs the cluster with the database.
/// Checks if the cluster in the database exists and if the directory exists.
#[tracing::instrument]
pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult<SyncAction> {
let path = PathBuf::from(cluster.path.clone());
if !path.exists() {
return Ok(SyncAction::MissingFs);
}
// TODO: sync packages
Ok(SyncAction::NoAction)
}
/// Syncs the cluster with the database.
/// Checks if the cluster in the database exists and if the directory exists.
#[tracing::instrument]
pub async fn sync_cluster_by_id(id: i32) -> LauncherResult<SyncAction> {
let cluster = dao::get_cluster_by_id(id)
.await?
.ok_or(DaoError::NotFound)?;
sync_cluster(&cluster).await
}
/// Syncs the cluster with the database.
/// Checks if the cluster in the database exists and if the directory exists.
#[tracing::instrument]
pub async fn sync_cluster_by_path(path: &PathBuf) -> LauncherResult<SyncAction> {
let Some(cluster) = dao::get_cluster_by_path(path).await? else {
tracing::warn!("cluster with path {path:?} not found in database");
return Ok(SyncAction::MissingDb);
};
sync_cluster(&cluster).await
}
#[tracing::instrument]
async fn sync_from_fs_to_db(path: &PathBuf) -> LauncherResult<clusters::Model> {
// TODO: Create database entry from what can be inferred from the directory
Err(anyhow::anyhow!(
"TODO: failed to sync cluster from fs to db: {}",
path.display()
).into())
}

View file

@ -51,13 +51,15 @@ pub async fn send_ingress_opt(
#[cfg(all(feature = "cli", test))]
pub mod tests {
use crate::store::CoreOptions;
#[tokio::test]
pub async fn create_and_update_ingress() -> crate::LauncherResult<()> {
use std::time::Duration;
use crate::{api::{ingress::{init_ingress, send_ingress}, proxy::proxy_cli::ProxyCli}, initialize_core, store::ingress::IngressType};
initialize_core(ProxyCli::new()).await?;
initialize_core(CoreOptions::default(), ProxyCli::new()).await?;
let id = init_ingress(IngressType::Download { file_name: "Some-Mod-1.8.9.jar".into() }, "This is a test message", 100.0).await?;
tokio::time::sleep(Duration::from_millis(2350)).await;

View file

@ -1,9 +1,29 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use onelauncher_entity::java_versions;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder};
use sea_orm::{ActiveValue::Set, ActiveModelTrait, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder};
use crate::{api::java::{get_java_model, JavaInfo}, store::State, LauncherResult};
use crate::{error::LauncherResult, store::State};
use super::{JavaError, JavaInfo};
/// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`]
pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult<java_versions::ActiveModel> {
let major_version = if info.java_version[0..2].eq("1.") {
info.java_version[2..3].parse::<i32>()
} else {
let split = info.java_version.split_once('.').unwrap_or_default();
split.0.parse::<i32>()
}.map_err(JavaError::from)?;
Ok(java_versions::ActiveModel {
absolute_path: Set(absolute_path.to_string_lossy().to_string()),
major_version: Set(major_version),
full_version: Set(info.java_version.clone()),
vendor_name: Set(info.java_vendor.clone()),
..Default::default()
})
}
/// Returns all Java versions
pub async fn get_java_all() -> LauncherResult<Vec<java_versions::Model>> {
@ -58,5 +78,4 @@ pub async fn insert_java_many(java: Vec<(PathBuf, JavaInfo)>) -> LauncherResult<
Ok(java_versions::Entity::insert_many(models)
.exec_with_returning_many(db)
.await?)
}
}

View file

@ -1,12 +1,10 @@
use std::{collections::HashMap, path::{Path, PathBuf}};
use std::{collections::HashMap, path::PathBuf};
use onelauncher_entity::java_versions;
use sea_orm::ActiveValue::Set;
use serde::Serialize;
use crate::{error::LauncherResult, store::ingress::IngressType, utils::io};
use crate::{api::ingress::{init_ingress, init_ingress_opt, send_ingress, send_ingress_opt}, error::LauncherResult, store::ingress::IngressType, utils::io};
use super::ingress::{init_ingress, init_ingress_opt, send_ingress, send_ingress_opt};
pub mod dao;
#[derive(Debug, thiserror::Error)]
pub enum JavaError {
@ -29,7 +27,7 @@ pub fn get_java_bin() -> PathBuf {
}
#[cfg(target_os = "windows")]
{
PathBuf::new().join("bin").join("java.exe")
PathBuf::new().join("bin").join("javaw.exe")
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
@ -45,7 +43,7 @@ pub struct JavaInfo {
pub java_vendor: String,
}
const JAVA_INFO_CLASS: &[u8] = include_bytes!("../../assets/java/JavaInfo.class");
const JAVA_INFO_CLASS: &[u8] = include_bytes!("../../../assets/java/JavaInfo.class");
/// Accepts a path to a java executable and returns the [`JavaInfo`]
pub async fn check_java_runtime(absolute_path: &PathBuf, with_ingress: bool) -> LauncherResult<JavaInfo> {
@ -87,24 +85,6 @@ pub async fn check_java_runtime(absolute_path: &PathBuf, with_ingress: bool) ->
})
}
/// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`]
pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult<java_versions::ActiveModel> {
let major_version = if info.java_version[0..2].eq("1.") {
info.java_version[2..3].parse::<i32>()
} else {
let split = info.java_version.split_once('.').unwrap_or_default();
split.0.parse::<i32>()
}.map_err(JavaError::from)?;
Ok(java_versions::ActiveModel {
absolute_path: Set(absolute_path.to_string_lossy().to_string()),
major_version: Set(major_version),
full_version: Set(info.java_version.clone()),
vendor_name: Set(info.java_vendor.clone()),
..Default::default()
})
}
/// Attempts to scan common paths for Java installations.
///
/// **Can be heavy on I/O, don't run often!**

View file

@ -1,3 +1,4 @@
pub mod cluster;
pub mod ingress;
pub mod proxy;
pub mod java;

View file

@ -12,6 +12,8 @@ pub enum LauncherError {
CryptoError(#[from] crate::utils::crypto::CryptoError),
#[error(transparent)]
DiscordError(#[from] crate::store::discord::DiscordError),
#[error(transparent)]
DaoError(#[from] DaoError),
#[error(transparent)]
SerdeError(#[from] serde_json::Error),
@ -29,4 +31,12 @@ pub enum LauncherError {
TauriError(#[from] tauri::Error),
}
pub type LauncherResult<T> = Result<T, LauncherError>;
pub type LauncherResult<T> = Result<T, LauncherError>;
#[derive(thiserror::Error, Debug)]
pub enum DaoError {
#[error("entity was not found")]
NotFound,
#[error("entity already exists")]
AlreadyExists
}

View file

@ -58,6 +58,12 @@ impl Dirs {
// --- GAME DIRECTORIES ---
/// Get the `config_dir/clusters` folder for the clusters.
#[must_use]
pub fn clusters_dir(&self) -> PathBuf {
self.base_dir().join("clusters")
}
/// Get the `config_dir/metadata` folder within the core config directory.
#[inline]
#[must_use]

View file

@ -2,7 +2,6 @@ pub(super) mod db;
pub mod discord;
pub mod ingress;
pub mod proxy;
pub mod java;
mod core_options;
mod dirs;
mod settings;

View file

@ -25,4 +25,5 @@ serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
uuid = { workspace = true }
url = { workspace = true }
specta = { workspace = true, optional = true }

View file

@ -0,0 +1,14 @@
use sea_orm::{DeriveActiveEnum, EnumIter};
use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "u8", db_type = "Integer")]
#[serde(rename_all = "lowercase")]
pub enum ClusterStage {
#[default]
NotReady = 0,
Downloading = 1,
Repairing = 2,
Ready = 3,
}

View file

@ -0,0 +1,82 @@
use std::{fmt::Display, ops::Deref, str::FromStr};
use sea_orm::{sea_query::Nullable, DeriveValueType};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType)]
pub struct Icon(String);
impl Nullable for Icon {
fn null() -> sea_orm::Value {
sea_orm::Value::String(None)
}
}
impl Icon {
pub fn new(icon: String) -> Self {
Self(icon)
}
pub fn get_type(&self) -> IconType {
if self.is_path() {
IconType::Path
} else if self.is_url() {
IconType::Url
} else {
IconType::Stored
}
}
pub fn is_path(&self) -> bool {
self.0.starts_with("file://")
}
pub fn is_url(&self) -> bool {
self.0.starts_with("http://") || self.0.starts_with("https://")
}
pub fn is_stored(&self) -> bool {
!self.is_path() && !self.is_url()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum IconType {
Path,
Url,
Stored,
}
impl Display for IconType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl Deref for Icon {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<String> for Icon {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl FromStr for Icon {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::new(s.to_string()))
}
}
impl Display for Icon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Icon(IconType::{}, \"{}\")", self.get_type(), self.0)
}
}

View file

@ -0,0 +1,66 @@
use std::{fmt::Display, str::FromStr};
use sea_orm::{DeriveActiveEnum, EnumIter};
use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
#[serde(rename_all = "lowercase")]
#[sea_orm(rs_type = "u8", db_type = "Integer")]
pub enum GameLoader {
#[default]
Vanilla = 0,
Unknown = 1,
Forge = 2,
NeoForge = 3,
Quilt = 4,
Fabric = 5,
LegacyFabric = 6,
}
impl GameLoader {
#[must_use]
pub const fn is_mod_loader(&self) -> bool {
matches!(self, Self::Forge | Self::NeoForge | Self::Quilt | Self::Fabric | Self::LegacyFabric)
}
}
impl From<String> for GameLoader {
fn from(s: String) -> Self {
match s.to_lowercase().replace(['_', '.', ' '], "").as_str() {
"vanilla" => Self::Vanilla,
"forge" => Self::Forge,
"neoforge" => Self::NeoForge,
"quilt" => Self::Quilt,
"fabric" => Self::Fabric,
"legacyfabric" => Self::LegacyFabric,
_ => Self::Unknown,
}
}
}
impl FromStr for GameLoader {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(s.to_string()))
}
}
impl Display for GameLoader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
match self {
Self::Vanilla => "Vanilla",
Self::Forge => "Forge",
Self::NeoForge => "NeoForge",
Self::Quilt => "Quilt",
Self::Fabric => "Fabric",
Self::LegacyFabric => "LegacyFabric",
Self::Unknown => "Unknown",
}
)
}
}

View file

@ -0,0 +1,3 @@
pub mod cluster_stage;
pub mod icon;
pub mod loader;

View file

@ -0,0 +1,48 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "cluster_packages")]
#[onelauncher_macro::specta]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub cluster_id: i32,
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
pub package_hash: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::clusters::Entity",
from = "Column::ClusterId",
to = "super::clusters::Column::Id",
on_update = "NoAction",
on_delete = "NoAction"
)]
Clusters,
#[sea_orm(
belongs_to = "super::packages::Entity",
from = "Column::PackageHash",
to = "super::packages::Column::Hash",
on_update = "NoAction",
on_delete = "NoAction"
)]
Packages,
}
impl Related<super::clusters::Entity> for Entity {
fn to() -> RelationDef {
Relation::Clusters.def()
}
}
impl Related<super::packages::Entity> for Entity {
fn to() -> RelationDef {
Relation::Packages.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,14 +1,17 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::{cluster_stage::ClusterStage, icon::Icon, loader::GameLoader};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "clusters")]
#[onelauncher_macro::specta]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(column_type = "Text")]
pub path: String,
pub stage: ClusterStage,
pub created_at: i32,
pub updated_at: i32,
pub group_id: Option<i32>,
@ -16,14 +19,13 @@ pub struct Model {
pub name: String,
#[sea_orm(column_type = "Text")]
pub mc_version: String,
#[sea_orm(column_type = "Text")]
pub mc_loader: String,
pub mc_loader: GameLoader,
#[sea_orm(column_type = "Text", nullable)]
pub mc_loader_version: Option<String>,
pub last_played: Option<i32>,
pub overall_played: Option<i32>,
#[sea_orm(column_type = "Text", nullable)]
pub icon_url: Option<String>,
pub icon_url: Option<Icon>,
#[sea_orm(column_type = "Text", nullable)]
pub setting_profile_name: Option<String>,
#[sea_orm(column_type = "Text", nullable)]
@ -41,6 +43,8 @@ pub enum Relation {
on_delete = "NoAction"
)]
ClusterGroups,
#[sea_orm(has_many = "super::cluster_packages::Entity")]
ClusterPackages,
#[sea_orm(
belongs_to = "super::setting_profiles::Entity",
from = "Column::SettingProfileName",
@ -57,10 +61,25 @@ impl Related<super::cluster_groups::Entity> for Entity {
}
}
impl Related<super::cluster_packages::Entity> for Entity {
fn to() -> RelationDef {
Relation::ClusterPackages.def()
}
}
impl Related<super::setting_profiles::Entity> for Entity {
fn to() -> RelationDef {
Relation::SettingProfiles.def()
}
}
impl Related<super::packages::Entity> for Entity {
fn to() -> RelationDef {
super::cluster_packages::Relation::Packages.def()
}
fn via() -> Option<RelationDef> {
Some(super::cluster_packages::Relation::Clusters.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -3,6 +3,7 @@
pub mod prelude;
pub mod cluster_groups;
pub mod cluster_packages;
pub mod clusters;
pub mod java_versions;
pub mod packages;

View file

@ -3,17 +3,20 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::icon::Icon;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "packages")]
#[onelauncher_macro::specta]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false, column_type = "Text")]
pub hash: String,
#[sea_orm(column_type = "Text")]
pub file_name: String,
#[sea_orm(column_type = "Text")]
pub display_name: String,
#[sea_orm(column_type = "Text")]
pub display_version: String,
#[sea_orm(primary_key, auto_increment = false)]
pub type_id: i32,
pub provider_id: i32,
#[sea_orm(column_type = "Text")]
@ -22,13 +25,29 @@ pub struct Model {
pub mc_versions: String,
#[sea_orm(column_type = "Text")]
pub mc_loader: String,
#[sea_orm(column_type = "Text")]
pub hash: String,
#[sea_orm(column_type = "Text", nullable)]
pub icon_url: Option<String>,
pub icon_url: Option<Icon>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
pub enum Relation {
#[sea_orm(has_many = "super::cluster_packages::Entity")]
ClusterPackages,
}
impl Related<super::cluster_packages::Entity> for Entity {
fn to() -> RelationDef {
Relation::ClusterPackages.def()
}
}
impl Related<super::clusters::Entity> for Entity {
fn to() -> RelationDef {
super::cluster_packages::Relation::Clusters.def()
}
fn via() -> Option<RelationDef> {
Some(super::cluster_packages::Relation::Packages.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,6 +1,7 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.5
pub use super::cluster_groups::Entity as ClusterGroups;
pub use super::cluster_packages::Entity as ClusterPackages;
pub use super::clusters::Entity as Clusters;
pub use super::java_versions::Entity as JavaVersions;
pub use super::packages::Entity as Packages;

View file

@ -1,2 +1,5 @@
mod entities;
pub use entities::*;
mod domain;
pub use entities::*;
pub use domain::*;

View file

@ -48,12 +48,13 @@ create table setting_profiles (
create table clusters (
id integer not null,
path text not null,
stage integer default (0) not null,
created_at integer default (unixepoch()) not null,
updated_at integer default (unixepoch()) not null,
group_id integer,
name text not null,
mc_version text not null,
mc_loader text not null,
mc_loader integer not null,
mc_loader_version text,
last_played integer,
overall_played integer,