Separate model for GQL introspection data (#222)

This commit is contained in:
Gregory Schier
2025-06-01 06:56:00 -07:00
committed by GitHub
parent f9ac36caf0
commit af230a8f45
18 changed files with 267 additions and 60 deletions

View File

@@ -1,6 +1,6 @@
use crate::error::Error::GenericError;
use crate::error::Result;
use crate::models::{AnyModel, GrpcEvent, Settings, WebsocketEvent};
use crate::models::{AnyModel, GraphQlIntrospection, GrpcEvent, Settings, WebsocketEvent};
use crate::query_manager::QueryManagerExt;
use crate::util::UpdateSource;
use tauri::{AppHandle, Runtime, WebviewWindow};
@@ -90,6 +90,26 @@ pub(crate) fn get_settings<R: Runtime>(app_handle: AppHandle<R>) -> Result<Setti
Ok(app_handle.db().get_settings())
}
#[tauri::command]
pub(crate) fn get_graphql_introspection<R: Runtime>(
app_handle: AppHandle<R>,
request_id: &str,
) -> Result<Option<GraphQlIntrospection>> {
Ok(app_handle.db().get_graphql_introspection(request_id))
}
#[tauri::command]
pub(crate) fn upsert_graphql_introspection<R: Runtime>(
app_handle: AppHandle<R>,
request_id: &str,
workspace_id: &str,
content: Option<String>,
window: WebviewWindow<R>,
) -> Result<GraphQlIntrospection> {
let source = UpdateSource::from_window(&window);
Ok(app_handle.db().upsert_graphql_introspection(workspace_id, request_id, content, &source)?)
}
#[tauri::command]
pub(crate) fn workspace_models<R: Runtime>(
window: WebviewWindow<R>,
@@ -121,11 +141,11 @@ pub(crate) fn workspace_models<R: Runtime>(
}
let j = serde_json::to_string(&l)?;
// NOTE: There's something weird that happens on Linux. If we send Cyrillic (or maybe other)
// unicode characters in this response (doesn't matter where) then the following bug happens:
// https://feedback.yaak.app/p/editing-the-url-sometimes-freezes-the-app
//
//
// It's as if every string resulting from the JSON.parse of the models gets encoded slightly
// wrong or something, causing the above bug where Codemirror can't calculate the cursor
// position anymore (even when none of the characters are included directly in the input).
@@ -137,19 +157,22 @@ pub(crate) fn workspace_models<R: Runtime>(
}
fn escape_str_for_webview(input: &str) -> String {
input.chars().map(|c| {
let code = c as u32;
// ASCII
if code <= 0x7F {
c.to_string()
// BMP characters encoded normally
} else if code < 0xFFFF {
format!("\\u{:04X}", code)
// Beyond BMP encoded a surrogate pairs
} else {
let high = ((code - 0x10000) >> 10) + 0xD800;
let low = ((code - 0x10000) & 0x3FF) + 0xDC00;
format!("\\u{:04X}\\u{:04X}", high, low)
}
}).collect()
}
input
.chars()
.map(|c| {
let code = c as u32;
// ASCII
if code <= 0x7F {
c.to_string()
// BMP characters encoded normally
} else if code < 0xFFFF {
format!("\\u{:04X}", code)
// Beyond BMP encoded a surrogate pairs
} else {
let high = ((code - 0x10000) >> 10) + 0xD800;
let low = ((code - 0x10000) & 0x3FF) + 0xDC00;
format!("\\u{:04X}\\u{:04X}", high, low)
}
})
.collect()
}

View File

