Compare commits

..

28 Commits

Author SHA1 Message Date
Gregory Schier
aadfbfdfca Fix lint errors 2025-06-10 08:16:02 -07:00
Gregory Schier
383fd05c6c Split appearance settings into theme/interface 2025-06-09 21:19:44 -07:00
Gregory Schier
be0a8fc27a Add proxy bypass setting and rewrite proxy logic 2025-06-09 14:29:12 -07:00
Gregory Schier
648a1ac53c Update DEVELOPMENT.md 2025-06-08 22:49:43 -07:00
Gregory Schier
9fab37fb17 Custom font selection (#226) 2025-06-08 22:48:27 -07:00
Gregory Schier
e0aaa33ccb Update README 2025-06-08 08:17:11 -07:00
Gregory Schier
20f7d20031 Enable socks reqwest feature 2025-06-08 08:10:55 -07:00
Gregory Schier
4d90bc78b1 Link docs in readme 2025-06-08 08:05:59 -07:00
Gregory Schier
97763a1301 Add README to types package 2025-06-08 08:03:11 -07:00
Gregory Schier
d8b5a201b6 I'm stupid 2025-06-07 20:17:28 -07:00
Gregory Schier
88e87a1999 Fix stupid typo 2025-06-07 20:15:32 -07:00
Gregory Schier
2c4c1abd20 Pin tauri cli 2025-06-07 20:04:04 -07:00
Gregory Schier
67026fc5b3 Tweak 2025-06-07 19:37:28 -07:00
Gregory Schier
423a1a0a52 Fix environment color editing 2025-06-07 19:32:23 -07:00
Gregory Schier
1abe01aa5a Embed migrations into Rust binary 2025-06-07 19:25:36 -07:00
Gregory Schier
d0fde99b1c Environment colors (#225) 2025-06-07 18:21:54 -07:00
Gregory Schier
27901231dc Clarify proxy HTTP/HTTPS setting 2025-06-06 20:34:23 -07:00
Gregory Schier
1d9d80319b Upgrade dependencies 2025-06-06 19:32:25 -07:00
Gregory Schier
f62e90297d Fix recent workspaces when open in new window 2025-06-06 14:10:30 -07:00
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
135 changed files with 2496 additions and 2247 deletions

View File

@@ -47,15 +47,12 @@ 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.
Rerun the app to apply the migrations.
If nothing happens, try `cargo clean` and run the app again.
_Note: Development builds use a separate database location from production builds._
_Note: For safety, development builds use a separate database location from production builds._
## Lezer Grammer Generation

72
package-lock.json generated
View File

@@ -36,6 +36,7 @@
"plugins/template-function-xml",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-fonts",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
@@ -50,7 +51,7 @@
"jotai": "^2.12.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@tauri-apps/cli": "2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.1.5",
@@ -63,7 +64,7 @@
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"workspaces-run": "^1.0.2"
}
},
@@ -2801,9 +2802,9 @@
}
},
"node_modules/@tauri-apps/api": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.4.1.tgz",
"integrity": "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.5.0.tgz",
"integrity": "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@@ -3037,36 +3038,36 @@
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.1.tgz",
"integrity": "sha512-wZmCouo4PgTosh/UoejPw9DPs6RllS5Pp3fuOV2JobCu36mR5AXU2MzU9NZiVaFi/5Zfc8RN0IhcZHnksJ1o8A==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.2.tgz",
"integrity": "sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-fs": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.2.1.tgz",
"integrity": "sha512-KdGzvvA4Eg0Dhw55MwczFbjxLxsTx0FvwwC/0StXlr6IxwPUxh5ziZQoaugkBFs8t+wfebdQrjBEzd8NmmDXNw==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.3.0.tgz",
"integrity": "sha512-G9gEyYVUaaxhdRJBgQTTLmzAe0vtHYxYyN1oTQzU3zwvb8T+tVLcAqCdFMWHq0qGeGbmynI5whvYpcXo5LvZ1w==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-log": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.3.1.tgz",
"integrity": "sha512-nnKGHENWt7teqvUlIKxd6bp2wCUrrLvCvajN6CWbyrHBNKPi/pyKELzD511siEMDEdndbiZ/GEhiK0xBtZopRg==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.4.0.tgz",
"integrity": "sha512-j7yrDtLNmayCBOO2esl3aZv9jSXy2an8MDLry3Ys9ZXerwUg35n1Y2uD8HoCR+8Ng/EUgx215+qOUfJasjYrHw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-opener": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.6.tgz",
"integrity": "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==",
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.7.tgz",
"integrity": "sha512-uduEyvOdjpPOEeDRrhwlCspG/f9EQalHumWBtLBnp3fRp++fKGLqDOyUhSIn7PzX45b/rKep//ZQSAQoIxobLA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
@@ -3606,6 +3607,10 @@
"resolved": "src-tauri/yaak-crypto",
"link": true
},
"node_modules/@yaakapp-internal/fonts": {
"resolved": "src-tauri/yaak-fonts",
"link": true
},
"node_modules/@yaakapp-internal/git": {
"resolved": "src-tauri/yaak-git",
"link": true
@@ -12946,6 +12951,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-colorful": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
@@ -16097,9 +16112,9 @@
}
},
"node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -17284,7 +17299,7 @@
},
"packages/plugin-runtime-types": {
"name": "@yaakapp/api",
"version": "0.6.0",
"version": "0.6.4",
"dependencies": {
"@types/node": "^22.5.4"
},
@@ -17533,6 +17548,10 @@
"name": "@yaakapp-internal/crypto",
"version": "1.0.0"
},
"src-tauri/yaak-fonts": {
"name": "@yaakapp-internal/fonts",
"version": "1.0.0"
},
"src-tauri/yaak-git": {
"name": "@yaakapp-internal/git",
"version": "1.0.0"
@@ -17598,12 +17617,12 @@
"@tanstack/react-query": "^5.76.1",
"@tanstack/react-router": "^1.120.3",
"@tanstack/react-virtual": "^3.13.8",
"@tauri-apps/api": "^2.4.1",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-log": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "^2.3.0",
"@tauri-apps/plugin-log": "^2.4.0",
"@tauri-apps/plugin-opener": "^2.2.7",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-shell": "^2.2.1",
"buffer": "^6.0.3",
@@ -17626,6 +17645,7 @@
"papaparse": "^5.4.1",
"parse-color": "^1.0.0",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",

View File

@@ -35,6 +35,7 @@
"plugins/template-function-xml",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-fonts",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
@@ -49,6 +50,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",
@@ -65,7 +67,7 @@
"jotai": "^2.12.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@tauri-apps/cli": "2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.1.5",
@@ -78,7 +80,7 @@
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"workspaces-run": "^1.0.2"
}
}

