Compare commits

..

9 Commits

Author SHA1 Message Date
Gregory Schier
fcda6f8d32 Fix lint errors 2025-06-04 11:33:10 -07:00
Gregory Schier
021f2171d6 Show error dialog on migration failure 2025-06-04 11:20:28 -07:00
Gregory Schier
2562cf7c55 Setting to colorize HTTP methods
https://feedback.yaak.app/p/support-colors-for-http-method-in-sidebar
2025-06-04 10:59:40 -07:00
Gregory Schier
58873ea606 Fix text streaming breaking scroll 2025-06-04 10:38:55 -07:00
Gregory Schier
9d9e83d59f Remove sqlx for migrations (#224) 2025-06-03 14:42:32 -07:00
Gregory Schier
01d40f5b0d Fix context_id for Workspace/Folder auth 2025-06-03 13:08:48 -07:00
Gregory Schier
bdb1adcce1 Fix TS type 2025-06-03 12:44:14 -07:00
Gregory Schier
9f6a3da8d3 Disable auth for OAuth token http requests 2025-06-03 12:42:31 -07:00
Gregory Schier
158487e3a6 sqlx log DEBUG to debug failed migrations 2025-06-03 11:01:51 -07:00
46 changed files with 381 additions and 666 deletions

View File

@@ -47,8 +47,7 @@ npm start
New migrations can be created from the `src-tauri/` directory:
```shell
cd src-tauri
sqlx migrate add migration-name
npm run migration
```
Run the app to apply the migrations.

View File

@@ -49,6 +49,7 @@
"start": "npm run app-dev",
"app-build": "tauri build",
"app-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
"migration": "node scripts/create-migration.cjs",
"build": "npm run --workspaces --if-present build",
"build-plugins": "npm run --workspaces --if-present build",
"bootstrap": "run-p bootstrap:* && npm run --workspaces --if-present bootstrap",

View File

@@ -11,7 +11,6 @@ import {
HttpRequestAction,
InternalEvent,
InternalEventPayload,
JsonPrimitive,
ListCookieNamesResponse,
PluginWindowContext,
PromptTextResponse,
@@ -22,6 +21,7 @@ import {
TemplateRenderResponse,
} from '@yaakapp-internal/plugins';
import { Context, PluginDefinition } from '@yaakapp/api';
import { JsonValue } from '@yaakapp/api/lib/bindings/serde_json/JsonValue';
import console from 'node:console';
import { readFileSync, type Stats, statSync, watch } from 'node:fs';
import path from 'node:path';
@@ -568,7 +568,7 @@ function genId(len = 5): string {
/** Recursively apply form input defaults to a set of values */
function applyFormInputDefaults(
inputs: TemplateFunctionArg[],
values: { [p: string]: JsonPrimitive | undefined },
values: { [p: string]: JsonValue | undefined },
) {
for (const input of inputs) {
if ('inputs' in input) {

View File

@@ -50,8 +50,11 @@ export async function getAccessToken(
httpRequest.headers!.push({ name: 'Authorization', value });
}
httpRequest.authenticationType = 'none'; // Don't inherit workspace auth
const resp = await ctx.httpRequest.send({ httpRequest });
console.log('[oauth2] Got access token response', resp.status);
const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : '';
if (resp.status < 200 || resp.status >= 300) {

View File

@@ -63,6 +63,7 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
httpRequest.headers!.push({ name: 'Authorization', value });
}
httpRequest.authenticationType = 'none'; // Don't inherit workspace auth
const resp = await ctx.httpRequest.send({ httpRequest });
if (resp.status === 401) {
@@ -75,6 +76,8 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
const body = resp.bodyPath ? readFileSync(resp.bodyPath, 'utf8') : '';
console.log('[oauth2] Got refresh token response', resp.status);
if (resp.status < 200 || resp.status >= 300) {
throw new Error('Failed to refresh access token with status=' + resp.status + ' and body=' + body);
}
@@ -95,5 +98,6 @@ export async function getOrRefreshAccessToken(ctx: Context, contextId: string, {
// Assign a new one or keep the old one,
refresh_token: response.refresh_token ?? token.response.refresh_token,
};
return storeToken(ctx, contextId, newResponse);
}

View File

@@ -0,0 +1,46 @@
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const slugify = require('slugify');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function generateTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}${hours}${minutes}${seconds}`;
}
async function createMigration() {
try {
const migrationName = await new Promise((resolve) => {
rl.question('Enter migration name: ', resolve);
});
const timestamp = generateTimestamp();
const fileName = `${timestamp}_${slugify(String(migrationName), { lower: true })}.sql`;
const migrationsDir = path.join(__dirname, '../src-tauri/migrations');
const filePath = path.join(migrationsDir, fileName);
if (!fs.existsSync(migrationsDir)) {
fs.mkdirSync(migrationsDir, { recursive: true });
}
fs.writeFileSync(filePath, '-- Add migration SQL here\n');
console.log(`Created migration file: ${fileName}`);
} catch (error) {
console.error('Error creating migration:', error);
} finally {
rl.close();
}
}
createMigration().catch(console.error);

508
src-tauri/Cargo.lock generated
View File

@@ -91,12 +91,6 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -393,15 +387,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
"num-traits",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -494,12 +479,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -973,12 +952,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -1096,21 +1069,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
@@ -1129,15 +1087,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@@ -1262,17 +1211,6 @@ dependencies = [
"sha2",
]
[[package]]
name = "der"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
[[package]]
name = "deranged"
version = "0.3.11"
@@ -1336,7 +1274,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
]
@@ -1411,12 +1348,6 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "downcast-rs"
version = "1.2.1"
@@ -1464,9 +1395,6 @@ name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
dependencies = [
"serde",
]
[[package]]
name = "embed-resource"
@@ -1566,17 +1494,6 @@ version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
[[package]]
name = "etcetera"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.48.0",
]
[[package]]
name = "event-listener"
version = "5.3.1"
@@ -1682,17 +1599,6 @@ dependencies = [
"miniz_oxide 0.8.0",
]
[[package]]
name = "flume"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
dependencies = [
"futures-core",
"futures-sink",
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1782,7 +1688,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -1802,17 +1707,6 @@ dependencies = [
"futures-util",
]
[[package]]
name = "futures-intrusive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
"parking_lot",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@@ -2248,7 +2142,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash 0.8.11",
"allocator-api2",
]
[[package]]
@@ -2317,15 +2210,6 @@ dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "html5ever"
version = "0.26.0"
@@ -2834,9 +2718,6 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "libappindicator"
@@ -2911,12 +2792,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "libm"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "libredox"
version = "0.1.3"
@@ -2925,7 +2800,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.9.0",
"libc",
"redox_syscall 0.5.3",
"redox_syscall",
]
[[package]]
@@ -3031,16 +2906,6 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "md5"
version = "0.7.0"
@@ -3307,23 +3172,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
dependencies = [
"byteorder",
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]]
name = "num-complex"
version = "0.4.6"
@@ -3377,7 +3225,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@@ -3879,17 +3726,11 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.3",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "path-slash"
version = "0.2.1"
@@ -3902,15 +3743,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -4104,27 +3936,6 @@ dependencies = [
"futures-io",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.30"
@@ -4586,15 +4397,6 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.5.3"
@@ -4786,26 +4588,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "rsa"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
dependencies = [
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rusqlite"
version = "0.32.1"
@@ -5331,9 +5113,9 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.10.8"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -5365,16 +5147,6 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core 0.6.4",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
@@ -5407,9 +5179,6 @@ name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
dependencies = [
"serde",
]
[[package]]
name = "socket2"
@@ -5438,7 +5207,7 @@ dependencies = [
"objc2-foundation 0.2.2",
"objc2-quartz-core 0.2.2",
"raw-window-handle",
"redox_syscall 0.5.3",
"redox_syscall",
"wasm-bindgen",
"web-sys",
"windows-sys 0.52.0",
@@ -5470,229 +5239,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "sqlformat"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f"
dependencies = [
"nom",
"unicode_categories",
]
[[package]]
name = "sqlx"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e"
dependencies = [
"sqlx-core",
"sqlx-macros",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
]
[[package]]
name = "sqlx-core"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e"
dependencies = [
"atoi",
"byteorder",
"bytes",
"crc",
"crossbeam-queue",
"either",
"event-listener",
"futures-channel",
"futures-core",
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.14.5",
"hashlink",
"hex",
"indexmap 2.3.0",
"log",
"memchr",
"once_cell",
"paste",
"percent-encoding",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"sha2",
"smallvec",
"sqlformat",
"thiserror 1.0.63",
"tokio",
"tokio-stream",
"tracing",
"url",
"webpki-roots",
]
[[package]]
name = "sqlx-macros"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657"
dependencies = [
"proc-macro2",
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.87",
]
[[package]]
name = "sqlx-macros-core"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5"
dependencies = [
"dotenvy",
"either",
"heck 0.5.0",
"hex",
"once_cell",
"proc-macro2",
"quote",
"serde",
"serde_json",
"sha2",
"sqlx-core",
"sqlx-sqlite",
"syn 2.0.87",
"tempfile",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
dependencies = [
"atoi",
"base64 0.22.1",
"bitflags 2.9.0",
"byteorder",
"bytes",
"crc",
"digest",
"dotenvy",
"either",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"generic-array",
"hex",
"hkdf",
"hmac",
"itoa 1.0.11",
"log",
"md-5",
"memchr",
"once_cell",
"percent-encoding",
"rand 0.8.5",
"rsa",
"sha1",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 1.0.63",
"tracing",
"whoami",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
dependencies = [
"atoi",
"base64 0.22.1",
"bitflags 2.9.0",
"byteorder",
"crc",
"dotenvy",
"etcetera",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"hex",
"hkdf",
"hmac",
"home",
"itoa 1.0.11",
"log",
"md-5",
"memchr",
"once_cell",
"rand 0.8.5",
"serde",
"serde_json",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 1.0.63",
"tracing",
"whoami",
]
[[package]]
name = "sqlx-sqlite"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680"
dependencies = [
"atoi",
"flume",
"futures-channel",
"futures-core",
"futures-executor",
"futures-intrusive",
"futures-util",
"libsqlite3-sys",
"log",
"percent-encoding",
"serde",
"serde_urlencoded",
"sqlx-core",
"tracing",
"url",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@@ -5731,17 +5277,6 @@ dependencies = [
"quote",
]
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]]
name = "strsim"
version = "0.11.1"
@@ -6731,7 +6266,6 @@ version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -6936,24 +6470,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291"
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "universal-hash"
version = "0.5.1"
@@ -7113,12 +6635,6 @@ dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
@@ -7403,16 +6919,6 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]]
name = "whoami"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
dependencies = [
"redox_syscall 0.4.1",
"wasite",
]
[[package]]
name = "winapi"
version = "0.3.9"
@@ -8199,6 +7705,7 @@ name = "yaak-models"
version = "0.1.0"
dependencies = [
"chrono",
"hex",
"log",
"nanoid",
"r2d2",
@@ -8208,9 +7715,10 @@ dependencies = [
"sea-query-rusqlite",
"serde",
"serde_json",
"sqlx",
"sha2",
"tauri",
"tauri-plugin",
"tauri-plugin-dialog",
"thiserror 2.0.12",
"tokio",
"ts-rs",

View File

@@ -54,7 +54,7 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
tauri-plugin-clipboard-manager = "2.2.2"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-dialog = { workspace = true }
tauri-plugin-fs = "2.2.0"
tauri-plugin-log = { version = "2.3.1", features = ["colored"] }
tauri-plugin-opener = "2.2.6"
@@ -87,6 +87,7 @@ serde = "1.0.219"
serde_json = "1.0.140"
tauri = "2.4.1"
tauri-plugin = "2.1.1"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-shell = "2.2.1"
tokio = "1.44.2"
thiserror = "2.0.12"

View File

@@ -164,8 +164,6 @@ DROP TABLE websocket_requests;
ALTER TABLE websocket_requests_dg_tmp
RENAME TO websocket_requests;
PRAGMA foreign_keys = ON;
---------------------------
-- Remove environment FK --
---------------------------

View File

@@ -0,0 +1,2 @@
ALTER TABLE settings
ADD COLUMN colored_methods BOOLEAN DEFAULT FALSE;

View File

@@ -1,13 +1,13 @@
use std::collections::BTreeMap;
use crate::error::Result;
use KeyAndValueRef::{Ascii, Binary};
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_grpc::{KeyAndValueRef, MetadataMap};
use yaak_models::models::GrpcRequest;
use yaak_models::query_manager::QueryManagerExt;
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader};
use yaak_plugins::manager::PluginManager;
use KeyAndValueRef::{Ascii, Binary};
pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String> {
let mut entries = BTreeMap::new();
@@ -23,10 +23,10 @@ pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap<String, String>
pub(crate) fn resolve_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
request: &GrpcRequest,
) -> Result<GrpcRequest> {
) -> Result<(GrpcRequest, String)> {
let mut new_request = request.clone();
let (authentication_type, authentication) =
let (authentication_type, authentication, authentication_context_id) =
window.db().resolve_auth_for_grpc_request(request)?;
new_request.authentication_type = authentication_type;
new_request.authentication = authentication;
@@ -34,12 +34,13 @@ pub(crate) fn resolve_grpc_request<R: Runtime>(
let metadata = window.db().resolve_metadata_for_grpc_request(request)?;
new_request.metadata = metadata;
Ok(new_request)
Ok((new_request, authentication_context_id))
}
pub(crate) async fn build_metadata<R: Runtime>(
window: &WebviewWindow<R>,
request: &GrpcRequest,
authentication_context_id: &str,
) -> Result<BTreeMap<String, String>> {
let plugin_manager = window.state::<PluginManager>();
let mut metadata = BTreeMap::new();
@@ -67,7 +68,7 @@ pub(crate) async fn build_metadata<R: Runtime>(
Some(authentication_type) => {
let auth = request.authentication.clone();
let plugin_req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request.id.clone())),
context_id: format!("{:x}", md5::compute(authentication_context_id)),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
method: "POST".to_string(),
url: request.url.clone(),

View File

@@ -62,7 +62,8 @@ pub async fn send_http_request<R: Runtime>(
);
let update_source = UpdateSource::from_window(window);
let resolved_request = match resolve_http_request(window, unrendered_request) {
let (resolved_request, auth_context_id) = match resolve_http_request(window, unrendered_request)
{
Ok(r) => r,
Err(e) => {
return Ok(response_err(
@@ -429,7 +430,7 @@ pub async fn send_http_request<R: Runtime>(
}
Some(authentication_type) => {
let req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request.id)),
context_id: format!("{:x}", md5::compute(auth_context_id)),
values: serde_json::from_value(
serde_json::to_value(&request.authentication).unwrap(),
)
@@ -667,10 +668,10 @@ pub async fn send_http_request<R: Runtime>(
fn resolve_http_request<R: Runtime>(
window: &WebviewWindow<R>,
request: &HttpRequest,
) -> Result<HttpRequest> {
) -> Result<(HttpRequest, String)> {
let mut new_request = request.clone();
let (authentication_type, authentication) =
let (authentication_type, authentication, authentication_context_id) =
window.db().resolve_auth_for_http_request(request)?;
new_request.authentication_type = authentication_type;
new_request.authentication = authentication;
@@ -678,7 +679,7 @@ fn resolve_http_request<R: Runtime>(
let headers = window.db().resolve_headers_for_http_request(request)?;
new_request.headers = headers;
Ok(new_request)
Ok((new_request, authentication_context_id))
}
fn ensure_proto(url_str: &str) -> String {

View File

@@ -151,7 +151,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
None => None,
};
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
let resolved_request = resolve_grpc_request(&window, &unrendered_request)?;
let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?;
let base_environment =
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
@@ -170,7 +170,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
.await?;
let uri = safe_uri(&req.url);
let metadata = build_metadata(&window, &req).await?;
let metadata = build_metadata(&window, &req, &auth_context_id).await?;
Ok(grpc_handle
.lock()
@@ -200,7 +200,7 @@ async fn cmd_grpc_go<R: Runtime>(
None => None,
};
let unrendered_request = app_handle.db().get_grpc_request(request_id)?;
let resolved_request = resolve_grpc_request(&window, &unrendered_request)?;
let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?;
let base_environment =
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
@@ -217,7 +217,7 @@ async fn cmd_grpc_go<R: Runtime>(
)
.await?;
let metadata = build_metadata(&window, &request).await?;
let metadata = build_metadata(&window, &request, &auth_context_id).await?;
let conn = app_handle.db().upsert_grpc_connection(
&GrpcConnection {
@@ -1232,7 +1232,7 @@ pub fn run() {
.level_for("hyper_util", log::LevelFilter::Info)
.level_for("hyper_rustls", log::LevelFilter::Info)
.level_for("reqwest", log::LevelFilter::Info)
.level_for("sqlx", log::LevelFilter::Warn)
.level_for("sqlx", log::LevelFilter::Debug)
.level_for("tao", log::LevelFilter::Info)
.level_for("tokio_util", log::LevelFilter::Info)
.level_for("tonic", log::LevelFilter::Info)

View File

@@ -16,11 +16,13 @@ sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] }
sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sqlx = { version = "0.8.0", default-features = false, features = ["migrate", "sqlite", "runtime-tokio-rustls"] }
tauri = { workspace = true }
sha2 = "0.10.9"
tauri = { workspace = true}
tauri-plugin-dialog = { workspace = true }
thiserror = "2.0.11"
tokio = { workspace = true }
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
hex = "0.4.3"
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -62,7 +62,7 @@ export type ProxySetting = { "type": "enabled", disabled: boolean, http: string,
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, coloredMethods: boolean, };
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };

View File

@@ -1,9 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models";
import type { Folder } from "./gen_models";
import type { GrpcRequest } from "./gen_models";
import type { HttpRequest } from "./gen_models";
import type { WebsocketRequest } from "./gen_models";
import type { Workspace } from "./gen_models";
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };

View File

@@ -20,6 +20,9 @@ pub enum Error {
#[error("Model error: {0}")]
GenericError(String),
#[error("DB Migration Failed: {0}")]
MigrationError(String),
#[error("No base environment for {0}")]
MissingBaseEnvironment(String),

View File

@@ -1,20 +1,16 @@
use crate::commands::*;
use crate::migrate::migrate_db;
use crate::query_manager::QueryManager;
use crate::util::ModelChangeEvent;
use log::info;
use log::error;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use sqlx::SqlitePool;
use sqlx::migrate::Migrator;
use sqlx::sqlite::SqliteConnectOptions;
use std::fs::create_dir_all;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use tauri::async_runtime::Mutex;
use tauri::path::BaseDirectory;
use tauri::plugin::TauriPlugin;
use tauri::{AppHandle, Emitter, Manager, Runtime, generate_handler};
use tauri::{Emitter, Manager, Runtime, generate_handler};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
use tokio::sync::mpsc;
mod commands;
@@ -22,6 +18,7 @@ mod commands;
mod connection_or_tx;
pub mod db_context;
pub mod error;
mod migrate;
pub mod models;
pub mod queries;
pub mod query_manager;
@@ -55,13 +52,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
let db_file_path = app_path.join("db.sqlite");
{
let db_file_path = db_file_path.clone();
tauri::async_runtime::block_on(async move {
must_migrate_db(app_handle.app_handle(), &db_file_path).await;
});
};
let manager = SqliteConnectionManager::file(db_file_path);
let pool = Pool::builder()
.max_size(100) // Up from 10 (just in case)
@@ -69,6 +59,16 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.build(manager)
.unwrap();
if let Err(e) = migrate_db(app_handle.app_handle(), &pool) {
error!("Failed to run database migration {e:?}");
app_handle
.dialog()
.message(e.to_string())
.kind(MessageDialogKind::Error)
.blocking_show();
return Err(Box::from(e.to_string()));
};
app_handle.manage(SqliteConnection::new(pool.clone()));
{
@@ -90,21 +90,3 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
})
.build()
}
async fn must_migrate_db<R: Runtime>(app_handle: &AppHandle<R>, sqlite_file_path: &PathBuf) {
info!("Connecting to database at {sqlite_file_path:?}");
let sqlite_file_path = sqlite_file_path.to_str().unwrap().to_string();
let opts = SqliteConnectOptions::from_str(&sqlite_file_path).unwrap().create_if_missing(true);
let pool = SqlitePool::connect_with(opts).await.expect("Failed to connect to database");
let p = app_handle
.path()
.resolve("migrations", BaseDirectory::Resource)
.expect("failed to resolve resource");
info!("Running database migrations from: {}", p.to_string_lossy());
let mut m = Migrator::new(p).await.expect("Failed to load migrations");
m.set_ignore_missing(true); // So we can roll back versions and not crash
m.run(&pool).await.expect("Failed to run migrations");
info!("Database migrations complete");
}

View File

@@ -0,0 +1,129 @@
use crate::error::Error::MigrationError;
use crate::error::Result;
use log::info;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{OptionalExtension, TransactionBehavior, params};
use sha2::{Digest, Sha384};
use std::fs;
use std::path::Path;
use std::result::Result as StdResult;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager, Runtime};
pub(crate) fn migrate_db<R: Runtime>(
app_handle: &AppHandle<R>,
pool: &Pool<SqliteConnectionManager>,
) -> Result<()> {
let migrations_dir = app_handle
.path()
.resolve("migrations", BaseDirectory::Resource)
.expect("failed to resolve resource");
info!("Running database migrations from: {:?}", migrations_dir);
// Ensure the table exists
// NOTE: Yaak used to use sqlx for migrations, so we need to mirror that table structure. We
// are writing checksum but not verifying because we want to be able to change migrations after
// a release in case something breaks.
pool.get()?.execute(
"CREATE TABLE IF NOT EXISTS _sqlx_migrations (
version BIGINT PRIMARY KEY,
description TEXT NOT NULL,
installed_on TIMESTAMP default CURRENT_TIMESTAMP NOT NULL,
success BOOLEAN NOT NULL,
checksum BLOB NOT NULL,
execution_time BIGINT NOT NULL
)",
[],
)?;
// Read and sort all .sql files
let mut entries = fs::read_dir(migrations_dir)
.expect("Failed to find migrations directory")
.filter_map(StdResult::ok)
.filter(|e| e.path().extension().map(|ext| ext == "sql").unwrap_or(false))
.collect::<Vec<_>>();
// Ensure they're in the correct order
entries.sort_by_key(|e| e.file_name());
// Run each migration in a transaction
for entry in entries {
let mut conn = pool.get()?;
let mut tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
match run_migration(entry.path().as_path(), &mut tx) {
Ok(_) => tx.commit()?,
Err(e) => {
let msg = format!(
"{} failed with {}",
entry.path().file_name().unwrap().to_str().unwrap(),
e.to_string()
);
tx.rollback()?;
return Err(MigrationError(msg));
}
};
}
info!("Finished running migrations");
Ok(())
}
fn run_migration(migration_path: &Path, tx: &mut rusqlite::Transaction) -> Result<bool> {
let start = std::time::Instant::now();
let (version, description) =
split_migration_filename(migration_path.file_name().unwrap().to_str().unwrap())
.expect("Failed to parse migration filename");
// Skip if already applied
let row: Option<i64> = tx
.query_row("SELECT 1 FROM _sqlx_migrations WHERE version = ?", [version.clone()], |r| {
r.get(0)
})
.optional()?;
if row.is_some() {
// Migration was already run
return Ok(false);
}
let sql = fs::read_to_string(migration_path).expect("Failed to read migration file");
info!("Applying migration {description}");
// Split on `;`? → optional depending on how your SQL is structured
tx.execute_batch(&sql)?;
let execution_time = start.elapsed().as_nanos() as i64;
let checksum = sha384_hex_prefixed(sql.as_bytes());
// NOTE: The success column is never used. It's just there for sqlx compatibility.
tx.execute(
"INSERT INTO _sqlx_migrations (version, description, execution_time, checksum, success) VALUES (?, ?, ?, ?, ?)",
params![version, description, execution_time, checksum, true],
)?;
Ok(true)
}
fn split_migration_filename(filename: &str) -> Option<(String, String)> {
// Remove the .sql extension
let trimmed = filename.strip_suffix(".sql")?;
// Split on the first underscore
let mut parts = trimmed.splitn(2, '_');
let version = parts.next()?.to_string();
let description = parts.next()?.to_string();
Some((version, description))
}
fn sha384_hex_prefixed(input: &[u8]) -> String {
let mut hasher = Sha384::new();
hasher.update(input);
let result = hasher.finalize();
// Format as 0x... with uppercase hex
format!("0x{}", hex::encode_upper(result))
}

View File

@@ -114,6 +114,7 @@ pub struct Settings {
pub theme_light: String,
pub update_channel: String,
pub editor_keymap: EditorKeymap,
pub colored_methods: bool,
}
impl UpsertModelInfo for Settings {
@@ -160,6 +161,7 @@ impl UpsertModelInfo for Settings {
(ThemeDark, self.theme_dark.as_str().into()),
(ThemeLight, self.theme_light.as_str().into()),
(UpdateChannel, self.update_channel.into()),
(ColoredMethods, self.colored_methods.into()),
(Proxy, proxy.into()),
])
}
@@ -179,6 +181,7 @@ impl UpsertModelInfo for Settings {
SettingsIden::ThemeDark,
SettingsIden::ThemeLight,
SettingsIden::UpdateChannel,
SettingsIden::ColoredMethods,
]
}
@@ -205,6 +208,7 @@ impl UpsertModelInfo for Settings {
theme_light: row.get("theme_light")?,
hide_window_controls: row.get("hide_window_controls")?,
update_channel: row.get("update_channel")?,
colored_methods: row.get("colored_methods")?,
})
}
}

View File

@@ -115,15 +115,15 @@ impl<'a> DbContext<'a> {
pub fn resolve_auth_for_folder(
&self,
folder: Folder,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
if let Some(at) = folder.authentication_type {
return Ok((Some(at), folder.authentication));
folder: &Folder,
) -> Result<(Option<String>, BTreeMap<String, Value>, String)> {
if let Some(at) = folder.authentication_type.clone() {
return Ok((Some(at), folder.authentication.clone(), folder.id.clone()));
}
if let Some(folder_id) = folder.folder_id {
if let Some(folder_id) = folder.folder_id.clone() {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
return self.resolve_auth_for_folder(&folder);
}
let workspace = self.get_workspace(&folder.workspace_id)?;

View File

@@ -54,14 +54,14 @@ impl<'a> DbContext<'a> {
pub fn resolve_auth_for_grpc_request(
&self,
grpc_request: &GrpcRequest,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
) -> Result<(Option<String>, BTreeMap<String, Value>, String)> {
if let Some(at) = grpc_request.authentication_type.clone() {
return Ok((Some(at), grpc_request.authentication.clone()));
return Ok((Some(at), grpc_request.authentication.clone(), grpc_request.id.clone()));
}
if let Some(folder_id) = grpc_request.folder_id.clone() {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
return self.resolve_auth_for_folder(&folder);
}
let workspace = self.get_workspace(&grpc_request.workspace_id)?;

View File

@@ -54,14 +54,14 @@ impl<'a> DbContext<'a> {
pub fn resolve_auth_for_http_request(
&self,
http_request: &HttpRequest,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
) -> Result<(Option<String>, BTreeMap<String, Value>, String)> {
if let Some(at) = http_request.authentication_type.clone() {
return Ok((Some(at), http_request.authentication.clone()));
return Ok((Some(at), http_request.authentication.clone(), http_request.id.clone()));
}
if let Some(folder_id) = http_request.folder_id.clone() {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
return self.resolve_auth_for_folder(&folder);
}
let workspace = self.get_workspace(&http_request.workspace_id)?;

View File

@@ -29,6 +29,7 @@ impl<'a> DbContext<'a> {
theme_dark: "yaak-dark".to_string(),
theme_light: "yaak-light".to_string(),
update_channel: "stable".to_string(),
colored_methods: false,
};
self.upsert(&settings, &UpdateSource::Background).expect("Failed to upsert settings")
}

View File

@@ -54,14 +54,14 @@ impl<'a> DbContext<'a> {
pub fn resolve_auth_for_websocket_request(
&self,
websocket_request: &WebsocketRequest,
) -> Result<(Option<String>, BTreeMap<String, Value>)> {
) -> Result<(Option<String>, BTreeMap<String, Value>, String)> {
if let Some(at) = websocket_request.authentication_type.clone() {
return Ok((Some(at), websocket_request.authentication.clone()));
return Ok((Some(at), websocket_request.authentication.clone(), websocket_request.id.clone()));
}
if let Some(folder_id) = websocket_request.folder_id.clone() {
let folder = self.get_folder(&folder_id)?;
return self.resolve_auth_for_folder(folder);
return self.resolve_auth_for_folder(&folder);
}
let workspace = self.get_workspace(&websocket_request.workspace_id)?;

View File

@@ -71,8 +71,12 @@ impl<'a> DbContext<'a> {
pub fn resolve_auth_for_workspace(
&self,
workspace: &Workspace,
) -> (Option<String>, BTreeMap<String, Value>) {
(workspace.authentication_type.clone(), workspace.authentication.clone())
) -> (Option<String>, BTreeMap<String, Value>, String) {
(
workspace.authentication_type.clone(),
workspace.authentication.clone(),
workspace.id.clone(),
)
}
pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec<HttpRequestHeader> {

View File

@@ -120,7 +120,8 @@ pub(crate) async fn send<R: Runtime>(
};
let base_environment =
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
let resolved_request = resolve_websocket_request(&window, &unrendered_request)?;
let (resolved_request, _auth_context_id) =
resolve_websocket_request(&window, &unrendered_request)?;
let request = render_websocket_request(
&resolved_request,
&base_environment,
@@ -197,7 +198,8 @@ pub(crate) async fn connect<R: Runtime>(
let base_environment =
app_handle.db().get_base_environment(&unrendered_request.workspace_id)?;
let workspace = app_handle.db().get_workspace(&unrendered_request.workspace_id)?;
let resolved_request = resolve_websocket_request(&window, &unrendered_request)?;
let (resolved_request, auth_context_id) =
resolve_websocket_request(&window, &unrendered_request)?;
let request = render_websocket_request(
&resolved_request,
&base_environment,
@@ -237,7 +239,7 @@ pub(crate) async fn connect<R: Runtime>(
Some(authentication_type) => {
let auth = request.authentication.clone();
let plugin_req = CallHttpAuthenticationRequest {
context_id: format!("{:x}", md5::compute(request_id.to_string())),
context_id: format!("{:x}", md5::compute(auth_context_id)),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
method: "POST".to_string(),
url: request.url.clone(),
@@ -299,13 +301,15 @@ pub(crate) async fn connect<R: Runtime>(
}
}
let response = match ws_manager.connect(
&connection.id,
url.as_str(),
headers,
receive_tx,
workspace.setting_validate_certificates,
).await
let response = match ws_manager
.connect(
&connection.id,
url.as_str(),
headers,
receive_tx,
workspace.setting_validate_certificates,
)
.await
{
Ok(r) => r,
Err(e) => {

View File

@@ -6,10 +6,10 @@ use yaak_models::query_manager::QueryManagerExt;
pub(crate) fn resolve_websocket_request<R: Runtime>(
window: &WebviewWindow<R>,
request: &WebsocketRequest,
) -> Result<WebsocketRequest> {
) -> Result<(WebsocketRequest, String)> {
let mut new_request = request.clone();
let (authentication_type, authentication) =
let (authentication_type, authentication, authentication_context_id) =
window.db().resolve_auth_for_websocket_request(request)?;
new_request.authentication_type = authentication_type;
new_request.authentication = authentication;
@@ -17,5 +17,5 @@ pub(crate) fn resolve_websocket_request<R: Runtime>(
let headers = window.db().resolve_headers_for_websocket_request(request)?;
new_request.headers = headers;
Ok(new_request)
Ok((new_request, authentication_context_id))
}

View File

@@ -236,7 +236,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
return workspaces;
}
const r = [...workspaces].sort((a, b) => {
return [...workspaces].sort((a, b) => {
const aRecentIndex = recentWorkspaces?.indexOf(a.id);
const bRecentIndex = recentWorkspaces?.indexOf(b.id);
@@ -250,7 +250,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
return a.createdAt.localeCompare(b.createdAt);
}
});
return r;
}, [recentWorkspaces, workspaces]);
const groups = useMemo<CommandPaletteGroup[]>(() => {
@@ -272,7 +271,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
searchText: resolvedModelNameWithFolders(r),
label: (
<HStack space={2}>
<HttpMethodTag className="text-text-subtlest" request={r} />
<HttpMethodTag short className="text-xs" request={r} />
<div className="truncate">{resolvedModelNameWithFolders(r)}</div>
</HStack>
),

View File

@@ -176,7 +176,6 @@ export function GrpcRequestPane({
<UrlBar
key={forceUpdateKey}
url={activeRequest.url ?? ''}
method={null}
submitIcon={null}
forceUpdateKey={forceUpdateKey}
placeholder="localhost:50051"

View File

@@ -41,8 +41,8 @@ import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { InlineCode } from './core/InlineCode';
import type { Pair } from './core/PairEditor';
import { PlainInput } from './core/PlainInput';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import type { TabItem } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { FormMultipartEditor } from './FormMultipartEditor';
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
@@ -50,6 +50,7 @@ import { GraphQLEditor } from './GraphQLEditor';
import { HeadersEditor } from './HeadersEditor';
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
import { MarkdownEditor } from './MarkdownEditor';
import { RequestMethodDropdown } from './RequestMethodDropdown';
import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor';
@@ -138,10 +139,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ||
activeRequest.bodyType === BODY_TYPE_FORM_MULTIPART
) {
const n = Array.isArray(activeRequest.body?.form)
numParams = Array.isArray(activeRequest.body?.form)
? activeRequest.body.form.filter((p) => p.name).length
: 0;
numParams = n;
}
const tabs = useMemo<TabItem[]>(
@@ -314,11 +314,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
[activeRequest.id, sendRequest],
);
const handleMethodChange = useCallback(
(method: string) => patchModel(activeRequest, { method }),
[activeRequest],
);
const handleUrlChange = useCallback(
(url: string) => patchModel(activeRequest, { url }),
[activeRequest],
@@ -335,14 +330,17 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
stateKey={`url.${activeRequest.id}`}
key={forceUpdateKey + urlKey}
url={activeRequest.url}
method={activeRequest.method}
placeholder="https://example.com"
onPasteOverwrite={handlePaste}
autocomplete={autocomplete}
onSend={handleSend}
onCancel={cancelResponse}
onMethodChange={handleMethodChange}
onUrlChange={handleUrlChange}
leftSlot={
<div className="py-0.5">
<RequestMethodDropdown request={activeRequest} className="ml-0.5 !h-full" />
</div>
}
forceUpdateKey={updateKey}
isLoading={activeResponse != null && activeResponse.state !== 'closed'}
/>

View File

@@ -67,7 +67,7 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
...responses.map((r: HttpResponse) => ({
label: (
<HStack space={2}>
<HttpStatusTag className="text-sm" response={r} />
<HttpStatusTag short className="text-xs" response={r} />
<span className="text-text-subtle">&rarr;</span>{' '}
<span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : 'n/a'}</span>
</HStack>

View File

@@ -58,7 +58,7 @@ export function RecentRequestsDropdown({ className }: Props) {
recentRequestItems.push({
label: resolvedModelName(request),
leftSlot: <HttpMethodTag request={request} />,
leftSlot: <HttpMethodTag short className="text-xs" request={request} />,
onSelect: async () => {
await router.navigate({
to: '/workspaces/$workspaceId',

View File

@@ -1,16 +1,18 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { showPrompt } from '../lib/prompt';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { HttpMethodTag } from './core/HttpMethodTag';
import { Icon } from './core/Icon';
import type { RadioDropdownItem } from './core/RadioDropdown';
import { RadioDropdown } from './core/RadioDropdown';
type Props = {
method: string;
request: HttpRequest;
className?: string;
onChange: (method: string) => void;
};
const radioItems: RadioDropdownItem<string>[] = [
@@ -28,10 +30,16 @@ const radioItems: RadioDropdownItem<string>[] = [
}));
export const RequestMethodDropdown = memo(function RequestMethodDropdown({
method,
onChange,
request,
className,
}: Props) {
const handleChange = useCallback(
async (method: string) => {
await patchModel(request, { method });
},
[request],
);
const itemsAfter = useMemo<DropdownItem[]>(
() => [
{
@@ -49,17 +57,22 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
placeholder: 'CUSTOM',
});
if (newMethod == null) return;
onChange(newMethod);
await handleChange(newMethod);
},
},
],
[onChange],
[handleChange],
);
return (
<RadioDropdown value={method} items={radioItems} itemsAfter={itemsAfter} onChange={onChange}>
<RadioDropdown
value={request.method}
items={radioItems}
itemsAfter={itemsAfter}
onChange={handleChange}
>
<Button size="xs" className={classNames(className, 'text-text-subtle hover:text')}>
{method.toUpperCase()}
<HttpMethodTag request={request} />
</Button>
</RadioDropdown>
);

View File

@@ -122,6 +122,11 @@ export function SettingsAppearance() {
title="Wrap Editor Lines"
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
/>
<Checkbox
checked={settings.coloredMethods}
title="Colorize Request Methods"
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
/>
{type() !== 'macos' && (
<Checkbox

View File

@@ -9,11 +9,9 @@ import { IconButton } from './core/IconButton';
import type { InputProps } from './core/Input';
import { Input } from './core/Input';
import { HStack } from './core/Stacks';
import { RequestMethodDropdown } from './RequestMethodDropdown';
type Props = Pick<HttpRequest, 'url'> & {
className?: string;
method: HttpRequest['method'] | null;
placeholder: string;
onSend: () => void;
onUrlChange: (url: string) => void;
@@ -21,10 +19,10 @@ type Props = Pick<HttpRequest, 'url'> & {
onPasteOverwrite?: InputProps['onPasteOverwrite'];
onCancel: () => void;
submitIcon?: IconProps['icon'] | null;
onMethodChange?: (method: string) => void;
isLoading: boolean;
forceUpdateKey: string;
rightSlot?: ReactNode;
leftSlot?: ReactNode;
autocomplete?: InputProps['autocomplete'];
stateKey: InputProps['stateKey'];
};
@@ -33,16 +31,15 @@ export const UrlBar = memo(function UrlBar({
forceUpdateKey,
onUrlChange,
url,
method,
placeholder,
className,
onSend,
onCancel,
onMethodChange,
onPaste,
onPasteOverwrite,
submitIcon = 'send_horizontal',
autocomplete,
leftSlot,
rightSlot,
isLoading,
stateKey,
@@ -87,18 +84,7 @@ export const UrlBar = memo(function UrlBar({
onChange={onUrlChange}
defaultValue={url}
placeholder={placeholder}
leftSlot={
method != null &&
onMethodChange != null && (
<div className="py-0.5">
<RequestMethodDropdown
method={method}
onChange={onMethodChange}
className="ml-0.5 !h-full"
/>
</div>
)
}
leftSlot={leftSlot}
rightSlot={
<HStack space={0.5}>
{rightSlot && <div className="py-0.5 h-full">{rightSlot}</div>}

View File

@@ -54,10 +54,9 @@ const TAB_DESCRIPTION = 'description';
const nonActiveRequestUrlsAtom = atom((get) => {
const activeRequestId = get(activeRequestIdAtom);
const requests = get(allRequestsAtom);
const urls = requests
return requests
.filter((r) => r.id !== activeRequestId)
.map((r): GenericCompletionOption => ({ type: 'constant', label: r.url }));
return urls;
});
const memoNotActiveRequestUrlsAtom = deepEqualAtom(nonActiveRequestUrlsAtom);
@@ -227,7 +226,6 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
onUrlChange={handleUrlChange}
forceUpdateKey={forceUpdateKey}
isLoading={activeResponse != null && activeResponse.state !== 'closed'}
method={null}
/>
</div>
<Tabs

View File

@@ -55,10 +55,9 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
if (hexDump) {
return activeEvent?.message ? hexy(activeEvent?.message) : '';
}
const text = activeEvent?.message
return activeEvent?.message
? new TextDecoder('utf-8').decode(Uint8Array.from(activeEvent.message))
: '';
return text;
}, [activeEvent?.message, hexDump]);
const language = languageFromContentType(null, message);
@@ -152,7 +151,7 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
title="Copy message"
icon="copy"
size="xs"
onClick={() => copyToClipboard(formattedMessage.data ?? '')}
onClick={() => copyToClipboard(formattedMessage ?? '')}
/>
</HStack>
)}
@@ -183,7 +182,7 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
) : (
<Editor
language={language}
defaultValue={formattedMessage.data ?? ''}
defaultValue={formattedMessage ?? ''}
wrapLines={false}
readOnly={true}
stateKey={null}

View File

@@ -642,7 +642,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
justify="start"
leftSlot={
(isLoading || item.leftSlot) && (
<div className={classNames('pr-2 flex justify-start opacity-70')}>
<div className={classNames('pr-2 flex justify-start [&_svg]:opacity-70')}>
{isLoading ? <LoadingIcon /> : item.leftSlot}
</div>
)

View File

@@ -1,10 +1,12 @@
import { settingsAtom } from '@yaakapp-internal/models';
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
interface Props {
request: HttpRequest | GrpcRequest | WebsocketRequest;
className?: string;
shortNames?: boolean;
short?: boolean;
}
const methodNames: Record<string, string> = {
@@ -18,7 +20,8 @@ const methodNames: Record<string, string> = {
query: 'QURY',
};
export function HttpMethodTag({ request, className }: Props) {
export function HttpMethodTag({ request, className, short }: Props) {
const settings = useAtomValue(settingsAtom);
const method =
request.model === 'http_request' && request.bodyType === 'graphql'
? 'GQL'
@@ -26,19 +29,34 @@ export function HttpMethodTag({ request, className }: Props) {
? 'GRPC'
: request.model === 'websocket_request'
? 'WS'
: (methodNames[request.method.toLowerCase()] ?? request.method.slice(0, 4));
: request.method;
let label = method.toUpperCase();
const paddedMethod = method.padStart(4, ' ').toUpperCase();
if (short) {
label = methodNames[method.toLowerCase()] ?? method.slice(0, 4);
label = label.padStart(4, ' ');
}
return (
<span
className={classNames(
className,
'text-xs font-mono text-text-subtle flex-shrink-0 whitespace-pre',
!settings.coloredMethods && 'text-text-subtle',
settings.coloredMethods && method === 'GQL' && 'text-info',
settings.coloredMethods && method === 'WS' && 'text-info',
settings.coloredMethods && method === 'GRPC' && 'text-info',
settings.coloredMethods && method === 'OPTIONS' && 'text-info',
settings.coloredMethods && method === 'HEAD' && 'text-info',
settings.coloredMethods && method === 'GET' && 'text-primary',
settings.coloredMethods && method === 'PUT' && 'text-warning',
settings.coloredMethods && method === 'PATCH' && 'text-notice',
settings.coloredMethods && method === 'POST' && 'text-success',
settings.coloredMethods && method === 'DELETE' && 'text-danger',
'font-mono flex-shrink-0 whitespace-pre',
'pt-[0.25em]', // Fix for monospace font not vertically centering
)}
>
{paddedMethod}
{label}
</span>
);
}

View File

@@ -5,19 +5,20 @@ interface Props {
response: HttpResponse;
className?: string;
showReason?: boolean;
short?: boolean;
}
export function HttpStatusTag({ response, className, showReason }: Props) {
export function HttpStatusTag({ response, className, showReason, short }: Props) {
const { status, state } = response;
let colorClass;
let label = `${status}`;
if (state === 'initialized') {
label = 'CONNECTING';
label = short ? 'CONN' : 'CONNECTING';
colorClass = 'text-text-subtle';
} else if (status < 100) {
label = 'ERROR';
label = short ? 'ERR' : 'ERROR';
colorClass = 'text-danger';
} else if (status < 200) {
colorClass = 'text-info';
@@ -33,8 +34,7 @@ export function HttpStatusTag({ response, className, showReason }: Props) {
return (
<span className={classNames(className, 'font-mono', colorClass)}>
{label}{' '}
{showReason && 'statusReason' in response ? response.statusReason : null}
{label} {showReason && 'statusReason' in response ? response.statusReason : null}
</span>
);
}

View File

@@ -53,11 +53,11 @@ export type InputProps = Pick<
> & {
className?: string;
containerClassName?: string;
inputWrapperClassName?: string;
defaultValue?: string | null;
disableObscureToggle?: boolean;
fullHeight?: boolean;
hideLabel?: boolean;
inputWrapperClassName?: string;
help?: ReactNode;
label: ReactNode;
labelClassName?: string;

View File

@@ -125,8 +125,8 @@ function ActualEventStreamViewer({ response }: Props) {
function FormattedEditor({ text, language }: { text: string; language: EditorProps['language'] }) {
const formatted = useFormatText({ text, language, pretty: true });
if (formatted.data == null) return null;
return <Editor readOnly defaultValue={formatted.data} language={language} stateKey={null} />;
if (formatted == null) return null;
return <Editor readOnly defaultValue={formatted} language={language} stateKey={null} />;
}
function EventRow({

View File

@@ -100,8 +100,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
]);
const formattedBody = useFormatText({ text, language, pretty });
if (formattedBody.data == null) {
if (formattedBody == null) {
return null;
}
@@ -113,7 +112,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
body = filteredResponse.data != null ? filteredResponse.data : '';
}
} else {
body = formattedBody.data;
body = formattedBody;
}
// Decode unicode sequences in the text to readable characters

View File

@@ -12,7 +12,7 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from '
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { activeRequestAtom } from '../../hooks/useActiveRequest';
import {allRequestsAtom} from "../../hooks/useAllRequests";
import { allRequestsAtom } from '../../hooks/useAllRequests';
import { useScrollIntoView } from '../../hooks/useScrollIntoView';
import { useSidebarItemCollapsed } from '../../hooks/useSidebarItemCollapsed';
import { jotaiStore } from '../../lib/jotai';
@@ -214,10 +214,13 @@ export const SidebarItem = memo(function SidebarItem({
return null;
}
const opacitySubtle = 'opacity-80';
const itemPrefix = item.model !== 'folder' && (
<HttpMethodTag
short
request={item}
className={classNames(!(active || selected) && 'text-text-subtlest')}
className={classNames('text-xs', !(active || selected) && opacitySubtle)}
/>
);
@@ -287,7 +290,11 @@ export const SidebarItem = memo(function SidebarItem({
{latestHttpResponse.state !== 'closed' ? (
<LoadingIcon size="sm" className="text-text-subtlest" />
) : (
<HttpStatusTag className="text-xs" response={latestHttpResponse} />
<HttpStatusTag
short
className={classNames('text-xs', !(active || selected) && opacitySubtle)}
response={latestHttpResponse}
/>
)}
</div>
) : null}

View File

@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { tryFormatJson, tryFormatXml } from '../lib/formatters';
import type { EditorProps } from '../components/core/Editor/Editor';
import { tryFormatJson, tryFormatXml } from '../lib/formatters';
export function useFormatText({
text,
@@ -12,6 +12,7 @@ export function useFormatText({
pretty: boolean;
}) {
return useQuery({
placeholderData: (prev) => prev, // Keep previous data on refetch
queryKey: [text, language, pretty],
queryFn: async () => {
if (text === '' || !pretty) {
@@ -24,5 +25,5 @@ export function useFormatText({
return text;
}
},
});
}).data;
}