@@ -4,9 +4,9 @@ use crate::util::ModelChangeEvent;
use log::info;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use sqlx::SqlitePool;
use sqlx::migrate::Migrator;
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::SqlitePool;
use std::fs::create_dir_all;
use std::path::PathBuf;
use std::str::FromStr;
@@ -14,7 +14,7 @@ use std::time::Duration;
use tauri::async_runtime::Mutex;
use tauri::path::BaseDirectory;
use tauri::plugin::TauriPlugin;
use tauri::{generate_handler, AppHandle, Emitter, Manager, Runtime};
use tauri::{AppHandle, Emitter, Manager, Runtime, generate_handler};
use tokio::sync::mpsc;
mod commands;
@@ -39,13 +39,15 @@ impl SqliteConnection {
pub fn init<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("yaak-models")
.invoke_handler(generate_handler![
upsert,
delete,
duplicate,
workspace_models,
grpc_events,
websocket_events,
get_graphql_introspection,
get_settings,
grpc_events,
upsert,
upsert_graphql_introspection,
websocket_events,
workspace_models,
])
.setup(|app_handle, _api| {
let app_path = app_handle.path().app_data_dir().unwrap();

View File

@@ -1342,6 +1342,79 @@ impl UpsertModelInfo for HttpResponse {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
#[enum_def(table_name = "graphql_introspections")]
pub struct GraphQlIntrospection {
#[ts(type = "\"graphql_introspection\"")]
pub model: String,
pub id: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub workspace_id: String,
pub request_id: String,
pub content: Option<String>,
}
impl UpsertModelInfo for GraphQlIntrospection {
fn table_name() -> impl IntoTableRef {
GraphQlIntrospectionIden::Table
}
fn id_column() -> impl IntoIden + Eq + Clone {
GraphQlIntrospectionIden::Id
}
fn generate_id() -> String {
generate_prefixed_id("gi")
}
fn order_by() -> (impl IntoColumnRef, Order) {
(GraphQlIntrospectionIden::CreatedAt, Desc)
}
fn get_id(&self) -> String {
self.id.clone()
}
fn insert_values(
self,
source: &UpdateSource,
) -> Result<Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>> {
use GraphQlIntrospectionIden::*;
Ok(vec![
(CreatedAt, upsert_date(source, self.created_at)),
(UpdatedAt, upsert_date(source, self.updated_at)),
(WorkspaceId, self.workspace_id.into()),
(RequestId, self.request_id.into()),
(Content, self.content.into()),
])
}
fn update_columns() -> Vec<impl IntoIden> {
vec![
GraphQlIntrospectionIden::UpdatedAt,
GraphQlIntrospectionIden::Content,
]
}
fn from_row(r: &Row) -> rusqlite::Result<Self>
where
Self: Sized,
{
Ok(Self {
id: r.get("id")?,
model: r.get("model")?,
created_at: r.get("created_at")?,
updated_at: r.get("updated_at")?,
workspace_id: r.get("workspace_id")?,
request_id: r.get("request_id")?,
content: r.get("content")?,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
@@ -2002,6 +2075,7 @@ define_any_model! {
CookieJar,
Environment,
Folder,
GraphQlIntrospection,
GrpcConnection,
GrpcEvent,
GrpcRequest,
@@ -2031,6 +2105,9 @@ impl<'de> Deserialize<'de> for AnyModel {
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 == "graphql_introspection" => {
AnyModel::GraphQlIntrospection(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 == "grpc_request" => AnyModel::GrpcRequest(fv(value).unwrap()),

View File

@@ -0,0 +1,55 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{GraphQlIntrospection, GraphQlIntrospectionIden};
use crate::util::UpdateSource;
use chrono::{Duration, Utc};
use sea_query::{Expr, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder;
impl<'a> DbContext<'a> {
pub fn get_graphql_introspection(&self, request_id: &str) -> Option<GraphQlIntrospection> {
self.find_optional(GraphQlIntrospectionIden::RequestId, request_id)
}
pub fn upsert_graphql_introspection(
&self,
workspace_id: &str,
request_id: &str,
content: Option<String>,
source: &UpdateSource,
) -> Result<GraphQlIntrospection> {
// Clean up old ones every time a new one is upserted
self.delete_expired_graphql_introspections()?;
match self.get_graphql_introspection(request_id) {
None => self.upsert(
&GraphQlIntrospection {
content,
request_id: request_id.to_string(),
workspace_id: workspace_id.to_string(),
..Default::default()
},
source,
),
Some(introspection) => self.upsert(
&GraphQlIntrospection {
content,
..introspection
},
source,
),
}
}
pub fn delete_expired_graphql_introspections(&self) -> Result<()> {
let cutoff = Utc::now().naive_utc() - Duration::days(7);
let (sql, params) = Query::delete()
.from_table(GraphQlIntrospectionIden::Table)
.cond_where(Expr::col(GraphQlIntrospectionIden::UpdatedAt).lt(cutoff))
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = self.conn.resolve().prepare(sql.as_str())?;
stmt.execute(&*params.as_params())?;
Ok(())
}
}

View File

@@ -2,6 +2,7 @@ mod batch;
mod cookie_jars;
mod environments;
mod folders;
mod graphql_introspections;
mod grpc_connections;
mod grpc_events;
mod grpc_requests;