View File

@@ -0,0 +1,28 @@
# Yaak Plugin API
Yaak is a desktop [API client](https://yaak.app/blog/yet-another-api-client) for
interacting with REST, GraphQL, Server Sent Events (SSE), WebSocket, and gRPC APIs. It's
built using Tauri, Rust, and ReactJS.
Plugins can be created in TypeScript, which are executed alongside Yaak in a NodeJS
runtime. This package contains the TypeScript type definitions required to make building
Yaak plugins a breeze.
## Quick Start
The easiest way to get started is by generating a plugin with the Yaak CLI:
```shell
npx @yaakapp/cli generate
```
For more details on creating plugins, check out
the [Quick Start Guide](https://feedback.yaak.app/help/articles/6911763-plugins-quick-start)
## Installation
If you prefer starting from scratch, manually install the types package:
```shell
npm install @yaakapp/api
```

View File

@@ -1,6 +1,20 @@
{
"name": "@yaakapp/api",
"version": "0.6.0",
"version": "0.6.4",
"keywords": [
"api-client",
"insomnia-alternative",
"bruno-alternative",
"postman-alternative"
],
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak"
},
"bugs": {
"url": "https://feedback.yaak.app"
},
"homepage": "https://yaak.app",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [

View File

@@ -1,12 +1,6 @@
// 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.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

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/yaak-models/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);

3262
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ members = [
"yaak-crypto",
"yaak-git",
"yaak-grpc",
"yaak-fonts",
"yaak-http",
"yaak-license",
"yaak-mac-window",
@@ -33,7 +34,7 @@ strip = true # Automatically strip symbols from the binary.
cargo-clippy = []
[build-dependencies]
tauri-build = { version = "2.1.1", features = [] }
tauri-build = { version = "2.2.0", features = [] }
[target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
@@ -48,13 +49,13 @@ log = "0.4.27"
md5 = "0.7.0"
mime_guess = "2.0.5"
rand = "0.9.0"
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] }
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks"] }
reqwest_cookie_store = "0.8.0"
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"
@@ -75,6 +76,7 @@ yaak-http = { workspace = true }
yaak-license = { path = "yaak-license" }
yaak-mac-window = { path = "yaak-mac-window" }
yaak-models = { workspace = true }
yaak-fonts = { workspace = true }
yaak-plugins = { workspace = true }
yaak-sse = { workspace = true }
yaak-sync = { workspace = true }
@@ -82,18 +84,20 @@ yaak-templates = { workspace = true }
yaak-ws = { path = "yaak-ws" }
[workspace.dependencies]
reqwest = "0.12.15"
reqwest = "0.12.19"
serde = "1.0.219"
serde_json = "1.0.140"
tauri = "2.4.1"
tauri-plugin = "2.1.1"
tauri = "2.5.1"
tauri-plugin = "2.2.0"
tauri-plugin-dialog = "2.2.2"
tauri-plugin-shell = "2.2.1"
tokio = "1.44.2"
tokio = "1.45.1"
thiserror = "2.0.12"
ts-rs = "10.1.0"
rustls = { version = "0.23.25", default-features = false }
rustls-platform-verifier = "0.5.1"
ts-rs = "11.0.1"
rustls = { version = "0.23.27", default-features = false }
rustls-platform-verifier = "0.6.0"
yaak-common = { path = "yaak-common" }
yaak-fonts = { path = "yaak-fonts" }
yaak-http = { path = "yaak-http" }
yaak-models = { path = "yaak-models" }
yaak-plugins = { path = "yaak-plugins" }

View File

@@ -53,6 +53,7 @@
"shell:allow-open",
"yaak-crypto:default",
"yaak-git:default",
"yaak-fonts:default",
"yaak-license:default",
"yaak-mac-window:default",
"yaak-models:default",

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

@@ -7,7 +7,7 @@ use http::{HeaderMap, HeaderName, HeaderValue};
use log::{debug, error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
use reqwest::{Method, Response};
use reqwest::{Method, NoProxy, Response};
use reqwest::{Proxy, Url, multipart};
use serde_json::Value;
use std::collections::BTreeMap;
@@ -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(
@@ -119,25 +120,39 @@ pub async fn send_http_request<R: Runtime>(
https,
auth,
disabled,
bypass,
}) if !disabled => {
debug!("Using proxy http={http} https={https}");
let mut proxy = Proxy::custom(move |url| {
let http = if http.is_empty() { None } else { Some(http.to_owned()) };
let https = if https.is_empty() { None } else { Some(https.to_owned()) };
let proxy_url = match (url.scheme(), http, https) {
("http", Some(proxy_url), _) => Some(proxy_url),
("https", _, Some(proxy_url)) => Some(proxy_url),
_ => None,
debug!("Using proxy http={http} https={https} bypass={bypass}");
if !http.is_empty() {
match Proxy::http(http) {
Ok(mut proxy) => {
if let Some(ProxySettingAuth { user, password }) = auth.clone() {
debug!("Using http proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
client_builder = client_builder.proxy(proxy);
}
Err(e) => {
warn!("Failed to apply http proxy {e:?}");
}
};
}
if !https.is_empty() {
match Proxy::https(https) {
Ok(mut proxy) => {
if let Some(ProxySettingAuth { user, password }) = auth {
debug!("Using https proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
client_builder = client_builder.proxy(proxy);
}
Err(e) => {
warn!("Failed to apply https proxy {e:?}");
}
};
proxy_url
});
if let Some(ProxySettingAuth { user, password }) = auth {
debug!("Using proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
client_builder = client_builder.proxy(proxy);
}
_ => {} // Nothing to do for this one, as it is the default
}
@@ -429,7 +444,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 +682,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 +693,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)
@@ -1265,6 +1265,7 @@ pub fn run() {
.plugin(yaak_models::init())
.plugin(yaak_plugins::init())
.plugin(yaak_crypto::init())
.plugin(yaak_fonts::init())
.plugin(yaak_git::init())
.plugin(yaak_ws::init())
.plugin(yaak_sync::init());

View File

@@ -57,7 +57,6 @@
],
"longDescription": "A cross-platform desktop app for interacting with REST, GraphQL, and gRPC",
"resources": [
"migrations",
"vendored/protoc/include",
"vendored/plugins",
"vendored/plugin-runtime"

View File

@@ -0,0 +1,16 @@
[package]
name = "yaak-fonts"
links = "yaak-fonts"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
font-loader = "0.11.0"
tauri = { workspace = true }
ts-rs = { workspace = true }
serde = "1.0"
thiserror = { workspace = true }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Fonts = { editorFonts: Array<string>, uiFonts: Array<string>, };

View File

@@ -0,0 +1,5 @@
const COMMANDS: &[&str] = &["list"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}

View File

@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import { Fonts } from './bindings/gen_fonts';
export async function listFonts() {
return invoke<Fonts>('plugin:yaak-fonts|list', {});
}
export function useFonts() {
return useQuery({
queryKey: ['list_fonts'],
queryFn: () => listFonts(),
});
}

View File

@@ -0,0 +1,6 @@
{
"name": "@yaakapp-internal/fonts",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -0,0 +1,3 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-list"]

View File

@@ -0,0 +1,41 @@
use crate::Result;
use font_loader::system_fonts;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use tauri::command;
use ts_rs::TS;
#[derive(Default, Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_fonts.ts")]
pub struct Fonts {
pub editor_fonts: Vec<String>,
pub ui_fonts: Vec<String>,
}
#[command]
pub(crate) async fn list() -> Result<Fonts> {
let mut ui_fonts = HashSet::new();
let mut editor_fonts = HashSet::new();
let mut property = system_fonts::FontPropertyBuilder::new().monospace().build();
for font in &system_fonts::query_specific(&mut property) {
editor_fonts.insert(font.to_string());
}
for font in &system_fonts::query_all() {
if !editor_fonts.contains(font) {
ui_fonts.insert(font.to_string());
}
}
let mut ui_fonts: Vec<String> = ui_fonts.into_iter().collect();
let mut editor_fonts: Vec<String> = editor_fonts.into_iter().collect();
ui_fonts.sort();
editor_fonts.sort();
Ok(Fonts {
ui_fonts,
editor_fonts,
})
}

View File

@@ -0,0 +1,15 @@
use serde::{ser::Serializer, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum Error {}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -0,0 +1,15 @@
use tauri::{
generate_handler,
plugin::{Builder, TauriPlugin},
Runtime,
};
mod commands;
mod error;
use crate::commands::list;
pub use error::{Error, Result};
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-fonts").invoke_handler(generate_handler![list]).build()
}

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -8,9 +8,8 @@ publish = false
anyhow = "1.0.97"
async-recursion = "1.1.1"
dunce = "1.0.4"
hyper = "1.5.2"
hyper-rustls = { version = "0.27.5", default-features = false, features = ["http2"] }
hyper-util = { version = "0.1.10", default-features = false, features = ["client-legacy"] }
hyper-rustls = { version = "0.27.7", default-features = false, features = ["http2"] }
hyper-util = { version = "0.1.13", default-features = false, features = ["client-legacy"] }
log = "0.4.20"
md5 = "0.7.0"
prost = "0.13.4"
@@ -25,4 +24,4 @@ tokio-stream = "0.1.14"
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
tonic-reflection = "0.12.3"
uuid = { version = "1.7.0", features = ["v4"] }
yaak-http = { workspace = true }
yaak-http = { workspace = true }

View File

@@ -6,7 +6,7 @@ publish = false
[dependencies]
yaak-models = { workspace = true }
regex = "1.11.0"
regex = "1.11.1"
rustls = { workspace = true, default-features = false, features = ["ring"] }
rustls-platform-verifier = { workspace = true }
urlencoding = "2.1.3"

View File

@@ -1,9 +1,9 @@
use std::sync::Arc;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls::crypto::ring;
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls_platform_verifier::BuilderVerifierExt;
use std::sync::Arc;
pub fn get_config(validate_certificates: bool) -> ClientConfig {
let arc_crypto_provider = Arc::new(ring::default_provider());
@@ -12,9 +12,7 @@ pub fn get_config(validate_certificates: bool) -> ClientConfig {
.unwrap();
if validate_certificates {
// Use platform-native verifier to validate certificates
config_builder
.with_platform_verifier()
.with_no_client_auth()
config_builder.with_platform_verifier().unwrap().with_no_client_auth()
} else {
config_builder
.dangerous()

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use hex_color::HexColor;
use objc::{msg_send, sel, sel_impl};
use tauri::{Emitter, Runtime, Window};
@@ -65,12 +66,7 @@ pub(crate) fn update_window_theme<R: Runtime>(window: Window<R>, color: HexColor
}
}
fn position_traffic_lights(
ns_window_handle: UnsafeWindowHandle,
x: f64,
y: f64,
label: String,
) {
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64, label: String) {
if !label.starts_with(MAIN_WINDOW_PREFIX) {
return;
}

View File

@@ -7,6 +7,8 @@ publish = false
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
hex = "0.4.3"
include_dir = "0.7"
log = "0.4.22"
nanoid = "0.4.0"
r2d2 = "0.8.10"
@@ -16,8 +18,9 @@ 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"] }

View File

@@ -14,7 +14,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
export type EncryptedKey = { encryptedKey: string, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
@@ -58,11 +58,11 @@ export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, } | { "type": "disabled" };
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, bypass: string, } | { "type": "disabled" };
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, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, };
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,4 @@
// 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, Folder, GrpcRequest, HttpRequest, WebsocketRequest, 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

@@ -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

@@ -0,0 +1,2 @@
ALTER TABLE settings ADD COLUMN interface_font TEXT;
ALTER TABLE settings ADD COLUMN editor_font TEXT;

View File

@@ -0,0 +1 @@
ALTER TABLE environments ADD COLUMN color TEXT;

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(&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,122 @@
use crate::error::Error::MigrationError;
use crate::error::Result;
use include_dir::{include_dir, Dir, DirEntry};
use log::{debug, info};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{params, OptionalExtension, TransactionBehavior};
use sha2::{Digest, Sha384};
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
pub(crate) fn migrate_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> {
info!("Running database migrations");
// 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 = MIGRATIONS_DIR
.entries()
.into_iter()
.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.path());
// Run each migration in a transaction
let mut num_migrations = 0;
for entry in entries {
num_migrations += 1;
let mut conn = pool.get()?;
let mut tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
match run_migration(entry, &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", num_migrations);
Ok(())
}
fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> Result<bool> {
let start = std::time::Instant::now();
let (version, description) = split_migration_filename(migration_path.path().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() {
debug!("Skipping migration {description}");
// Migration was already run
return Ok(false);
}
let sql =
migration_path.as_file().unwrap().contents_utf8().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

@@ -31,12 +31,15 @@ macro_rules! impl_model {
#[ts(export, export_to = "gen_models.ts")]
pub enum ProxySetting {
Enabled {
#[serde(default)]
// This was added after on so give it a default to be able to deserialize older values
disabled: bool,
http: String,
https: String,
auth: Option<ProxySettingAuth>,
// These were added later, so give them defaults
#[serde(default)]
bypass: String,
#[serde(default)]
disabled: bool,
},
Disabled,
}
@@ -103,9 +106,13 @@ pub struct Settings {
pub updated_at: NaiveDateTime,
pub appearance: String,
pub colored_methods: bool,
pub editor_font: Option<String>,
pub editor_font_size: i32,
pub editor_keymap: EditorKeymap,
pub editor_soft_wrap: bool,
pub hide_window_controls: bool,
pub interface_font: Option<String>,
pub interface_font_size: i32,
pub interface_scale: f32,
pub open_workspace_new_window: Option<bool>,
@@ -113,7 +120,6 @@ pub struct Settings {
pub theme_dark: String,
pub theme_light: String,
pub update_channel: String,
pub editor_keymap: EditorKeymap,
}
impl UpsertModelInfo for Settings {
@@ -153,6 +159,8 @@ impl UpsertModelInfo for Settings {
(EditorFontSize, self.editor_font_size.into()),
(EditorKeymap, self.editor_keymap.to_string().into()),
(EditorSoftWrap, self.editor_soft_wrap.into()),
(EditorFont, self.editor_font.into()),
(InterfaceFont, self.interface_font.into()),
(InterfaceFontSize, self.interface_font_size.into()),
(InterfaceScale, self.interface_scale.into()),
(HideWindowControls, self.hide_window_controls.into()),
@@ -160,6 +168,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()),
])
}
@@ -171,14 +180,17 @@ impl UpsertModelInfo for Settings {
SettingsIden::EditorFontSize,
SettingsIden::EditorKeymap,
SettingsIden::EditorSoftWrap,
SettingsIden::EditorFont,
SettingsIden::InterfaceFontSize,
SettingsIden::InterfaceScale,
SettingsIden::InterfaceFont,
SettingsIden::HideWindowControls,
SettingsIden::OpenWorkspaceNewWindow,
SettingsIden::Proxy,
SettingsIden::ThemeDark,
SettingsIden::ThemeLight,
SettingsIden::UpdateChannel,
SettingsIden::ColoredMethods,
]
}
@@ -195,16 +207,19 @@ impl UpsertModelInfo for Settings {
updated_at: row.get("updated_at")?,
appearance: row.get("appearance")?,
editor_font_size: row.get("editor_font_size")?,
editor_font: row.get("editor_font")?,
editor_keymap: EditorKeymap::from_str(editor_keymap.as_str()).unwrap(),
editor_soft_wrap: row.get("editor_soft_wrap")?,
interface_font_size: row.get("interface_font_size")?,
interface_scale: row.get("interface_scale")?,
interface_font: row.get("interface_font")?,
open_workspace_new_window: row.get("open_workspace_new_window")?,
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
theme_dark: row.get("theme_dark")?,
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")?,
})
}
}
@@ -516,6 +531,7 @@ pub struct Environment {
pub public: bool,
pub base: bool,
pub variables: Vec<EnvironmentVariable>,
pub color: Option<String>,
}
impl UpsertModelInfo for Environment {
@@ -549,6 +565,7 @@ impl UpsertModelInfo for Environment {
(UpdatedAt, upsert_date(source, self.updated_at)),
(WorkspaceId, self.workspace_id.into()),
(Base, self.base.into()),
(Color, self.color.into()),
(Name, self.name.trim().into()),
(Public, self.public.into()),
(Variables, serde_json::to_string(&self.variables)?.into()),
@@ -559,6 +576,7 @@ impl UpsertModelInfo for Environment {
vec![
EnvironmentIden::UpdatedAt,
EnvironmentIden::Base,
EnvironmentIden::Color,
EnvironmentIden::Name,
EnvironmentIden::Public,
EnvironmentIden::Variables,
@@ -577,6 +595,7 @@ impl UpsertModelInfo for Environment {
created_at: row.get("created_at")?,
updated_at: row.get("updated_at")?,
base: row.get("base")?,
color: row.get("color")?,
name: row.get("name")?,
public: row.get("public")?,
variables: serde_json::from_str(variables.as_str()).unwrap_or_default(),

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

@@ -19,16 +19,19 @@ impl<'a> DbContext<'a> {
appearance: "system".to_string(),
editor_font_size: 13,
editor_font: None,
editor_keymap: EditorKeymap::Default,
editor_soft_wrap: true,
interface_font_size: 15,
interface_scale: 1.0,
interface_font: None,
hide_window_controls: false,
open_workspace_new_window: None,
proxy: None,
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

@@ -1,12 +1,6 @@
// 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.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -1,6 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SyncModel } from "./gen_models.js";
import type { SyncState } from "./gen_models.js";
import type { SyncModel, SyncState } from "./gen_models.js";
export type FsCandidate = { "type": "FsCandidate", model: SyncModel, relPath: string, checksum: string, };

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

@@ -4,11 +4,12 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { toggleDialog } from '../lib/dialog';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { ButtonProps } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
type Props = {
@@ -38,6 +39,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
(e) => ({
key: e.id,
label: e.name,
rightSlot: <EnvironmentColorIndicator environment={e} />,
leftSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: async () => {
if (e.id !== activeEnvironment?.id) {
@@ -80,6 +82,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
onClick={subEnvironments.length === 0 ? showEnvironmentDialog : undefined}
{...buttonProps}
>
<EnvironmentColorIndicator environment={activeEnvironment ?? null} />
{activeEnvironment?.name ?? (hasBaseVars ? 'Environment' : 'No Environment')}
</Button>
</Dropdown>

View File

@@ -0,0 +1,29 @@
import type { Environment } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { showColorPicker } from '../lib/showColorPicker';
export function EnvironmentColorIndicator({
environment,
clickToEdit,
}: {
environment: Environment | null;
clickToEdit?: boolean;
}) {
if (environment?.color == null) return null;
const style = { backgroundColor: environment.color };
const className =
'inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent';
if (clickToEdit) {
return (
<button
onClick={() => showColorPicker(environment)}
style={style}
className={classNames(className, 'hover:border-text')}
/>
);
} else {
return <span style={style} className={className} />;
}
}

Some files were not shown because too many files have changed in this diff Show More