mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-10 03:03:37 +02:00
Clean up DB refactor access (#192)
This commit is contained in:
@@ -1,169 +0,0 @@
|
||||
use crate::error::Error::RowNotFound;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{AnyModel, ModelType, UpsertModelInfo};
|
||||
use crate::queries_legacy::{generate_model_id, ModelChangeEvent, ModelPayload, UpdateSource};
|
||||
use rusqlite::OptionalExtension;
|
||||
use sea_query::{
|
||||
Asterisk, Expr, IntoColumnRef, IntoIden, IntoTableRef, OnConflict, Query, SimpleExpr,
|
||||
SqliteQueryBuilder,
|
||||
};
|
||||
use sea_query_rusqlite::RusqliteBinder;
|
||||
|
||||
pub(crate) const MAX_HISTORY_ITEMS: usize = 20;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub(crate) fn find_one<'s, M>(
|
||||
&self,
|
||||
col: impl IntoColumnRef,
|
||||
value: impl Into<SimpleExpr>,
|
||||
) -> Result<M>
|
||||
where
|
||||
M: Into<AnyModel> + Clone + UpsertModelInfo,
|
||||
{
|
||||
match self.find_optional::<M>(col, value) {
|
||||
Ok(Some(v)) => Ok(v),
|
||||
Ok(None) => Err(RowNotFound),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_optional<'s, M>(
|
||||
&self,
|
||||
col: impl IntoColumnRef,
|
||||
value: impl Into<SimpleExpr>,
|
||||
) -> Result<Option<M>>
|
||||
where
|
||||
M: Into<AnyModel> + Clone + UpsertModelInfo,
|
||||
{
|
||||
let (sql, params) = Query::select()
|
||||
.from(M::table_name())
|
||||
.column(Asterisk)
|
||||
.cond_where(Expr::col(col).eq(value))
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let mut stmt = self.conn.prepare(sql.as_str())?;
|
||||
Ok(stmt.query_row(&*params.as_params(), M::from_row).optional()?)
|
||||
}
|
||||
|
||||
pub fn find_all<'s, M>(&self) -> Result<Vec<M>>
|
||||
where
|
||||
M: Into<AnyModel> + Clone + UpsertModelInfo,
|
||||
{
|
||||
let (sql, params) = Query::select()
|
||||
.from(M::table_name())
|
||||
.column(Asterisk)
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
let mut stmt = self.conn.resolve().prepare(sql.as_str())?;
|
||||
let items = stmt.query_map(&*params.as_params(), M::from_row)?;
|
||||
Ok(items.map(|v| v.unwrap()).collect())
|
||||
}
|
||||
|
||||
pub fn find_many<'s, M>(
|
||||
&self,
|
||||
col: impl IntoColumnRef,
|
||||
value: impl Into<SimpleExpr>,
|
||||
limit: Option<u64>,
|
||||
) -> Result<Vec<M>>
|
||||
where
|
||||
M: Into<AnyModel> + Clone + UpsertModelInfo,
|
||||
{
|
||||
// TODO: Figure out how to do this conditional builder better
|
||||
let (sql, params) = if let Some(limit) = limit {
|
||||
Query::select()
|
||||
.from(M::table_name())
|
||||
.column(Asterisk)
|
||||
.cond_where(Expr::col(col).eq(value))
|
||||
.limit(limit)
|
||||
.build_rusqlite(SqliteQueryBuilder)
|
||||
} else {
|
||||
Query::select()
|
||||
.from(M::table_name())
|
||||
.column(Asterisk)
|
||||
.cond_where(Expr::col(col).eq(value))
|
||||
.build_rusqlite(SqliteQueryBuilder)
|
||||
};
|
||||
|
||||
let mut stmt = self.conn.resolve().prepare(sql.as_str())?;
|
||||
let items = stmt.query_map(&*params.as_params(), M::from_row)?;
|
||||
Ok(items.map(|v| v.unwrap()).collect())
|
||||
}
|
||||
|
||||
pub fn upsert<M>(&self, model: &M, source: &UpdateSource) -> Result<M>
|
||||
where
|
||||
M: Into<AnyModel> + From<AnyModel> + UpsertModelInfo + Clone,
|
||||
{
|
||||
self.upsert_one(
|
||||
M::table_name(),
|
||||
M::id_column(),
|
||||
model.get_id().as_str(),
|
||||
|| generate_model_id(ModelType::TypeEnvironment),
|
||||
model.clone().insert_values(source)?,
|
||||
M::update_columns(),
|
||||
source,
|
||||
)
|
||||
}
|
||||
|
||||
fn upsert_one<M>(
|
||||
&self,
|
||||
table: impl IntoTableRef,
|
||||
id_col: impl IntoIden + Eq + Clone,
|
||||
id_val: &str,
|
||||
gen_id: fn() -> String,
|
||||
other_values: Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>,
|
||||
update_columns: Vec<impl IntoIden>,
|
||||
source: &UpdateSource,
|
||||
) -> Result<M>
|
||||
where
|
||||
M: Into<AnyModel> + From<AnyModel> + UpsertModelInfo + Clone,
|
||||
{
|
||||
let id_iden = id_col.into_iden();
|
||||
let mut column_vec = vec![id_iden.clone()];
|
||||
let mut value_vec = vec![if id_val == "" { gen_id().into() } else { id_val.into() }];
|
||||
|
||||
for (col, val) in other_values {
|
||||
value_vec.push(val.into());
|
||||
column_vec.push(col.into_iden());
|
||||
}
|
||||
|
||||
let on_conflict = OnConflict::column(id_iden).update_columns(update_columns).to_owned();
|
||||
let (sql, params) = Query::insert()
|
||||
.into_table(table)
|
||||
.columns(column_vec)
|
||||
.values_panic(value_vec)
|
||||
.on_conflict(on_conflict)
|
||||
.returning_all()
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
|
||||
let mut stmt = self.conn.resolve().prepare(sql.as_str())?;
|
||||
let m: M = stmt.query_row(&*params.as_params(), |row| M::from_row(row))?;
|
||||
|
||||
let payload = ModelPayload {
|
||||
model: m.clone().into(),
|
||||
update_source: source.clone(),
|
||||
change: ModelChangeEvent::Upsert,
|
||||
};
|
||||
self.tx.try_send(payload).unwrap();
|
||||
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
pub(crate) fn delete<'s, M>(&self, m: &M, update_source: &UpdateSource) -> Result<M>
|
||||
where
|
||||
M: Into<AnyModel> + Clone + UpsertModelInfo,
|
||||
{
|
||||
let (sql, params) = Query::delete()
|
||||
.from_table(M::table_name())
|
||||
.cond_where(Expr::col(M::id_column().into_iden()).eq(m.get_id()))
|
||||
.build_rusqlite(SqliteQueryBuilder);
|
||||
self.conn.execute(sql.as_str(), &*params.as_params())?;
|
||||
|
||||
let payload = ModelPayload {
|
||||
model: m.clone().into(),
|
||||
update_source: update_source.clone(),
|
||||
change: ModelChangeEvent::Delete,
|
||||
};
|
||||
|
||||
self.tx.try_send(payload).unwrap();
|
||||
Ok(m.clone())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace};
|
||||
use crate::queries_legacy::{BatchUpsertResult, UpdateSource};
|
||||
use crate::util::{BatchUpsertResult, UpdateSource};
|
||||
use log::info;
|
||||
use crate::db_context::DbContext;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn batch_upsert(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{CookieJar, CookieJarIden};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_cookie_jar(&self, id: &str) -> Result<CookieJar> {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{Environment, EnvironmentIden, UpsertModelInfo};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
use log::info;
|
||||
use sea_query::ColumnRef::Asterisk;
|
||||
use sea_query::{Cond, Expr, Query, SqliteQueryBuilder};
|
||||
use sea_query_rusqlite::RusqliteBinder;
|
||||
use crate::db_context::DbContext;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_environment(&self, id: &str) -> Result<Environment> {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{
|
||||
Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden,
|
||||
WebsocketRequest, WebsocketRequestIden,
|
||||
};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_folder(&self, id: &str) -> Result<Folder> {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{GrpcConnection, GrpcConnectionIden, GrpcConnectionState};
|
||||
use crate::queries::base::MAX_HISTORY_ITEMS;
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
use log::debug;
|
||||
use sea_query::{Expr, Query, SqliteQueryBuilder};
|
||||
use sea_query_rusqlite::RusqliteBinder;
|
||||
use crate::db_context::DbContext;
|
||||
use crate::queries::MAX_HISTORY_ITEMS;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_grpc_connection(&self, id: &str) -> Result<GrpcConnection> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{GrpcEvent, GrpcEventIden};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_grpc_events(&self, id: &str) -> Result<GrpcEvent> {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{GrpcRequest, GrpcRequestIden};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_grpc_request(&self, id: &str) -> Result<Option<GrpcRequest>> {
|
||||
self.find_optional(GrpcRequestIden::Id, id)
|
||||
pub fn get_grpc_request(&self, id: &str) -> Result<GrpcRequest> {
|
||||
self.find_one(GrpcRequestIden::Id, id)
|
||||
}
|
||||
|
||||
pub fn list_grpc_requests(&self, workspace_id: &str) -> Result<Vec<GrpcRequest>> {
|
||||
@@ -26,7 +26,7 @@ impl<'a> DbContext<'a> {
|
||||
id: &str,
|
||||
source: &UpdateSource,
|
||||
) -> Result<GrpcRequest> {
|
||||
let request = self.get_grpc_request(id)?.unwrap();
|
||||
let request = self.get_grpc_request(id)?;
|
||||
self.delete_grpc_request(&request, source)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{HttpRequest, HttpRequestIden};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_http_request(&self, id: &str) -> Result<Option<HttpRequest>> {
|
||||
self.find_optional(HttpRequestIden::Id, id)
|
||||
pub fn get_http_request(&self, id: &str) -> Result<HttpRequest> {
|
||||
self.find_one(HttpRequestIden::Id, id)
|
||||
}
|
||||
|
||||
pub fn list_http_requests(&self, workspace_id: &str) -> Result<Vec<HttpRequest>> {
|
||||
@@ -26,7 +26,7 @@ impl<'a> DbContext<'a> {
|
||||
id: &str,
|
||||
source: &UpdateSource,
|
||||
) -> Result<HttpRequest> {
|
||||
let http_request = self.get_http_request(id)?.unwrap();
|
||||
let http_request = self.get_http_request(id)?;
|
||||
self.delete_http_request(&http_request, source)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{HttpResponse, HttpResponseIden, HttpResponseState};
|
||||
use crate::queries::base::MAX_HISTORY_ITEMS;
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
use log::{debug, error};
|
||||
use sea_query::{Expr, Query, SqliteQueryBuilder};
|
||||
use sea_query_rusqlite::RusqliteBinder;
|
||||
use std::fs;
|
||||
use crate::db_context::DbContext;
|
||||
use crate::queries::MAX_HISTORY_ITEMS;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_http_response(&self, id: &str) -> Result<HttpResponse> {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{KeyValue, KeyValueIden};
|
||||
use crate::queries_legacy::{ModelChangeEvent, ModelPayload, UpdateSource};
|
||||
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
|
||||
use log::error;
|
||||
use sea_query::Keyword::CurrentTimestamp;
|
||||
use sea_query::{Asterisk, Cond, Expr, OnConflict, Query, SqliteQueryBuilder};
|
||||
use sea_query_rusqlite::RusqliteBinder;
|
||||
use crate::db_context::DbContext;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn list_key_values_raw(&self) -> Result<Vec<KeyValue>> {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
mod base;
|
||||
mod batch;
|
||||
mod cookie_jars;
|
||||
mod environments;
|
||||
@@ -18,3 +17,5 @@ mod websocket_events;
|
||||
mod websocket_requests;
|
||||
mod workspace_metas;
|
||||
mod workspaces;
|
||||
|
||||
const MAX_HISTORY_ITEMS: usize = 20;
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{PluginKeyValue, PluginKeyValueIden};
|
||||
use sea_query::Keyword::CurrentTimestamp;
|
||||
use sea_query::{Asterisk, Cond, Expr, OnConflict, Query, SqliteQueryBuilder};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{Plugin, PluginIden};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_plugin(&self, id: &str) -> Result<Plugin> {
|
||||
self.find_one(PluginIden::Id, id)
|
||||
}
|
||||
|
||||
|
||||
pub fn list_plugins(&self) -> Result<Vec<Plugin>> {
|
||||
self.find_all()
|
||||
}
|
||||
@@ -20,7 +20,7 @@ impl<'a> DbContext<'a> {
|
||||
let plugin = self.get_plugin(id)?;
|
||||
self.delete_plugin(&plugin, source)
|
||||
}
|
||||
|
||||
|
||||
pub fn upsert_plugin(&self, plugin: &Plugin, source: &UpdateSource) -> Result<Plugin> {
|
||||
self.upsert(plugin, source)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{Settings, SettingsIden};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_or_create_settings(&self, source: &UpdateSource) -> Result<Settings> {
|
||||
let id = "default";
|
||||
if let Some(s) = self.find_optional::<Settings>(SettingsIden::Id, id)? {
|
||||
return Ok(s);
|
||||
pub fn get_or_create_settings(&self, source: &UpdateSource) -> Settings {
|
||||
let id = "default".to_string();
|
||||
|
||||
if let Some(s) = self.find_optional::<Settings>(SettingsIden::Id, &id) {
|
||||
return s;
|
||||
};
|
||||
|
||||
self.upsert(
|
||||
&Settings {
|
||||
id: id.to_string(),
|
||||
id,
|
||||
..Default::default()
|
||||
},
|
||||
source,
|
||||
)
|
||||
.expect("Failed to upsert settings")
|
||||
}
|
||||
|
||||
pub fn upsert_settings(&self, settings: &Settings, source: &UpdateSource) -> Result<Settings> {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{SyncState, SyncStateIden, UpsertModelInfo};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
use sea_query::{Asterisk, Cond, Expr, Query, SqliteQueryBuilder};
|
||||
use sea_query_rusqlite::RusqliteBinder;
|
||||
use std::path::Path;
|
||||
use crate::db_context::DbContext;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_sync_state(&self, id: &str) -> Result<SyncState> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{WebsocketConnection, WebsocketConnectionIden, WebsocketConnectionState};
|
||||
use crate::queries::base::MAX_HISTORY_ITEMS;
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::queries::MAX_HISTORY_ITEMS;
|
||||
use crate::util::UpdateSource;
|
||||
use log::debug;
|
||||
use sea_query::{Expr, Query, SqliteQueryBuilder};
|
||||
use sea_query_rusqlite::RusqliteBinder;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{
|
||||
WebsocketEvent,
|
||||
WebsocketEventIden,
|
||||
};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_websocket_event(&self, id: &str) -> Result<WebsocketEvent> {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{WebsocketRequest, WebsocketRequestIden};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_websocket_request(&self, id: &str) -> Result<Option<WebsocketRequest>> {
|
||||
self.find_optional(WebsocketRequestIden::Id, id)
|
||||
pub fn get_websocket_request(&self, id: &str) -> Result<WebsocketRequest> {
|
||||
self.find_one(WebsocketRequestIden::Id, id)
|
||||
}
|
||||
|
||||
pub fn list_websocket_requests(&self, workspace_id: &str) -> Result<Vec<WebsocketRequest>> {
|
||||
@@ -26,7 +26,7 @@ impl<'a> DbContext<'a> {
|
||||
id: &str,
|
||||
source: &UpdateSource,
|
||||
) -> Result<WebsocketRequest> {
|
||||
let request = self.get_websocket_request(id)?.unwrap();
|
||||
let request = self.get_websocket_request(id)?;
|
||||
self.delete_websocket_request(&request, source)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{Workspace, WorkspaceMeta, WorkspaceMetaIden};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_workspace_meta(&self, workspace: &Workspace) -> Result<Option<WorkspaceMeta>> {
|
||||
pub fn get_workspace_meta(&self, workspace: &Workspace) -> Option<WorkspaceMeta> {
|
||||
self.find_optional(WorkspaceMetaIden::WorkspaceId, &workspace.id)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ impl<'a> DbContext<'a> {
|
||||
workspace: &Workspace,
|
||||
source: &UpdateSource,
|
||||
) -> Result<WorkspaceMeta> {
|
||||
let workspace_meta = self.get_workspace_meta(workspace)?;
|
||||
let workspace_meta = self.get_workspace_meta(workspace);
|
||||
if let Some(workspace_meta) = workspace_meta {
|
||||
return Ok(workspace_meta);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::db_context::DbContext;
|
||||
use crate::error::Result;
|
||||
use crate::manager::DbContext;
|
||||
use crate::models::{
|
||||
Folder, FolderIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden,
|
||||
WebsocketRequest, WebsocketRequestIden, Workspace, WorkspaceIden,
|
||||
};
|
||||
use crate::queries_legacy::UpdateSource;
|
||||
use crate::util::UpdateSource;
|
||||
|
||||
impl<'a> DbContext<'a> {
|
||||
pub fn get_workspace(&self, id: &str) -> Result<Workspace> {
|
||||
|
||||
Reference in New Issue
Block a user