mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-09-10 03:41:52 +02:00
feat(rpc): expose snapshot, compare and restore for request versions
Three commands, all host-independent so the browser build answers them from the same model layer as the desktop: - models_snapshot_request, for the edit-session boundaries only the frontend can see. It takes a reason and nothing else — the caller does not decide whether anything changed, because content addressing already has. - models_request_version, which returns a version and the live request's content together so they are guaranteed comparable, plus the same content-hash verdict the backend uses rather than a second opinion formed in TypeScript. - models_restore_request_version. The browser host also snapshots before its own send, since its send pipeline is in TypeScript rather than in the shared crate. Bindings regenerated with CI's `cargo test --all --features yaak-app-client/wry`, which also drops a stale ImportSourceResource from the rpc-schema copy and adds a missing ImportSource to the plugins copy.
This commit is contained in:
+51
-11
@@ -138,6 +138,10 @@ export type GrpcConnection = {
|
||||
state: GrpcConnectionState;
|
||||
trailers: { [key in string]?: string };
|
||||
url: string;
|
||||
/**
|
||||
* The request version this connection was opened from, when one was captured.
|
||||
*/
|
||||
versionId: string | null;
|
||||
};
|
||||
|
||||
export type GrpcConnectionState = "initialized" | "connected" | "closed";
|
||||
@@ -242,6 +246,10 @@ export type HttpResponse = {
|
||||
state: HttpResponseState;
|
||||
url: string;
|
||||
version: string | null;
|
||||
/**
|
||||
* The request version this response was sent from, when one was captured.
|
||||
*/
|
||||
versionId: string | null;
|
||||
};
|
||||
|
||||
export type HttpResponseEvent = {
|
||||
@@ -331,17 +339,6 @@ export type ImportSource = {
|
||||
lastImportedAt: string;
|
||||
};
|
||||
|
||||
export type ImportSourceResource = {
|
||||
model: "import_source_resource";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
importSourceId: string;
|
||||
sourceKey: string;
|
||||
modelType: string;
|
||||
modelId: string;
|
||||
snapshot: string;
|
||||
};
|
||||
|
||||
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
||||
|
||||
export type InheritedHttpVersionSetting = { enabled?: boolean; value: HttpVersion };
|
||||
@@ -358,6 +355,28 @@ export type KeyValue = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type ModelVersion = {
|
||||
model: "model_version";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
/**
|
||||
* The `model` field of the versioned model, eg. `http_request`.
|
||||
*/
|
||||
modelType: string;
|
||||
modelId: string;
|
||||
contentHash: string;
|
||||
document: Record<string, any>;
|
||||
reason: ModelVersionReason;
|
||||
};
|
||||
|
||||
/**
|
||||
* Why a version was captured. Not a UI label — the frontend decides how to
|
||||
* phrase these — but it is what makes a history readable when debugging.
|
||||
*/
|
||||
export type ModelVersionReason = "send" | "switch" | "idle" | "restore" | "manual";
|
||||
|
||||
export type Plugin = {
|
||||
model: "plugin";
|
||||
id: string;
|
||||
@@ -385,6 +404,23 @@ export type ProxySetting =
|
||||
|
||||
export type ProxySettingAuth = { user: string; password: string };
|
||||
|
||||
/**
|
||||
* One version, next to the request as it stands now.
|
||||
*
|
||||
* Both halves come from the same place so they are guaranteed comparable: the
|
||||
* frontend renders them side by side, and `differs` is the same content-hash
|
||||
* comparison the backend uses everywhere else rather than a second opinion
|
||||
* formed in TypeScript.
|
||||
*/
|
||||
export type RequestVersionComparison = {
|
||||
version: ModelVersion;
|
||||
/**
|
||||
* The live request's editable content, in the same shape as the version's document.
|
||||
*/
|
||||
currentDocument: Record<string, any>;
|
||||
differs: boolean;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
model: "settings";
|
||||
id: string;
|
||||
@@ -449,6 +485,10 @@ export type WebsocketConnection = {
|
||||
state: WebsocketConnectionState;
|
||||
status: number;
|
||||
url: string;
|
||||
/**
|
||||
* The request version this connection was opened from, when one was captured.
|
||||
*/
|
||||
versionId: string | null;
|
||||
};
|
||||
|
||||
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
|
||||
|
||||
+8
-2
File diff suppressed because one or more lines are too long
@@ -21,8 +21,8 @@ use yaak_git::{
|
||||
use yaak_grpc::ServiceDefinition;
|
||||
use yaak_models::models::{
|
||||
AnyModel, GraphQlIntrospection, GrpcEvent, HttpRequest, HttpRequestHeader, HttpResponse,
|
||||
HttpResponseEvent, ImportSource, Plugin, Settings, WebsocketConnection, WebsocketEvent,
|
||||
WorkspaceMeta,
|
||||
HttpResponseEvent, ImportSource, ModelVersion, ModelVersionReason, Plugin,
|
||||
RequestVersionComparison, Settings, WebsocketConnection, WebsocketEvent, WorkspaceMeta,
|
||||
};
|
||||
use yaak_models::util::{BatchUpsertResult, ImportDestination, ImportPlan};
|
||||
use yaak_plugins::api::{PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse};
|
||||
@@ -534,6 +534,28 @@ pub struct ModelsDuplicateReq {
|
||||
pub model_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ModelsSnapshotRequestReq {
|
||||
pub request_id: String,
|
||||
pub reason: ModelVersionReason,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ModelsRequestVersionReq {
|
||||
pub version_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
pub struct ModelsRestoreRequestVersionReq {
|
||||
pub version_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_rpc.ts")]
|
||||
@@ -981,6 +1003,9 @@ macro_rules! with_commands {
|
||||
models_upsert(ModelsUpsertReq) -> String,
|
||||
models_delete(ModelsDeleteReq) -> String,
|
||||
models_duplicate(ModelsDuplicateReq) -> String,
|
||||
models_snapshot_request(ModelsSnapshotRequestReq) -> ModelVersion,
|
||||
models_request_version(ModelsRequestVersionReq) -> RequestVersionComparison,
|
||||
models_restore_request_version(ModelsRestoreRequestVersionReq) -> String,
|
||||
models_websocket_events(ModelsWebsocketEventsReq) -> Vec<WebsocketEvent>,
|
||||
models_grpc_events(ModelsGrpcEventsReq) -> Vec<GrpcEvent>,
|
||||
models_get_settings(ModelsGetSettingsReq) -> Settings,
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
use crate::error::Result;
|
||||
use crate::host::{Host, PluginHost};
|
||||
use yaak_models::models::{
|
||||
AnyModel, GraphQlIntrospection, GrpcEvent, HttpRequestHeader, Settings, WebsocketEvent,
|
||||
WorkspaceMeta,
|
||||
AnyModel, GraphQlIntrospection, GrpcEvent, HttpRequestHeader, ModelVersion,
|
||||
RequestVersionComparison, Settings, WebsocketEvent, WorkspaceMeta,
|
||||
};
|
||||
use yaak_models::versions::version_document;
|
||||
use yaak_models::queries::workspaces::default_headers;
|
||||
use yaak_rpc_schema::*;
|
||||
|
||||
@@ -45,6 +46,42 @@ pub async fn models_duplicate<H: Host>(host: H, req: ModelsDuplicateReq) -> Resu
|
||||
})?)
|
||||
}
|
||||
|
||||
/// Capture the request's current content, from an edit-session boundary the
|
||||
/// frontend can see: switching away, losing focus, closing, or falling idle.
|
||||
///
|
||||
/// The frontend does not track whether anything actually changed — versions are
|
||||
/// content-addressed, so an unchanged request returns the version it already
|
||||
/// had and the trigger code stays a one-liner.
|
||||
pub async fn models_snapshot_request<H: Host>(
|
||||
host: H,
|
||||
req: ModelsSnapshotRequestReq,
|
||||
) -> Result<ModelVersion> {
|
||||
Ok(host.db().snapshot_request_by_id(&req.request_id, req.reason)?)
|
||||
}
|
||||
|
||||
/// A version and the live request side by side, for the diff and for deciding
|
||||
/// whether there is anything worth offering.
|
||||
pub async fn models_request_version<H: Host>(
|
||||
host: H,
|
||||
req: ModelsRequestVersionReq,
|
||||
) -> Result<RequestVersionComparison> {
|
||||
let db = host.db();
|
||||
let version = db.get_model_version(&req.version_id)?;
|
||||
let current_document = version_document(&db.get_any_request(&version.model_id)?.to_value()?)?;
|
||||
let differs = !db.request_matches_version(&version)?;
|
||||
Ok(RequestVersionComparison { version, current_document, differs })
|
||||
}
|
||||
|
||||
/// Returns the id of the request that was restored.
|
||||
pub async fn models_restore_request_version<H: Host>(
|
||||
host: H,
|
||||
req: ModelsRestoreRequestVersionReq,
|
||||
) -> Result<String> {
|
||||
let source = host.update_source();
|
||||
let restored = host.db().restore_request_version(&req.version_id, &source)?;
|
||||
Ok(restored.id().to_string())
|
||||
}
|
||||
|
||||
pub async fn models_websocket_events<H: Host>(
|
||||
host: H,
|
||||
req: ModelsWebsocketEventsReq,
|
||||
|
||||
+51
@@ -139,6 +139,10 @@ export type GrpcConnection = {
|
||||
state: GrpcConnectionState;
|
||||
trailers: { [key in string]?: string };
|
||||
url: string;
|
||||
/**
|
||||
* The request version this connection was opened from, when one was captured.
|
||||
*/
|
||||
versionId: string | null;
|
||||
};
|
||||
|
||||
export type GrpcConnectionState = "initialized" | "connected" | "closed";
|
||||
@@ -243,6 +247,10 @@ export type HttpResponse = {
|
||||
state: HttpResponseState;
|
||||
url: string;
|
||||
version: string | null;
|
||||
/**
|
||||
* The request version this response was sent from, when one was captured.
|
||||
*/
|
||||
versionId: string | null;
|
||||
};
|
||||
|
||||
export type HttpResponseEvent = {
|
||||
@@ -382,6 +390,28 @@ export type ModelPayload = {
|
||||
change: ModelChangeEvent;
|
||||
};
|
||||
|
||||
export type ModelVersion = {
|
||||
model: "model_version";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
/**
|
||||
* The `model` field of the versioned model, eg. `http_request`.
|
||||
*/
|
||||
modelType: string;
|
||||
modelId: string;
|
||||
contentHash: string;
|
||||
document: Record<string, any>;
|
||||
reason: ModelVersionReason;
|
||||
};
|
||||
|
||||
/**
|
||||
* Why a version was captured. Not a UI label — the frontend decides how to
|
||||
* phrase these — but it is what makes a history readable when debugging.
|
||||
*/
|
||||
export type ModelVersionReason = "send" | "switch" | "idle" | "restore" | "manual";
|
||||
|
||||
export type ParentAuthentication = {
|
||||
authentication: Record<string, any>;
|
||||
authenticationType: string | null;
|
||||
@@ -425,6 +455,23 @@ export type ProxySetting =
|
||||
|
||||
export type ProxySettingAuth = { user: string; password: string };
|
||||
|
||||
/**
|
||||
* One version, next to the request as it stands now.
|
||||
*
|
||||
* Both halves come from the same place so they are guaranteed comparable: the
|
||||
* frontend renders them side by side, and `differs` is the same content-hash
|
||||
* comparison the backend uses everywhere else rather than a second opinion
|
||||
* formed in TypeScript.
|
||||
*/
|
||||
export type RequestVersionComparison = {
|
||||
version: ModelVersion;
|
||||
/**
|
||||
* The live request's editable content, in the same shape as the version's document.
|
||||
*/
|
||||
currentDocument: Record<string, any>;
|
||||
differs: boolean;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
model: "settings";
|
||||
id: string;
|
||||
@@ -488,6 +535,10 @@ export type WebsocketConnection = {
|
||||
state: WebsocketConnectionState;
|
||||
status: number;
|
||||
url: string;
|
||||
/**
|
||||
* The request version this connection was opened from, when one was captured.
|
||||
*/
|
||||
versionId: string | null;
|
||||
};
|
||||
|
||||
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
|
||||
|
||||
@@ -3285,6 +3285,23 @@ impl UpsertModelInfo for ModelVersion {
|
||||
}
|
||||
}
|
||||
|
||||
/// One version, next to the request as it stands now.
|
||||
///
|
||||
/// Both halves come from the same place so they are guaranteed comparable: the
|
||||
/// frontend renders them side by side, and `differs` is the same content-hash
|
||||
/// comparison the backend uses everywhere else rather than a second opinion
|
||||
/// formed in TypeScript.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_models.ts")]
|
||||
pub struct RequestVersionComparison {
|
||||
pub version: ModelVersion,
|
||||
/// The live request's editable content, in the same shape as the version's document.
|
||||
#[ts(type = "Record<string, any>")]
|
||||
pub current_document: Value,
|
||||
pub differs: bool,
|
||||
}
|
||||
|
||||
/// Only used as a `from_row` fallback for an unparseable settings column. The
|
||||
/// value a *new* model gets comes from that model's `Default` impl.
|
||||
fn default_request_message_size_setting() -> InheritedIntSetting {
|
||||
|
||||
@@ -33,13 +33,46 @@ pub fn version_document<T: Serialize>(model: &T) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// The hash a version is addressed by.
|
||||
///
|
||||
/// Canonical by construction: `serde_json::Map` is a `BTreeMap` here, so
|
||||
/// serialization already visits keys in sorted order and two documents that
|
||||
/// differ only in key order hash the same.
|
||||
pub fn content_hash(document: &Value) -> Result<String> {
|
||||
let canonical = serde_json::to_vec(document)?;
|
||||
Ok(hex::encode(Sha256::digest(&canonical)))
|
||||
let mut canonical = String::new();
|
||||
write_canonical(document, &mut canonical);
|
||||
Ok(hex::encode(Sha256::digest(canonical.as_bytes())))
|
||||
}
|
||||
|
||||
/// Serialize with object keys in sorted order.
|
||||
///
|
||||
/// Plain `to_string` would not do: whether `serde_json::Map` preserves
|
||||
/// insertion order or sorts is a workspace-wide feature decision, and a
|
||||
/// document read back from SQLite has whatever order it was written in. Sorting
|
||||
/// here makes the hash depend on the content and nothing else, in every build.
|
||||
fn write_canonical(value: &Value, out: &mut String) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
let mut keys = map.keys().collect::<Vec<_>>();
|
||||
keys.sort_unstable();
|
||||
out.push('{');
|
||||
for (i, key) in keys.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
write_canonical(&Value::String(key.clone()), out);
|
||||
out.push(':');
|
||||
write_canonical(&map[key], out);
|
||||
}
|
||||
out.push('}');
|
||||
}
|
||||
Value::Array(items) => {
|
||||
out.push('[');
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
write_canonical(item, out);
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
scalar => out.push_str(&scalar.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lay a version's document back over a live model.
|
||||
@@ -142,8 +175,10 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The hash has to survive being written to and read back from the
|
||||
/// database, which does not preserve key order.
|
||||
/// The hash has to survive a round trip through SQLite, which stores the
|
||||
/// document as text and hands back whatever order it was written in. It
|
||||
/// also has to survive `serde_json`'s `preserve_order` feature being on in
|
||||
/// one build of the workspace and off in another.
|
||||
#[test]
|
||||
fn key_order_does_not_change_the_hash() {
|
||||
let a: Value = serde_json::from_str(r#"{"url":"a","method":"GET"}"#).unwrap();
|
||||
@@ -151,6 +186,25 @@ mod tests {
|
||||
assert_eq!(content_hash(&a).unwrap(), content_hash(&b).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_order_does_not_change_the_hash_when_nested() {
|
||||
let a: Value =
|
||||
serde_json::from_str(r#"{"body":{"text":"x","type":"json"},"headers":[{"a":1,"b":2}]}"#)
|
||||
.unwrap();
|
||||
let b: Value =
|
||||
serde_json::from_str(r#"{"headers":[{"b":2,"a":1}],"body":{"type":"json","text":"x"}}"#)
|
||||
.unwrap();
|
||||
assert_eq!(content_hash(&a).unwrap(), content_hash(&b).unwrap());
|
||||
}
|
||||
|
||||
/// Sorting keys must not make different documents collide.
|
||||
#[test]
|
||||
fn array_order_still_changes_the_hash() {
|
||||
let a: Value = serde_json::from_str(r#"{"headers":[{"n":"a"},{"n":"b"}]}"#).unwrap();
|
||||
let b: Value = serde_json::from_str(r#"{"headers":[{"n":"b"},{"n":"a"}]}"#).unwrap();
|
||||
assert_ne!(content_hash(&a).unwrap(), content_hash(&b).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applying_a_document_keeps_the_live_model_identity() {
|
||||
let live = serde_json::to_value(request()).unwrap();
|
||||
|
||||
+25
@@ -11,6 +11,7 @@ export type AnyModel =
|
||||
| HttpRequest
|
||||
| HttpResponse
|
||||
| HttpResponseEvent
|
||||
| ImportSource
|
||||
| KeyValue
|
||||
| Plugin
|
||||
| Settings
|
||||
@@ -137,6 +138,10 @@ export type GrpcConnection = {
|
||||
state: GrpcConnectionState;
|
||||
trailers: { [key in string]?: string };
|
||||
url: string;
|
||||
/**
|
||||
* The request version this connection was opened from, when one was captured.
|
||||
*/
|
||||
versionId: string | null;
|
||||
};
|
||||
|
||||
export type GrpcConnectionState = "initialized" | "connected" | "closed";
|
||||
@@ -241,6 +246,10 @@ export type HttpResponse = {
|
||||
state: HttpResponseState;
|
||||
url: string;
|
||||
version: string | null;
|
||||
/**
|
||||
* The request version this response was sent from, when one was captured.
|
||||
*/
|
||||
versionId: string | null;
|
||||
};
|
||||
|
||||
export type HttpResponseEvent = {
|
||||
@@ -318,6 +327,18 @@ export type HttpUrlParameter = {
|
||||
|
||||
export type HttpVersion = "auto" | "http1" | "http2";
|
||||
|
||||
export type ImportSource = {
|
||||
model: "import_source";
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workspaceId: string;
|
||||
importer: string;
|
||||
origin: string;
|
||||
originLabel: string;
|
||||
lastImportedAt: string;
|
||||
};
|
||||
|
||||
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
|
||||
|
||||
export type InheritedHttpVersionSetting = { enabled?: boolean; value: HttpVersion };
|
||||
@@ -417,6 +438,10 @@ export type WebsocketConnection = {
|
||||
state: WebsocketConnectionState;
|
||||
status: number;
|
||||
url: string;
|
||||
/**
|
||||
* The request version this connection was opened from, when one was captured.
|
||||
*/
|
||||
versionId: string | null;
|
||||
};
|
||||
|
||||
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
|
||||
|
||||
@@ -32,12 +32,13 @@ use yaak_models::blob_manager::{BlobManager, BodyChunk};
|
||||
use yaak_models::cookies::apply_cookie_changes;
|
||||
use yaak_models::models::{
|
||||
AnyModel, Cookie, CookieJar, HttpRequest, HttpResponseEvent, HttpResponseEventData,
|
||||
HttpSendSettings,
|
||||
HttpSendSettings, ModelVersionReason, RequestVersionComparison,
|
||||
};
|
||||
use yaak_models::models_ops;
|
||||
use yaak_models::query_manager::QueryManager;
|
||||
use yaak_models::render::render_http_request;
|
||||
use yaak_models::util::{ModelPayload, UpdateSource};
|
||||
use yaak_models::versions::version_document;
|
||||
use yaak_templates::{RenderOptions, TemplateCallback};
|
||||
|
||||
/// Names inside the VFS, not paths on any disk. Two files because the desktop
|
||||
@@ -218,6 +219,19 @@ struct UpsertIntrospectionReq {
|
||||
content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SnapshotRequestReq {
|
||||
request_id: String,
|
||||
reason: ModelVersionReason,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VersionIdReq {
|
||||
version_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ResponseIdReq {
|
||||
@@ -319,6 +333,37 @@ fn dispatch(
|
||||
to_json(id)
|
||||
}
|
||||
|
||||
"models_snapshot_request" => {
|
||||
let req: SnapshotRequestReq = from_js(payload)?;
|
||||
to_json(
|
||||
host.queries
|
||||
.connect()
|
||||
.snapshot_request_by_id(&req.request_id, req.reason)
|
||||
.map_err(js_error)?,
|
||||
)
|
||||
}
|
||||
|
||||
"models_request_version" => {
|
||||
let req: VersionIdReq = from_js(payload)?;
|
||||
let db = host.queries.connect();
|
||||
let version = db.get_model_version(&req.version_id).map_err(js_error)?;
|
||||
let request = db.get_any_request(&version.model_id).map_err(js_error)?;
|
||||
let current_document =
|
||||
version_document(&request.to_value().map_err(js_error)?).map_err(js_error)?;
|
||||
let differs = !db.request_matches_version(&version).map_err(js_error)?;
|
||||
to_json(RequestVersionComparison { version, current_document, differs })
|
||||
}
|
||||
|
||||
"models_restore_request_version" => {
|
||||
let req: VersionIdReq = from_js(payload)?;
|
||||
let restored = host
|
||||
.queries
|
||||
.connect()
|
||||
.restore_request_version(&req.version_id, source)
|
||||
.map_err(js_error)?;
|
||||
to_json(restored.id().to_string())
|
||||
}
|
||||
|
||||
"models_get_settings" => to_json(host.queries.connect().get_settings()),
|
||||
|
||||
"models_get_graphql_introspection" => {
|
||||
|
||||
Reference in New Issue
Block a user