From 7967632b498fd344ceafda835f7ef8c451a09d15 Mon Sep 17 00:00:00 2001 From: Sungbin Jo Date: Mon, 23 Mar 2026 15:03:10 +0900 Subject: [PATCH 1/2] chore: remove db2 plugin --- Cargo.lock | 127 +------ Cargo.toml | 1 - apps/desktop/package.json | 1 - apps/desktop/src-tauri/Cargo.toml | 1 - .../src-tauri/capabilities/default.json | 1 - apps/desktop/src-tauri/src/lib.rs | 10 - plugins/db2/.gitignore | 17 - plugins/db2/Cargo.toml | 34 -- plugins/db2/build.rs | 5 - plugins/db2/js/bindings.gen.ts | 97 ----- plugins/db2/js/index.ts | 1 - plugins/db2/package.json | 11 - .../autogenerated/commands/execute_cloud.toml | 13 - .../autogenerated/commands/execute_local.toml | 13 - .../permissions/autogenerated/reference.md | 70 ---- plugins/db2/permissions/default.toml | 3 - plugins/db2/permissions/schemas/schema.json | 330 ------------------ plugins/db2/src/commands.rs | 27 -- plugins/db2/src/error.rs | 24 -- plugins/db2/src/ext.rs | 191 ---------- plugins/db2/src/lib.rs | 64 ---- plugins/db2/tsconfig.json | 5 - plugins/notification/Cargo.toml | 1 - 23 files changed, 3 insertions(+), 1044 deletions(-) delete mode 100644 plugins/db2/.gitignore delete mode 100644 plugins/db2/Cargo.toml delete mode 100644 plugins/db2/build.rs delete mode 100644 plugins/db2/js/bindings.gen.ts delete mode 100644 plugins/db2/js/index.ts delete mode 100644 plugins/db2/package.json delete mode 100644 plugins/db2/permissions/autogenerated/commands/execute_cloud.toml delete mode 100644 plugins/db2/permissions/autogenerated/commands/execute_local.toml delete mode 100644 plugins/db2/permissions/autogenerated/reference.md delete mode 100644 plugins/db2/permissions/default.toml delete mode 100644 plugins/db2/permissions/schemas/schema.json delete mode 100644 plugins/db2/src/commands.rs delete mode 100644 plugins/db2/src/error.rs delete mode 100644 plugins/db2/src/ext.rs delete mode 100644 plugins/db2/src/lib.rs delete mode 100644 plugins/db2/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index 50a70c5414..8e5699fca3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4736,7 +4736,6 @@ dependencies = [ "tauri-plugin-bedrock", "tauri-plugin-calendar", "tauri-plugin-clipboard-manager", - "tauri-plugin-db2", "tauri-plugin-deep-link", "tauri-plugin-deeplink2", "tauri-plugin-detect", @@ -11985,15 +11984,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-system-configuration" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" -dependencies = [ - "objc2-core-foundation", -] - [[package]] name = "objc2-ui-kit" version = "0.3.2" @@ -13313,37 +13303,6 @@ dependencies = [ "serde", ] -[[package]] -name = "postgres-protocol" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" -dependencies = [ - "base64 0.22.1", - "byteorder", - "bytes", - "fallible-iterator 0.2.0", - "hmac", - "md-5", - "memchr", - "rand 0.9.2", - "sha2", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" -dependencies = [ - "bytes", - "fallible-iterator 0.2.0", - "postgres-protocol", - "serde_core", - "serde_json", -] - [[package]] name = "posthog-rs" version = "0.4.3" @@ -16631,7 +16590,7 @@ dependencies = [ "stringprep", "thiserror 2.0.18", "tracing", - "whoami 1.6.1", + "whoami", ] [[package]] @@ -16668,7 +16627,7 @@ dependencies = [ "stringprep", "thiserror 2.0.18", "tracing", - "whoami 1.6.1", + "whoami", ] [[package]] @@ -17952,28 +17911,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "tauri-plugin-db2" -version = "0.1.0" -dependencies = [ - "db-core", - "dirs 6.0.0", - "futures-util", - "serde", - "serde_json", - "specta", - "specta-typescript", - "strum 0.27.2", - "tauri", - "tauri-plugin", - "tauri-plugin-settings", - "tauri-specta", - "thiserror 2.0.18", - "tokio", - "tokio-postgres", - "tracing", -] - [[package]] name = "tauri-plugin-deep-link" version = "2.4.7" @@ -18483,7 +18420,6 @@ dependencies = [ "apalis", "apalis-cron", "chrono", - "db-user", "detect", "host", "intercept", @@ -19674,32 +19610,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-postgres" -version = "0.7.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator 0.2.0", - "futures-channel", - "futures-util", - "log", - "parking_lot", - "percent-encoding", - "phf 0.13.1", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.9.2", - "socket2 0.6.3", - "tokio", - "tokio-util", - "whoami 2.1.1", -] - [[package]] name = "tokio-rustls" version = "0.24.1" @@ -21697,15 +21607,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -21730,15 +21631,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" -[[package]] -name = "wasite" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" -dependencies = [ - "wasi 0.14.7+wasi-0.2.4", -] - [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -22295,20 +22187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ "libredox", - "wasite 0.1.0", -] - -[[package]] -name = "whoami" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" -dependencies = [ - "libc", - "libredox", - "objc2-system-configuration", - "wasite 1.0.2", - "web-sys", + "wasite", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4175751147..7c01d93b10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -195,7 +195,6 @@ tauri-plugin-audio-priority = { path = "plugins/audio-priority" } tauri-plugin-auth = { path = "plugins/auth" } tauri-plugin-bedrock = { path = "plugins/bedrock" } tauri-plugin-calendar = { path = "plugins/calendar" } -tauri-plugin-db2 = { path = "plugins/db2" } tauri-plugin-deeplink2 = { path = "plugins/deeplink2" } tauri-plugin-detect = { path = "plugins/detect" } tauri-plugin-dock = { path = "plugins/dock" } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 68bed1c666..06446b3271 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -41,7 +41,6 @@ "@hypr/plugin-auth": "workspace:*", "@hypr/plugin-bedrock": "workspace:*", "@hypr/plugin-calendar": "workspace:*", - "@hypr/plugin-db2": "workspace:*", "@hypr/plugin-deeplink2": "workspace:*", "@hypr/plugin-detect": "workspace:*", "@hypr/plugin-export": "workspace:*", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 6a57e02837..e14aafc430 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -29,7 +29,6 @@ tauri-plugin-autostart = { workspace = true } tauri-plugin-bedrock = { workspace = true } tauri-plugin-calendar = { workspace = true } tauri-plugin-clipboard-manager = { workspace = true } -tauri-plugin-db2 = { workspace = true } tauri-plugin-deep-link = { workspace = true } tauri-plugin-deeplink2 = { workspace = true } tauri-plugin-detect = { workspace = true } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 566c4ed595..f9c07cf5ea 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -23,7 +23,6 @@ "calendar:default", "audio-priority:default", "auth:default", - "db2:default", "windows:default", "tracing:default", "tray:default", diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index ea795a0d13..f0a1a7f3af 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -101,7 +101,6 @@ pub async fn main() { .plugin(tauri_plugin_importer::init()) .plugin(tauri_plugin_calendar::init()) .plugin(tauri_plugin_auth::init()) - .plugin(tauri_plugin_db2::init()) .plugin(tauri_plugin_tracing::init()) .plugin(tauri_plugin_hooks::init()) .plugin(tauri_plugin_icon::init()) @@ -184,7 +183,6 @@ pub async fn main() { .on_window_event(tauri_plugin_windows::on_window_event) .setup(move |app| { let app_handle = app.handle().clone(); - let app_clone = app_handle.clone(); specta_builder.mount_events(&app_handle); @@ -219,14 +217,6 @@ pub async fn main() { } } - tokio::spawn(async move { - use tauri_plugin_db2::Database2PluginExt; - - if let Err(e) = app_clone.db2().init_local().await { - tracing::error!("failed_to_init_local: {}", e); - } - }); - if let (Some(ctx), Some(handle)) = (&root_supervisor_ctx, root_supervisor_handle) { supervisor::monitor_supervisor(handle, ctx.is_exiting.clone(), app_handle.clone()); } diff --git a/plugins/db2/.gitignore b/plugins/db2/.gitignore deleted file mode 100644 index 50d8e32e89..0000000000 --- a/plugins/db2/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -/.vs -.DS_Store -.Thumbs.db -*.sublime* -.idea/ -debug.log -package-lock.json -.vscode/settings.json -yarn.lock - -/.tauri -/target -Cargo.lock -node_modules/ - -dist-js -dist diff --git a/plugins/db2/Cargo.toml b/plugins/db2/Cargo.toml deleted file mode 100644 index a6c6aa6abb..0000000000 --- a/plugins/db2/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "tauri-plugin-db2" -version = "0.1.0" -authors = ["You"] -edition = "2024" -exclude = ["/js", "/node_modules"] -links = "tauri-plugin-db2" -description = "" - -[build-dependencies] -tauri-plugin = { workspace = true, features = ["build"] } - -[dev-dependencies] -specta-typescript = { workspace = true } - -[dependencies] -tauri = { workspace = true, features = ["test"] } -tauri-specta = { workspace = true, features = ["derive", "typescript"] } - -tauri-plugin-settings = { workspace = true } - -hypr-db-core = { workspace = true, features = ["encryption"] } -tokio-postgres = { version = "0.7.14", features = ["with-serde_json-1"] } - -futures-util = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -tracing = { workspace = true } - -dirs = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -specta = { workspace = true, features = ["serde_json"] } -strum = { workspace = true, features = ["derive"] } -thiserror = { workspace = true } diff --git a/plugins/db2/build.rs b/plugins/db2/build.rs deleted file mode 100644 index a5213108e4..0000000000 --- a/plugins/db2/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -const COMMANDS: &[&str] = &["execute_local", "execute_cloud"]; - -fn main() { - tauri_plugin::Builder::new(COMMANDS).build(); -} diff --git a/plugins/db2/js/bindings.gen.ts b/plugins/db2/js/bindings.gen.ts deleted file mode 100644 index 12aa486ea6..0000000000 --- a/plugins/db2/js/bindings.gen.ts +++ /dev/null @@ -1,97 +0,0 @@ -// @ts-nocheck - -// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. - -/** user-defined commands **/ - - -export const commands = { -async executeLocal(sql: string, args: string[]) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("plugin:db2|execute_local", { sql, args }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async executeCloud(sql: string, args: string[]) : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("plugin:db2|execute_cloud", { sql, args }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -} -} - -/** user-defined events **/ - - - -/** user-defined constants **/ - - - -/** user-defined types **/ - -export type JsonValue = null | boolean | number | string | JsonValue[] | Partial<{ [key in string]: JsonValue }> - -/** tauri-specta globals **/ - -import { - invoke as TAURI_INVOKE, - Channel as TAURI_CHANNEL, -} from "@tauri-apps/api/core"; -import * as TAURI_API_EVENT from "@tauri-apps/api/event"; -import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; - -type __EventObj__ = { - listen: ( - cb: TAURI_API_EVENT.EventCallback, - ) => ReturnType>; - once: ( - cb: TAURI_API_EVENT.EventCallback, - ) => ReturnType>; - emit: null extends T - ? (payload?: T) => ReturnType - : (payload: T) => ReturnType; -}; - -export type Result = - | { status: "ok"; data: T } - | { status: "error"; error: E }; - -function __makeEvents__>( - mappings: Record, -) { - return new Proxy( - {} as unknown as { - [K in keyof T]: __EventObj__ & { - (handle: __WebviewWindow__): __EventObj__; - }; - }, - { - get: (_, event) => { - const name = mappings[event as keyof T]; - - return new Proxy((() => {}) as any, { - apply: (_, __, [window]: [__WebviewWindow__]) => ({ - listen: (arg: any) => window.listen(name, arg), - once: (arg: any) => window.once(name, arg), - emit: (arg: any) => window.emit(name, arg), - }), - get: (_, command: keyof __EventObj__) => { - switch (command) { - case "listen": - return (arg: any) => TAURI_API_EVENT.listen(name, arg); - case "once": - return (arg: any) => TAURI_API_EVENT.once(name, arg); - case "emit": - return (arg: any) => TAURI_API_EVENT.emit(name, arg); - } - }, - }); - }, - }, - ); -} diff --git a/plugins/db2/js/index.ts b/plugins/db2/js/index.ts deleted file mode 100644 index a96e122f03..0000000000 --- a/plugins/db2/js/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./bindings.gen"; diff --git a/plugins/db2/package.json b/plugins/db2/package.json deleted file mode 100644 index 1e24ced97c..0000000000 --- a/plugins/db2/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@hypr/plugin-db2", - "private": true, - "main": "./js/index.ts", - "scripts": { - "codegen": "cargo test -p tauri-plugin-db2" - }, - "dependencies": { - "@tauri-apps/api": "^2.10.1" - } -} diff --git a/plugins/db2/permissions/autogenerated/commands/execute_cloud.toml b/plugins/db2/permissions/autogenerated/commands/execute_cloud.toml deleted file mode 100644 index c67a93fad5..0000000000 --- a/plugins/db2/permissions/autogenerated/commands/execute_cloud.toml +++ /dev/null @@ -1,13 +0,0 @@ -# Automatically generated - DO NOT EDIT! - -"$schema" = "../../schemas/schema.json" - -[[permission]] -identifier = "allow-execute-cloud" -description = "Enables the execute_cloud command without any pre-configured scope." -commands.allow = ["execute_cloud"] - -[[permission]] -identifier = "deny-execute-cloud" -description = "Denies the execute_cloud command without any pre-configured scope." -commands.deny = ["execute_cloud"] diff --git a/plugins/db2/permissions/autogenerated/commands/execute_local.toml b/plugins/db2/permissions/autogenerated/commands/execute_local.toml deleted file mode 100644 index 8ecb1e73fc..0000000000 --- a/plugins/db2/permissions/autogenerated/commands/execute_local.toml +++ /dev/null @@ -1,13 +0,0 @@ -# Automatically generated - DO NOT EDIT! - -"$schema" = "../../schemas/schema.json" - -[[permission]] -identifier = "allow-execute-local" -description = "Enables the execute_local command without any pre-configured scope." -commands.allow = ["execute_local"] - -[[permission]] -identifier = "deny-execute-local" -description = "Denies the execute_local command without any pre-configured scope." -commands.deny = ["execute_local"] diff --git a/plugins/db2/permissions/autogenerated/reference.md b/plugins/db2/permissions/autogenerated/reference.md deleted file mode 100644 index 2fc16b21a5..0000000000 --- a/plugins/db2/permissions/autogenerated/reference.md +++ /dev/null @@ -1,70 +0,0 @@ -## Default Permission - -Default permissions for the plugin - -#### This default permission set includes the following: - -- `allow-execute-local` -- `allow-execute-cloud` - -## Permission Table - - - - - - - - - - - - - - - - - - - - - - - - - - - -
IdentifierDescription
- -`db2:allow-execute-cloud` - - - -Enables the execute_cloud command without any pre-configured scope. - -
- -`db2:deny-execute-cloud` - - - -Denies the execute_cloud command without any pre-configured scope. - -
- -`db2:allow-execute-local` - - - -Enables the execute_local command without any pre-configured scope. - -
- -`db2:deny-execute-local` - - - -Denies the execute_local command without any pre-configured scope. - -
diff --git a/plugins/db2/permissions/default.toml b/plugins/db2/permissions/default.toml deleted file mode 100644 index 68fa9da254..0000000000 --- a/plugins/db2/permissions/default.toml +++ /dev/null @@ -1,3 +0,0 @@ -[default] -description = "Default permissions for the plugin" -permissions = ["allow-execute-local", "allow-execute-cloud"] diff --git a/plugins/db2/permissions/schemas/schema.json b/plugins/db2/permissions/schemas/schema.json deleted file mode 100644 index 6a13381392..0000000000 --- a/plugins/db2/permissions/schemas/schema.json +++ /dev/null @@ -1,330 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PermissionFile", - "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", - "type": "object", - "properties": { - "default": { - "description": "The default permission set for the plugin", - "anyOf": [ - { - "$ref": "#/definitions/DefaultPermission" - }, - { - "type": "null" - } - ] - }, - "set": { - "description": "A list of permissions sets defined", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionSet" - } - }, - "permission": { - "description": "A list of inlined permissions", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Permission" - } - } - }, - "definitions": { - "DefaultPermission": { - "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", - "type": "object", - "required": [ - "permissions" - ], - "properties": { - "version": { - "description": "The version of the permission.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 1.0 - }, - "description": { - "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", - "type": [ - "string", - "null" - ] - }, - "permissions": { - "description": "All permissions this set contains.", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PermissionSet": { - "description": "A set of direct permissions grouped together under a new name.", - "type": "object", - "required": [ - "description", - "identifier", - "permissions" - ], - "properties": { - "identifier": { - "description": "A unique identifier for the permission.", - "type": "string" - }, - "description": { - "description": "Human-readable description of what the permission does.", - "type": "string" - }, - "permissions": { - "description": "All permissions this set contains.", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionKind" - } - } - } - }, - "Permission": { - "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", - "type": "object", - "required": [ - "identifier" - ], - "properties": { - "version": { - "description": "The version of the permission.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 1.0 - }, - "identifier": { - "description": "A unique identifier for the permission.", - "type": "string" - }, - "description": { - "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", - "type": [ - "string", - "null" - ] - }, - "commands": { - "description": "Allowed or denied commands when using this permission.", - "default": { - "allow": [], - "deny": [] - }, - "allOf": [ - { - "$ref": "#/definitions/Commands" - } - ] - }, - "scope": { - "description": "Allowed or denied scoped when using this permission.", - "allOf": [ - { - "$ref": "#/definitions/Scopes" - } - ] - }, - "platforms": { - "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Target" - } - } - } - }, - "Commands": { - "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", - "type": "object", - "properties": { - "allow": { - "description": "Allowed command.", - "default": [], - "type": "array", - "items": { - "type": "string" - } - }, - "deny": { - "description": "Denied command, which takes priority.", - "default": [], - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "Scopes": { - "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", - "type": "object", - "properties": { - "allow": { - "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - }, - "deny": { - "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - } - } - }, - "Value": { - "description": "All supported ACL values.", - "anyOf": [ - { - "description": "Represents a null JSON value.", - "type": "null" - }, - { - "description": "Represents a [`bool`].", - "type": "boolean" - }, - { - "description": "Represents a valid ACL [`Number`].", - "allOf": [ - { - "$ref": "#/definitions/Number" - } - ] - }, - { - "description": "Represents a [`String`].", - "type": "string" - }, - { - "description": "Represents a list of other [`Value`]s.", - "type": "array", - "items": { - "$ref": "#/definitions/Value" - } - }, - { - "description": "Represents a map of [`String`] keys to [`Value`]s.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Value" - } - } - ] - }, - "Number": { - "description": "A valid ACL number.", - "anyOf": [ - { - "description": "Represents an [`i64`].", - "type": "integer", - "format": "int64" - }, - { - "description": "Represents a [`f64`].", - "type": "number", - "format": "double" - } - ] - }, - "Target": { - "description": "Platform target.", - "oneOf": [ - { - "description": "MacOS.", - "type": "string", - "enum": [ - "macOS" - ] - }, - { - "description": "Windows.", - "type": "string", - "enum": [ - "windows" - ] - }, - { - "description": "Linux.", - "type": "string", - "enum": [ - "linux" - ] - }, - { - "description": "Android.", - "type": "string", - "enum": [ - "android" - ] - }, - { - "description": "iOS.", - "type": "string", - "enum": [ - "iOS" - ] - } - ] - }, - "PermissionKind": { - "type": "string", - "oneOf": [ - { - "description": "Enables the execute_cloud command without any pre-configured scope.", - "type": "string", - "const": "allow-execute-cloud", - "markdownDescription": "Enables the execute_cloud command without any pre-configured scope." - }, - { - "description": "Denies the execute_cloud command without any pre-configured scope.", - "type": "string", - "const": "deny-execute-cloud", - "markdownDescription": "Denies the execute_cloud command without any pre-configured scope." - }, - { - "description": "Enables the execute_local command without any pre-configured scope.", - "type": "string", - "const": "allow-execute-local", - "markdownDescription": "Enables the execute_local command without any pre-configured scope." - }, - { - "description": "Denies the execute_local command without any pre-configured scope.", - "type": "string", - "const": "deny-execute-local", - "markdownDescription": "Denies the execute_local command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-execute-local`\n- `allow-execute-cloud`", - "type": "string", - "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-execute-local`\n- `allow-execute-cloud`" - } - ] - } - } -} \ No newline at end of file diff --git a/plugins/db2/src/commands.rs b/plugins/db2/src/commands.rs deleted file mode 100644 index 39bf1fee78..0000000000 --- a/plugins/db2/src/commands.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::Database2PluginExt; - -#[tauri::command] -#[specta::specta] -pub(crate) async fn execute_local( - app: tauri::AppHandle, - sql: String, - args: Vec, -) -> Result, String> { - app.db2() - .execute_local(sql, args) - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -#[specta::specta] -pub(crate) async fn execute_cloud( - app: tauri::AppHandle, - sql: String, - args: Vec, -) -> Result, String> { - app.db2() - .execute_cloud(sql, args) - .await - .map_err(|e| e.to_string()) -} diff --git a/plugins/db2/src/error.rs b/plugins/db2/src/error.rs deleted file mode 100644 index c8018a9c17..0000000000 --- a/plugins/db2/src/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -use serde::{Serialize, ser::Serializer}; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error(transparent)] - PostgresError(#[from] tokio_postgres::Error), - #[error(transparent)] - HyprDbError(#[from] hypr_db_core::Error), - #[error(transparent)] - TauriError(#[from] tauri::Error), - #[error(transparent)] - IoError(#[from] std::io::Error), - #[error(transparent)] - SettingsError(#[from] tauri_plugin_settings::Error), -} - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} diff --git a/plugins/db2/src/ext.rs b/plugins/db2/src/ext.rs deleted file mode 100644 index 12b1bbf562..0000000000 --- a/plugins/db2/src/ext.rs +++ /dev/null @@ -1,191 +0,0 @@ -pub struct Database2<'a, R: tauri::Runtime, M: tauri::Manager> { - manager: &'a M, - _runtime: std::marker::PhantomData R>, -} - -impl<'a, R: tauri::Runtime, M: tauri::Manager> Database2<'a, R, M> { - pub async fn init_local(&self) -> Result<(), crate::Error> { - let db = { - if cfg!(debug_assertions) { - hypr_db_core::DatabaseBuilder::default() - .memory() - .build() - .await - .unwrap() - } else { - use tauri_plugin_settings::SettingsPluginExt; - let dir_path = self.manager.settings().global_base()?; - let file_path = dir_path.join("db.sqlite").into_std_path_buf(); - - hypr_db_core::DatabaseBuilder::default() - .local(file_path) - .build() - .await - .unwrap() - } - }; - { - let state = self.manager.state::(); - let mut guard = state.lock().await; - guard.local_db = Some(db); - } - Ok(()) - } - - pub async fn init_cloud(&self, connection_str: &str) -> Result<(), crate::Error> { - let (client, connection) = - tokio_postgres::connect(connection_str, tokio_postgres::NoTls).await?; - - tokio::spawn(async move { - if let Err(e) = connection.await { - eprintln!("connection error: {}", e); - } - }); - - { - let state = self.manager.state::(); - let mut guard = state.lock().await; - guard.cloud_db = Some(client); - } - Ok(()) - } - - pub async fn execute_local( - &self, - sql: String, - args: Vec, - ) -> Result, crate::Error> { - let state = self.manager.state::(); - let guard = state.lock().await; - - let mut items = Vec::new(); - - if let Some(db) = &guard.local_db { - let conn = db.conn()?; - - match conn.query(&sql, args).await { - Ok(mut rows) => loop { - match rows.next().await { - Ok(Some(row)) => { - let mut map = serde_json::Map::new(); - - for idx in 0..row.column_count() { - if let Some(column_name) = row.column_name(idx) { - let value = match row.get_value(idx) { - Ok(hypr_db_core::libsql::Value::Null) => { - serde_json::Value::Null - } - Ok(hypr_db_core::libsql::Value::Integer(i)) => { - serde_json::json!(i) - } - Ok(hypr_db_core::libsql::Value::Real(f)) => { - serde_json::json!(f) - } - Ok(hypr_db_core::libsql::Value::Text(s)) => { - serde_json::json!(s) - } - Ok(hypr_db_core::libsql::Value::Blob(b)) => { - serde_json::json!(b) - } - Err(_) => serde_json::Value::Null, - }; - map.insert(column_name.to_string(), value); - } - } - - items.push(serde_json::Value::Object(map)); - } - Ok(None) => break, - Err(e) => { - tracing::error!("{:?}", e); - break; - } - } - }, - Err(e) => { - tracing::error!("{:?}", e); - } - } - } - - Ok(items) - } - - pub async fn execute_cloud( - &self, - sql: String, - args: Vec, - ) -> Result, crate::Error> { - let state = self.manager.state::(); - let guard = state.lock().await; - - let mut items = Vec::new(); - - if let Some(db) = &guard.cloud_db { - use futures_util::TryStreamExt; - let mut stream = std::pin::pin!(db.query_raw(&sql, args).await?); - - while let Some(row) = stream.try_next().await? { - let mut map = serde_json::Map::new(); - - for (idx, column) in row.columns().iter().enumerate() { - let value = match *column.type_() { - tokio_postgres::types::Type::BOOL => row - .try_get::<_, Option>(idx)? - .map(|v| serde_json::json!(v)), - tokio_postgres::types::Type::INT2 | tokio_postgres::types::Type::INT4 => { - row.try_get::<_, Option>(idx)? - .map(|v| serde_json::json!(v)) - } - tokio_postgres::types::Type::INT8 => row - .try_get::<_, Option>(idx)? - .map(|v| serde_json::json!(v)), - tokio_postgres::types::Type::FLOAT4 => row - .try_get::<_, Option>(idx)? - .map(|v| serde_json::json!(v)), - tokio_postgres::types::Type::FLOAT8 => row - .try_get::<_, Option>(idx)? - .map(|v| serde_json::json!(v)), - tokio_postgres::types::Type::TEXT - | tokio_postgres::types::Type::VARCHAR => row - .try_get::<_, Option>(idx)? - .map(|v| serde_json::json!(v)), - tokio_postgres::types::Type::JSON | tokio_postgres::types::Type::JSONB => { - row.try_get::<_, Option>(idx)? - } - _ => row - .try_get::<_, Option>(idx)? - .map(|v| serde_json::json!(v)), - }; - - map.insert( - column.name().to_string(), - value.unwrap_or(serde_json::Value::Null), - ); - } - - items.push(serde_json::Value::Object(map)); - } - } - - Ok(items) - } -} - -pub trait Database2PluginExt { - fn db2(&self) -> Database2<'_, R, Self> - where - Self: tauri::Manager + Sized; -} - -impl> Database2PluginExt for T { - fn db2(&self) -> Database2<'_, R, Self> - where - Self: Sized, - { - Database2 { - manager: self, - _runtime: std::marker::PhantomData, - } - } -} diff --git a/plugins/db2/src/lib.rs b/plugins/db2/src/lib.rs deleted file mode 100644 index 0751fbc73d..0000000000 --- a/plugins/db2/src/lib.rs +++ /dev/null @@ -1,64 +0,0 @@ -use tokio::sync::Mutex; - -mod commands; -mod error; -mod ext; - -pub use error::*; -pub use ext::*; -use tauri::Manager; - -const PLUGIN_NAME: &str = "db2"; - -pub type ManagedState = Mutex; - -#[derive(Default)] -pub struct State { - pub local_db: Option, - pub cloud_db: Option, -} - -fn make_specta_builder() -> tauri_specta::Builder { - tauri_specta::Builder::::new() - .plugin_name(PLUGIN_NAME) - .commands(tauri_specta::collect_commands![ - commands::execute_local::, - commands::execute_cloud::, - ]) - .error_handling(tauri_specta::ErrorHandlingMode::Result) -} - -pub fn init() -> tauri::plugin::TauriPlugin { - let specta_builder = make_specta_builder(); - - tauri::plugin::Builder::new(PLUGIN_NAME) - .invoke_handler(specta_builder.invoke_handler()) - .setup(|app, _api| { - let state = ManagedState::default(); - app.manage(state); - Ok(()) - }) - .build() -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn export_types() { - const OUTPUT_FILE: &str = "./js/bindings.gen.ts"; - - make_specta_builder::() - .export( - specta_typescript::Typescript::default() - .formatter(specta_typescript::formatter::prettier) - .bigint(specta_typescript::BigIntExportBehavior::Number), - OUTPUT_FILE, - ) - .unwrap(); - - let content = std::fs::read_to_string(OUTPUT_FILE).unwrap(); - std::fs::write(OUTPUT_FILE, format!("// @ts-nocheck\n{content}")).unwrap(); - } -} diff --git a/plugins/db2/tsconfig.json b/plugins/db2/tsconfig.json deleted file mode 100644 index 13b985325d..0000000000 --- a/plugins/db2/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "include": ["./js/*.ts"], - "exclude": ["node_modules"] -} diff --git a/plugins/notification/Cargo.toml b/plugins/notification/Cargo.toml index c48ccad13d..022f1f9aa1 100644 --- a/plugins/notification/Cargo.toml +++ b/plugins/notification/Cargo.toml @@ -14,7 +14,6 @@ tauri-plugin = { workspace = true, features = ["build"] } specta-typescript = { workspace = true } [dependencies] -hypr-db-user = { workspace = true } hypr-detect = { workspace = true } hypr-host = { workspace = true } hypr-intercept = { workspace = true } From 291cec300281143a699ecaee3e72a7d40ef8a450 Mon Sep 17 00:00:00 2001 From: Sungbin Jo Date: Mon, 23 Mar 2026 16:25:57 +0900 Subject: [PATCH 2/2] feat: add vibed reactive db hook and screen --- Cargo.lock | 20 + Cargo.toml | 1 + apps/desktop/package.json | 1 + apps/desktop/src-tauri/Cargo.toml | 1 + .../src-tauri/capabilities/default.json | 1 + apps/desktop/src-tauri/src/lib.rs | 11 +- apps/desktop/src/sample/index.tsx | 144 ++++++++ apps/desktop/src/sample/use-live-query.ts | 61 ++++ apps/desktop/src/shared/main/empty/index.tsx | 5 + apps/desktop/src/shared/main/index.tsx | 18 + apps/desktop/src/store/zustand/tabs/schema.ts | 7 +- plugins/reactive-db/Cargo.toml | 27 ++ plugins/reactive-db/build.rs | 5 + plugins/reactive-db/js/index.ts | 49 +++ plugins/reactive-db/package.json | 11 + .../autogenerated/commands/execute.toml | 13 + .../autogenerated/commands/subscribe.toml | 13 + .../autogenerated/commands/unsubscribe.toml | 13 + .../permissions/autogenerated/reference.md | 97 +++++ plugins/reactive-db/permissions/default.toml | 3 + .../permissions/schemas/schema.json | 342 ++++++++++++++++++ plugins/reactive-db/src/commands.rs | 114 ++++++ plugins/reactive-db/src/error.rs | 20 + plugins/reactive-db/src/explain.rs | 122 +++++++ plugins/reactive-db/src/ext.rs | 104 ++++++ plugins/reactive-db/src/lib.rs | 186 ++++++++++ plugins/windows/js/bindings.gen.ts | 2 +- plugins/windows/src/tab/mod.rs | 2 + pnpm-lock.yaml | 18 +- 29 files changed, 1399 insertions(+), 12 deletions(-) create mode 100644 apps/desktop/src/sample/index.tsx create mode 100644 apps/desktop/src/sample/use-live-query.ts create mode 100644 plugins/reactive-db/Cargo.toml create mode 100644 plugins/reactive-db/build.rs create mode 100644 plugins/reactive-db/js/index.ts create mode 100644 plugins/reactive-db/package.json create mode 100644 plugins/reactive-db/permissions/autogenerated/commands/execute.toml create mode 100644 plugins/reactive-db/permissions/autogenerated/commands/subscribe.toml create mode 100644 plugins/reactive-db/permissions/autogenerated/commands/unsubscribe.toml create mode 100644 plugins/reactive-db/permissions/autogenerated/reference.md create mode 100644 plugins/reactive-db/permissions/default.toml create mode 100644 plugins/reactive-db/permissions/schemas/schema.json create mode 100644 plugins/reactive-db/src/commands.rs create mode 100644 plugins/reactive-db/src/error.rs create mode 100644 plugins/reactive-db/src/explain.rs create mode 100644 plugins/reactive-db/src/ext.rs create mode 100644 plugins/reactive-db/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 8e5699fca3..f67ec91c8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4768,6 +4768,7 @@ dependencies = [ "tauri-plugin-permissions", "tauri-plugin-prevent-default", "tauri-plugin-process", + "tauri-plugin-reactive-db", "tauri-plugin-relay", "tauri-plugin-screen", "tauri-plugin-sentry", @@ -18597,6 +18598,25 @@ dependencies = [ "tauri-plugin", ] +[[package]] +name = "tauri-plugin-reactive-db" +version = "0.1.0" +dependencies = [ + "libsqlite3-sys", + "serde", + "serde_json", + "specta", + "specta-typescript", + "sqlx", + "tauri", + "tauri-plugin", + "tauri-specta", + "thiserror 2.0.18", + "tokio", + "tracing", + "uuid", +] + [[package]] name = "tauri-plugin-relay" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7c01d93b10..8c5f8e6acf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -233,6 +233,7 @@ tauri-plugin-tracing = { path = "plugins/tracing" } tauri-plugin-tray = { path = "plugins/tray" } tauri-plugin-updater2 = { path = "plugins/updater2" } tauri-plugin-webhook = { path = "plugins/webhook" } +tauri-plugin-reactive-db = { path = "plugins/reactive-db" } tauri-plugin-windows = { path = "plugins/windows" } async-stream = "0.3.6" diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 06446b3271..57a96616d6 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -63,6 +63,7 @@ "@hypr/plugin-notify": "workspace:*", "@hypr/plugin-opener2": "workspace:*", "@hypr/plugin-overlay": "workspace:*", + "@hypr/plugin-reactive-db": "workspace:*", "@hypr/plugin-path2": "workspace:*", "@hypr/plugin-permissions": "workspace:*", "@hypr/plugin-relay": "workspace:*", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index e14aafc430..d8fa09c61e 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -57,6 +57,7 @@ tauri-plugin-opener = { workspace = true } tauri-plugin-opener2 = { workspace = true } tauri-plugin-os = { workspace = true } tauri-plugin-overlay = { workspace = true } +tauri-plugin-reactive-db = { workspace = true } tauri-plugin-path2 = { workspace = true } tauri-plugin-permissions = { workspace = true } tauri-plugin-prevent-default = { workspace = true } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index f9c07cf5ea..cad0e59f71 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -83,6 +83,7 @@ "autostart:default", "js:default", "flag:default", + "reactive-db:default", "relay:default", { "identifier": "http:default", diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index f0a1a7f3af..472e23cce2 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -157,7 +157,8 @@ pub async fn main() { tauri_plugin_autostart::MacosLauncher::LaunchAgent, Some(vec!["--background"]), )) - .plugin(tauri_plugin_updater2::init()); + .plugin(tauri_plugin_updater2::init()) + .plugin(tauri_plugin_reactive_db::init()); if let Some(client) = sentry_client.as_ref() { builder = builder.plugin(tauri_plugin_sentry::init_with_no_injection(client)); @@ -223,6 +224,14 @@ pub async fn main() { // control::setup(&app_handle); + { + use tauri_plugin_reactive_db::ReactiveDbExt; + let handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + handle.reactive_db().init_memory().await.unwrap(); + }); + } + Ok(()) }) .build(tauri::generate_context!()) diff --git a/apps/desktop/src/sample/index.tsx b/apps/desktop/src/sample/index.tsx new file mode 100644 index 0000000000..8fd2276528 --- /dev/null +++ b/apps/desktop/src/sample/index.tsx @@ -0,0 +1,144 @@ +import { DatabaseIcon } from "lucide-react"; +import { useCallback, useEffect, useState } from "react"; + +import { execute } from "@hypr/plugin-reactive-db"; +import { Button } from "@hypr/ui/components/ui/button"; +import { cn } from "@hypr/utils"; + +import { useLiveQuery } from "./use-live-query"; + +import { StandardTabWrapper } from "~/shared/main"; +import { TabItemBase, type TabItem } from "~/shared/tabs"; +import type { Tab } from "~/store/zustand/tabs"; + +type SampleTab = Extract; + +export const TabItemSample: TabItem = ({ + tab, + tabIndex, + handleCloseThis, + handleSelectThis, + handleCloseOthers, + handleCloseAll, + handlePinThis, + handleUnpinThis, +}) => ( + } + title="Sample" + selected={tab.active} + pinned={tab.pinned} + tabIndex={tabIndex} + handleCloseThis={() => handleCloseThis(tab)} + handleSelectThis={() => handleSelectThis(tab)} + handleCloseOthers={handleCloseOthers} + handleCloseAll={handleCloseAll} + handlePinThis={() => handlePinThis(tab)} + handleUnpinThis={() => handleUnpinThis(tab)} + /> +); + +export function TabContentSample({ tab }: { tab: SampleTab }) { + return ( + + + + ); +} + +interface Session { + id: string; + title: string; + created_at: string; +} + +function SampleView() { + const [title, setTitle] = useState(""); + const [ready, setReady] = useState(false); + + useEffect(() => { + execute( + `CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + )`, + ).then(() => setReady(true)); + }, []); + + const { data: sessions, isLoading } = useLiveQuery( + "SELECT id, title, created_at FROM sessions ORDER BY created_at DESC", + [], + { enabled: ready }, + ); + + const { data: countResult } = useLiveQuery<{ count: number }>( + "SELECT COUNT(*) as count FROM sessions", + [], + { enabled: ready }, + ); + + const count = countResult?.[0]?.count ?? 0; + + const handleCreate = useCallback(async () => { + const trimmed = title.trim(); + if (!trimmed) return; + + await execute( + "INSERT INTO sessions (id, title, created_at) VALUES (?, ?, datetime('now'))", + [crypto.randomUUID(), trimmed], + ); + setTitle(""); + }, [title]); + + return ( +
+
+ setTitle(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleCreate(); + }} + placeholder="New session title..." + className="flex-1 rounded-md border border-neutral-300 px-3 py-1.5 text-sm outline-none focus:border-blue-400" + /> + + + {count} session{count !== 1 ? "s" : ""} + +
+ +
+ {isLoading ? ( +
Loading...
+ ) : sessions?.length === 0 ? ( +
+ No sessions yet. Create one above. +
+ ) : ( +
    + {sessions?.map((session) => ( +
  • + {session.title} + + {session.created_at} + +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/apps/desktop/src/sample/use-live-query.ts b/apps/desktop/src/sample/use-live-query.ts new file mode 100644 index 0000000000..910857fa35 --- /dev/null +++ b/apps/desktop/src/sample/use-live-query.ts @@ -0,0 +1,61 @@ +import { useEffect, useState } from "react"; + +import { subscribe } from "@hypr/plugin-reactive-db"; + +export interface LiveQueryResult { + data: T[] | undefined; + error: string | undefined; + isLoading: boolean; +} + +export function useLiveQuery>( + sql: string, + params: unknown[] = [], + options?: { enabled?: boolean }, +): LiveQueryResult { + const [data, setData] = useState(); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(true); + + const paramsKey = JSON.stringify(params); + + useEffect(() => { + if (options?.enabled === false) { + setIsLoading(false); + return; + } + + setIsLoading(true); + setError(undefined); + + let cancelled = false; + let cleanup: (() => void) | undefined; + + subscribe(sql, params, { + onData: (rows) => { + if (cancelled) return; + setData(rows); + setError(undefined); + setIsLoading(false); + }, + onError: (msg) => { + if (cancelled) return; + setError(msg); + setIsLoading(false); + }, + }).then((unsub) => { + if (cancelled) { + unsub(); + } else { + cleanup = unsub; + } + }); + + return () => { + cancelled = true; + cleanup?.(); + }; + }, [sql, paramsKey, options?.enabled]); + + return { data, error, isLoading }; +} diff --git a/apps/desktop/src/shared/main/empty/index.tsx b/apps/desktop/src/shared/main/empty/index.tsx index d6e39ff5f0..a62a0dd698 100644 --- a/apps/desktop/src/shared/main/empty/index.tsx +++ b/apps/desktop/src/shared/main/empty/index.tsx @@ -77,6 +77,10 @@ function EmptyView() { () => openCurrent({ type: "search" }), [openCurrent], ); + const openSample = useCallback( + () => openCurrent({ type: "sample" }), + [openCurrent], + ); useHotkeys( "mod+o", @@ -110,6 +114,7 @@ function EmptyView() { shortcut={["⌘", "⇧", "F"]} onClick={openAdvancedSearch} /> +
); } + if (tab.type === "sample") { + return ( + + ); + } return null; } @@ -643,6 +658,9 @@ function ContentWrapper({ tab }: { tab: Tab }) { if (tab.type === "edit") { return ; } + if (tab.type === "sample") { + return ; + } return null; } diff --git a/apps/desktop/src/store/zustand/tabs/schema.ts b/apps/desktop/src/store/zustand/tabs/schema.ts index 2e72169153..3a8e5f72eb 100644 --- a/apps/desktop/src/store/zustand/tabs/schema.ts +++ b/apps/desktop/src/store/zustand/tabs/schema.ts @@ -119,7 +119,8 @@ export type Tab = }) | (BaseTab & { type: "onboarding" }) | (BaseTab & { type: "daily" }) - | (BaseTab & { type: "edit"; requestId: string }); + | (BaseTab & { type: "edit"; requestId: string }) + | (BaseTab & { type: "sample" }); export const getDefaultState = (tab: TabInput): Tab => { const base = { active: false, slotId: "", pinned: false }; @@ -227,6 +228,8 @@ export const getDefaultState = (tab: TabInput): Tab => { return { ...base, type: "daily" }; case "edit": return { ...base, type: "edit", requestId: tab.requestId }; + case "sample": + return { ...base, type: "sample" }; default: const _exhaustive: never = tab; return _exhaustive; @@ -275,6 +278,8 @@ export const uniqueIdfromTab = (tab: Tab): string => { return `daily`; case "edit": return `edit-${tab.requestId}`; + case "sample": + return "sample"; } }; diff --git a/plugins/reactive-db/Cargo.toml b/plugins/reactive-db/Cargo.toml new file mode 100644 index 0000000000..179e8cda54 --- /dev/null +++ b/plugins/reactive-db/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tauri-plugin-reactive-db" +version = "0.1.0" +edition = "2024" +links = "tauri-plugin-reactive-db" + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dev-dependencies] +specta-typescript = { workspace = true } + +[dependencies] +tauri = { workspace = true } +tauri-specta = { workspace = true, features = ["derive", "typescript"] } + +sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "sqlite-unbundled"] } +libsqlite3-sys = "0.30" + +tokio = { workspace = true, features = ["sync"] } + +serde = { workspace = true } +serde_json = { workspace = true } +specta = { workspace = true, features = ["serde_json"] } +tracing = { workspace = true } +thiserror = { workspace = true } +uuid = { workspace = true, features = ["v4"] } diff --git a/plugins/reactive-db/build.rs b/plugins/reactive-db/build.rs new file mode 100644 index 0000000000..7691f5c061 --- /dev/null +++ b/plugins/reactive-db/build.rs @@ -0,0 +1,5 @@ +const COMMANDS: &[&str] = &["subscribe", "unsubscribe", "execute"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS).build(); +} diff --git a/plugins/reactive-db/js/index.ts b/plugins/reactive-db/js/index.ts new file mode 100644 index 0000000000..8a67e5bbde --- /dev/null +++ b/plugins/reactive-db/js/index.ts @@ -0,0 +1,49 @@ +import { Channel, invoke } from "@tauri-apps/api/core"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +export type QueryEvent = + | { event: "result"; data: Record[] } + | { event: "error"; data: string }; + +export interface SubscribeOptions> { + onData: (rows: T[]) => void; + onError?: (error: string) => void; +} + +// ── Commands ───────────────────────────────────────────────────────────────── + +export async function subscribe>( + sql: string, + params: unknown[], + options: SubscribeOptions, +): Promise<() => void> { + const channel = new Channel(); + + channel.onmessage = (event: QueryEvent) => { + if (event.event === "result") { + options.onData(event.data as T[]); + } else if (event.event === "error") { + options.onError?.(event.data); + } + }; + + const subscriptionId: string = await invoke("plugin:reactive-db|subscribe", { + sql, + params, + onEvent: channel, + }); + + return () => { + invoke("plugin:reactive-db|unsubscribe", { subscriptionId }).catch( + () => {}, + ); + }; +} + +export async function execute( + sql: string, + params: unknown[] = [], +): Promise[]> { + return invoke("plugin:reactive-db|execute", { sql, params }); +} diff --git a/plugins/reactive-db/package.json b/plugins/reactive-db/package.json new file mode 100644 index 0000000000..c429e700ef --- /dev/null +++ b/plugins/reactive-db/package.json @@ -0,0 +1,11 @@ +{ + "name": "@hypr/plugin-reactive-db", + "version": "0.0.0", + "private": true, + "exports": { + ".": "./js/index.ts" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.0" + } +} diff --git a/plugins/reactive-db/permissions/autogenerated/commands/execute.toml b/plugins/reactive-db/permissions/autogenerated/commands/execute.toml new file mode 100644 index 0000000000..d98be89931 --- /dev/null +++ b/plugins/reactive-db/permissions/autogenerated/commands/execute.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-execute" +description = "Enables the execute command without any pre-configured scope." +commands.allow = ["execute"] + +[[permission]] +identifier = "deny-execute" +description = "Denies the execute command without any pre-configured scope." +commands.deny = ["execute"] diff --git a/plugins/reactive-db/permissions/autogenerated/commands/subscribe.toml b/plugins/reactive-db/permissions/autogenerated/commands/subscribe.toml new file mode 100644 index 0000000000..0277f2aec4 --- /dev/null +++ b/plugins/reactive-db/permissions/autogenerated/commands/subscribe.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-subscribe" +description = "Enables the subscribe command without any pre-configured scope." +commands.allow = ["subscribe"] + +[[permission]] +identifier = "deny-subscribe" +description = "Denies the subscribe command without any pre-configured scope." +commands.deny = ["subscribe"] diff --git a/plugins/reactive-db/permissions/autogenerated/commands/unsubscribe.toml b/plugins/reactive-db/permissions/autogenerated/commands/unsubscribe.toml new file mode 100644 index 0000000000..7d23bab710 --- /dev/null +++ b/plugins/reactive-db/permissions/autogenerated/commands/unsubscribe.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-unsubscribe" +description = "Enables the unsubscribe command without any pre-configured scope." +commands.allow = ["unsubscribe"] + +[[permission]] +identifier = "deny-unsubscribe" +description = "Denies the unsubscribe command without any pre-configured scope." +commands.deny = ["unsubscribe"] diff --git a/plugins/reactive-db/permissions/autogenerated/reference.md b/plugins/reactive-db/permissions/autogenerated/reference.md new file mode 100644 index 0000000000..45fd7cbacc --- /dev/null +++ b/plugins/reactive-db/permissions/autogenerated/reference.md @@ -0,0 +1,97 @@ +## Default Permission + +Default permissions for the reactive-db plugin + +#### This default permission set includes the following: + +- `allow-subscribe` +- `allow-unsubscribe` +- `allow-execute` + +## Permission Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`reactive-db:allow-execute` + + + +Enables the execute command without any pre-configured scope. + +
+ +`reactive-db:deny-execute` + + + +Denies the execute command without any pre-configured scope. + +
+ +`reactive-db:allow-subscribe` + + + +Enables the subscribe command without any pre-configured scope. + +
+ +`reactive-db:deny-subscribe` + + + +Denies the subscribe command without any pre-configured scope. + +
+ +`reactive-db:allow-unsubscribe` + + + +Enables the unsubscribe command without any pre-configured scope. + +
+ +`reactive-db:deny-unsubscribe` + + + +Denies the unsubscribe command without any pre-configured scope. + +
diff --git a/plugins/reactive-db/permissions/default.toml b/plugins/reactive-db/permissions/default.toml new file mode 100644 index 0000000000..3a3bcb09fa --- /dev/null +++ b/plugins/reactive-db/permissions/default.toml @@ -0,0 +1,3 @@ +[default] +description = "Default permissions for the reactive-db plugin" +permissions = ["allow-subscribe", "allow-unsubscribe", "allow-execute"] diff --git a/plugins/reactive-db/permissions/schemas/schema.json b/plugins/reactive-db/permissions/schemas/schema.json new file mode 100644 index 0000000000..dc3736b0c4 --- /dev/null +++ b/plugins/reactive-db/permissions/schemas/schema.json @@ -0,0 +1,342 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the execute command without any pre-configured scope.", + "type": "string", + "const": "allow-execute", + "markdownDescription": "Enables the execute command without any pre-configured scope." + }, + { + "description": "Denies the execute command without any pre-configured scope.", + "type": "string", + "const": "deny-execute", + "markdownDescription": "Denies the execute command without any pre-configured scope." + }, + { + "description": "Enables the subscribe command without any pre-configured scope.", + "type": "string", + "const": "allow-subscribe", + "markdownDescription": "Enables the subscribe command without any pre-configured scope." + }, + { + "description": "Denies the subscribe command without any pre-configured scope.", + "type": "string", + "const": "deny-subscribe", + "markdownDescription": "Denies the subscribe command without any pre-configured scope." + }, + { + "description": "Enables the unsubscribe command without any pre-configured scope.", + "type": "string", + "const": "allow-unsubscribe", + "markdownDescription": "Enables the unsubscribe command without any pre-configured scope." + }, + { + "description": "Denies the unsubscribe command without any pre-configured scope.", + "type": "string", + "const": "deny-unsubscribe", + "markdownDescription": "Denies the unsubscribe command without any pre-configured scope." + }, + { + "description": "Default permissions for the reactive-db plugin\n#### This default permission set includes:\n\n- `allow-subscribe`\n- `allow-unsubscribe`\n- `allow-execute`", + "type": "string", + "const": "default", + "markdownDescription": "Default permissions for the reactive-db plugin\n#### This default permission set includes:\n\n- `allow-subscribe`\n- `allow-unsubscribe`\n- `allow-execute`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/reactive-db/src/commands.rs b/plugins/reactive-db/src/commands.rs new file mode 100644 index 0000000000..f18f464325 --- /dev/null +++ b/plugins/reactive-db/src/commands.rs @@ -0,0 +1,114 @@ +use crate::{ManagedState, QueryEvent, execute_query}; + +#[tauri::command] +#[specta::specta] +pub async fn subscribe( + _app: tauri::AppHandle, + state: tauri::State<'_, ManagedState>, + sql: String, + params: Vec, + on_event: tauri::ipc::Channel, +) -> Result { + let mut guard = state.lock().await; + + let pool = guard.pool.as_ref().ok_or("not initialized")?; + let updates_tx = guard.updates_tx.as_ref().ok_or("not initialized")?; + + // Extract which tables this query depends on. + let tables = crate::explain::extract_tables(pool, &sql) + .await + .map_err(|e| e.to_string())?; + + // Execute the query once and send the initial result set. + let rows = execute_query(pool, &sql, ¶ms) + .await + .map_err(|e| e.to_string())?; + on_event + .send(QueryEvent::Result(rows)) + .map_err(|e| e.to_string())?; + + // Clone everything the spawned task needs. + let rx = updates_tx.subscribe(); + let task_pool = pool.clone(); + let task_sql = sql.clone(); + let task_params = params.clone(); + let task_tables = tables.clone(); + let task_channel = on_event.clone(); + + tokio::spawn(async move { + let mut rx = rx; + loop { + match rx.recv().await { + Ok(update) => { + if !task_tables.contains(&update.table) { + continue; + } + + // Drain any additional pending updates before re-querying + // so we don't re-run the same query multiple times for a + // batch of writes. + while rx.try_recv().is_ok() {} + + match execute_query(&task_pool, &task_sql, &task_params).await { + Ok(rows) => { + if task_channel.send(QueryEvent::Result(rows)).is_err() { + break; + } + } + Err(e) => { + if task_channel.send(QueryEvent::Error(e.to_string())).is_err() { + break; + } + } + } + } + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue, + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, + } + } + }); + + let id = uuid::Uuid::new_v4().to_string(); + + guard.subscriptions.insert( + id.clone(), + crate::Subscription { + sql, + params, + tables, + channel: on_event, + }, + ); + + Ok(id) +} + +#[tauri::command] +#[specta::specta] +pub async fn unsubscribe( + _app: tauri::AppHandle, + state: tauri::State<'_, ManagedState>, + subscription_id: String, +) -> Result<(), String> { + let mut guard = state.lock().await; + guard + .subscriptions + .remove(&subscription_id) + .ok_or_else(|| format!("subscription not found: {subscription_id}"))?; + Ok(()) +} + +#[tauri::command] +#[specta::specta] +pub async fn execute( + _app: tauri::AppHandle, + state: tauri::State<'_, ManagedState>, + sql: String, + params: Vec, +) -> Result, String> { + let guard = state.lock().await; + let pool = guard.pool.as_ref().ok_or("not initialized")?; + execute_query(pool, &sql, ¶ms) + .await + .map_err(|e| e.to_string()) +} diff --git a/plugins/reactive-db/src/error.rs b/plugins/reactive-db/src/error.rs new file mode 100644 index 0000000000..c7a6f84b38 --- /dev/null +++ b/plugins/reactive-db/src/error.rs @@ -0,0 +1,20 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("not initialized — call init() first")] + NotInitialized, + + #[error("subscription not found: {0}")] + SubscriptionNotFound(String), + + #[error(transparent)] + Sqlx(#[from] sqlx::Error), + + #[error(transparent)] + Tauri(#[from] tauri::Error), +} + +impl serde::Serialize for Error { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} diff --git a/plugins/reactive-db/src/explain.rs b/plugins/reactive-db/src/explain.rs new file mode 100644 index 0000000000..1f962feadf --- /dev/null +++ b/plugins/reactive-db/src/explain.rs @@ -0,0 +1,122 @@ +use std::collections::{HashMap, HashSet}; + +use sqlx::{Row, SqlitePool}; + +/// Extract the set of table names that `sql` reads from, using `EXPLAIN QUERY PLAN`. +/// +/// # Safety +/// +/// `sql` is interpolated into `format!("EXPLAIN QUERY PLAN {sql}")` and executed directly. +/// Only pass SQL from trusted code, never user input. +pub async fn extract_tables(pool: &SqlitePool, sql: &str) -> Result, sqlx::Error> { + let master_rows = sqlx::query( + "SELECT tbl_name FROM sqlite_master WHERE type = 'table' AND tbl_name NOT LIKE 'sqlite_%'", + ) + .fetch_all(pool) + .await?; + + let known_tables: HashSet = master_rows + .iter() + .map(|r| r.get::("tbl_name")) + .collect(); + + let alias_map = build_alias_map(sql, &known_tables); + + let eqp_rows = sqlx::query(&format!("EXPLAIN QUERY PLAN {sql}")) + .fetch_all(pool) + .await?; + + let mut tables = HashSet::new(); + for row in &eqp_rows { + let detail: &str = row.get("detail"); + if let Some(name) = parse_table_from_detail(detail) { + if known_tables.contains(name) { + tables.insert(name.to_string()); + } else if let Some(real) = alias_map.get(name) { + tables.insert(real.clone()); + } + } + } + + Ok(tables) +} + +fn parse_table_from_detail(detail: &str) -> Option<&str> { + let trimmed = detail.trim(); + let rest = trimmed + .strip_prefix("SCAN ") + .or_else(|| trimmed.strip_prefix("SEARCH "))?; + rest.split_whitespace().next() +} + +fn build_alias_map(sql: &str, known_tables: &HashSet) -> HashMap { + let mut map = HashMap::new(); + let upper = sql.to_uppercase(); + let tokens: Vec<&str> = sql.split_whitespace().collect(); + let upper_tokens: Vec<&str> = upper.split_whitespace().collect(); + + for i in 0..tokens.len() { + let is_from_or_join = matches!( + upper_tokens[i], + "FROM" | "JOIN" | "INNER" | "LEFT" | "RIGHT" | "CROSS" + ); + if !is_from_or_join { + continue; + } + + let table_idx = if matches!(upper_tokens[i], "INNER" | "LEFT" | "RIGHT" | "CROSS") { + if i + 1 < tokens.len() && upper_tokens[i + 1] == "JOIN" { + i + 2 + } else { + continue; + } + } else { + i + 1 + }; + + if table_idx >= tokens.len() { + continue; + } + + let raw_table = tokens[table_idx].trim_end_matches(|c| c == ',' || c == ')'); + if !known_tables.contains(raw_table) { + continue; + } + + let alias_idx = + if table_idx + 1 < upper_tokens.len() && upper_tokens[table_idx + 1] == "AS" { + table_idx + 2 + } else { + table_idx + 1 + }; + + if alias_idx < tokens.len() { + let alias = + tokens[alias_idx].trim_end_matches(|c: char| c == ',' || c == ')' || c == ';'); + let alias_upper = alias.to_uppercase(); + if !alias.is_empty() + && !matches!( + alias_upper.as_str(), + "ON" | "WHERE" + | "SET" + | "JOIN" + | "INNER" + | "LEFT" + | "RIGHT" + | "CROSS" + | "ORDER" + | "GROUP" + | "HAVING" + | "LIMIT" + | "UNION" + | "EXCEPT" + | "INTERSECT" + ) + && !known_tables.contains(alias) + { + map.insert(alias.to_string(), raw_table.to_string()); + } + } + } + map +} diff --git a/plugins/reactive-db/src/ext.rs b/plugins/reactive-db/src/ext.rs new file mode 100644 index 0000000000..500a467df7 --- /dev/null +++ b/plugins/reactive-db/src/ext.rs @@ -0,0 +1,104 @@ +use std::ffi::c_void; +use std::sync::Arc; + +use sqlx::SqlitePool; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use tokio::sync::broadcast; + +use crate::{ManagedState, TableUpdate, update_hook_callback}; + +pub struct ReactiveDb<'a, R: tauri::Runtime, M: tauri::Manager> { + manager: &'a M, + _runtime: std::marker::PhantomData R>, +} + +impl<'a, R: tauri::Runtime, M: tauri::Manager> ReactiveDb<'a, R, M> { + /// Initialize with custom connection options. + /// Returns a clone of the pool so the caller can run migrations, etc. + pub async fn init(&self, options: SqliteConnectOptions) -> Result { + let (tx, _) = broadcast::channel::(256); + let tx_for_hook = tx.clone(); + + let pool = SqlitePoolOptions::new() + .max_connections(1) + .after_connect(move |conn, _| { + let tx = tx_for_hook.clone(); + Box::pin(async move { + let mut handle = conn.lock_handle().await?; + let raw = handle.as_raw_handle().as_ptr(); + + // Leak an Arc so the broadcast sender outlives the C callback. + let tx_ptr = Arc::into_raw(Arc::new(tx)) as *mut c_void; + unsafe { + libsqlite3_sys::sqlite3_update_hook( + raw, + Some(update_hook_callback), + tx_ptr, + ); + } + + Ok(()) + }) + }) + .connect_with(options) + .await?; + + { + let state = self.manager.state::(); + let mut guard = state.lock().await; + guard.pool = Some(pool.clone()); + guard.updates_tx = Some(tx); + } + + Ok(pool) + } + + /// Convenience: open a local file-backed database. + pub async fn init_local( + &self, + path: impl AsRef, + ) -> Result { + if let Some(parent) = path.as_ref().parent() { + std::fs::create_dir_all(parent).ok(); + } + let options = SqliteConnectOptions::new() + .filename(path) + .create_if_missing(true) + .pragma("journal_mode", "WAL") + .pragma("foreign_keys", "ON"); + self.init(options).await + } + + /// Convenience: open an in-memory database (useful for tests). + pub async fn init_memory(&self) -> Result { + let options = SqliteConnectOptions::new() + .filename(":memory:") + .pragma("foreign_keys", "ON"); + self.init(options).await + } + + /// Access the underlying pool (e.g. for migrations). + pub async fn pool(&self) -> Option { + let state = self.manager.state::(); + let guard = state.lock().await; + guard.pool.clone() + } +} + +pub trait ReactiveDbExt { + fn reactive_db(&self) -> ReactiveDb<'_, R, Self> + where + Self: tauri::Manager + Sized; +} + +impl> ReactiveDbExt for T { + fn reactive_db(&self) -> ReactiveDb<'_, R, Self> + where + Self: Sized, + { + ReactiveDb { + manager: self, + _runtime: std::marker::PhantomData, + } + } +} diff --git a/plugins/reactive-db/src/lib.rs b/plugins/reactive-db/src/lib.rs new file mode 100644 index 0000000000..6bab14da89 --- /dev/null +++ b/plugins/reactive-db/src/lib.rs @@ -0,0 +1,186 @@ +use std::collections::HashMap; +use std::ffi::{c_void, CStr}; + +use sqlx::SqlitePool; +use tokio::sync::broadcast; + +mod commands; +mod error; +mod explain; +mod ext; + +pub use error::*; +pub use ext::*; + +const PLUGIN_NAME: &str = "reactive-db"; + +#[derive(Debug, Clone, serde::Serialize, specta::Type)] +#[serde(tag = "event", content = "data")] +pub enum QueryEvent { + #[serde(rename = "result")] + Result(Vec), + #[serde(rename = "error")] + Error(String), +} + +#[derive(Debug, Clone)] +pub(crate) struct TableUpdate { + pub table: String, +} + +#[allow(dead_code)] +pub(crate) struct Subscription { + pub sql: String, + pub params: Vec, + pub tables: std::collections::HashSet, + pub channel: tauri::ipc::Channel, +} + +pub struct State { + pub(crate) pool: Option, + pub(crate) updates_tx: Option>, + pub(crate) subscriptions: HashMap, +} + +impl Default for State { + fn default() -> Self { + Self { + pool: None, + updates_tx: None, + subscriptions: HashMap::new(), + } + } +} + +pub type ManagedState = tokio::sync::Mutex; + +fn make_specta_builder() -> tauri_specta::Builder { + tauri_specta::Builder::::new() + .plugin_name(PLUGIN_NAME) + .commands(tauri_specta::collect_commands![ + commands::subscribe::, + commands::unsubscribe::, + commands::execute::, + ]) + .error_handling(tauri_specta::ErrorHandlingMode::Result) +} + +pub fn init() -> tauri::plugin::TauriPlugin { + let specta_builder = make_specta_builder(); + + tauri::plugin::Builder::new(PLUGIN_NAME) + .invoke_handler(specta_builder.invoke_handler()) + .setup(|app, _api| { + use tauri::Manager; + app.manage(ManagedState::default()); + Ok(()) + }) + .build() +} + +// ── Query helpers ──────────────────────────────────────────────────────────── + +pub(crate) async fn execute_query( + pool: &SqlitePool, + sql: &str, + params: &[serde_json::Value], +) -> Result, sqlx::Error> { + let mut query = sqlx::query(sql); + for param in params { + query = match param { + serde_json::Value::Null => query.bind(None::), + serde_json::Value::Bool(b) => query.bind(*b), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + query.bind(i) + } else { + query.bind(n.as_f64().unwrap_or_default()) + } + } + serde_json::Value::String(s) => query.bind(s.clone()), + other => query.bind(other.to_string()), + }; + } + let rows = query.fetch_all(pool).await?; + Ok(rows.iter().map(row_to_json).collect()) +} + +fn row_to_json(row: &sqlx::sqlite::SqliteRow) -> serde_json::Value { + use sqlx::{Column, Row, TypeInfo, ValueRef}; + + let mut map = serde_json::Map::new(); + for (idx, col) in row.columns().iter().enumerate() { + let value = match row.try_get_raw(idx) { + Ok(raw) if !raw.is_null() => match raw.type_info().name() { + "TEXT" => row + .get::, _>(idx) + .map(serde_json::Value::String) + .unwrap_or(serde_json::Value::Null), + "INTEGER" | "INT" | "BOOLEAN" => row + .get::, _>(idx) + .map(|v| serde_json::json!(v)) + .unwrap_or(serde_json::Value::Null), + "REAL" => row + .get::, _>(idx) + .map(|v| serde_json::json!(v)) + .unwrap_or(serde_json::Value::Null), + "BLOB" => row + .get::>, _>(idx) + .map(|v| serde_json::json!(v)) + .unwrap_or(serde_json::Value::Null), + _ => row + .get::, _>(idx) + .map(serde_json::Value::String) + .unwrap_or(serde_json::Value::Null), + }, + _ => serde_json::Value::Null, + }; + map.insert(col.name().to_string(), value); + } + serde_json::Value::Object(map) +} + +// ── C callback for sqlite3_update_hook ────────────────────────────────────── + +unsafe extern "C" fn update_hook_callback( + user_data: *mut c_void, + op: std::os::raw::c_int, + _db_name: *const std::os::raw::c_char, + table_name: *const std::os::raw::c_char, + _rowid: libsqlite3_sys::sqlite3_int64, +) { + unsafe { + let is_mutation = op == libsqlite3_sys::SQLITE_INSERT + || op == libsqlite3_sys::SQLITE_UPDATE + || op == libsqlite3_sys::SQLITE_DELETE; + if !is_mutation { + return; + } + + let tx = &*(user_data as *const broadcast::Sender); + let table = CStr::from_ptr(table_name).to_string_lossy().into_owned(); + let _ = tx.send(TableUpdate { table }); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn export_types() { + const OUTPUT_FILE: &str = "./js/bindings.gen.ts"; + + make_specta_builder::() + .export( + specta_typescript::Typescript::default() + .formatter(specta_typescript::formatter::prettier) + .bigint(specta_typescript::BigIntExportBehavior::Number), + OUTPUT_FILE, + ) + .unwrap(); + + let content = std::fs::read_to_string(OUTPUT_FILE).unwrap(); + std::fs::write(OUTPUT_FILE, format!("// @ts-nocheck\n{content}")).unwrap(); + } +} diff --git a/plugins/windows/js/bindings.gen.ts b/plugins/windows/js/bindings.gen.ts index b9a2282f17..7fbaa9975c 100644 --- a/plugins/windows/js/bindings.gen.ts +++ b/plugins/windows/js/bindings.gen.ts @@ -85,7 +85,7 @@ export type OpenTab = { tab: TabInput } export type PromptsState = { selectedTask: string | null } export type SearchState = { selectedTypes: string[] | null; initialQuery: string | null } export type SessionsState = { view: EditorView | null; autoStart: boolean | null } -export type TabInput = { type: "sessions"; id: string; state?: SessionsState | null } | { type: "contacts"; state?: ContactsState | null } | { type: "templates"; state?: TemplatesState | null } | { type: "prompts"; state?: PromptsState | null } | { type: "chat_shortcuts"; state?: ChatShortcutsState | null } | { type: "extensions"; state?: ExtensionsState | null } | { type: "humans"; id: string } | { type: "organizations"; id: string } | { type: "folders"; id: string | null } | { type: "empty" } | { type: "extension"; extensionId: string; state?: Partial<{ [key in string]: JsonValue }> | null } | { type: "calendar" } | { type: "changelog"; state: ChangelogState } | { type: "settings" } | { type: "ai"; state?: AiState | null } | { type: "search"; state?: SearchState | null } | { type: "chat_support"; state?: ChatState | null } | { type: "onboarding" } | { type: "daily" } | { type: "edit"; requestId: string } +export type TabInput = { type: "sessions"; id: string; state?: SessionsState | null } | { type: "contacts"; state?: ContactsState | null } | { type: "templates"; state?: TemplatesState | null } | { type: "prompts"; state?: PromptsState | null } | { type: "chat_shortcuts"; state?: ChatShortcutsState | null } | { type: "extensions"; state?: ExtensionsState | null } | { type: "humans"; id: string } | { type: "organizations"; id: string } | { type: "folders"; id: string | null } | { type: "empty" } | { type: "extension"; extensionId: string; state?: Partial<{ [key in string]: JsonValue }> | null } | { type: "calendar" } | { type: "changelog"; state: ChangelogState } | { type: "settings" } | { type: "ai"; state?: AiState | null } | { type: "search"; state?: SearchState | null } | { type: "chat_support"; state?: ChatState | null } | { type: "onboarding" } | { type: "daily" } | { type: "edit"; requestId: string } | { type: "sample" } export type TemplatesState = { showHomepage: boolean | null; isWebMode: boolean | null; selectedMineId: string | null; selectedWebIndex: number | null } export type VisibilityEvent = { window: AppWindow; visible: boolean } export type WindowDestroyed = { window: AppWindow } diff --git a/plugins/windows/src/tab/mod.rs b/plugins/windows/src/tab/mod.rs index 45b3006a07..0102f8f1f1 100644 --- a/plugins/windows/src/tab/mod.rs +++ b/plugins/windows/src/tab/mod.rs @@ -89,5 +89,7 @@ common_derives! { #[serde(rename = "requestId")] request_id: String, }, + #[serde(rename = "sample")] + Sample, } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd599d4991..4634863d3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,9 +223,6 @@ importers: '@hypr/plugin-calendar': specifier: workspace:* version: link:../../plugins/calendar - '@hypr/plugin-db2': - specifier: workspace:* - version: link:../../plugins/db2 '@hypr/plugin-deeplink2': specifier: workspace:* version: link:../../plugins/deeplink2 @@ -298,6 +295,9 @@ importers: '@hypr/plugin-permissions': specifier: workspace:* version: link:../../plugins/permissions + '@hypr/plugin-reactive-db': + specifier: workspace:* + version: link:../../plugins/reactive-db '@hypr/plugin-relay': specifier: workspace:* version: link:../../plugins/relay @@ -1786,12 +1786,6 @@ importers: specifier: ^2.10.1 version: 2.10.1 - plugins/db2: - dependencies: - '@tauri-apps/api': - specifier: ^2.10.1 - version: 2.10.1 - plugins/deeplink2: dependencies: '@tauri-apps/api': @@ -1942,6 +1936,12 @@ importers: specifier: ^2.10.1 version: 2.10.1 + plugins/reactive-db: + dependencies: + '@tauri-apps/api': + specifier: ^2.0.0 + version: 2.10.1 + plugins/relay: {} plugins/screen: