Generalized frontend model store (#193)

This commit is contained in:
Gregory Schier
2025-03-31 11:56:17 -07:00
committed by GitHub
parent ce885c3551
commit f1757ae427
201 changed files with 2185 additions and 2865 deletions

View File

@@ -1,24 +1,22 @@
use crate::error::Error::GenericError;
use crate::error::Result;
use crate::models::AnyModel;
use crate::models::{AnyModel, GrpcEvent, Settings, WebsocketEvent};
use crate::query_manager::QueryManagerExt;
use crate::util::UpdateSource;
use tauri::{Runtime, WebviewWindow};
use tauri::{AppHandle, Runtime, WebviewWindow};
#[tauri::command]
pub(crate) async fn upsert<R: Runtime>(
window: WebviewWindow<R>,
model: AnyModel,
) -> Result<String> {
pub(crate) fn upsert<R: Runtime>(window: WebviewWindow<R>, model: AnyModel) -> Result<String> {
let db = window.db();
let source = &UpdateSource::from_window(&window);
let id = match model {
AnyModel::HttpRequest(m) => db.upsert_http_request(&m, source)?.id,
AnyModel::CookieJar(m) => db.upsert_cookie_jar(&m, source)?.id,
AnyModel::Environment(m) => db.upsert_environment(&m, source)?.id,
AnyModel::Folder(m) => db.upsert_folder(&m, source)?.id,
AnyModel::GrpcRequest(m) => db.upsert_grpc_request(&m, source)?.id,
AnyModel::HttpRequest(m) => db.upsert_http_request(&m, source)?.id,
AnyModel::HttpResponse(m) => db.upsert_http_response(&m, source)?.id,
AnyModel::KeyValue(m) => db.upsert_key_value(&m, source)?.id,
AnyModel::Plugin(m) => db.upsert_plugin(&m, source)?.id,
AnyModel::Settings(m) => db.upsert_settings(&m, source)?.id,
AnyModel::WebsocketRequest(m) => db.upsert_websocket_request(&m, source)?.id,
@@ -32,22 +30,95 @@ pub(crate) async fn upsert<R: Runtime>(
#[tauri::command]
pub(crate) fn delete<R: Runtime>(window: WebviewWindow<R>, model: AnyModel) -> Result<String> {
let db = window.db();
let source = &UpdateSource::from_window(&window);
let id = match model {
AnyModel::HttpRequest(m) => db.delete_http_request(&m, source)?.id,
AnyModel::CookieJar(m) => db.delete_cookie_jar(&m, source)?.id,
AnyModel::Environment(m) => db.delete_environment(&m, source)?.id,
AnyModel::Folder(m) => db.delete_folder(&m, source)?.id,
AnyModel::GrpcConnection(m) => db.delete_grpc_connection(&m, source)?.id,
AnyModel::GrpcRequest(m) => db.delete_grpc_request(&m, source)?.id,
AnyModel::HttpResponse(m) => db.delete_http_response(&m, source)?.id,
AnyModel::Plugin(m) => db.delete_plugin(&m, source)?.id,
AnyModel::WebsocketConnection(m) => db.delete_websocket_connection(&m, source)?.id,
AnyModel::WebsocketRequest(m) => db.delete_websocket_request(&m, source)?.id,
AnyModel::Workspace(m) => db.delete_workspace(&m, source)?.id,
a => return Err(GenericError(format!("Cannot delete AnyModel {a:?})"))),
};
Ok(id)
// Use transaction for deletions because it might recurse
window.with_tx(|tx| {
let source = &UpdateSource::from_window(&window);
let id = match model {
AnyModel::CookieJar(m) => tx.delete_cookie_jar(&m, source)?.id,
AnyModel::Environment(m) => tx.delete_environment(&m, source)?.id,
AnyModel::Folder(m) => tx.delete_folder(&m, source)?.id,
AnyModel::GrpcConnection(m) => tx.delete_grpc_connection(&m, source)?.id,
AnyModel::GrpcRequest(m) => tx.delete_grpc_request(&m, source)?.id,
AnyModel::HttpRequest(m) => tx.delete_http_request(&m, source)?.id,
AnyModel::HttpResponse(m) => tx.delete_http_response(&m, source)?.id,
AnyModel::Plugin(m) => tx.delete_plugin(&m, source)?.id,
AnyModel::WebsocketConnection(m) => tx.delete_websocket_connection(&m, source)?.id,
AnyModel::WebsocketRequest(m) => tx.delete_websocket_request(&m, source)?.id,
AnyModel::Workspace(m) => tx.delete_workspace(&m, source)?.id,
a => return Err(GenericError(format!("Cannot delete AnyModel {a:?})"))),
};
Ok(id)
})
}
#[tauri::command]
pub(crate) fn duplicate<R: Runtime>(window: WebviewWindow<R>, model: AnyModel) -> Result<String> {
// Use transaction for duplications because it might recurse
window.with_tx(|tx| {
let source = &UpdateSource::from_window(&window);
let id = match model {
AnyModel::Environment(m) => tx.duplicate_environment(&m, source)?.id,
AnyModel::Folder(m) => tx.duplicate_folder(&m, source)?.id,
AnyModel::GrpcRequest(m) => tx.duplicate_grpc_request(&m, source)?.id,
AnyModel::HttpRequest(m) => tx.duplicate_http_request(&m, source)?.id,
AnyModel::WebsocketRequest(m) => tx.duplicate_websocket_request(&m, source)?.id,
a => return Err(GenericError(format!("Cannot duplicate AnyModel {a:?})"))),
};
Ok(id)
})
}
#[tauri::command]
pub(crate) fn websocket_events<R: Runtime>(
app_handle: AppHandle<R>,
connection_id: &str,
) -> Result<Vec<WebsocketEvent>> {
Ok(app_handle.db().list_websocket_events(connection_id)?)
}
#[tauri::command]
pub(crate) fn grpc_events<R: Runtime>(
app_handle: AppHandle<R>,
connection_id: &str,
) -> Result<Vec<GrpcEvent>> {
Ok(app_handle.db().list_grpc_events(connection_id)?)
}
#[tauri::command]
pub(crate) fn get_settings<R: Runtime>(app_handle: AppHandle<R>) -> Result<Settings> {
Ok(app_handle.db().get_settings())
}
#[tauri::command]
pub(crate) fn workspace_models<R: Runtime>(
window: WebviewWindow<R>,
workspace_id: Option<&str>,
) -> Result<Vec<AnyModel>> {
let db = window.db();
let mut l: Vec<AnyModel> = Vec::new();
// Add the settings
l.push(db.get_settings().into());
// Add global models
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect());
l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
l.append(&mut db.list_plugins()?.into_iter().map(Into::into).collect());
// Add the workspace children
if let Some(wid) = workspace_id {
l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_environments(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_grpc_connections(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_grpc_requests(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_http_requests(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_http_responses(wid, None)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_websocket_connections(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_websocket_requests(wid)?.into_iter().map(Into::into).collect());
l.append(&mut db.list_workspace_metas(wid)?.into_iter().map(Into::into).collect());
}
Ok(l)
}

View File

@@ -11,7 +11,7 @@ use sea_query_rusqlite::RusqliteBinder;
use tokio::sync::mpsc;
pub struct DbContext<'a> {
pub(crate) tx: mpsc::Sender<ModelPayload>,
pub(crate) events_tx: mpsc::Sender<ModelPayload>,
pub(crate) conn: ConnectionOrTx<'a>,
}
@@ -145,7 +145,7 @@ impl<'a> DbContext<'a> {
update_source: source.clone(),
change: ModelChangeEvent::Upsert,
};
self.tx.try_send(payload).unwrap();
self.events_tx.try_send(payload).unwrap();
Ok(m)
}
@@ -170,7 +170,7 @@ impl<'a> DbContext<'a> {
change: ModelChangeEvent::Delete,
};
self.tx.try_send(payload).unwrap();
self.events_tx.try_send(payload).unwrap();
Ok(m.clone())
}
}

View File

@@ -1,4 +1,4 @@
use crate::commands::{delete, upsert};
use crate::commands::*;
use crate::query_manager::QueryManager;
use crate::util::ModelChangeEvent;
use log::info;
@@ -22,11 +22,11 @@ mod commands;
mod connection_or_tx;
mod db_context;
pub mod error;
pub mod query_manager;
pub mod models;
pub mod queries;
pub mod util;
pub mod query_manager;
pub mod render;
pub mod util;
pub struct SqliteConnection(pub Mutex<Pool<SqliteConnectionManager>>);
@@ -38,7 +38,15 @@ impl SqliteConnection {
pub fn init<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("yaak-models")
.invoke_handler(generate_handler![upsert, delete])
.invoke_handler(generate_handler![
upsert,
delete,
duplicate,
workspace_models,
grpc_events,
websocket_events,
get_settings,
])
.setup(|app_handle, _api| {
let app_path = app_handle.path().app_data_dir().unwrap();
create_dir_all(app_path.clone()).expect("Problem creating App directory!");

View File

@@ -105,7 +105,6 @@ pub struct Settings {
pub interface_scale: f32,
pub open_workspace_new_window: Option<bool>,
pub proxy: Option<ProxySetting>,
pub theme: String,
pub theme_dark: String,
pub theme_light: String,
pub update_channel: String,
@@ -148,7 +147,6 @@ impl UpsertModelInfo for Settings {
(InterfaceFontSize, self.interface_font_size.into()),
(InterfaceScale, self.interface_scale.into()),
(OpenWorkspaceNewWindow, self.open_workspace_new_window.into()),
(Theme, self.theme.as_str().into()),
(ThemeDark, self.theme_dark.as_str().into()),
(ThemeLight, self.theme_light.as_str().into()),
(UpdateChannel, self.update_channel.into()),
@@ -167,7 +165,6 @@ impl UpsertModelInfo for Settings {
SettingsIden::InterfaceScale,
SettingsIden::OpenWorkspaceNewWindow,
SettingsIden::Proxy,
SettingsIden::Theme,
SettingsIden::ThemeDark,
SettingsIden::ThemeLight,
SettingsIden::UpdateChannel,
@@ -193,7 +190,6 @@ impl UpsertModelInfo for Settings {
interface_scale: row.get("interface_scale")?,
open_workspace_new_window: row.get("open_workspace_new_window")?,
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
theme: row.get("theme")?,
theme_dark: row.get("theme_dark")?,
theme_light: row.get("theme_light")?,
update_channel: row.get("update_channel")?,
@@ -1751,6 +1747,7 @@ impl UpsertModelInfo for SyncState {
pub struct KeyValue {
#[ts(type = "\"key_value\"")]
pub model: String,
pub id: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
@@ -1759,17 +1756,53 @@ pub struct KeyValue {
pub value: String,
}
impl<'s> TryFrom<&Row<'s>> for KeyValue {
type Error = rusqlite::Error;
impl UpsertModelInfo for KeyValue {
fn table_name() -> impl IntoTableRef {
KeyValueIden::Table
}
fn try_from(r: &Row<'s>) -> std::result::Result<Self, Self::Error> {
fn id_column() -> impl IntoIden + Eq + Clone {
KeyValueIden::Id
}
fn generate_id() -> String {
generate_prefixed_id("kv")
}
fn get_id(&self) -> String {
self.id.clone()
}
fn insert_values(
self,
source: &UpdateSource,
) -> Result<Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>> {
use KeyValueIden::*;
Ok(vec![
(CreatedAt, upsert_date(source, self.created_at)),
(UpdatedAt, upsert_date(source, self.updated_at)),
(Namespace, self.namespace.clone().into()),
(Key, self.key.clone().into()),
(Value, self.value.clone().into()),
])
}
fn update_columns() -> Vec<impl IntoIden> {
vec![KeyValueIden::UpdatedAt, KeyValueIden::Value]
}
fn from_row(row: &Row) -> rusqlite::Result<Self>
where
Self: Sized,
{
Ok(Self {
model: r.get("model")?,
created_at: r.get("created_at")?,
updated_at: r.get("updated_at")?,
namespace: r.get("namespace")?,
key: r.get("key")?,
value: r.get("value")?,
id: row.get("id")?,
model: row.get("model")?,
created_at: row.get("created_at")?,
updated_at: row.get("updated_at")?,
namespace: row.get("namespace")?,
key: row.get("key")?,
value: row.get("value")?,
})
}
}
@@ -1876,18 +1909,23 @@ impl<'de> Deserialize<'de> for AnyModel {
use serde_json::from_value as fv;
let model = match model.get("model") {
Some(m) if m == "http_request" => AnyModel::HttpRequest(fv(value).unwrap()),
Some(m) if m == "grpc_request" => AnyModel::GrpcRequest(fv(value).unwrap()),
Some(m) if m == "workspace" => AnyModel::Workspace(fv(value).unwrap()),
Some(m) if m == "cookie_jar" => AnyModel::CookieJar(fv(value).unwrap()),
Some(m) if m == "environment" => AnyModel::Environment(fv(value).unwrap()),
Some(m) if m == "folder" => AnyModel::Folder(fv(value).unwrap()),
Some(m) if m == "key_value" => AnyModel::KeyValue(fv(value).unwrap()),
Some(m) if m == "grpc_connection" => AnyModel::GrpcConnection(fv(value).unwrap()),
Some(m) if m == "grpc_event" => AnyModel::GrpcEvent(fv(value).unwrap()),
Some(m) if m == "cookie_jar" => AnyModel::CookieJar(fv(value).unwrap()),
Some(m) if m == "grpc_request" => AnyModel::GrpcRequest(fv(value).unwrap()),
Some(m) if m == "http_request" => AnyModel::HttpRequest(fv(value).unwrap()),
Some(m) if m == "key_value" => AnyModel::KeyValue(fv(value).unwrap()),
Some(m) if m == "plugin" => AnyModel::Plugin(fv(value).unwrap()),
Some(m) if m == "settings" => AnyModel::Settings(fv(value).unwrap()),
Some(m) if m == "websocket_connection" => AnyModel::WebsocketConnection(fv(value).unwrap()),
Some(m) if m == "websocket_event" => AnyModel::WebsocketEvent(fv(value).unwrap()),
Some(m) if m == "websocket_request" => AnyModel::WebsocketRequest(fv(value).unwrap()),
Some(m) if m == "workspace" => AnyModel::Workspace(fv(value).unwrap()),
Some(m) if m == "workspace_meta" => AnyModel::WorkspaceMeta(fv(value).unwrap()),
Some(m) => {
return Err(serde::de::Error::custom(format!("Unknown model {}", m)));
return Err(serde::de::Error::custom(format!("Failed to deserialize AnyModel {}", m)));
}
None => {
return Err(serde::de::Error::custom("Missing or invalid model"));
@@ -1905,11 +1943,7 @@ impl AnyModel {
return name.to_string();
}
let without_variables = url.replace(r"\$\{\[\s*([^\]\s]+)\s*]}", "$1");
if without_variables.is_empty() {
fallback.to_string()
} else {
without_variables
}
if without_variables.is_empty() { fallback.to_string() } else { without_variables }
};
match self.clone() {

View File

@@ -9,7 +9,18 @@ impl<'a> DbContext<'a> {
}
pub fn list_cookie_jars(&self, workspace_id: &str) -> Result<Vec<CookieJar>> {
self.find_many(CookieJarIden::WorkspaceId, workspace_id, None)
let mut cookie_jars = self.find_many(CookieJarIden::WorkspaceId, workspace_id, None)?;
if cookie_jars.is_empty() {
let jar = CookieJar {
name: "Default".to_string(),
workspace_id: workspace_id.to_string(),
..Default::default()
};
cookie_jars.push(self.upsert_cookie_jar(&jar, &UpdateSource::Background)?);
}
Ok(cookie_jars)
}
pub fn delete_cookie_jar(

View File

@@ -1,11 +1,8 @@
use crate::error::Result;
use crate::models::{Environment, EnvironmentIden, UpsertModelInfo};
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;
use crate::error::Error::GenericError;
use crate::error::Result;
use crate::models::{Environment, EnvironmentIden};
use crate::util::UpdateSource;
impl<'a> DbContext<'a> {
pub fn get_environment(&self, id: &str) -> Result<Environment> {
@@ -13,42 +10,38 @@ impl<'a> DbContext<'a> {
}
pub fn get_base_environment(&self, workspace_id: &str) -> Result<Environment> {
let (sql, params) = Query::select()
.from(EnvironmentIden::Table)
.column(Asterisk)
.cond_where(
Cond::all()
.add(Expr::col(EnvironmentIden::WorkspaceId).eq(workspace_id))
.add(Expr::col(EnvironmentIden::EnvironmentId).is_null()),
)
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = self.conn.prepare(sql.as_str())?;
Ok(stmt.query_row(&*params.as_params(), Environment::from_row)?)
// Will create base environment if it doesn't exist
let environments = self.list_environments(workspace_id)?;
let base_environment = environments
.into_iter()
.find(|e| e.environment_id == None && e.workspace_id == workspace_id)
.ok_or(GenericError(format!("No base environment found for {workspace_id}")))?;
Ok(base_environment)
}
pub fn ensure_base_environment(&self, workspace_id: &str) -> Result<()> {
let environments = self.list_environments(workspace_id)?;
pub fn list_environments(&self, workspace_id: &str) -> Result<Vec<Environment>> {
let mut environments =
self.find_many::<Environment>(EnvironmentIden::WorkspaceId, workspace_id, None)?;
let base_environment = environments
.iter()
.find(|e| e.environment_id == None && e.workspace_id == workspace_id);
if let None = base_environment {
info!("Creating base environment for {workspace_id}");
self.upsert_environment(
environments.push(self.upsert_environment(
&Environment {
workspace_id: workspace_id.to_string(),
environment_id: None,
name: "Global Variables".to_string(),
..Default::default()
},
&UpdateSource::Background,
)?;
)?);
}
Ok(())
}
pub fn list_environments(&self, workspace_id: &str) -> Result<Vec<Environment>> {
self.find_many(EnvironmentIden::WorkspaceId, workspace_id, None)
Ok(environments)
}
pub fn delete_environment(
@@ -69,6 +62,16 @@ impl<'a> DbContext<'a> {
self.delete_environment(&environment, source)
}
pub fn duplicate_environment(
&self,
environment: &Environment,
source: &UpdateSource,
) -> Result<Environment> {
let mut environment = environment.clone();
environment.id = "".to_string();
self.upsert(&environment, source)
}
pub fn upsert_environment(
&self,
environment: &Environment,

View File

@@ -1,3 +1,4 @@
use crate::connection_or_tx::ConnectionOrTx;
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{
@@ -16,20 +17,29 @@ impl<'a> DbContext<'a> {
}
pub fn delete_folder(&self, folder: &Folder, source: &UpdateSource) -> Result<Folder> {
for folder in self.find_many::<Folder>(FolderIden::FolderId, &folder.id, None)? {
match self.conn {
ConnectionOrTx::Connection(_) => {}
ConnectionOrTx::Transaction(_) => {}
}
let fid = &folder.id;
for m in self.find_many::<HttpRequest>(HttpRequestIden::FolderId, fid, None)? {
self.delete_http_request(&m, source)?;
}
for m in self.find_many::<GrpcRequest>(GrpcRequestIden::FolderId, fid, None)? {
self.delete_grpc_request(&m, source)?;
}
for m in self.find_many::<WebsocketRequest>(WebsocketRequestIden::FolderId, fid, None)? {
self.delete_websocket_request(&m, source)?;
}
// Recurse down into child folders
for folder in self.find_many::<Folder>(FolderIden::FolderId, fid, None)? {
self.delete_folder(&folder, source)?;
}
for request in self.find_many::<HttpRequest>(HttpRequestIden::FolderId, &folder.id, None)? {
self.delete_http_request(&request, source)?;
}
for request in self.find_many::<GrpcRequest>(GrpcRequestIden::FolderId, &folder.id, None)? {
self.delete_grpc_request(&request, source)?;
}
for request in
self.find_many::<WebsocketRequest>(WebsocketRequestIden::FolderId, &folder.id, None)?
{
self.delete_websocket_request(&request, source)?;
}
self.delete(folder, source)
}
@@ -43,22 +53,7 @@ impl<'a> DbContext<'a> {
}
pub fn duplicate_folder(&self, src_folder: &Folder, source: &UpdateSource) -> Result<Folder> {
let workspace_id = src_folder.workspace_id.as_str();
let http_requests = self
.find_many::<HttpRequest>(HttpRequestIden::WorkspaceId, workspace_id, None)?
.into_iter()
.filter(|m| m.folder_id.as_ref() == Some(&src_folder.id));
let grpc_requests = self
.find_many::<GrpcRequest>(GrpcRequestIden::WorkspaceId, workspace_id, None)?
.into_iter()
.filter(|m| m.folder_id.as_ref() == Some(&src_folder.id));
let folders = self
.find_many::<Folder>(FolderIden::WorkspaceId, workspace_id, None)?
.into_iter()
.filter(|m| m.folder_id.as_ref() == Some(&src_folder.id));
let fid = &src_folder.id;
let new_folder = self.upsert_folder(
&Folder {
@@ -69,29 +64,40 @@ impl<'a> DbContext<'a> {
source,
)?;
for m in http_requests {
for m in self.find_many::<HttpRequest>(HttpRequestIden::FolderId, fid, None)? {
self.upsert_http_request(
&HttpRequest {
id: "".into(),
folder_id: Some(new_folder.id.clone()),
sort_priority: m.sort_priority + 0.001,
..m
},
source,
)?;
}
for m in grpc_requests {
for m in self.find_many::<WebsocketRequest>(WebsocketRequestIden::FolderId, fid, None)? {
self.upsert_websocket_request(
&WebsocketRequest {
id: "".into(),
folder_id: Some(new_folder.id.clone()),
..m
},
source,
)?;
}
for m in self.find_many::<GrpcRequest>(GrpcRequestIden::FolderId, fid, None)? {
self.upsert_grpc_request(
&GrpcRequest {
id: "".into(),
folder_id: Some(new_folder.id.clone()),
sort_priority: m.sort_priority + 0.001,
..m
},
source,
)?;
}
for m in folders {
for m in self.find_many::<Folder>(FolderIden::FolderId, fid, None)? {
// Recurse down
self.duplicate_folder(
&Folder {
@@ -101,6 +107,7 @@ impl<'a> DbContext<'a> {
source,
)?;
}
Ok(new_folder)
}
}

View File

@@ -1,11 +1,11 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{GrpcConnection, GrpcConnectionIden, GrpcConnectionState};
use crate::queries::MAX_HISTORY_ITEMS;
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> {
@@ -29,7 +29,7 @@ impl<'a> DbContext<'a> {
workspace_id: &str,
source: &UpdateSource,
) -> Result<()> {
for m in self.list_grpc_connections_for_workspace(workspace_id, None)? {
for m in self.list_grpc_connections(workspace_id)? {
self.delete(&m, source)?;
}
Ok(())
@@ -60,12 +60,8 @@ impl<'a> DbContext<'a> {
self.find_many(GrpcConnectionIden::RequestId, request_id, limit)
}
pub fn list_grpc_connections_for_workspace(
&self,
workspace_id: &str,
limit: Option<u64>,
) -> Result<Vec<GrpcConnection>> {
self.find_many(GrpcConnectionIden::WorkspaceId, workspace_id, limit)
pub fn list_grpc_connections(&self, workspace_id: &str) -> Result<Vec<GrpcConnection>> {
self.find_many(GrpcConnectionIden::WorkspaceId, workspace_id, None)
}
pub fn cancel_pending_grpc_connections(&self) -> Result<()> {

View File

@@ -21,7 +21,7 @@ impl<'a> DbContext<'a> {
self.find_many(HttpResponseIden::RequestId, request_id, limit)
}
pub fn list_http_responses_for_workspace(
pub fn list_http_responses(
&self,
workspace_id: &str,
limit: Option<u64>,

View File

@@ -1,20 +1,19 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{KeyValue, KeyValueIden};
use crate::util::{ModelChangeEvent, ModelPayload, UpdateSource};
use crate::models::{KeyValue, KeyValueIden, UpsertModelInfo};
use crate::util::UpdateSource;
use log::error;
use sea_query::Keyword::CurrentTimestamp;
use sea_query::{Asterisk, Cond, Expr, OnConflict, Query, SqliteQueryBuilder};
use sea_query::{Asterisk, Cond, Expr, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder;
impl<'a> DbContext<'a> {
pub fn list_key_values_raw(&self) -> Result<Vec<KeyValue>> {
pub fn list_key_values(&self) -> Result<Vec<KeyValue>> {
let (sql, params) = Query::select()
.from(KeyValueIden::Table)
.column(Asterisk)
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = self.conn.prepare(sql.as_str())?;
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
let items = stmt.query_map(&*params.as_params(), KeyValue::from_row)?;
Ok(items.map(|v| v.unwrap()).collect())
}
@@ -60,7 +59,7 @@ impl<'a> DbContext<'a> {
.add(Expr::col(KeyValueIden::Key).eq(key)),
)
.build_rusqlite(SqliteQueryBuilder);
self.conn.resolve().query_row(sql.as_str(), &*params.as_params(), |row| row.try_into()).ok()
self.conn.resolve().query_row(sql.as_str(), &*params.as_params(), KeyValue::from_row).ok()
}
pub fn set_key_value_string(
@@ -125,43 +124,7 @@ impl<'a> DbContext<'a> {
key_value: &KeyValue,
source: &UpdateSource,
) -> Result<KeyValue> {
let (sql, params) = Query::insert()
.into_table(KeyValueIden::Table)
.columns([
KeyValueIden::CreatedAt,
KeyValueIden::UpdatedAt,
KeyValueIden::Namespace,
KeyValueIden::Key,
KeyValueIden::Value,
])
.values_panic([
CurrentTimestamp.into(),
CurrentTimestamp.into(),
key_value.namespace.clone().into(),
key_value.key.clone().into(),
key_value.value.clone().into(),
])
.on_conflict(
OnConflict::new()
.update_columns([KeyValueIden::UpdatedAt, KeyValueIden::Value])
.to_owned(),
)
.returning_all()
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = self.conn.prepare(sql.as_str()).expect("Failed to prepare KeyValue upsert");
let m: KeyValue = stmt
.query_row(&*params.as_params(), |row| row.try_into())
.expect("Failed to upsert KeyValue");
let payload = ModelPayload {
model: m.clone().into(),
update_source: source.clone(),
change: ModelChangeEvent::Upsert,
};
self.tx.try_send(payload).unwrap();
Ok(m)
self.upsert(key_value, source)
}
pub fn delete_key_value(
@@ -175,21 +138,7 @@ impl<'a> DbContext<'a> {
Some(m) => m,
};
let (sql, params) = Query::delete()
.from_table(KeyValueIden::Table)
.cond_where(
Cond::all()
.add(Expr::col(KeyValueIden::Namespace).eq(namespace))
.add(Expr::col(KeyValueIden::Key).eq(key)),
)
.build_rusqlite(SqliteQueryBuilder);
self.conn.execute(sql.as_str(), &*params.as_params())?;
let payload = ModelPayload {
model: kv.clone().into(),
update_source: source.clone(),
change: ModelChangeEvent::Delete,
};
self.tx.try_send(payload).unwrap();
self.delete(&kv, source)?;
Ok(())
}
}

View File

@@ -18,4 +18,4 @@ mod websocket_requests;
mod workspace_metas;
mod workspaces;
const MAX_HISTORY_ITEMS: usize = 20;
const MAX_HISTORY_ITEMS: usize = 20;

View File

@@ -1,24 +1,35 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{Settings, SettingsIden};
use crate::models::{EditorKeymap, Settings, SettingsIden};
use crate::util::UpdateSource;
impl<'a> DbContext<'a> {
pub fn get_or_create_settings(&self, source: &UpdateSource) -> Settings {
pub fn get_settings(&self) -> Settings {
let id = "default".to_string();
if let Some(s) = self.find_optional::<Settings>(SettingsIden::Id, &id) {
return s;
};
self.upsert(
&Settings {
id,
..Default::default()
},
source,
)
.expect("Failed to upsert settings")
let settings = Settings {
model: "settings".to_string(),
id,
created_at: Default::default(),
updated_at: Default::default(),
appearance: "system".to_string(),
editor_font_size: 13,
editor_keymap: EditorKeymap::Default,
editor_soft_wrap: true,
interface_font_size: 15,
interface_scale: 1.0,
open_workspace_new_window: None,
proxy: None,
theme_dark: "yaak-dark".to_string(),
theme_light: "yaak-light".to_string(),
update_channel: "stable".to_string(),
};
self.upsert(&settings, &UpdateSource::Background).expect("Failed to upsert settings")
}
pub fn upsert_settings(&self, settings: &Settings, source: &UpdateSource) -> Result<Settings> {

View File

@@ -29,14 +29,14 @@ impl<'a> DbContext<'a> {
workspace_id: &str,
source: &UpdateSource,
) -> Result<()> {
let responses = self.list_websocket_connections_for_workspace(workspace_id)?;
let responses = self.list_websocket_connections(workspace_id)?;
for m in responses {
self.delete(&m, source)?;
}
Ok(())
}
pub fn list_websocket_connections_for_workspace(
pub fn list_websocket_connections(
&self,
workspace_id: &str,
) -> Result<Vec<WebsocketConnection>> {

View File

@@ -1,25 +1,40 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{Workspace, WorkspaceMeta, WorkspaceMetaIden};
use crate::models::{WorkspaceMeta, WorkspaceMetaIden};
use crate::util::UpdateSource;
impl<'a> DbContext<'a> {
pub fn get_workspace_meta(&self, workspace: &Workspace) -> Option<WorkspaceMeta> {
self.find_optional(WorkspaceMetaIden::WorkspaceId, &workspace.id)
pub fn get_workspace_meta(&self, workspace_id: &str) -> Option<WorkspaceMeta> {
self.find_optional(WorkspaceMetaIden::WorkspaceId, workspace_id)
}
pub fn list_workspace_metas(&self, workspace_id: &str) -> Result<Vec<WorkspaceMeta>> {
let mut workspace_metas =
self.find_many(WorkspaceMetaIden::WorkspaceId, workspace_id, None)?;
if workspace_metas.is_empty() {
let wm = WorkspaceMeta {
workspace_id: workspace_id.to_string(),
..Default::default()
};
workspace_metas.push(self.upsert_workspace_meta(&wm, &UpdateSource::Background)?)
}
Ok(workspace_metas)
}
pub fn get_or_create_workspace_meta(
&self,
workspace: &Workspace,
workspace_id: &str,
source: &UpdateSource,
) -> Result<WorkspaceMeta> {
let workspace_meta = self.get_workspace_meta(workspace);
let workspace_meta = self.get_workspace_meta(workspace_id);
if let Some(workspace_meta) = workspace_meta {
return Ok(workspace_meta);
}
let workspace_meta = WorkspaceMeta {
workspace_id: workspace.to_owned().id,
workspace_id: workspace_id.to_string(),
..Default::default()
};

View File

@@ -12,7 +12,21 @@ impl<'a> DbContext<'a> {
}
pub fn list_workspaces(&self) -> Result<Vec<Workspace>> {
self.find_all()
let mut workspaces = self.find_all()?;
if workspaces.is_empty() {
workspaces.push(self.upsert_workspace(
&Workspace {
name: "Yaak".to_string(),
setting_follow_redirects: true,
setting_validate_certificates: true,
..Default::default()
},
&UpdateSource::Background,
)?)
}
Ok(workspaces)
}
pub fn delete_workspace(
@@ -20,24 +34,24 @@ impl<'a> DbContext<'a> {
workspace: &Workspace,
source: &UpdateSource,
) -> Result<Workspace> {
for m in self.find_many::<HttpRequest>(HttpRequestIden::WorkspaceId, &workspace.id, None)? {
self.delete_http_request(&m, source)?;
}
for m in self.find_many::<GrpcRequest>(GrpcRequestIden::WorkspaceId, &workspace.id, None)? {
self.delete_grpc_request(&m, source)?;
}
for m in
self.find_many::<WebsocketRequest>(WebsocketRequestIden::FolderId, &workspace.id, None)?
{
self.delete_websocket_request(&m, source)?;
}
for folder in self.find_many::<Folder>(FolderIden::WorkspaceId, &workspace.id, None)? {
self.delete_folder(&folder, source)?;
}
for request in
self.find_many::<HttpRequest>(HttpRequestIden::WorkspaceId, &workspace.id, None)?
{
self.delete_http_request(&request, source)?;
}
for request in
self.find_many::<GrpcRequest>(GrpcRequestIden::WorkspaceId, &workspace.id, None)?
{
self.delete_grpc_request(&request, source)?;
}
for request in
self.find_many::<WebsocketRequest>(WebsocketRequestIden::FolderId, &workspace.id, None)?
{
self.delete_websocket_request(&request, source)?;
}
self.delete(workspace, source)
}

View File

@@ -22,7 +22,7 @@ pub trait QueryManagerExt<'a, R> {
impl<'a, R: Runtime, M: Manager<R>> QueryManagerExt<'a, R> for M {
fn db(&'a self) -> DbContext<'a> {
let qm = self.state::<QueryManager>();
qm.inner().connect_2()
qm.inner().connect()
}
fn with_db<F, T>(&'a self, func: F) -> T
@@ -59,7 +59,7 @@ impl QueryManager {
}
}
pub fn connect_2(&self) -> DbContext {
pub fn connect(&self) -> DbContext {
let conn = self
.pool
.lock()
@@ -67,7 +67,7 @@ impl QueryManager {
.get()
.expect("Failed to get a new DB connection from the pool");
DbContext {
tx: self.events_tx.clone(),
events_tx: self.events_tx.clone(),
conn: ConnectionOrTx::Connection(conn),
}
}
@@ -84,7 +84,7 @@ impl QueryManager {
.expect("Failed to get new DB connection from the pool");
let db_context = DbContext {
tx: self.events_tx.clone(),
events_tx: self.events_tx.clone(),
conn: ConnectionOrTx::Connection(conn),
};
@@ -106,7 +106,7 @@ impl QueryManager {
.expect("Failed to start DB transaction");
let db_context = DbContext {
tx: self.events_tx.clone(),
events_tx: self.events_tx.clone(),
conn: ConnectionOrTx::Transaction(&tx),
};

View File

@@ -45,11 +45,11 @@ pub enum ModelChangeEvent {
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "gen_models.ts")]
pub enum UpdateSource {
Sync,
Window { label: String },
Plugin,
Background,
Import,
Plugin,
Sync,
Window { label: String },
}
impl UpdateSource {