mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-19 07:20:50 +01:00
Compare commits
44 Commits
922ae539ba
...
v0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 4833dee89a | |||
| a80c783576 | |||
| 4e1e4ae601 | |||
| 6a7f58a513 | |||
| 3ed8d6bc05 | |||
| 81a72da26c | |||
| 4fa3515e32 | |||
| c5c30fd4c4 | |||
| 8c7a02a019 | |||
| 465fe19542 | |||
| d2d0f8996b | |||
| f727d00639 | |||
| a946b14f69 | |||
| 471baec284 | |||
| 8298d807f3 | |||
| 42e6459fbf | |||
| 6ae87fc694 | |||
| f7867a5bde | |||
| d82599f588 | |||
| 72bb211a76 | |||
| f14ce0d6ad | |||
| af09f4524d | |||
| 102832675d | |||
| 3490de2f51 | |||
| 7c3af10938 | |||
| 5c5d0785b9 | |||
| 121dd9dd00 | |||
| 4ff6aee4d8 | |||
| dceb49ae90 | |||
| 5ea04a80e0 | |||
| 65cf2e2c3c | |||
| 68d542b4d7 | |||
| f97cd4ad97 | |||
| ef225b281f | |||
| 16b71d9ea8 | |||
| 5ee5ced8c0 | |||
| 86b65f117d | |||
| 5fdea155d1 | |||
| cb0c8d71f4 | |||
| 9281a85deb | |||
| 8f8bbb5558 | |||
| 252b8711de | |||
| 4f839aa856 | |||
| 99ccadce00 |
@ -1,227 +0,0 @@
|
|||||||
{
|
|
||||||
"session_date": "2025-10-20",
|
|
||||||
"project": "haex-hub System Windows Architecture + Drizzle CRDT RETURNING Fix + PK-Remapping Refactor",
|
|
||||||
"status": "system_windows_ui_integration_completed",
|
|
||||||
|
|
||||||
"context": {
|
|
||||||
"main_work_today": [
|
|
||||||
"Fixed Drizzle CRDT integration with RETURNING support",
|
|
||||||
"Implemented System Windows architecture (Settings, Marketplace as Desktop Windows)",
|
|
||||||
"Refactored executor functions: Split execute/query paths for cleaner code",
|
|
||||||
"Integrated System Windows UI: Launcher, Window Component, Placeholder Components"
|
|
||||||
],
|
|
||||||
"completed_today": [
|
|
||||||
"Added AST-based statement_has_returning() for safe RETURNING detection",
|
|
||||||
"Created SqlExecutor::query_internal() for INSERT/UPDATE/DELETE with RETURNING",
|
|
||||||
"Simplified execute_internal to use execute_internal_typed (now with PK-Remapping!)",
|
|
||||||
"Added sql_query_with_crdt Tauri command",
|
|
||||||
"Updated drizzleCallback in index.ts to route correctly",
|
|
||||||
"Extended IWindow interface: type ('system' | 'extension'), sourceId",
|
|
||||||
"Added SystemWindowDefinition interface",
|
|
||||||
"Created system windows registry in windowManager store",
|
|
||||||
"Extended openWindow() to support both system and extension windows",
|
|
||||||
"Added singleton support for system windows",
|
|
||||||
"Split execute_internal_typed_with_context into two functions (execute vs query)",
|
|
||||||
"Created query_internal_typed_with_context with full PK-Remapping support",
|
|
||||||
"Updated query_internal to use new typed function with PK-Remapping",
|
|
||||||
"Fixed manager.rs to use query_internal_typed_with_context for INSERT RETURNING",
|
|
||||||
"Extended Launcher to show System Windows + Extensions alphabetically",
|
|
||||||
"Adapted Window Component to render System Windows as Vue Components, Extensions as iFrames",
|
|
||||||
"Created placeholder components: Settings.vue and Marketplace.vue"
|
|
||||||
],
|
|
||||||
"tech_stack": "Vue 3, TypeScript, Pinia, Nuxt UI, Tauri, Rust, Drizzle ORM, SQLite"
|
|
||||||
},
|
|
||||||
|
|
||||||
"drizzle_crdt_implementation": {
|
|
||||||
"problem": "Drizzle .insert().returning() executed SQL twice and lost RETURNING data",
|
|
||||||
"solution": "Separate execute and query paths based on RETURNING clause",
|
|
||||||
|
|
||||||
"typescript_side": {
|
|
||||||
"file": "src/stores/vault/index.ts",
|
|
||||||
"drizzleCallback_logic": {
|
|
||||||
"select": "sql_select (unchanged)",
|
|
||||||
"with_returning": "sql_query_with_crdt (NEW)",
|
|
||||||
"without_returning": "sql_execute_with_crdt (unchanged)"
|
|
||||||
},
|
|
||||||
"hasReturning_check": "String-based RETURNING regex (safe enough for generated SQL)"
|
|
||||||
},
|
|
||||||
|
|
||||||
"rust_side": {
|
|
||||||
"files": [
|
|
||||||
"src-tauri/src/database/core.rs",
|
|
||||||
"src-tauri/src/extension/database/executor.rs",
|
|
||||||
"src-tauri/src/database/mod.rs"
|
|
||||||
],
|
|
||||||
"core_rs_changes": {
|
|
||||||
"statement_has_returning": {
|
|
||||||
"line": 84,
|
|
||||||
"purpose": "AST-based RETURNING check (INSERT, UPDATE, DELETE)",
|
|
||||||
"safety": "Checks actual AST, not string matching"
|
|
||||||
},
|
|
||||||
"convert_value_ref_to_json": {
|
|
||||||
"line": 333,
|
|
||||||
"visibility": "Made public for reuse"
|
|
||||||
},
|
|
||||||
"removed": "query_with_crdt function (replaced by SqlExecutor::query_internal)"
|
|
||||||
},
|
|
||||||
"executor_rs_changes": {
|
|
||||||
"execute_internal_typed_with_context": {
|
|
||||||
"line": 100,
|
|
||||||
"purpose": "Execute SQL WITHOUT RETURNING (with CRDT and FK-Remapping)",
|
|
||||||
"returns": "Result<HashSet<String>, DatabaseError>",
|
|
||||||
"behavior": "Handles INSERTs with FK-Remapping, uses execute()"
|
|
||||||
},
|
|
||||||
"query_internal_typed_with_context": {
|
|
||||||
"line": 186,
|
|
||||||
"purpose": "Execute SQL WITH RETURNING (with CRDT, PK-Remapping, FK-Remapping)",
|
|
||||||
"returns": "Result<(HashSet<String>, Vec<Vec<JsonValue>>), DatabaseError>",
|
|
||||||
"behavior": "Handles INSERTs with full PK-Remapping + FK-Remapping, returns all RETURNING columns"
|
|
||||||
},
|
|
||||||
"query_internal": {
|
|
||||||
"line": 454,
|
|
||||||
"purpose": "Execute with CRDT + return full RETURNING results (JsonValue params)",
|
|
||||||
"behavior": "Wrapper around query_internal_typed_with_context"
|
|
||||||
},
|
|
||||||
"execute_internal_refactor": {
|
|
||||||
"line": 345,
|
|
||||||
"change": "Now wrapper around execute_internal_typed",
|
|
||||||
"benefit": "Drizzle now gets PK-Remapping for ON CONFLICT!"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mod_rs_changes": {
|
|
||||||
"sql_query_with_crdt": {
|
|
||||||
"line": 59,
|
|
||||||
"calls": "SqlExecutor::query_internal",
|
|
||||||
"returns": "Vec<Vec<JsonValue>>"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lib_rs_changes": {
|
|
||||||
"registered_command": "sql_query_with_crdt added to invoke_handler"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"benefits": [
|
|
||||||
"SQL executed only once (not twice)",
|
|
||||||
"Full RETURNING results available to Drizzle",
|
|
||||||
"PK-Remapping now works for Drizzle (both execute and query paths)",
|
|
||||||
"AST-based RETURNING detection (safe)",
|
|
||||||
"Less code duplication",
|
|
||||||
"Cleaner code separation: execute vs query functions",
|
|
||||||
"FK-Remapping works across transactions with PkRemappingContext"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
"system_windows_architecture": {
|
|
||||||
"concept": "ALL UI (Settings, Marketplace, etc.) as DesktopWindows, same as Extensions",
|
|
||||||
"status": "Store completed, UI integration pending",
|
|
||||||
|
|
||||||
"window_manager_store": {
|
|
||||||
"file": "src/stores/desktop/windowManager.ts",
|
|
||||||
"changes": {
|
|
||||||
"IWindow_interface": {
|
|
||||||
"added_fields": [
|
|
||||||
"type: 'system' | 'extension'",
|
|
||||||
"sourceId: string (replaces extensionId)"
|
|
||||||
],
|
|
||||||
"removed_fields": ["extensionId"]
|
|
||||||
},
|
|
||||||
"SystemWindowDefinition": {
|
|
||||||
"fields": "id, name, icon, component, defaultWidth, defaultHeight, resizable, singleton"
|
|
||||||
},
|
|
||||||
"system_windows_registry": {
|
|
||||||
"line": 46,
|
|
||||||
"entries": ["settings", "marketplace"],
|
|
||||||
"structure": "Record<string, SystemWindowDefinition>"
|
|
||||||
},
|
|
||||||
"openWindow_function": {
|
|
||||||
"line": 101,
|
|
||||||
"signature": "(type, sourceId, title?, icon?, width?, height?, sourcePosition?)",
|
|
||||||
"features": [
|
|
||||||
"Type-based handling (system vs extension)",
|
|
||||||
"Singleton check for system windows",
|
|
||||||
"Auto-loads defaults from registry",
|
|
||||||
"Activates existing singleton if already open"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"new_exports": ["getAllSystemWindows", "getSystemWindow"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"ui_integration": {
|
|
||||||
"launcher": {
|
|
||||||
"file": "src/components/haex/extension/launcher.vue",
|
|
||||||
"changes": [
|
|
||||||
"Combined system windows and extensions in unified launcherItems computed",
|
|
||||||
"Alphabetically sorted by name",
|
|
||||||
"openItem() function handles both types with correct openWindow() signature",
|
|
||||||
"Uses windowManagerStore.getAllSystemWindows()"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"desktop_window_component": {
|
|
||||||
"file": "src/components/haex/desktop/index.vue",
|
|
||||||
"changes": [
|
|
||||||
"Dynamic component rendering: <component :is='getSystemWindowComponent()'> for system windows",
|
|
||||||
"HaexDesktopExtensionFrame for extensions (iFrame)",
|
|
||||||
"getSystemWindowComponent() function to retrieve Vue component from registry",
|
|
||||||
"Applied to both normal mode and overview mode"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"placeholder_components": {
|
|
||||||
"created": [
|
|
||||||
"src/components/haex/system/settings.vue",
|
|
||||||
"src/components/haex/system/marketplace.vue"
|
|
||||||
],
|
|
||||||
"description": "Simple placeholder UI with sections and styling"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"next_steps": {
|
|
||||||
"priority": [
|
|
||||||
"Desktop Icons: Support system window icons (alongside extension icons)",
|
|
||||||
"Drag & Drop: Launcher → Desktop for all types (system + extension)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"workspace_overview_context": {
|
|
||||||
"still_active": "GNOME-style workspace overview with UDrawer, implemented yesterday",
|
|
||||||
"file": "src/components/haex/desktop/index.vue",
|
|
||||||
"status": "Working, workspace switching functional"
|
|
||||||
},
|
|
||||||
|
|
||||||
"file_changes_today": {
|
|
||||||
"modified": [
|
|
||||||
"src/stores/vault/index.ts (drizzleCallback with hasReturning)",
|
|
||||||
"src-tauri/src/database/core.rs (statement_has_returning, removed query_with_crdt)",
|
|
||||||
"src-tauri/src/extension/database/executor.rs (split execute/query functions, PK-Remapping)",
|
|
||||||
"src-tauri/src/database/mod.rs (sql_query_with_crdt command)",
|
|
||||||
"src-tauri/src/lib.rs (registered sql_query_with_crdt)",
|
|
||||||
"src/stores/desktop/windowManager.ts (system windows support)",
|
|
||||||
"src-tauri/src/extension/core/manager.rs (updated to use query_internal_typed_with_context)",
|
|
||||||
"src/components/haex/extension/launcher.vue (unified launcher for system + extensions)",
|
|
||||||
"src/components/haex/desktop/index.vue (dynamic component rendering)"
|
|
||||||
],
|
|
||||||
"created": [
|
|
||||||
"src/components/haex/system/settings.vue",
|
|
||||||
"src/components/haex/system/marketplace.vue"
|
|
||||||
],
|
|
||||||
"deleted": []
|
|
||||||
},
|
|
||||||
|
|
||||||
"important_notes": [
|
|
||||||
"Drizzle RETURNING now fully functional with CRDT",
|
|
||||||
"System windows use Vue components, Extensions use iFrame",
|
|
||||||
"sourceId is generic: extensionId for extensions, systemWindowId for system windows",
|
|
||||||
"Singleton system windows auto-activate if already open",
|
|
||||||
"PK-Remapping now works for both execute and query paths",
|
|
||||||
"executor.rs: Two separate functions for execute (no RETURNING) vs query (with RETURNING)",
|
|
||||||
"query_internal_typed_with_context returns full RETURNING results as Vec<Vec<JsonValue>>",
|
|
||||||
"FK-Remapping works across transaction using PkRemappingContext",
|
|
||||||
"Next session: Implement Launcher UI integration for system windows"
|
|
||||||
],
|
|
||||||
|
|
||||||
"todos_remaining": [
|
|
||||||
"Desktop Icons für System Windows unterstützen (neben Extension Icons)",
|
|
||||||
"Drag & Drop vom Launcher zum Desktop implementieren (für beide Typen)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
228
.github/workflows/build.yml
vendored
Normal file
228
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
tags-ignore:
|
||||||
|
- '**'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-desktop:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: 'macos-latest'
|
||||||
|
args: '--target aarch64-apple-darwin'
|
||||||
|
- platform: 'macos-latest'
|
||||||
|
args: '--target x86_64-apple-darwin'
|
||||||
|
- platform: 'ubuntu-22.04'
|
||||||
|
args: ''
|
||||||
|
- platform: 'windows-latest'
|
||||||
|
args: ''
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- name: Install dependencies (Ubuntu)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Setup Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build Tauri app
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
args: ${{ matrix.args }}
|
||||||
|
|
||||||
|
- name: Upload artifacts (macOS)
|
||||||
|
if: matrix.platform == 'macos-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-${{ contains(matrix.args, 'aarch64') && 'aarch64' || 'x86_64' }}
|
||||||
|
path: |
|
||||||
|
src-tauri/target/*/release/bundle/dmg/*.dmg
|
||||||
|
src-tauri/target/*/release/bundle/macos/*.app
|
||||||
|
|
||||||
|
- name: Upload artifacts (Ubuntu)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux
|
||||||
|
path: |
|
||||||
|
src-tauri/target/release/bundle/deb/*.deb
|
||||||
|
src-tauri/target/release/bundle/appimage/*.AppImage
|
||||||
|
src-tauri/target/release/bundle/rpm/*.rpm
|
||||||
|
|
||||||
|
- name: Upload artifacts (Windows)
|
||||||
|
if: matrix.platform == 'windows-latest'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows
|
||||||
|
path: |
|
||||||
|
src-tauri/target/release/bundle/msi/*.msi
|
||||||
|
src-tauri/target/release/bundle/nsis/*.exe
|
||||||
|
|
||||||
|
build-android:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install Rust Android targets
|
||||||
|
run: |
|
||||||
|
rustup target add aarch64-linux-android
|
||||||
|
rustup target add armv7-linux-androideabi
|
||||||
|
rustup target add i686-linux-android
|
||||||
|
rustup target add x86_64-linux-android
|
||||||
|
|
||||||
|
- name: Setup NDK
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
with:
|
||||||
|
ndk-version: r26d
|
||||||
|
id: setup-ndk
|
||||||
|
|
||||||
|
- name: Setup Android NDK environment for OpenSSL
|
||||||
|
run: |
|
||||||
|
echo "ANDROID_NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
|
||||||
|
echo "NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Add all Android toolchains to PATH for OpenSSL cross-compilation
|
||||||
|
echo "${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
# Set CC, AR, RANLIB for each target
|
||||||
|
echo "CC_aarch64_linux_android=aarch64-linux-android24-clang" >> $GITHUB_ENV
|
||||||
|
echo "AR_aarch64_linux_android=llvm-ar" >> $GITHUB_ENV
|
||||||
|
echo "RANLIB_aarch64_linux_android=llvm-ranlib" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "CC_armv7_linux_androideabi=armv7a-linux-androideabi24-clang" >> $GITHUB_ENV
|
||||||
|
echo "AR_armv7_linux_androideabi=llvm-ar" >> $GITHUB_ENV
|
||||||
|
echo "RANLIB_armv7_linux_androideabi=llvm-ranlib" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "CC_i686_linux_android=i686-linux-android24-clang" >> $GITHUB_ENV
|
||||||
|
echo "AR_i686_linux_android=llvm-ar" >> $GITHUB_ENV
|
||||||
|
echo "RANLIB_i686_linux_android=llvm-ranlib" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "CC_x86_64_linux_android=x86_64-linux-android24-clang" >> $GITHUB_ENV
|
||||||
|
echo "AR_x86_64_linux_android=llvm-ar" >> $GITHUB_ENV
|
||||||
|
echo "RANLIB_x86_64_linux_android=llvm-ranlib" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install build dependencies for OpenSSL
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y perl make
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Setup Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Setup Keystore (if secrets available)
|
||||||
|
env:
|
||||||
|
ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
|
||||||
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
if [ -n "$ANDROID_KEYSTORE" ]; then
|
||||||
|
echo "$ANDROID_KEYSTORE" | base64 -d > $HOME/keystore.jks
|
||||||
|
echo "ANDROID_KEYSTORE_PATH=$HOME/keystore.jks" >> $GITHUB_ENV
|
||||||
|
echo "ANDROID_KEYSTORE_PASSWORD=$ANDROID_KEYSTORE_PASSWORD" >> $GITHUB_ENV
|
||||||
|
echo "ANDROID_KEY_ALIAS=$ANDROID_KEY_ALIAS" >> $GITHUB_ENV
|
||||||
|
echo "ANDROID_KEY_PASSWORD=$ANDROID_KEY_PASSWORD" >> $GITHUB_ENV
|
||||||
|
echo "Keystore configured for signing"
|
||||||
|
else
|
||||||
|
echo "No keystore configured, building unsigned APK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build Android APK and AAB (unsigned if no keystore)
|
||||||
|
run: pnpm tauri android build
|
||||||
|
|
||||||
|
- name: Upload Android artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: android
|
||||||
|
path: |
|
||||||
|
src-tauri/gen/android/app/build/outputs/apk/**/*.apk
|
||||||
|
src-tauri/gen/android/app/build/outputs/bundle/**/*.aab
|
||||||
251
.github/workflows/release.yml
vendored
Normal file
251
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-release:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
outputs:
|
||||||
|
release_id: ${{ steps.create-release.outputs.release_id }}
|
||||||
|
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
run: echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
id: create-release
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { data } = await github.rest.repos.createRelease({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
tag_name: `v${process.env.PACKAGE_VERSION}`,
|
||||||
|
name: `haex-hub v${process.env.PACKAGE_VERSION}`,
|
||||||
|
body: 'Take a look at the assets to download and install this app.',
|
||||||
|
draft: true,
|
||||||
|
prerelease: false
|
||||||
|
})
|
||||||
|
core.setOutput('release_id', data.id)
|
||||||
|
core.setOutput('upload_url', data.upload_url)
|
||||||
|
return data.id
|
||||||
|
|
||||||
|
build-desktop:
|
||||||
|
needs: create-release
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: 'macos-latest'
|
||||||
|
args: '--target aarch64-apple-darwin'
|
||||||
|
- platform: 'macos-latest'
|
||||||
|
args: '--target x86_64-apple-darwin'
|
||||||
|
- platform: 'ubuntu-22.04'
|
||||||
|
args: ''
|
||||||
|
- platform: 'windows-latest'
|
||||||
|
args: ''
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- name: Install dependencies (Ubuntu)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Setup Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build and release Tauri app
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
releaseId: ${{ needs.create-release.outputs.release_id }}
|
||||||
|
args: ${{ matrix.args }}
|
||||||
|
|
||||||
|
build-android:
|
||||||
|
needs: create-release
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install Rust Android targets
|
||||||
|
run: |
|
||||||
|
rustup target add aarch64-linux-android
|
||||||
|
rustup target add armv7-linux-androideabi
|
||||||
|
rustup target add i686-linux-android
|
||||||
|
rustup target add x86_64-linux-android
|
||||||
|
|
||||||
|
- name: Setup NDK
|
||||||
|
uses: nttld/setup-ndk@v1
|
||||||
|
with:
|
||||||
|
ndk-version: r26d
|
||||||
|
id: setup-ndk
|
||||||
|
|
||||||
|
- name: Setup Android NDK environment for OpenSSL
|
||||||
|
run: |
|
||||||
|
echo "ANDROID_NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
|
||||||
|
echo "NDK_HOME=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Add all Android toolchains to PATH for OpenSSL cross-compilation
|
||||||
|
echo "${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
# Set CC, AR, RANLIB for each target
|
||||||
|
echo "CC_aarch64_linux_android=aarch64-linux-android24-clang" >> $GITHUB_ENV
|
||||||
|
echo "AR_aarch64_linux_android=llvm-ar" >> $GITHUB_ENV
|
||||||
|
echo "RANLIB_aarch64_linux_android=llvm-ranlib" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "CC_armv7_linux_androideabi=armv7a-linux-androideabi24-clang" >> $GITHUB_ENV
|
||||||
|
echo "AR_armv7_linux_androideabi=llvm-ar" >> $GITHUB_ENV
|
||||||
|
echo "RANLIB_armv7_linux_androideabi=llvm-ranlib" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "CC_i686_linux_android=i686-linux-android24-clang" >> $GITHUB_ENV
|
||||||
|
echo "AR_i686_linux_android=llvm-ar" >> $GITHUB_ENV
|
||||||
|
echo "RANLIB_i686_linux_android=llvm-ranlib" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "CC_x86_64_linux_android=x86_64-linux-android24-clang" >> $GITHUB_ENV
|
||||||
|
echo "AR_x86_64_linux_android=llvm-ar" >> $GITHUB_ENV
|
||||||
|
echo "RANLIB_x86_64_linux_android=llvm-ranlib" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install build dependencies for OpenSSL
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y perl make
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Setup Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: src-tauri
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Setup Keystore (required for release)
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 -d > $HOME/keystore.jks
|
||||||
|
echo "ANDROID_KEYSTORE_PATH=$HOME/keystore.jks" >> $GITHUB_ENV
|
||||||
|
echo "ANDROID_KEYSTORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> $GITHUB_ENV
|
||||||
|
echo "ANDROID_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" >> $GITHUB_ENV
|
||||||
|
echo "ANDROID_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build Android APK and AAB (signed)
|
||||||
|
run: pnpm tauri android build
|
||||||
|
|
||||||
|
- name: Upload Android artifacts to Release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
gh release upload ${{ github.ref_name }} \
|
||||||
|
src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk \
|
||||||
|
src-tauri/gen/android/app/build/outputs/bundle/universalRelease/app-universal-release.aab \
|
||||||
|
--clobber
|
||||||
|
|
||||||
|
publish-release:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: [create-release, build-desktop, build-android]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Publish release
|
||||||
|
id: publish-release
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
env:
|
||||||
|
release_id: ${{ needs.create-release.outputs.release_id }}
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.repos.updateRelease({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
release_id: process.env.release_id,
|
||||||
|
draft: false,
|
||||||
|
prerelease: false
|
||||||
|
})
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -26,4 +26,6 @@ dist-ssr
|
|||||||
src-tauri/target
|
src-tauri/target
|
||||||
nogit*
|
nogit*
|
||||||
.claude
|
.claude
|
||||||
.output
|
.output
|
||||||
|
target
|
||||||
|
CLAUDE.md
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'drizzle-kit'
|
import { defineConfig } from 'drizzle-kit'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: './src-tauri/database/schemas/**.ts',
|
schema: './src/database/schemas/**.ts',
|
||||||
out: './src-tauri/database/migrations',
|
out: './src-tauri/database/migrations',
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
//import tailwindcss from '@tailwindcss/vite'
|
|
||||||
|
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
@ -16,6 +14,9 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
|
head: {
|
||||||
|
viewport: 'width=device-width, initial-scale=1.0, viewport-fit=cover',
|
||||||
|
},
|
||||||
pageTransition: {
|
pageTransition: {
|
||||||
name: 'fade',
|
name: 'fade',
|
||||||
},
|
},
|
||||||
@ -28,7 +29,6 @@ export default defineNuxtConfig({
|
|||||||
'@vueuse/nuxt',
|
'@vueuse/nuxt',
|
||||||
'@nuxt/icon',
|
'@nuxt/icon',
|
||||||
'@nuxt/eslint',
|
'@nuxt/eslint',
|
||||||
//"@nuxt/image",
|
|
||||||
'@nuxt/fonts',
|
'@nuxt/fonts',
|
||||||
'@nuxt/ui',
|
'@nuxt/ui',
|
||||||
],
|
],
|
||||||
@ -108,8 +108,7 @@ export default defineNuxtConfig({
|
|||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
haexVault: {
|
haexVault: {
|
||||||
lastVaultFileName: 'lastVaults.json',
|
deviceFileName: 'device.json',
|
||||||
instanceFileName: 'instance.json',
|
|
||||||
defaultVaultName: 'HaexHub',
|
defaultVaultName: 'HaexHub',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -123,7 +122,6 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
//plugins: [tailwindcss()],
|
|
||||||
// Better support for Tauri CLI output
|
// Better support for Tauri CLI output
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
// Enable environment variables
|
// Enable environment variables
|
||||||
|
|||||||
32
package.json
32
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "haex-hub",
|
"name": "haex-hub",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
@ -21,50 +21,48 @@
|
|||||||
"@nuxt/eslint": "1.9.0",
|
"@nuxt/eslint": "1.9.0",
|
||||||
"@nuxt/fonts": "0.11.4",
|
"@nuxt/fonts": "0.11.4",
|
||||||
"@nuxt/icon": "2.0.0",
|
"@nuxt/icon": "2.0.0",
|
||||||
"@nuxt/ui": "4.0.0",
|
"@nuxt/ui": "4.1.0",
|
||||||
"@nuxtjs/i18n": "10.0.6",
|
"@nuxtjs/i18n": "10.0.6",
|
||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
"@tailwindcss/vite": "^4.1.15",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@tauri-apps/api": "^2.9.0",
|
"@tauri-apps/api": "^2.9.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||||
"@tauri-apps/plugin-http": "2.5.2",
|
|
||||||
"@tauri-apps/plugin-notification": "2.3.1",
|
"@tauri-apps/plugin-notification": "2.3.1",
|
||||||
"@tauri-apps/plugin-opener": "^2.5.0",
|
"@tauri-apps/plugin-opener": "^2.5.2",
|
||||||
"@tauri-apps/plugin-os": "^2.3.1",
|
"@tauri-apps/plugin-os": "^2.3.2",
|
||||||
"@tauri-apps/plugin-sql": "2.3.0",
|
"@tauri-apps/plugin-store": "^2.4.1",
|
||||||
"@tauri-apps/plugin-store": "^2.4.0",
|
|
||||||
"@vueuse/components": "^13.9.0",
|
"@vueuse/components": "^13.9.0",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"@vueuse/gesture": "^2.0.0",
|
"@vueuse/gesture": "^2.0.0",
|
||||||
"@vueuse/nuxt": "^13.9.0",
|
"@vueuse/nuxt": "^13.9.0",
|
||||||
"drizzle-orm": "^0.44.6",
|
"drizzle-orm": "^0.44.7",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.38.0",
|
||||||
"fuse.js": "^7.1.0",
|
|
||||||
"nuxt": "^4.1.3",
|
|
||||||
"nuxt-zod-i18n": "^1.12.1",
|
"nuxt-zod-i18n": "^1.12.1",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.0.3",
|
||||||
"tailwindcss": "^4.1.15",
|
"tailwindcss": "^4.1.16",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3",
|
"vue-router": "^4.6.3",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/hugeicons": "^1.2.17",
|
"@iconify-json/hugeicons": "^1.2.17",
|
||||||
"@iconify/json": "^2.2.398",
|
"@iconify-json/lucide": "^1.2.71",
|
||||||
|
"@iconify/json": "^2.2.401",
|
||||||
"@iconify/tailwind4": "^1.0.6",
|
"@iconify/tailwind4": "^1.0.6",
|
||||||
"@libsql/client": "^0.15.15",
|
"@libsql/client": "^0.15.15",
|
||||||
"@tauri-apps/cli": "^2.9.0",
|
"@tauri-apps/cli": "^2.9.1",
|
||||||
"@types/node": "^24.9.1",
|
"@types/node": "^24.9.1",
|
||||||
"@vitejs/plugin-vue": "6.0.1",
|
"@vitejs/plugin-vue": "6.0.1",
|
||||||
"@vue/compiler-sfc": "^3.5.22",
|
"@vue/compiler-sfc": "^3.5.22",
|
||||||
"drizzle-kit": "^0.31.5",
|
"drizzle-kit": "^0.31.5",
|
||||||
"globals": "^16.4.0",
|
"globals": "^16.4.0",
|
||||||
|
"nuxt": "^4.2.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.20.6",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "7.1.3",
|
"vite": "^7.1.3",
|
||||||
"vue-tsc": "3.0.6"
|
"vue-tsc": "3.0.6"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
|||||||
2185
pnpm-lock.yaml
generated
2185
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1303
src-tauri/Cargo.lock
generated
1303
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "haex-hub"
|
name = "haex-hub"
|
||||||
version = "0.1.0"
|
version = "0.1.4"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@ -20,14 +20,7 @@ tauri-build = { version = "2.2", features = [] }
|
|||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rusqlite = { version = "0.37.0", features = [
|
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||||
"load_extension",
|
|
||||||
"bundled-sqlcipher-vendored-openssl",
|
|
||||||
"functions",
|
|
||||||
] }
|
|
||||||
|
|
||||||
#tauri-plugin-sql = { version = "2", features = ["sqlite"] }tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }#libsqlite3-sys = { version = "0.31", features = ["bundled-sqlcipher"] }
|
|
||||||
#sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }
|
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
ed25519-dalek = "2.1"
|
ed25519-dalek = "2.1"
|
||||||
fs_extra = "1.3.0"
|
fs_extra = "1.3.0"
|
||||||
@ -39,18 +32,26 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1.0.143"
|
serde_json = "1.0.143"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
sqlparser = { version = "0.59.0", features = ["visitor"] }
|
sqlparser = { version = "0.59.0", features = ["visitor"] }
|
||||||
tauri = { version = "2.8.5", features = ["protocol-asset", "devtools"] }
|
tauri = { version = "2.9.1", features = ["protocol-asset", "devtools"] }
|
||||||
tauri-plugin-dialog = "2.4.0"
|
tauri-plugin-dialog = "2.4.2"
|
||||||
tauri-plugin-fs = "2.4.0"
|
tauri-plugin-fs = "2.4.0"
|
||||||
tauri-plugin-http = "2.5.2"
|
tauri-plugin-http = "2.5.4"
|
||||||
tauri-plugin-notification = "2.3.1"
|
tauri-plugin-notification = "2.3.3"
|
||||||
tauri-plugin-opener = "2.5.0"
|
tauri-plugin-opener = "2.5.2"
|
||||||
tauri-plugin-os = "2.3"
|
tauri-plugin-os = "2.3.2"
|
||||||
tauri-plugin-persisted-scope = "2.3.2"
|
tauri-plugin-persisted-scope = "2.3.4"
|
||||||
tauri-plugin-store = "2.4.0"
|
tauri-plugin-store = "2.4.1"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
ts-rs = { version = "11.1.0", features = ["serde-compat"] }
|
ts-rs = { version = "11.1.0", features = ["serde-compat"] }
|
||||||
uhlc = "0.8.2"
|
uhlc = "0.8.2"
|
||||||
|
url = "2.5.7"
|
||||||
uuid = { version = "1.18.1", features = ["v4"] }
|
uuid = { version = "1.18.1", features = ["v4"] }
|
||||||
zip = "6.0.0"
|
zip = "6.0.0"
|
||||||
url = "2.5.7"
|
rusqlite = { version = "0.37.0", features = [
|
||||||
|
"load_extension",
|
||||||
|
"bundled-sqlcipher-vendored-openssl",
|
||||||
|
"functions",
|
||||||
|
] }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
|
trash = "5.2.5"
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type ExtensionInfoResponse = { id: string, publicKey: string, name: string, version: string, author: string | null, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, devServerUrl: string | null, };
|
export type ExtensionInfoResponse = { id: string, publicKey: string, name: string, version: string, author: string | null, enabled: boolean, description: string | null, homepage: string | null, icon: string | null, entry: string | null, singleInstance: boolean | null, devServerUrl: string | null, };
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { ExtensionPermissions } from "./ExtensionPermissions";
|
import type { ExtensionPermissions } from "./ExtensionPermissions";
|
||||||
|
|
||||||
export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, };
|
export type ExtensionManifest = { name: string, version: string, author: string | null, entry: string | null, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, single_instance: boolean | null, };
|
||||||
|
|||||||
@ -35,6 +35,7 @@
|
|||||||
"notification:allow-create-channel",
|
"notification:allow-create-channel",
|
||||||
"notification:allow-list-channels",
|
"notification:allow-list-channels",
|
||||||
"notification:allow-notify",
|
"notification:allow-notify",
|
||||||
|
"notification:allow-is-permission-granted",
|
||||||
"notification:default",
|
"notification:default",
|
||||||
"opener:allow-open-url",
|
"opener:allow-open-url",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { writeFileSync, mkdirSync } from 'node:fs'
|
import { writeFileSync, mkdirSync } from 'node:fs'
|
||||||
import { join, dirname } from 'node:path'
|
import { join, dirname } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import tablesNames from './tableNames.json'
|
import tablesNames from '../../src/database/tableNames.json'
|
||||||
import { schema } from './index'
|
import { schema } from '../../src/database/index'
|
||||||
import { getTableColumns } from 'drizzle-orm'
|
import { getTableColumns } from 'drizzle-orm'
|
||||||
import type { AnySQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'
|
import type { AnySQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'
|
||||||
|
|
||||||
@ -170,6 +170,14 @@ use serde::{Deserialize, Serialize};
|
|||||||
table: schema.haexCrdtSnapshots,
|
table: schema.haexCrdtSnapshots,
|
||||||
},
|
},
|
||||||
{ name: tablesNames.haex.crdt.configs.name, table: schema.haexCrdtConfigs },
|
{ name: tablesNames.haex.crdt.configs.name, table: schema.haexCrdtConfigs },
|
||||||
|
{
|
||||||
|
name: tablesNames.haex.desktop_items.name,
|
||||||
|
table: schema.haexDesktopItems,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: tablesNames.haex.workspaces.name,
|
||||||
|
table: schema.haexWorkspaces,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const { name, table } of schemas) {
|
for (const { name, table } of schemas) {
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
import { drizzle } from 'drizzle-orm/sqlite-proxy' // Adapter für Query Building ohne direkte Verbindung
|
|
||||||
import * as schema from './schemas' // Importiere alles aus deiner Schema-Datei
|
|
||||||
export * as schema from './schemas'
|
|
||||||
// sqlite-proxy benötigt eine (dummy) Ausführungsfunktion als Argument.
|
|
||||||
// Diese wird in unserem Tauri-Workflow nie aufgerufen, da wir nur .toSQL() verwenden.
|
|
||||||
// Sie muss aber vorhanden sein, um drizzle() aufrufen zu können.
|
|
||||||
const dummyExecutor = async (
|
|
||||||
sql: string,
|
|
||||||
params: unknown[],
|
|
||||||
method: 'all' | 'run' | 'get' | 'values',
|
|
||||||
) => {
|
|
||||||
console.warn(
|
|
||||||
`Frontend Drizzle Executor wurde aufgerufen (Methode: ${method}). Das sollte im Tauri-Invoke-Workflow nicht passieren!`,
|
|
||||||
)
|
|
||||||
// Wir geben leere Ergebnisse zurück, um die Typen zufriedenzustellen, falls es doch aufgerufen wird.
|
|
||||||
return { rows: [] } // Für 'run' (z.B. bei INSERT/UPDATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erstelle die Drizzle-Instanz für den SQLite-Dialekt
|
|
||||||
// Übergib den dummyExecutor und das importierte Schema
|
|
||||||
export const db = drizzle(dummyExecutor, { schema })
|
|
||||||
|
|
||||||
// Exportiere auch alle Schema-Definitionen weiter, damit man alles aus einer Datei importieren kann
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
CREATE TABLE `haex_crdt_configs` (
|
|
||||||
`key` text PRIMARY KEY NOT NULL,
|
|
||||||
`value` text
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_crdt_logs` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`haex_timestamp` text,
|
|
||||||
`table_name` text,
|
|
||||||
`row_pks` text,
|
|
||||||
`op_type` text,
|
|
||||||
`column_name` text,
|
|
||||||
`new_value` text,
|
|
||||||
`old_value` text
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE INDEX `idx_haex_timestamp` ON `haex_crdt_logs` (`haex_timestamp`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `idx_table_row` ON `haex_crdt_logs` (`table_name`,`row_pks`);--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_crdt_snapshots` (
|
|
||||||
`snapshot_id` text PRIMARY KEY NOT NULL,
|
|
||||||
`created` text,
|
|
||||||
`epoch_hlc` text,
|
|
||||||
`location_url` text,
|
|
||||||
`file_size_bytes` integer
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_extension_permissions` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`extension_id` text,
|
|
||||||
`resource_type` text,
|
|
||||||
`action` text,
|
|
||||||
`target` text,
|
|
||||||
`constraints` text,
|
|
||||||
`status` text DEFAULT 'denied' NOT NULL,
|
|
||||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
|
|
||||||
`updated_at` integer,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
`haex_timestamp` text,
|
|
||||||
FOREIGN KEY (`extension_id`) REFERENCES `haex_extensions`(`id`) ON UPDATE no action ON DELETE no action
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `haex_extension_permissions_extension_id_resource_type_action_target_unique` ON `haex_extension_permissions` (`extension_id`,`resource_type`,`action`,`target`);--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_extensions` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`author` text,
|
|
||||||
`description` text,
|
|
||||||
`entry` text,
|
|
||||||
`homepage` text,
|
|
||||||
`enabled` integer,
|
|
||||||
`icon` text,
|
|
||||||
`name` text,
|
|
||||||
`public_key` text,
|
|
||||||
`signature` text,
|
|
||||||
`url` text,
|
|
||||||
`version` text,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
`haex_timestamp` text
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_settings` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`key` text,
|
|
||||||
`type` text,
|
|
||||||
`value` text,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
`haex_timestamp` text
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_notifications` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`alt` text,
|
|
||||||
`date` text,
|
|
||||||
`icon` text,
|
|
||||||
`image` text,
|
|
||||||
`read` integer,
|
|
||||||
`source` text,
|
|
||||||
`text` text,
|
|
||||||
`title` text,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`haex_tombstone` integer
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_passwords_group_items` (
|
|
||||||
`group_id` text,
|
|
||||||
`item_id` text,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
PRIMARY KEY(`item_id`, `group_id`),
|
|
||||||
FOREIGN KEY (`group_id`) REFERENCES `haex_passwords_groups`(`id`) ON UPDATE no action ON DELETE no action,
|
|
||||||
FOREIGN KEY (`item_id`) REFERENCES `haex_passwords_item_details`(`id`) ON UPDATE no action ON DELETE no action
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_passwords_groups` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`name` text,
|
|
||||||
`description` text,
|
|
||||||
`icon` text,
|
|
||||||
`order` integer,
|
|
||||||
`color` text,
|
|
||||||
`parent_id` text,
|
|
||||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
|
|
||||||
`updated_at` integer,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
FOREIGN KEY (`parent_id`) REFERENCES `haex_passwords_groups`(`id`) ON UPDATE no action ON DELETE no action
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_passwords_item_details` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`title` text,
|
|
||||||
`username` text,
|
|
||||||
`password` text,
|
|
||||||
`note` text,
|
|
||||||
`icon` text,
|
|
||||||
`tags` text,
|
|
||||||
`url` text,
|
|
||||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
|
|
||||||
`updated_at` integer,
|
|
||||||
`haex_tombstone` integer
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_passwords_item_history` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`item_id` text,
|
|
||||||
`changed_property` text,
|
|
||||||
`old_value` text,
|
|
||||||
`new_value` text,
|
|
||||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
FOREIGN KEY (`item_id`) REFERENCES `haex_passwords_item_details`(`id`) ON UPDATE no action ON DELETE no action
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `haex_passwords_item_key_values` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`item_id` text,
|
|
||||||
`key` text,
|
|
||||||
`value` text,
|
|
||||||
`updated_at` integer,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
FOREIGN KEY (`item_id`) REFERENCES `haex_passwords_item_details`(`id`) ON UPDATE no action ON DELETE no action
|
|
||||||
);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
ALTER TABLE `haex_notifications` ADD `haex_timestamp` text;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_haex_extensions` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`public_key` text NOT NULL,
|
|
||||||
`name` text NOT NULL,
|
|
||||||
`version` text NOT NULL,
|
|
||||||
`author` text,
|
|
||||||
`description` text,
|
|
||||||
`entry` text DEFAULT 'index.html' NOT NULL,
|
|
||||||
`homepage` text,
|
|
||||||
`enabled` integer DEFAULT true,
|
|
||||||
`icon` text,
|
|
||||||
`signature` text NOT NULL,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
`haex_timestamp` text
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_haex_extensions`("id", "public_key", "name", "version", "author", "description", "entry", "homepage", "enabled", "icon", "signature", "haex_tombstone", "haex_timestamp") SELECT "id", "public_key", "name", "version", "author", "description", "entry", "homepage", "enabled", "icon", "signature", "haex_tombstone", "haex_timestamp" FROM `haex_extensions`;--> statement-breakpoint
|
|
||||||
DROP TABLE `haex_extensions`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_haex_extensions` RENAME TO `haex_extensions`;--> statement-breakpoint
|
|
||||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `haex_extensions_public_key_name_unique` ON `haex_extensions` (`public_key`,`name`);
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE `haex_desktop_items` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`item_type` text NOT NULL,
|
|
||||||
`reference_id` text NOT NULL,
|
|
||||||
`position_x` integer DEFAULT 0 NOT NULL,
|
|
||||||
`position_y` integer DEFAULT 0 NOT NULL,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
`haex_timestamp` text
|
|
||||||
);
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
CREATE TABLE `haex_workspaces` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`name` text NOT NULL,
|
|
||||||
`position` integer DEFAULT 0 NOT NULL,
|
|
||||||
`created_at` integer NOT NULL,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
`haex_timestamp` text
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE `haex_desktop_items` ADD `workspace_id` text NOT NULL REFERENCES haex_workspaces(id);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
CREATE UNIQUE INDEX `haex_workspaces_name_unique` ON `haex_workspaces` (`name`);
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
DROP INDEX `haex_workspaces_name_unique`;--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `haex_workspaces_position_unique` ON `haex_workspaces` (`position`);--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `haex_settings_key_type_value_unique` ON `haex_settings` (`key`,`type`,`value`);
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
DROP INDEX `haex_workspaces_position_unique`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `haex_workspaces` DROP COLUMN `created_at`;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
CREATE UNIQUE INDEX `haex_workspaces_position_unique` ON `haex_workspaces` (`position`);
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_haex_desktop_items` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`workspace_id` text NOT NULL,
|
|
||||||
`item_type` text NOT NULL,
|
|
||||||
`reference_id` text NOT NULL,
|
|
||||||
`position_x` integer DEFAULT 0 NOT NULL,
|
|
||||||
`position_y` integer DEFAULT 0 NOT NULL,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
`haex_timestamp` text,
|
|
||||||
FOREIGN KEY (`workspace_id`) REFERENCES `haex_workspaces`(`id`) ON UPDATE no action ON DELETE cascade
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_haex_desktop_items`("id", "workspace_id", "item_type", "reference_id", "position_x", "position_y", "haex_tombstone", "haex_timestamp") SELECT "id", "workspace_id", "item_type", "reference_id", "position_x", "position_y", "haex_tombstone", "haex_timestamp" FROM `haex_desktop_items`;--> statement-breakpoint
|
|
||||||
DROP TABLE `haex_desktop_items`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_haex_desktop_items` RENAME TO `haex_desktop_items`;--> statement-breakpoint
|
|
||||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_haex_extension_permissions` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`extension_id` text NOT NULL,
|
|
||||||
`resource_type` text,
|
|
||||||
`action` text,
|
|
||||||
`target` text,
|
|
||||||
`constraints` text,
|
|
||||||
`status` text DEFAULT 'denied' NOT NULL,
|
|
||||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
|
|
||||||
`updated_at` integer,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
`haex_timestamp` text,
|
|
||||||
FOREIGN KEY (`extension_id`) REFERENCES `haex_extensions`(`id`) ON UPDATE no action ON DELETE cascade
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_haex_extension_permissions`("id", "extension_id", "resource_type", "action", "target", "constraints", "status", "created_at", "updated_at", "haex_tombstone", "haex_timestamp") SELECT "id", "extension_id", "resource_type", "action", "target", "constraints", "status", "created_at", "updated_at", "haex_tombstone", "haex_timestamp" FROM `haex_extension_permissions`;--> statement-breakpoint
|
|
||||||
DROP TABLE `haex_extension_permissions`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_haex_extension_permissions` RENAME TO `haex_extension_permissions`;--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `haex_extension_permissions_extension_id_resource_type_action_target_unique` ON `haex_extension_permissions` (`extension_id`,`resource_type`,`action`,`target`);--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_haex_passwords_group_items` (
|
|
||||||
`group_id` text NOT NULL,
|
|
||||||
`item_id` text NOT NULL,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
PRIMARY KEY(`item_id`, `group_id`),
|
|
||||||
FOREIGN KEY (`group_id`) REFERENCES `haex_passwords_groups`(`id`) ON UPDATE no action ON DELETE cascade,
|
|
||||||
FOREIGN KEY (`item_id`) REFERENCES `haex_passwords_item_details`(`id`) ON UPDATE no action ON DELETE cascade
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_haex_passwords_group_items`("group_id", "item_id", "haex_tombstone") SELECT "group_id", "item_id", "haex_tombstone" FROM `haex_passwords_group_items`;--> statement-breakpoint
|
|
||||||
DROP TABLE `haex_passwords_group_items`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_haex_passwords_group_items` RENAME TO `haex_passwords_group_items`;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_haex_passwords_groups` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`name` text,
|
|
||||||
`description` text,
|
|
||||||
`icon` text,
|
|
||||||
`order` integer,
|
|
||||||
`color` text,
|
|
||||||
`parent_id` text,
|
|
||||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
|
|
||||||
`updated_at` integer,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
FOREIGN KEY (`parent_id`) REFERENCES `haex_passwords_groups`(`id`) ON UPDATE no action ON DELETE cascade
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_haex_passwords_groups`("id", "name", "description", "icon", "order", "color", "parent_id", "created_at", "updated_at", "haex_tombstone") SELECT "id", "name", "description", "icon", "order", "color", "parent_id", "created_at", "updated_at", "haex_tombstone" FROM `haex_passwords_groups`;--> statement-breakpoint
|
|
||||||
DROP TABLE `haex_passwords_groups`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_haex_passwords_groups` RENAME TO `haex_passwords_groups`;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_haex_passwords_item_history` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`item_id` text NOT NULL,
|
|
||||||
`changed_property` text,
|
|
||||||
`old_value` text,
|
|
||||||
`new_value` text,
|
|
||||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
FOREIGN KEY (`item_id`) REFERENCES `haex_passwords_item_details`(`id`) ON UPDATE no action ON DELETE cascade
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_haex_passwords_item_history`("id", "item_id", "changed_property", "old_value", "new_value", "created_at", "haex_tombstone") SELECT "id", "item_id", "changed_property", "old_value", "new_value", "created_at", "haex_tombstone" FROM `haex_passwords_item_history`;--> statement-breakpoint
|
|
||||||
DROP TABLE `haex_passwords_item_history`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_haex_passwords_item_history` RENAME TO `haex_passwords_item_history`;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_haex_passwords_item_key_values` (
|
|
||||||
`id` text PRIMARY KEY NOT NULL,
|
|
||||||
`item_id` text NOT NULL,
|
|
||||||
`key` text,
|
|
||||||
`value` text,
|
|
||||||
`updated_at` integer,
|
|
||||||
`haex_tombstone` integer,
|
|
||||||
FOREIGN KEY (`item_id`) REFERENCES `haex_passwords_item_details`(`id`) ON UPDATE no action ON DELETE cascade
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_haex_passwords_item_key_values`("id", "item_id", "key", "value", "updated_at", "haex_tombstone") SELECT "id", "item_id", "key", "value", "updated_at", "haex_tombstone" FROM `haex_passwords_item_key_values`;--> statement-breakpoint
|
|
||||||
DROP TABLE `haex_passwords_item_key_values`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_haex_passwords_item_key_values` RENAME TO `haex_passwords_item_key_values`;
|
|
||||||
@ -1,919 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "6",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "3bbe52b8-5933-4b21-8b24-de3927a2f9b0",
|
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
||||||
"tables": {
|
|
||||||
"haex_crdt_configs": {
|
|
||||||
"name": "haex_crdt_configs",
|
|
||||||
"columns": {
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_crdt_logs": {
|
|
||||||
"name": "haex_crdt_logs",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"table_name": {
|
|
||||||
"name": "table_name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"row_pks": {
|
|
||||||
"name": "row_pks",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"op_type": {
|
|
||||||
"name": "op_type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"column_name": {
|
|
||||||
"name": "column_name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"new_value": {
|
|
||||||
"name": "new_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"old_value": {
|
|
||||||
"name": "old_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"idx_haex_timestamp": {
|
|
||||||
"name": "idx_haex_timestamp",
|
|
||||||
"columns": [
|
|
||||||
"haex_timestamp"
|
|
||||||
],
|
|
||||||
"isUnique": false
|
|
||||||
},
|
|
||||||
"idx_table_row": {
|
|
||||||
"name": "idx_table_row",
|
|
||||||
"columns": [
|
|
||||||
"table_name",
|
|
||||||
"row_pks"
|
|
||||||
],
|
|
||||||
"isUnique": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_crdt_snapshots": {
|
|
||||||
"name": "haex_crdt_snapshots",
|
|
||||||
"columns": {
|
|
||||||
"snapshot_id": {
|
|
||||||
"name": "snapshot_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"name": "created",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"epoch_hlc": {
|
|
||||||
"name": "epoch_hlc",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"location_url": {
|
|
||||||
"name": "location_url",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"file_size_bytes": {
|
|
||||||
"name": "file_size_bytes",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_extension_permissions": {
|
|
||||||
"name": "haex_extension_permissions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"extension_id": {
|
|
||||||
"name": "extension_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"resource_type": {
|
|
||||||
"name": "resource_type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"name": "action",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"target": {
|
|
||||||
"name": "target",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"constraints": {
|
|
||||||
"name": "constraints",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"name": "status",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'denied'"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"haex_extension_permissions_extension_id_resource_type_action_target_unique": {
|
|
||||||
"name": "haex_extension_permissions_extension_id_resource_type_action_target_unique",
|
|
||||||
"columns": [
|
|
||||||
"extension_id",
|
|
||||||
"resource_type",
|
|
||||||
"action",
|
|
||||||
"target"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_extension_permissions_extension_id_haex_extensions_id_fk": {
|
|
||||||
"name": "haex_extension_permissions_extension_id_haex_extensions_id_fk",
|
|
||||||
"tableFrom": "haex_extension_permissions",
|
|
||||||
"tableTo": "haex_extensions",
|
|
||||||
"columnsFrom": [
|
|
||||||
"extension_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_extensions": {
|
|
||||||
"name": "haex_extensions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "author",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"entry": {
|
|
||||||
"name": "entry",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"homepage": {
|
|
||||||
"name": "homepage",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"name": "enabled",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"public_key": {
|
|
||||||
"name": "public_key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"signature": {
|
|
||||||
"name": "signature",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"name": "url",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"version": {
|
|
||||||
"name": "version",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_settings": {
|
|
||||||
"name": "haex_settings",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_notifications": {
|
|
||||||
"name": "haex_notifications",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"alt": {
|
|
||||||
"name": "alt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"date": {
|
|
||||||
"name": "date",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"name": "image",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"read": {
|
|
||||||
"name": "read",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"name": "source",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"name": "text",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_group_items": {
|
|
||||||
"name": "haex_passwords_group_items",
|
|
||||||
"columns": {
|
|
||||||
"group_id": {
|
|
||||||
"name": "group_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_group_items_group_id_haex_passwords_groups_id_fk": {
|
|
||||||
"name": "haex_passwords_group_items_group_id_haex_passwords_groups_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_group_items",
|
|
||||||
"tableTo": "haex_passwords_groups",
|
|
||||||
"columnsFrom": [
|
|
||||||
"group_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_group_items",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"haex_passwords_group_items_item_id_group_id_pk": {
|
|
||||||
"columns": [
|
|
||||||
"item_id",
|
|
||||||
"group_id"
|
|
||||||
],
|
|
||||||
"name": "haex_passwords_group_items_item_id_group_id_pk"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_groups": {
|
|
||||||
"name": "haex_passwords_groups",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"order": {
|
|
||||||
"name": "order",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"parent_id": {
|
|
||||||
"name": "parent_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_groups_parent_id_haex_passwords_groups_id_fk": {
|
|
||||||
"name": "haex_passwords_groups_parent_id_haex_passwords_groups_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_groups",
|
|
||||||
"tableTo": "haex_passwords_groups",
|
|
||||||
"columnsFrom": [
|
|
||||||
"parent_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_details": {
|
|
||||||
"name": "haex_passwords_item_details",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"name": "username",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"name": "password",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"name": "note",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"name": "tags",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"name": "url",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_history": {
|
|
||||||
"name": "haex_passwords_item_history",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"changed_property": {
|
|
||||||
"name": "changed_property",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"old_value": {
|
|
||||||
"name": "old_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"new_value": {
|
|
||||||
"name": "new_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_item_history",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_key_values": {
|
|
||||||
"name": "haex_passwords_item_key_values",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_item_key_values",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"enums": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,926 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "6",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "862ac1d5-3065-4244-8652-2b6782254862",
|
|
||||||
"prevId": "3bbe52b8-5933-4b21-8b24-de3927a2f9b0",
|
|
||||||
"tables": {
|
|
||||||
"haex_crdt_configs": {
|
|
||||||
"name": "haex_crdt_configs",
|
|
||||||
"columns": {
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_crdt_logs": {
|
|
||||||
"name": "haex_crdt_logs",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"table_name": {
|
|
||||||
"name": "table_name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"row_pks": {
|
|
||||||
"name": "row_pks",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"op_type": {
|
|
||||||
"name": "op_type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"column_name": {
|
|
||||||
"name": "column_name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"new_value": {
|
|
||||||
"name": "new_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"old_value": {
|
|
||||||
"name": "old_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"idx_haex_timestamp": {
|
|
||||||
"name": "idx_haex_timestamp",
|
|
||||||
"columns": [
|
|
||||||
"haex_timestamp"
|
|
||||||
],
|
|
||||||
"isUnique": false
|
|
||||||
},
|
|
||||||
"idx_table_row": {
|
|
||||||
"name": "idx_table_row",
|
|
||||||
"columns": [
|
|
||||||
"table_name",
|
|
||||||
"row_pks"
|
|
||||||
],
|
|
||||||
"isUnique": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_crdt_snapshots": {
|
|
||||||
"name": "haex_crdt_snapshots",
|
|
||||||
"columns": {
|
|
||||||
"snapshot_id": {
|
|
||||||
"name": "snapshot_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"name": "created",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"epoch_hlc": {
|
|
||||||
"name": "epoch_hlc",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"location_url": {
|
|
||||||
"name": "location_url",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"file_size_bytes": {
|
|
||||||
"name": "file_size_bytes",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_extension_permissions": {
|
|
||||||
"name": "haex_extension_permissions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"extension_id": {
|
|
||||||
"name": "extension_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"resource_type": {
|
|
||||||
"name": "resource_type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"name": "action",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"target": {
|
|
||||||
"name": "target",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"constraints": {
|
|
||||||
"name": "constraints",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"name": "status",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'denied'"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"haex_extension_permissions_extension_id_resource_type_action_target_unique": {
|
|
||||||
"name": "haex_extension_permissions_extension_id_resource_type_action_target_unique",
|
|
||||||
"columns": [
|
|
||||||
"extension_id",
|
|
||||||
"resource_type",
|
|
||||||
"action",
|
|
||||||
"target"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_extension_permissions_extension_id_haex_extensions_id_fk": {
|
|
||||||
"name": "haex_extension_permissions_extension_id_haex_extensions_id_fk",
|
|
||||||
"tableFrom": "haex_extension_permissions",
|
|
||||||
"tableTo": "haex_extensions",
|
|
||||||
"columnsFrom": [
|
|
||||||
"extension_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_extensions": {
|
|
||||||
"name": "haex_extensions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "author",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"entry": {
|
|
||||||
"name": "entry",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"homepage": {
|
|
||||||
"name": "homepage",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"name": "enabled",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"public_key": {
|
|
||||||
"name": "public_key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"signature": {
|
|
||||||
"name": "signature",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"name": "url",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"version": {
|
|
||||||
"name": "version",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_notifications": {
|
|
||||||
"name": "haex_notifications",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"alt": {
|
|
||||||
"name": "alt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"date": {
|
|
||||||
"name": "date",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"name": "image",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"read": {
|
|
||||||
"name": "read",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"name": "source",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"name": "text",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_settings": {
|
|
||||||
"name": "haex_settings",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_group_items": {
|
|
||||||
"name": "haex_passwords_group_items",
|
|
||||||
"columns": {
|
|
||||||
"group_id": {
|
|
||||||
"name": "group_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_group_items_group_id_haex_passwords_groups_id_fk": {
|
|
||||||
"name": "haex_passwords_group_items_group_id_haex_passwords_groups_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_group_items",
|
|
||||||
"tableTo": "haex_passwords_groups",
|
|
||||||
"columnsFrom": [
|
|
||||||
"group_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_group_items",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"haex_passwords_group_items_item_id_group_id_pk": {
|
|
||||||
"columns": [
|
|
||||||
"item_id",
|
|
||||||
"group_id"
|
|
||||||
],
|
|
||||||
"name": "haex_passwords_group_items_item_id_group_id_pk"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_groups": {
|
|
||||||
"name": "haex_passwords_groups",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"order": {
|
|
||||||
"name": "order",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"parent_id": {
|
|
||||||
"name": "parent_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_groups_parent_id_haex_passwords_groups_id_fk": {
|
|
||||||
"name": "haex_passwords_groups_parent_id_haex_passwords_groups_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_groups",
|
|
||||||
"tableTo": "haex_passwords_groups",
|
|
||||||
"columnsFrom": [
|
|
||||||
"parent_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_details": {
|
|
||||||
"name": "haex_passwords_item_details",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"name": "username",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"name": "password",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"name": "note",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"name": "tags",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"name": "url",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_history": {
|
|
||||||
"name": "haex_passwords_item_history",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"changed_property": {
|
|
||||||
"name": "changed_property",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"old_value": {
|
|
||||||
"name": "old_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"new_value": {
|
|
||||||
"name": "new_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_item_history",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_key_values": {
|
|
||||||
"name": "haex_passwords_item_key_values",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_item_key_values",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"enums": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,930 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "6",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "5387568f-75b3-4a85-86c5-67f539c3fedf",
|
|
||||||
"prevId": "862ac1d5-3065-4244-8652-2b6782254862",
|
|
||||||
"tables": {
|
|
||||||
"haex_crdt_configs": {
|
|
||||||
"name": "haex_crdt_configs",
|
|
||||||
"columns": {
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_crdt_logs": {
|
|
||||||
"name": "haex_crdt_logs",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"table_name": {
|
|
||||||
"name": "table_name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"row_pks": {
|
|
||||||
"name": "row_pks",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"op_type": {
|
|
||||||
"name": "op_type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"column_name": {
|
|
||||||
"name": "column_name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"new_value": {
|
|
||||||
"name": "new_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"old_value": {
|
|
||||||
"name": "old_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"idx_haex_timestamp": {
|
|
||||||
"name": "idx_haex_timestamp",
|
|
||||||
"columns": [
|
|
||||||
"haex_timestamp"
|
|
||||||
],
|
|
||||||
"isUnique": false
|
|
||||||
},
|
|
||||||
"idx_table_row": {
|
|
||||||
"name": "idx_table_row",
|
|
||||||
"columns": [
|
|
||||||
"table_name",
|
|
||||||
"row_pks"
|
|
||||||
],
|
|
||||||
"isUnique": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_crdt_snapshots": {
|
|
||||||
"name": "haex_crdt_snapshots",
|
|
||||||
"columns": {
|
|
||||||
"snapshot_id": {
|
|
||||||
"name": "snapshot_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"name": "created",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"epoch_hlc": {
|
|
||||||
"name": "epoch_hlc",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"location_url": {
|
|
||||||
"name": "location_url",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"file_size_bytes": {
|
|
||||||
"name": "file_size_bytes",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_extension_permissions": {
|
|
||||||
"name": "haex_extension_permissions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"extension_id": {
|
|
||||||
"name": "extension_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"resource_type": {
|
|
||||||
"name": "resource_type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"name": "action",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"target": {
|
|
||||||
"name": "target",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"constraints": {
|
|
||||||
"name": "constraints",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"name": "status",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'denied'"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"haex_extension_permissions_extension_id_resource_type_action_target_unique": {
|
|
||||||
"name": "haex_extension_permissions_extension_id_resource_type_action_target_unique",
|
|
||||||
"columns": [
|
|
||||||
"extension_id",
|
|
||||||
"resource_type",
|
|
||||||
"action",
|
|
||||||
"target"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_extension_permissions_extension_id_haex_extensions_id_fk": {
|
|
||||||
"name": "haex_extension_permissions_extension_id_haex_extensions_id_fk",
|
|
||||||
"tableFrom": "haex_extension_permissions",
|
|
||||||
"tableTo": "haex_extensions",
|
|
||||||
"columnsFrom": [
|
|
||||||
"extension_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_extensions": {
|
|
||||||
"name": "haex_extensions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"public_key": {
|
|
||||||
"name": "public_key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"version": {
|
|
||||||
"name": "version",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "author",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"entry": {
|
|
||||||
"name": "entry",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'index.html'"
|
|
||||||
},
|
|
||||||
"homepage": {
|
|
||||||
"name": "homepage",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"name": "enabled",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"signature": {
|
|
||||||
"name": "signature",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"haex_extensions_public_key_name_unique": {
|
|
||||||
"name": "haex_extensions_public_key_name_unique",
|
|
||||||
"columns": [
|
|
||||||
"public_key",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_notifications": {
|
|
||||||
"name": "haex_notifications",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"alt": {
|
|
||||||
"name": "alt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"date": {
|
|
||||||
"name": "date",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"name": "image",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"read": {
|
|
||||||
"name": "read",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"name": "source",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"name": "text",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_settings": {
|
|
||||||
"name": "haex_settings",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_group_items": {
|
|
||||||
"name": "haex_passwords_group_items",
|
|
||||||
"columns": {
|
|
||||||
"group_id": {
|
|
||||||
"name": "group_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_group_items_group_id_haex_passwords_groups_id_fk": {
|
|
||||||
"name": "haex_passwords_group_items_group_id_haex_passwords_groups_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_group_items",
|
|
||||||
"tableTo": "haex_passwords_groups",
|
|
||||||
"columnsFrom": [
|
|
||||||
"group_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_group_items",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"haex_passwords_group_items_item_id_group_id_pk": {
|
|
||||||
"columns": [
|
|
||||||
"item_id",
|
|
||||||
"group_id"
|
|
||||||
],
|
|
||||||
"name": "haex_passwords_group_items_item_id_group_id_pk"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_groups": {
|
|
||||||
"name": "haex_passwords_groups",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"order": {
|
|
||||||
"name": "order",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"parent_id": {
|
|
||||||
"name": "parent_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_groups_parent_id_haex_passwords_groups_id_fk": {
|
|
||||||
"name": "haex_passwords_groups_parent_id_haex_passwords_groups_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_groups",
|
|
||||||
"tableTo": "haex_passwords_groups",
|
|
||||||
"columnsFrom": [
|
|
||||||
"parent_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_details": {
|
|
||||||
"name": "haex_passwords_item_details",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"name": "username",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"name": "password",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"name": "note",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"name": "tags",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"name": "url",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_history": {
|
|
||||||
"name": "haex_passwords_item_history",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"changed_property": {
|
|
||||||
"name": "changed_property",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"old_value": {
|
|
||||||
"name": "old_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"new_value": {
|
|
||||||
"name": "new_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_item_history",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_key_values": {
|
|
||||||
"name": "haex_passwords_item_key_values",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_item_key_values",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"enums": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,991 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "6",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "2f40a42e-9b3f-42be-8951-8e94baadcd65",
|
|
||||||
"prevId": "5387568f-75b3-4a85-86c5-67f539c3fedf",
|
|
||||||
"tables": {
|
|
||||||
"haex_crdt_configs": {
|
|
||||||
"name": "haex_crdt_configs",
|
|
||||||
"columns": {
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_crdt_logs": {
|
|
||||||
"name": "haex_crdt_logs",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"table_name": {
|
|
||||||
"name": "table_name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"row_pks": {
|
|
||||||
"name": "row_pks",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"op_type": {
|
|
||||||
"name": "op_type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"column_name": {
|
|
||||||
"name": "column_name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"new_value": {
|
|
||||||
"name": "new_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"old_value": {
|
|
||||||
"name": "old_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"idx_haex_timestamp": {
|
|
||||||
"name": "idx_haex_timestamp",
|
|
||||||
"columns": [
|
|
||||||
"haex_timestamp"
|
|
||||||
],
|
|
||||||
"isUnique": false
|
|
||||||
},
|
|
||||||
"idx_table_row": {
|
|
||||||
"name": "idx_table_row",
|
|
||||||
"columns": [
|
|
||||||
"table_name",
|
|
||||||
"row_pks"
|
|
||||||
],
|
|
||||||
"isUnique": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_crdt_snapshots": {
|
|
||||||
"name": "haex_crdt_snapshots",
|
|
||||||
"columns": {
|
|
||||||
"snapshot_id": {
|
|
||||||
"name": "snapshot_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"name": "created",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"epoch_hlc": {
|
|
||||||
"name": "epoch_hlc",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"location_url": {
|
|
||||||
"name": "location_url",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"file_size_bytes": {
|
|
||||||
"name": "file_size_bytes",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_desktop_items": {
|
|
||||||
"name": "haex_desktop_items",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_type": {
|
|
||||||
"name": "item_type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"reference_id": {
|
|
||||||
"name": "reference_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"position_x": {
|
|
||||||
"name": "position_x",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"position_y": {
|
|
||||||
"name": "position_y",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_extension_permissions": {
|
|
||||||
"name": "haex_extension_permissions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"extension_id": {
|
|
||||||
"name": "extension_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"resource_type": {
|
|
||||||
"name": "resource_type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"name": "action",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"target": {
|
|
||||||
"name": "target",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"constraints": {
|
|
||||||
"name": "constraints",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"name": "status",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'denied'"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"haex_extension_permissions_extension_id_resource_type_action_target_unique": {
|
|
||||||
"name": "haex_extension_permissions_extension_id_resource_type_action_target_unique",
|
|
||||||
"columns": [
|
|
||||||
"extension_id",
|
|
||||||
"resource_type",
|
|
||||||
"action",
|
|
||||||
"target"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_extension_permissions_extension_id_haex_extensions_id_fk": {
|
|
||||||
"name": "haex_extension_permissions_extension_id_haex_extensions_id_fk",
|
|
||||||
"tableFrom": "haex_extension_permissions",
|
|
||||||
"tableTo": "haex_extensions",
|
|
||||||
"columnsFrom": [
|
|
||||||
"extension_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_extensions": {
|
|
||||||
"name": "haex_extensions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"public_key": {
|
|
||||||
"name": "public_key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"version": {
|
|
||||||
"name": "version",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "author",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"entry": {
|
|
||||||
"name": "entry",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'index.html'"
|
|
||||||
},
|
|
||||||
"homepage": {
|
|
||||||
"name": "homepage",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"name": "enabled",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"signature": {
|
|
||||||
"name": "signature",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"haex_extensions_public_key_name_unique": {
|
|
||||||
"name": "haex_extensions_public_key_name_unique",
|
|
||||||
"columns": [
|
|
||||||
"public_key",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_notifications": {
|
|
||||||
"name": "haex_notifications",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"alt": {
|
|
||||||
"name": "alt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"date": {
|
|
||||||
"name": "date",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"name": "image",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"read": {
|
|
||||||
"name": "read",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"source": {
|
|
||||||
"name": "source",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"name": "text",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_settings": {
|
|
||||||
"name": "haex_settings",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_timestamp": {
|
|
||||||
"name": "haex_timestamp",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_group_items": {
|
|
||||||
"name": "haex_passwords_group_items",
|
|
||||||
"columns": {
|
|
||||||
"group_id": {
|
|
||||||
"name": "group_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_group_items_group_id_haex_passwords_groups_id_fk": {
|
|
||||||
"name": "haex_passwords_group_items_group_id_haex_passwords_groups_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_group_items",
|
|
||||||
"tableTo": "haex_passwords_groups",
|
|
||||||
"columnsFrom": [
|
|
||||||
"group_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_group_items",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"haex_passwords_group_items_item_id_group_id_pk": {
|
|
||||||
"columns": [
|
|
||||||
"item_id",
|
|
||||||
"group_id"
|
|
||||||
],
|
|
||||||
"name": "haex_passwords_group_items_item_id_group_id_pk"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_groups": {
|
|
||||||
"name": "haex_passwords_groups",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"order": {
|
|
||||||
"name": "order",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"color": {
|
|
||||||
"name": "color",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"parent_id": {
|
|
||||||
"name": "parent_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_groups_parent_id_haex_passwords_groups_id_fk": {
|
|
||||||
"name": "haex_passwords_groups_parent_id_haex_passwords_groups_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_groups",
|
|
||||||
"tableTo": "haex_passwords_groups",
|
|
||||||
"columnsFrom": [
|
|
||||||
"parent_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_details": {
|
|
||||||
"name": "haex_passwords_item_details",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"name": "username",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"name": "password",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"note": {
|
|
||||||
"name": "note",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"name": "icon",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"name": "tags",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"name": "url",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_history": {
|
|
||||||
"name": "haex_passwords_item_history",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"changed_property": {
|
|
||||||
"name": "changed_property",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"old_value": {
|
|
||||||
"name": "old_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"new_value": {
|
|
||||||
"name": "new_value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(CURRENT_TIMESTAMP)"
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_item_history",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
},
|
|
||||||
"haex_passwords_item_key_values": {
|
|
||||||
"name": "haex_passwords_item_key_values",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"item_id": {
|
|
||||||
"name": "item_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"haex_tombstone": {
|
|
||||||
"name": "haex_tombstone",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk": {
|
|
||||||
"name": "haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk",
|
|
||||||
"tableFrom": "haex_passwords_item_key_values",
|
|
||||||
"tableTo": "haex_passwords_item_details",
|
|
||||||
"columnsFrom": [
|
|
||||||
"item_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"enums": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,76 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1759402321133,
|
|
||||||
"tag": "0000_glamorous_hulk",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1759418087677,
|
|
||||||
"tag": "0001_green_stark_industries",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 2,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1760272083150,
|
|
||||||
"tag": "0002_amazing_iron_fist",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 3,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1760611690801,
|
|
||||||
"tag": "0003_daily_polaris",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 4,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1760817142340,
|
|
||||||
"tag": "0004_mature_viper",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 5,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1760964548034,
|
|
||||||
"tag": "0005_tidy_yellowjacket",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 6,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1761137108127,
|
|
||||||
"tag": "0006_gigantic_bloodaxe",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 7,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1761141111765,
|
|
||||||
"tag": "0007_stale_longshot",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 8,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1761145177028,
|
|
||||||
"tag": "0008_dizzy_blue_shield",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 9,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1761203548348,
|
|
||||||
"tag": "0009_boring_arclight",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
import { sql } from 'drizzle-orm'
|
|
||||||
import {
|
|
||||||
integer,
|
|
||||||
primaryKey,
|
|
||||||
sqliteTable,
|
|
||||||
text,
|
|
||||||
type AnySQLiteColumn,
|
|
||||||
} from 'drizzle-orm/sqlite-core'
|
|
||||||
import tableNames from '../tableNames.json'
|
|
||||||
|
|
||||||
export const haexPasswordsItemDetails = sqliteTable(
|
|
||||||
tableNames.haex.passwords.item_details,
|
|
||||||
{
|
|
||||||
id: text().primaryKey(),
|
|
||||||
title: text(),
|
|
||||||
username: text(),
|
|
||||||
password: text(),
|
|
||||||
note: text(),
|
|
||||||
icon: text(),
|
|
||||||
tags: text(),
|
|
||||||
url: text(),
|
|
||||||
createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`),
|
|
||||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
|
||||||
() => new Date(),
|
|
||||||
),
|
|
||||||
haex_tombstone: integer({ mode: 'boolean' }),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
export type InsertHaexPasswordsItemDetails =
|
|
||||||
typeof haexPasswordsItemDetails.$inferInsert
|
|
||||||
export type SelectHaexPasswordsItemDetails =
|
|
||||||
typeof haexPasswordsItemDetails.$inferSelect
|
|
||||||
|
|
||||||
export const haexPasswordsItemKeyValues = sqliteTable(
|
|
||||||
tableNames.haex.passwords.item_key_values,
|
|
||||||
{
|
|
||||||
id: text().primaryKey(),
|
|
||||||
itemId: text('item_id')
|
|
||||||
.notNull()
|
|
||||||
.references((): AnySQLiteColumn => haexPasswordsItemDetails.id, {
|
|
||||||
onDelete: 'cascade',
|
|
||||||
}),
|
|
||||||
key: text(),
|
|
||||||
value: text(),
|
|
||||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
|
||||||
() => new Date(),
|
|
||||||
),
|
|
||||||
haex_tombstone: integer({ mode: 'boolean' }),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
export type InserthaexPasswordsItemKeyValues =
|
|
||||||
typeof haexPasswordsItemKeyValues.$inferInsert
|
|
||||||
export type SelectHaexPasswordsItemKeyValues =
|
|
||||||
typeof haexPasswordsItemKeyValues.$inferSelect
|
|
||||||
|
|
||||||
export const haexPasswordsItemHistory = sqliteTable(
|
|
||||||
tableNames.haex.passwords.item_histories,
|
|
||||||
{
|
|
||||||
id: text().primaryKey(),
|
|
||||||
itemId: text('item_id')
|
|
||||||
.notNull()
|
|
||||||
.references((): AnySQLiteColumn => haexPasswordsItemDetails.id, {
|
|
||||||
onDelete: 'cascade',
|
|
||||||
}),
|
|
||||||
changedProperty:
|
|
||||||
text('changed_property').$type<keyof typeof haexPasswordsItemDetails>(),
|
|
||||||
oldValue: text('old_value'),
|
|
||||||
newValue: text('new_value'),
|
|
||||||
createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`),
|
|
||||||
haex_tombstone: integer({ mode: 'boolean' }),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
export type InserthaexPasswordsItemHistory =
|
|
||||||
typeof haexPasswordsItemHistory.$inferInsert
|
|
||||||
export type SelectHaexPasswordsItemHistory =
|
|
||||||
typeof haexPasswordsItemHistory.$inferSelect
|
|
||||||
|
|
||||||
export const haexPasswordsGroups = sqliteTable(
|
|
||||||
tableNames.haex.passwords.groups,
|
|
||||||
{
|
|
||||||
id: text().primaryKey(),
|
|
||||||
name: text(),
|
|
||||||
description: text(),
|
|
||||||
icon: text(),
|
|
||||||
order: integer(),
|
|
||||||
color: text(),
|
|
||||||
parentId: text('parent_id').references(
|
|
||||||
(): AnySQLiteColumn => haexPasswordsGroups.id,
|
|
||||||
{ onDelete: 'cascade' },
|
|
||||||
),
|
|
||||||
createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`),
|
|
||||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
|
||||||
() => new Date(),
|
|
||||||
),
|
|
||||||
haex_tombstone: integer({ mode: 'boolean' }),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
export type InsertHaexPasswordsGroups = typeof haexPasswordsGroups.$inferInsert
|
|
||||||
export type SelectHaexPasswordsGroups = typeof haexPasswordsGroups.$inferSelect
|
|
||||||
|
|
||||||
export const haexPasswordsGroupItems = sqliteTable(
|
|
||||||
tableNames.haex.passwords.group_items,
|
|
||||||
{
|
|
||||||
groupId: text('group_id')
|
|
||||||
.notNull()
|
|
||||||
.references((): AnySQLiteColumn => haexPasswordsGroups.id, {
|
|
||||||
onDelete: 'cascade',
|
|
||||||
}),
|
|
||||||
itemId: text('item_id')
|
|
||||||
.notNull()
|
|
||||||
.references((): AnySQLiteColumn => haexPasswordsItemDetails.id, {
|
|
||||||
onDelete: 'cascade',
|
|
||||||
}),
|
|
||||||
haex_tombstone: integer({ mode: 'boolean' }),
|
|
||||||
},
|
|
||||||
(table) => [primaryKey({ columns: [table.itemId, table.groupId] })],
|
|
||||||
)
|
|
||||||
export type InsertHaexPasswordsGroupItems =
|
|
||||||
typeof haexPasswordsGroupItems.$inferInsert
|
|
||||||
export type SelectHaexPasswordsGroupItems =
|
|
||||||
typeof haexPasswordsGroupItems.$inferSelect
|
|
||||||
Binary file not shown.
@ -24,6 +24,23 @@ android {
|
|||||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
val keystorePath = System.getenv("ANDROID_KEYSTORE_PATH")
|
||||||
|
val keystorePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD")
|
||||||
|
val keyAlias = System.getenv("ANDROID_KEY_ALIAS")
|
||||||
|
val keyPassword = System.getenv("ANDROID_KEY_PASSWORD")
|
||||||
|
|
||||||
|
if (keystorePath != null && keystorePassword != null && keyAlias != null && keyPassword != null) {
|
||||||
|
storeFile = file(keystorePath)
|
||||||
|
storePassword = keystorePassword
|
||||||
|
this.keyAlias = keyAlias
|
||||||
|
this.keyPassword = keyPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
getByName("debug") {
|
getByName("debug") {
|
||||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||||
@ -43,6 +60,12 @@ android {
|
|||||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
.toList().toTypedArray()
|
.toList().toTypedArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Sign with release config if available
|
||||||
|
val releaseSigningConfig = signingConfigs.getByName("release")
|
||||||
|
if (releaseSigningConfig.storeFile != null) {
|
||||||
|
signingConfig = releaseSigningConfig
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -1400,10 +1400,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "An empty permission you can use to modify the global scope.",
|
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"const": "fs:scope",
|
||||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||||
@ -2277,10 +2277,10 @@
|
|||||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"const": "core:app:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@ -2324,12 +2324,24 @@
|
|||||||
"const": "core:app:allow-name",
|
"const": "core:app:allow-name",
|
||||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
"markdownDescription": "Enables the name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-register-listener",
|
||||||
|
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:allow-remove-data-store",
|
"const": "core:app:allow-remove-data-store",
|
||||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-remove-listener",
|
||||||
|
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2396,12 +2408,24 @@
|
|||||||
"const": "core:app:deny-name",
|
"const": "core:app:deny-name",
|
||||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
"markdownDescription": "Denies the name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-register-listener",
|
||||||
|
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:deny-remove-data-store",
|
"const": "core:app:deny-remove-data-store",
|
||||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-remove-listener",
|
||||||
|
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -5541,10 +5565,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "An empty permission you can use to modify the global scope.",
|
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"const": "fs:scope",
|
||||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-read-file","fs:allow-read-dir","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:default","opener:allow-open-url","opener:default","os:allow-hostname","os:default","store:default"]}}
|
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","core:webview:allow-create-webview-window","core:webview:allow-create-webview","core:webview:allow-webview-show","core:webview:default","core:window:allow-create","core:window:allow-get-all-windows","core:window:allow-show","core:window:default","dialog:default","fs:allow-appconfig-read-recursive","fs:allow-appconfig-write-recursive","fs:allow-appdata-read-recursive","fs:allow-appdata-write-recursive","fs:allow-read-file","fs:allow-read-dir","fs:allow-resource-read-recursive","fs:allow-resource-write-recursive","fs:allow-download-read-recursive","fs:allow-download-write-recursive","fs:default",{"identifier":"fs:scope","allow":[{"path":"**"}]},"http:allow-fetch-send","http:allow-fetch","http:default","notification:allow-create-channel","notification:allow-list-channels","notification:allow-notify","notification:allow-is-permission-granted","notification:default","opener:allow-open-url","opener:default","os:allow-hostname","os:default","store:default"]}}
|
||||||
@ -1400,10 +1400,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "An empty permission you can use to modify the global scope.",
|
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"const": "fs:scope",
|
||||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||||
@ -2277,10 +2277,10 @@
|
|||||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"const": "core:app:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@ -2324,12 +2324,24 @@
|
|||||||
"const": "core:app:allow-name",
|
"const": "core:app:allow-name",
|
||||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
"markdownDescription": "Enables the name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-register-listener",
|
||||||
|
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:allow-remove-data-store",
|
"const": "core:app:allow-remove-data-store",
|
||||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-remove-listener",
|
||||||
|
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2396,12 +2408,24 @@
|
|||||||
"const": "core:app:deny-name",
|
"const": "core:app:deny-name",
|
||||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
"markdownDescription": "Denies the name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-register-listener",
|
||||||
|
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:deny-remove-data-store",
|
"const": "core:app:deny-remove-data-store",
|
||||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-remove-listener",
|
||||||
|
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -5541,10 +5565,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "An empty permission you can use to modify the global scope.",
|
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"const": "fs:scope",
|
||||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||||
|
|||||||
@ -1400,10 +1400,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "An empty permission you can use to modify the global scope.",
|
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"const": "fs:scope",
|
||||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||||
@ -2277,10 +2277,10 @@
|
|||||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"const": "core:app:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@ -2324,12 +2324,24 @@
|
|||||||
"const": "core:app:allow-name",
|
"const": "core:app:allow-name",
|
||||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
"markdownDescription": "Enables the name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-register-listener",
|
||||||
|
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:allow-remove-data-store",
|
"const": "core:app:allow-remove-data-store",
|
||||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-remove-listener",
|
||||||
|
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2396,12 +2408,24 @@
|
|||||||
"const": "core:app:deny-name",
|
"const": "core:app:deny-name",
|
||||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
"markdownDescription": "Denies the name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-register-listener",
|
||||||
|
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:deny-remove-data-store",
|
"const": "core:app:deny-remove-data-store",
|
||||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-remove-listener",
|
||||||
|
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -5541,10 +5565,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "An empty permission you can use to modify the global scope.",
|
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"const": "fs:scope",
|
||||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||||
|
|||||||
@ -1400,10 +1400,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "An empty permission you can use to modify the global scope.",
|
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"const": "fs:scope",
|
||||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||||
@ -2277,10 +2277,10 @@
|
|||||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"const": "core:app:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@ -2324,12 +2324,24 @@
|
|||||||
"const": "core:app:allow-name",
|
"const": "core:app:allow-name",
|
||||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
"markdownDescription": "Enables the name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-register-listener",
|
||||||
|
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:allow-remove-data-store",
|
"const": "core:app:allow-remove-data-store",
|
||||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-remove-listener",
|
||||||
|
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2396,12 +2408,24 @@
|
|||||||
"const": "core:app:deny-name",
|
"const": "core:app:deny-name",
|
||||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
"markdownDescription": "Denies the name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-register-listener",
|
||||||
|
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:deny-remove-data-store",
|
"const": "core:app:deny-remove-data-store",
|
||||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-remove-listener",
|
||||||
|
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -5541,10 +5565,10 @@
|
|||||||
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
"markdownDescription": "This enables all index or metadata related commands without any pre-configured accessible paths."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "An empty permission you can use to modify the global scope.",
|
"description": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "fs:scope",
|
"const": "fs:scope",
|
||||||
"markdownDescription": "An empty permission you can use to modify the global scope."
|
"markdownDescription": "An empty permission you can use to modify the global scope.\n\n## Example\n\n```json\n{\n \"identifier\": \"read-documents\",\n \"windows\": [\"main\"],\n \"permissions\": [\n \"fs:allow-read\",\n {\n \"identifier\": \"fs:scope\",\n \"allow\": [\n \"$APPDATA/documents/**/*\"\n ],\n \"deny\": [\n \"$APPDATA/documents/secret.txt\"\n ]\n }\n ]\n}\n```\n"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
"description": "This scope permits access to all files and list content of top level directories in the application folders.",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// src-tarui/src/build/table_names.rs
|
// src-tarui/src/build/table_names.rs
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@ -8,25 +9,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct Schema {
|
struct Schema {
|
||||||
haex: Haex,
|
haex: HashMap<String, Value>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
struct Haex {
|
|
||||||
settings: TableDefinition,
|
|
||||||
extensions: TableDefinition,
|
|
||||||
extension_permissions: TableDefinition,
|
|
||||||
notifications: TableDefinition,
|
|
||||||
desktop_items: TableDefinition,
|
|
||||||
crdt: Crdt,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct Crdt {
|
|
||||||
logs: TableDefinition,
|
|
||||||
snapshots: TableDefinition,
|
|
||||||
configs: TableDefinition,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@ -38,196 +21,98 @@ struct TableDefinition {
|
|||||||
pub fn generate_table_names() {
|
pub fn generate_table_names() {
|
||||||
let out_dir = env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt.");
|
let out_dir = env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt.");
|
||||||
println!("Generiere Tabellennamen nach {}", out_dir);
|
println!("Generiere Tabellennamen nach {}", out_dir);
|
||||||
let schema_path = Path::new("database/tableNames.json");
|
let schema_path = Path::new("../src/database/tableNames.json");
|
||||||
let dest_path = Path::new(&out_dir).join("tableNames.rs");
|
let dest_path = Path::new(&out_dir).join("tableNames.rs");
|
||||||
|
|
||||||
let file = File::open(&schema_path).expect("Konnte tableNames.json nicht öffnen");
|
let file = File::open(&schema_path).expect("Konnte tableNames.json nicht öffnen");
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
let schema: Schema =
|
let schema: Schema =
|
||||||
serde_json::from_reader(reader).expect("Konnte tableNames.json nicht parsen");
|
serde_json::from_reader(reader).expect("Konnte tableNames.json nicht parsen");
|
||||||
let haex = schema.haex;
|
|
||||||
|
|
||||||
let code = format!(
|
let mut code = String::from(
|
||||||
r#"
|
r#"
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
// HINWEIS: Diese Datei wurde automatisch von build.rs generiert.
|
// HINWEIS: Diese Datei wurde automatisch von build.rs generiert.
|
||||||
// Manuelle Änderungen werden bei der nächsten Kompilierung überschrieben!
|
// Manuelle Änderungen werden bei der nächsten Kompilierung überschrieben!
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
|
|
||||||
// --- Table: haex_settings ---
|
|
||||||
pub const TABLE_SETTINGS: &str = "{t_settings}";
|
|
||||||
pub const COL_SETTINGS_ID: &str = "{c_settings_id}";
|
|
||||||
pub const COL_SETTINGS_KEY: &str = "{c_settings_key}";
|
|
||||||
pub const COL_SETTINGS_TYPE: &str = "{c_settings_type}";
|
|
||||||
pub const COL_SETTINGS_VALUE: &str = "{c_settings_value}";
|
|
||||||
pub const COL_SETTINGS_HAEX_TOMBSTONE: &str = "{c_settings_tombstone}";
|
|
||||||
pub const COL_SETTINGS_HAEX_TIMESTAMP: &str = "{c_settings_timestamp}";
|
|
||||||
|
|
||||||
// --- Table: haex_extensions ---
|
|
||||||
pub const TABLE_EXTENSIONS: &str = "{t_extensions}";
|
|
||||||
pub const COL_EXTENSIONS_ID: &str = "{c_ext_id}";
|
|
||||||
pub const COL_EXTENSIONS_AUTHOR: &str = "{c_ext_author}";
|
|
||||||
pub const COL_EXTENSIONS_DESCRIPTION: &str = "{c_ext_description}";
|
|
||||||
pub const COL_EXTENSIONS_ENTRY: &str = "{c_ext_entry}";
|
|
||||||
pub const COL_EXTENSIONS_HOMEPAGE: &str = "{c_ext_homepage}";
|
|
||||||
pub const COL_EXTENSIONS_ENABLED: &str = "{c_ext_enabled}";
|
|
||||||
pub const COL_EXTENSIONS_ICON: &str = "{c_ext_icon}";
|
|
||||||
pub const COL_EXTENSIONS_NAME: &str = "{c_ext_name}";
|
|
||||||
pub const COL_EXTENSIONS_PUBLIC_KEY: &str = "{c_ext_public_key}";
|
|
||||||
pub const COL_EXTENSIONS_SIGNATURE: &str = "{c_ext_signature}";
|
|
||||||
pub const COL_EXTENSIONS_URL: &str = "{c_ext_url}";
|
|
||||||
pub const COL_EXTENSIONS_VERSION: &str = "{c_ext_version}";
|
|
||||||
pub const COL_EXTENSIONS_HAEX_TOMBSTONE: &str = "{c_ext_tombstone}";
|
|
||||||
pub const COL_EXTENSIONS_HAEX_TIMESTAMP: &str = "{c_ext_timestamp}";
|
|
||||||
|
|
||||||
// --- Table: haex_extension_permissions ---
|
|
||||||
pub const TABLE_EXTENSION_PERMISSIONS: &str = "{t_ext_perms}";
|
|
||||||
pub const COL_EXT_PERMS_ID: &str = "{c_extp_id}";
|
|
||||||
pub const COL_EXT_PERMS_EXTENSION_ID: &str = "{c_extp_extensionId}";
|
|
||||||
pub const COL_EXT_PERMS_RESOURCE_TYPE: &str = "{c_extp_resourceType}";
|
|
||||||
pub const COL_EXT_PERMS_ACTION: &str = "{c_extp_action}";
|
|
||||||
pub const COL_EXT_PERMS_TARGET: &str = "{c_extp_target}";
|
|
||||||
pub const COL_EXT_PERMS_CONSTRAINTS: &str = "{c_extp_constraints}";
|
|
||||||
pub const COL_EXT_PERMS_STATUS: &str = "{c_extp_status}";
|
|
||||||
pub const COL_EXT_PERMS_CREATED_AT: &str = "{c_extp_createdAt}";
|
|
||||||
pub const COL_EXT_PERMS_UPDATE_AT: &str = "{c_extp_updateAt}";
|
|
||||||
pub const COL_EXT_PERMS_HAEX_TOMBSTONE: &str = "{c_extp_tombstone}";
|
|
||||||
pub const COL_EXT_PERMS_HAEX_TIMESTAMP: &str = "{c_extp_timestamp}";
|
|
||||||
|
|
||||||
// --- Table: haex_notifications ---
|
|
||||||
pub const TABLE_NOTIFICATIONS: &str = "{t_notifications}";
|
|
||||||
pub const COL_NOTIFICATIONS_ID: &str = "{c_notif_id}";
|
|
||||||
pub const COL_NOTIFICATIONS_ALT: &str = "{c_notif_alt}";
|
|
||||||
pub const COL_NOTIFICATIONS_DATE: &str = "{c_notif_date}";
|
|
||||||
pub const COL_NOTIFICATIONS_ICON: &str = "{c_notif_icon}";
|
|
||||||
pub const COL_NOTIFICATIONS_IMAGE: &str = "{c_notif_image}";
|
|
||||||
pub const COL_NOTIFICATIONS_READ: &str = "{c_notif_read}";
|
|
||||||
pub const COL_NOTIFICATIONS_SOURCE: &str = "{c_notif_source}";
|
|
||||||
pub const COL_NOTIFICATIONS_TEXT: &str = "{c_notif_text}";
|
|
||||||
pub const COL_NOTIFICATIONS_TITLE: &str = "{c_notif_title}";
|
|
||||||
pub const COL_NOTIFICATIONS_TYPE: &str = "{c_notif_type}";
|
|
||||||
pub const COL_NOTIFICATIONS_HAEX_TOMBSTONE: &str = "{c_notif_tombstone}";
|
|
||||||
|
|
||||||
// --- Table: haex_desktop_items ---
|
|
||||||
pub const TABLE_DESKTOP_ITEMS: &str = "{t_desktop_items}";
|
|
||||||
pub const COL_DESKTOP_ITEMS_ID: &str = "{c_desktop_id}";
|
|
||||||
pub const COL_DESKTOP_ITEMS_ITEM_TYPE: &str = "{c_desktop_itemType}";
|
|
||||||
pub const COL_DESKTOP_ITEMS_REFERENCE_ID: &str = "{c_desktop_referenceId}";
|
|
||||||
pub const COL_DESKTOP_ITEMS_POSITION_X: &str = "{c_desktop_positionX}";
|
|
||||||
pub const COL_DESKTOP_ITEMS_POSITION_Y: &str = "{c_desktop_positionY}";
|
|
||||||
pub const COL_DESKTOP_ITEMS_HAEX_TOMBSTONE: &str = "{c_desktop_tombstone}";
|
|
||||||
pub const COL_DESKTOP_ITEMS_HAEX_TIMESTAMP: &str = "{c_desktop_timestamp}";
|
|
||||||
|
|
||||||
// --- Table: haex_crdt_logs ---
|
|
||||||
pub const TABLE_CRDT_LOGS: &str = "{t_crdt_logs}";
|
|
||||||
pub const COL_CRDT_LOGS_ID: &str = "{c_crdt_logs_id}";
|
|
||||||
pub const COL_CRDT_LOGS_HAEX_TIMESTAMP: &str = "{c_crdt_logs_timestamp}";
|
|
||||||
pub const COL_CRDT_LOGS_TABLE_NAME: &str = "{c_crdt_logs_tableName}";
|
|
||||||
pub const COL_CRDT_LOGS_ROW_PKS: &str = "{c_crdt_logs_rowPks}";
|
|
||||||
pub const COL_CRDT_LOGS_OP_TYPE: &str = "{c_crdt_logs_opType}";
|
|
||||||
pub const COL_CRDT_LOGS_COLUMN_NAME: &str = "{c_crdt_logs_columnName}";
|
|
||||||
pub const COL_CRDT_LOGS_NEW_VALUE: &str = "{c_crdt_logs_newValue}";
|
|
||||||
pub const COL_CRDT_LOGS_OLD_VALUE: &str = "{c_crdt_logs_oldValue}";
|
|
||||||
|
|
||||||
// --- Table: haex_crdt_snapshots ---
|
|
||||||
pub const TABLE_CRDT_SNAPSHOTS: &str = "{t_crdt_snapshots}";
|
|
||||||
pub const COL_CRDT_SNAPSHOTS_ID: &str = "{c_crdt_snap_id}";
|
|
||||||
pub const COL_CRDT_SNAPSHOTS_CREATED: &str = "{c_crdt_snap_created}";
|
|
||||||
pub const COL_CRDT_SNAPSHOTS_EPOCH_HLC: &str = "{c_crdt_snap_epoch}";
|
|
||||||
pub const COL_CRDT_SNAPSHOTS_LOCATION_URL: &str = "{c_crdt_snap_location}";
|
|
||||||
pub const COL_CRDT_SNAPSHOTS_FILE_SIZE: &str = "{c_crdt_snap_size}";
|
|
||||||
|
|
||||||
// --- Table: haex_crdt_configs ---
|
|
||||||
pub const TABLE_CRDT_CONFIGS: &str = "{t_crdt_configs}";
|
|
||||||
pub const COL_CRDT_CONFIGS_KEY: &str = "{c_crdt_configs_key}";
|
|
||||||
pub const COL_CRDT_CONFIGS_VALUE: &str = "{c_crdt_configs_value}";
|
|
||||||
"#,
|
"#,
|
||||||
// Settings
|
|
||||||
t_settings = haex.settings.name,
|
|
||||||
c_settings_id = haex.settings.columns["id"],
|
|
||||||
c_settings_key = haex.settings.columns["key"],
|
|
||||||
c_settings_type = haex.settings.columns["type"],
|
|
||||||
c_settings_value = haex.settings.columns["value"],
|
|
||||||
c_settings_tombstone = haex.settings.columns["haexTombstone"],
|
|
||||||
c_settings_timestamp = haex.settings.columns["haexTimestamp"],
|
|
||||||
// Extensions
|
|
||||||
t_extensions = haex.extensions.name,
|
|
||||||
c_ext_id = haex.extensions.columns["id"],
|
|
||||||
c_ext_author = haex.extensions.columns["author"],
|
|
||||||
c_ext_description = haex.extensions.columns["description"],
|
|
||||||
c_ext_entry = haex.extensions.columns["entry"],
|
|
||||||
c_ext_homepage = haex.extensions.columns["homepage"],
|
|
||||||
c_ext_enabled = haex.extensions.columns["enabled"],
|
|
||||||
c_ext_icon = haex.extensions.columns["icon"],
|
|
||||||
c_ext_name = haex.extensions.columns["name"],
|
|
||||||
c_ext_public_key = haex.extensions.columns["public_key"],
|
|
||||||
c_ext_signature = haex.extensions.columns["signature"],
|
|
||||||
c_ext_url = haex.extensions.columns["url"],
|
|
||||||
c_ext_version = haex.extensions.columns["version"],
|
|
||||||
c_ext_tombstone = haex.extensions.columns["haexTombstone"],
|
|
||||||
c_ext_timestamp = haex.extensions.columns["haexTimestamp"],
|
|
||||||
// Extension Permissions
|
|
||||||
t_ext_perms = haex.extension_permissions.name,
|
|
||||||
c_extp_id = haex.extension_permissions.columns["id"],
|
|
||||||
c_extp_extensionId = haex.extension_permissions.columns["extensionId"],
|
|
||||||
c_extp_resourceType = haex.extension_permissions.columns["resourceType"],
|
|
||||||
c_extp_action = haex.extension_permissions.columns["action"],
|
|
||||||
c_extp_target = haex.extension_permissions.columns["target"],
|
|
||||||
c_extp_constraints = haex.extension_permissions.columns["constraints"],
|
|
||||||
c_extp_status = haex.extension_permissions.columns["status"],
|
|
||||||
c_extp_createdAt = haex.extension_permissions.columns["createdAt"],
|
|
||||||
c_extp_updateAt = haex.extension_permissions.columns["updateAt"],
|
|
||||||
c_extp_tombstone = haex.extension_permissions.columns["haexTombstone"],
|
|
||||||
c_extp_timestamp = haex.extension_permissions.columns["haexTimestamp"],
|
|
||||||
// Notifications
|
|
||||||
t_notifications = haex.notifications.name,
|
|
||||||
c_notif_id = haex.notifications.columns["id"],
|
|
||||||
c_notif_alt = haex.notifications.columns["alt"],
|
|
||||||
c_notif_date = haex.notifications.columns["date"],
|
|
||||||
c_notif_icon = haex.notifications.columns["icon"],
|
|
||||||
c_notif_image = haex.notifications.columns["image"],
|
|
||||||
c_notif_read = haex.notifications.columns["read"],
|
|
||||||
c_notif_source = haex.notifications.columns["source"],
|
|
||||||
c_notif_text = haex.notifications.columns["text"],
|
|
||||||
c_notif_title = haex.notifications.columns["title"],
|
|
||||||
c_notif_type = haex.notifications.columns["type"],
|
|
||||||
c_notif_tombstone = haex.notifications.columns["haexTombstone"],
|
|
||||||
// Desktop Items
|
|
||||||
t_desktop_items = haex.desktop_items.name,
|
|
||||||
c_desktop_id = haex.desktop_items.columns["id"],
|
|
||||||
c_desktop_itemType = haex.desktop_items.columns["itemType"],
|
|
||||||
c_desktop_referenceId = haex.desktop_items.columns["referenceId"],
|
|
||||||
c_desktop_positionX = haex.desktop_items.columns["positionX"],
|
|
||||||
c_desktop_positionY = haex.desktop_items.columns["positionY"],
|
|
||||||
c_desktop_tombstone = haex.desktop_items.columns["haexTombstone"],
|
|
||||||
c_desktop_timestamp = haex.desktop_items.columns["haexTimestamp"],
|
|
||||||
// CRDT Logs
|
|
||||||
t_crdt_logs = haex.crdt.logs.name,
|
|
||||||
c_crdt_logs_id = haex.crdt.logs.columns["id"],
|
|
||||||
c_crdt_logs_timestamp = haex.crdt.logs.columns["haexTimestamp"],
|
|
||||||
c_crdt_logs_tableName = haex.crdt.logs.columns["tableName"],
|
|
||||||
c_crdt_logs_rowPks = haex.crdt.logs.columns["rowPks"],
|
|
||||||
c_crdt_logs_opType = haex.crdt.logs.columns["opType"],
|
|
||||||
c_crdt_logs_columnName = haex.crdt.logs.columns["columnName"],
|
|
||||||
c_crdt_logs_newValue = haex.crdt.logs.columns["newValue"],
|
|
||||||
c_crdt_logs_oldValue = haex.crdt.logs.columns["oldValue"],
|
|
||||||
// CRDT Snapshots
|
|
||||||
t_crdt_snapshots = haex.crdt.snapshots.name,
|
|
||||||
c_crdt_snap_id = haex.crdt.snapshots.columns["snapshotId"],
|
|
||||||
c_crdt_snap_created = haex.crdt.snapshots.columns["created"],
|
|
||||||
c_crdt_snap_epoch = haex.crdt.snapshots.columns["epochHlc"],
|
|
||||||
c_crdt_snap_location = haex.crdt.snapshots.columns["locationUrl"],
|
|
||||||
c_crdt_snap_size = haex.crdt.snapshots.columns["fileSizeBytes"],
|
|
||||||
// CRDT Configs
|
|
||||||
t_crdt_configs = haex.crdt.configs.name,
|
|
||||||
c_crdt_configs_key = haex.crdt.configs.columns["key"],
|
|
||||||
c_crdt_configs_value = haex.crdt.configs.columns["value"]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Dynamisch über alle Einträge in haex iterieren
|
||||||
|
for (key, value) in &schema.haex {
|
||||||
|
// Spezialbehandlung für nested structures wie "crdt"
|
||||||
|
if key == "crdt" {
|
||||||
|
if let Some(crdt_obj) = value.as_object() {
|
||||||
|
for (crdt_key, crdt_value) in crdt_obj {
|
||||||
|
if let Ok(table) = serde_json::from_value::<TableDefinition>(crdt_value.clone())
|
||||||
|
{
|
||||||
|
let const_prefix = format!("CRDT_{}", to_screaming_snake_case(crdt_key));
|
||||||
|
code.push_str(&generate_table_constants(&table, &const_prefix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normale Tabelle (settings, extensions, notifications, workspaces, desktop_items, etc.)
|
||||||
|
if let Ok(table) = serde_json::from_value::<TableDefinition>(value.clone()) {
|
||||||
|
let const_prefix = to_screaming_snake_case(key);
|
||||||
|
code.push_str(&generate_table_constants(&table, &const_prefix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Datei schreiben ---
|
// --- Datei schreiben ---
|
||||||
let mut f = File::create(&dest_path).expect("Konnte Zieldatei nicht erstellen");
|
let mut f = File::create(&dest_path).expect("Konnte Zieldatei nicht erstellen");
|
||||||
f.write_all(code.as_bytes())
|
f.write_all(code.as_bytes())
|
||||||
.expect("Konnte nicht in Zieldatei schreiben");
|
.expect("Konnte nicht in Zieldatei schreiben");
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed=database/tableNames.json");
|
println!("cargo:rerun-if-changed=../src/database/tableNames.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Konvertiert einen String zu SCREAMING_SNAKE_CASE
|
||||||
|
fn to_screaming_snake_case(s: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut prev_is_lower = false;
|
||||||
|
|
||||||
|
for (i, ch) in s.chars().enumerate() {
|
||||||
|
if ch == '_' {
|
||||||
|
result.push('_');
|
||||||
|
prev_is_lower = false;
|
||||||
|
} else if ch.is_uppercase() {
|
||||||
|
if i > 0 && prev_is_lower {
|
||||||
|
result.push('_');
|
||||||
|
}
|
||||||
|
result.push(ch);
|
||||||
|
prev_is_lower = false;
|
||||||
|
} else {
|
||||||
|
result.push(ch.to_ascii_uppercase());
|
||||||
|
prev_is_lower = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generiert die Konstanten für eine Tabelle
|
||||||
|
fn generate_table_constants(table: &TableDefinition, const_prefix: &str) -> String {
|
||||||
|
let mut code = String::new();
|
||||||
|
|
||||||
|
// Tabellenname
|
||||||
|
code.push_str(&format!("// --- Table: {} ---\n", table.name));
|
||||||
|
code.push_str(&format!(
|
||||||
|
"pub const TABLE_{}: &str = \"{}\";\n",
|
||||||
|
const_prefix, table.name
|
||||||
|
));
|
||||||
|
|
||||||
|
// Spalten
|
||||||
|
for (col_key, col_value) in &table.columns {
|
||||||
|
let col_const_name = format!("COL_{}_{}", const_prefix, to_screaming_snake_case(col_key));
|
||||||
|
code.push_str(&format!(
|
||||||
|
"pub const {}: &str = \"{}\";\n",
|
||||||
|
col_const_name, col_value
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
code.push('\n');
|
||||||
|
code
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,19 @@
|
|||||||
// src-tauri/src/crdt/insert_transformer.rs
|
// src-tauri/src/crdt/insert_transformer.rs
|
||||||
// INSERT-spezifische CRDT-Transformationen (ON CONFLICT, RETURNING)
|
// INSERT-spezifische CRDT-Transformationen (ON CONFLICT, RETURNING)
|
||||||
|
|
||||||
use crate::crdt::trigger::{HLC_TIMESTAMP_COLUMN, TOMBSTONE_COLUMN};
|
use crate::crdt::trigger::HLC_TIMESTAMP_COLUMN;
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
use sqlparser::ast::{Expr, Ident, Insert, SelectItem, SetExpr, Value};
|
use sqlparser::ast::{Expr, Ident, Insert, SelectItem, SetExpr, Value};
|
||||||
use uhlc::Timestamp;
|
use uhlc::Timestamp;
|
||||||
|
|
||||||
/// Helper-Struct für INSERT-Transformationen
|
/// Helper-Struct für INSERT-Transformationen
|
||||||
pub struct InsertTransformer {
|
pub struct InsertTransformer {
|
||||||
tombstone_column: &'static str,
|
|
||||||
hlc_timestamp_column: &'static str,
|
hlc_timestamp_column: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InsertTransformer {
|
impl InsertTransformer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
tombstone_column: TOMBSTONE_COLUMN,
|
|
||||||
hlc_timestamp_column: HLC_TIMESTAMP_COLUMN,
|
hlc_timestamp_column: HLC_TIMESTAMP_COLUMN,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,11 +56,9 @@ impl InsertTransformer {
|
|||||||
insert_stmt: &mut Insert,
|
insert_stmt: &mut Insert,
|
||||||
timestamp: &Timestamp,
|
timestamp: &Timestamp,
|
||||||
) -> Result<(), DatabaseError> {
|
) -> Result<(), DatabaseError> {
|
||||||
// Add both haex_timestamp and haex_tombstone columns if not exists
|
// Add haex_timestamp column if not exists
|
||||||
let hlc_col_index =
|
let hlc_col_index =
|
||||||
Self::find_or_add_column(&mut insert_stmt.columns, self.hlc_timestamp_column);
|
Self::find_or_add_column(&mut insert_stmt.columns, self.hlc_timestamp_column);
|
||||||
let tombstone_col_index =
|
|
||||||
Self::find_or_add_column(&mut insert_stmt.columns, self.tombstone_column);
|
|
||||||
|
|
||||||
// ON CONFLICT Logik komplett entfernt!
|
// ON CONFLICT Logik komplett entfernt!
|
||||||
// Bei Hard Deletes gibt es keine Tombstone-Einträge mehr zu reaktivieren
|
// Bei Hard Deletes gibt es keine Tombstone-Einträge mehr zu reaktivieren
|
||||||
@ -74,24 +70,15 @@ impl InsertTransformer {
|
|||||||
for row in &mut values.rows {
|
for row in &mut values.rows {
|
||||||
let hlc_value =
|
let hlc_value =
|
||||||
Expr::Value(Value::SingleQuotedString(timestamp.to_string()).into());
|
Expr::Value(Value::SingleQuotedString(timestamp.to_string()).into());
|
||||||
let tombstone_value =
|
|
||||||
Expr::Value(Value::Number("0".to_string(), false).into());
|
|
||||||
|
|
||||||
Self::set_or_push_value(row, hlc_col_index, hlc_value);
|
Self::set_or_push_value(row, hlc_col_index, hlc_value);
|
||||||
Self::set_or_push_value(row, tombstone_col_index, tombstone_value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SetExpr::Select(select) => {
|
SetExpr::Select(select) => {
|
||||||
let hlc_value =
|
let hlc_value =
|
||||||
Expr::Value(Value::SingleQuotedString(timestamp.to_string()).into());
|
Expr::Value(Value::SingleQuotedString(timestamp.to_string()).into());
|
||||||
let tombstone_value = Expr::Value(Value::Number("0".to_string(), false).into());
|
|
||||||
|
|
||||||
Self::set_or_push_projection(&mut select.projection, hlc_col_index, hlc_value);
|
Self::set_or_push_projection(&mut select.projection, hlc_col_index, hlc_value);
|
||||||
Self::set_or_push_projection(
|
|
||||||
&mut select.projection,
|
|
||||||
tombstone_col_index,
|
|
||||||
tombstone_value,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(DatabaseError::UnsupportedStatement {
|
return Err(DatabaseError::UnsupportedStatement {
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
// src-tauri/src/crdt/transformer.rs
|
// src-tauri/src/crdt/transformer.rs
|
||||||
|
|
||||||
use crate::crdt::insert_transformer::InsertTransformer;
|
use crate::crdt::insert_transformer::InsertTransformer;
|
||||||
use crate::crdt::trigger::{HLC_TIMESTAMP_COLUMN, TOMBSTONE_COLUMN};
|
use crate::crdt::trigger::HLC_TIMESTAMP_COLUMN;
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
use crate::table_names::{TABLE_CRDT_CONFIGS, TABLE_CRDT_LOGS};
|
use crate::table_names::{TABLE_CRDT_CONFIGS, TABLE_CRDT_LOGS};
|
||||||
use sqlparser::ast::{
|
use sqlparser::ast::{
|
||||||
Assignment, AssignmentTarget, ColumnDef, DataType, Expr, Ident, ObjectName,
|
Assignment, AssignmentTarget, ColumnDef, DataType, Expr, Ident, ObjectName, ObjectNamePart,
|
||||||
ObjectNamePart, Statement, TableFactor, TableObject, Value,
|
Statement, TableFactor, TableObject, Value,
|
||||||
};
|
};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@ -15,13 +15,11 @@ use uhlc::Timestamp;
|
|||||||
/// Konfiguration für CRDT-Spalten
|
/// Konfiguration für CRDT-Spalten
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct CrdtColumns {
|
struct CrdtColumns {
|
||||||
tombstone: &'static str,
|
|
||||||
hlc_timestamp: &'static str,
|
hlc_timestamp: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CrdtColumns {
|
impl CrdtColumns {
|
||||||
const DEFAULT: Self = Self {
|
const DEFAULT: Self = Self {
|
||||||
tombstone: TOMBSTONE_COLUMN,
|
|
||||||
hlc_timestamp: HLC_TIMESTAMP_COLUMN,
|
hlc_timestamp: HLC_TIMESTAMP_COLUMN,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,13 +35,6 @@ impl CrdtColumns {
|
|||||||
|
|
||||||
/// Fügt CRDT-Spalten zu einer Tabellendefinition hinzu
|
/// Fügt CRDT-Spalten zu einer Tabellendefinition hinzu
|
||||||
fn add_to_table_definition(&self, columns: &mut Vec<ColumnDef>) {
|
fn add_to_table_definition(&self, columns: &mut Vec<ColumnDef>) {
|
||||||
if !columns.iter().any(|c| c.name.value == self.tombstone) {
|
|
||||||
columns.push(ColumnDef {
|
|
||||||
name: Ident::new(self.tombstone),
|
|
||||||
data_type: DataType::Integer(None),
|
|
||||||
options: vec![],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if !columns.iter().any(|c| c.name.value == self.hlc_timestamp) {
|
if !columns.iter().any(|c| c.name.value == self.hlc_timestamp) {
|
||||||
columns.push(ColumnDef {
|
columns.push(ColumnDef {
|
||||||
name: Ident::new(self.hlc_timestamp),
|
name: Ident::new(self.hlc_timestamp),
|
||||||
@ -86,7 +77,7 @@ impl CrdtTransformer {
|
|||||||
// =================================================================
|
// =================================================================
|
||||||
// ÖFFENTLICHE API-METHODEN
|
// ÖFFENTLICHE API-METHODEN
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
pub fn transform_execute_statement_with_table_info(
|
pub fn transform_execute_statement_with_table_info(
|
||||||
&self,
|
&self,
|
||||||
stmt: &mut Statement,
|
stmt: &mut Statement,
|
||||||
@ -171,7 +162,7 @@ impl CrdtTransformer {
|
|||||||
Statement::Update {
|
Statement::Update {
|
||||||
table, assignments, ..
|
table, assignments, ..
|
||||||
} => {
|
} => {
|
||||||
if let TableFactor::Table { name, ..} = &table.relation {
|
if let TableFactor::Table { name, .. } = &table.relation {
|
||||||
if self.is_crdt_sync_table(name) {
|
if self.is_crdt_sync_table(name) {
|
||||||
assignments.push(self.columns.create_hlc_assignment(hlc_timestamp));
|
assignments.push(self.columns.create_hlc_assignment(hlc_timestamp));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,19 +11,15 @@ const INSERT_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_insert";
|
|||||||
const UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update";
|
const UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update";
|
||||||
const DELETE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_delete";
|
const DELETE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_delete";
|
||||||
|
|
||||||
//const SYNC_ACTIVE_KEY: &str = "sync_active";
|
|
||||||
pub const TOMBSTONE_COLUMN: &str = "haex_tombstone";
|
|
||||||
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_timestamp";
|
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_timestamp";
|
||||||
|
|
||||||
|
/// Name der custom UUID-Generierungs-Funktion (registriert in database::core::open_and_init_db)
|
||||||
|
pub const UUID_FUNCTION_NAME: &str = "gen_uuid";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CrdtSetupError {
|
pub enum CrdtSetupError {
|
||||||
/// Kapselt einen Fehler, der von der rusqlite-Bibliothek kommt.
|
/// Kapselt einen Fehler, der von der rusqlite-Bibliothek kommt.
|
||||||
DatabaseError(rusqlite::Error),
|
DatabaseError(rusqlite::Error),
|
||||||
/// Die Tabelle hat keine Tombstone-Spalte, was eine CRDT-Voraussetzung ist.
|
|
||||||
TombstoneColumnMissing {
|
|
||||||
table_name: String,
|
|
||||||
column_name: String,
|
|
||||||
},
|
|
||||||
HlcColumnMissing {
|
HlcColumnMissing {
|
||||||
table_name: String,
|
table_name: String,
|
||||||
column_name: String,
|
column_name: String,
|
||||||
@ -37,14 +33,6 @@ impl Display for CrdtSetupError {
|
|||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
CrdtSetupError::DatabaseError(e) => write!(f, "Database error: {}", e),
|
CrdtSetupError::DatabaseError(e) => write!(f, "Database error: {}", e),
|
||||||
CrdtSetupError::TombstoneColumnMissing {
|
|
||||||
table_name,
|
|
||||||
column_name,
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"Table '{}' is missing the required tombstone column '{}'",
|
|
||||||
table_name, column_name
|
|
||||||
),
|
|
||||||
CrdtSetupError::HlcColumnMissing {
|
CrdtSetupError::HlcColumnMissing {
|
||||||
table_name,
|
table_name,
|
||||||
column_name,
|
column_name,
|
||||||
@ -95,7 +83,8 @@ impl ColumnInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_safe_identifier(name: &str) -> bool {
|
fn is_safe_identifier(name: &str) -> bool {
|
||||||
!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_')
|
// Allow alphanumeric characters, underscores, and hyphens (for extension names like "nuxt-app")
|
||||||
|
!name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-')
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Richtet CRDT-Trigger für eine einzelne Tabelle ein.
|
/// Richtet CRDT-Trigger für eine einzelne Tabelle ein.
|
||||||
@ -110,13 +99,6 @@ pub fn setup_triggers_for_table(
|
|||||||
return Ok(TriggerSetupResult::TableNotFound);
|
return Ok(TriggerSetupResult::TableNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !columns.iter().any(|c| c.name == TOMBSTONE_COLUMN) {
|
|
||||||
return Err(CrdtSetupError::TombstoneColumnMissing {
|
|
||||||
table_name: table_name.to_string(),
|
|
||||||
column_name: TOMBSTONE_COLUMN.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if !columns.iter().any(|c| c.name == HLC_TIMESTAMP_COLUMN) {
|
if !columns.iter().any(|c| c.name == HLC_TIMESTAMP_COLUMN) {
|
||||||
return Err(CrdtSetupError::HlcColumnMissing {
|
return Err(CrdtSetupError::HlcColumnMissing {
|
||||||
table_name: table_name.to_string(),
|
table_name: table_name.to_string(),
|
||||||
@ -138,7 +120,7 @@ pub fn setup_triggers_for_table(
|
|||||||
|
|
||||||
let cols_to_track: Vec<String> = columns
|
let cols_to_track: Vec<String> = columns
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| !c.is_pk) //&& c.name != TOMBSTONE_COLUMN && c.name != HLC_TIMESTAMP_COLUMN
|
.filter(|c| !c.is_pk)
|
||||||
.map(|c| c.name.clone())
|
.map(|c| c.name.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@ -269,9 +251,10 @@ fn generate_insert_trigger_sql(table_name: &str, pks: &[String], cols: &[String]
|
|||||||
let column_inserts = if cols.is_empty() {
|
let column_inserts = if cols.is_empty() {
|
||||||
// Nur PKs -> einfacher Insert ins Log
|
// Nur PKs -> einfacher Insert ins Log
|
||||||
format!(
|
format!(
|
||||||
"INSERT INTO {log_table} (haex_timestamp, op_type, table_name, row_pks)
|
"INSERT INTO {log_table} (id, haex_timestamp, op_type, table_name, row_pks)
|
||||||
VALUES (NEW.\"{hlc_col}\", 'INSERT', '{table}', json_object({pk_payload}));",
|
VALUES ({uuid_fn}(), NEW.\"{hlc_col}\", 'INSERT', '{table}', json_object({pk_payload}));",
|
||||||
log_table = TABLE_CRDT_LOGS,
|
log_table = TABLE_CRDT_LOGS,
|
||||||
|
uuid_fn = UUID_FUNCTION_NAME,
|
||||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||||
table = table_name,
|
table = table_name,
|
||||||
pk_payload = pk_json_payload
|
pk_payload = pk_json_payload
|
||||||
@ -280,9 +263,10 @@ fn generate_insert_trigger_sql(table_name: &str, pks: &[String], cols: &[String]
|
|||||||
cols.iter().fold(String::new(), |mut acc, col| {
|
cols.iter().fold(String::new(), |mut acc, col| {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut acc,
|
&mut acc,
|
||||||
"INSERT INTO {log_table} (haex_timestamp, op_type, table_name, row_pks, column_name, new_value)
|
"INSERT INTO {log_table} (id, haex_timestamp, op_type, table_name, row_pks, column_name, new_value)
|
||||||
VALUES (NEW.\"{hlc_col}\", 'INSERT', '{table}', json_object({pk_payload}), '{column}', json_object('value', NEW.\"{column}\"));",
|
VALUES ({uuid_fn}(), NEW.\"{hlc_col}\", 'INSERT', '{table}', json_object({pk_payload}), '{column}', json_object('value', NEW.\"{column}\"));",
|
||||||
log_table = TABLE_CRDT_LOGS,
|
log_table = TABLE_CRDT_LOGS,
|
||||||
|
uuid_fn = UUID_FUNCTION_NAME,
|
||||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||||
table = table_name,
|
table = table_name,
|
||||||
pk_payload = pk_json_payload,
|
pk_payload = pk_json_payload,
|
||||||
@ -324,11 +308,12 @@ fn generate_update_trigger_sql(table_name: &str, pks: &[String], cols: &[String]
|
|||||||
for col in cols {
|
for col in cols {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut body,
|
&mut body,
|
||||||
"INSERT INTO {log_table} (haex_timestamp, op_type, table_name, row_pks, column_name, new_value, old_value)
|
"INSERT INTO {log_table} (id, haex_timestamp, op_type, table_name, row_pks, column_name, new_value, old_value)
|
||||||
SELECT NEW.\"{hlc_col}\", 'UPDATE', '{table}', json_object({pk_payload}), '{column}',
|
SELECT {uuid_fn}(), NEW.\"{hlc_col}\", 'UPDATE', '{table}', json_object({pk_payload}), '{column}',
|
||||||
json_object('value', NEW.\"{column}\"), json_object('value', OLD.\"{column}\")
|
json_object('value', NEW.\"{column}\"), json_object('value', OLD.\"{column}\")
|
||||||
WHERE NEW.\"{column}\" IS NOT OLD.\"{column}\";",
|
WHERE NEW.\"{column}\" IS NOT OLD.\"{column}\";",
|
||||||
log_table = TABLE_CRDT_LOGS,
|
log_table = TABLE_CRDT_LOGS,
|
||||||
|
uuid_fn = UUID_FUNCTION_NAME,
|
||||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||||
table = table_name,
|
table = table_name,
|
||||||
pk_payload = pk_json_payload,
|
pk_payload = pk_json_payload,
|
||||||
@ -367,10 +352,11 @@ fn generate_delete_trigger_sql(table_name: &str, pks: &[String], cols: &[String]
|
|||||||
for col in cols {
|
for col in cols {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut body,
|
&mut body,
|
||||||
"INSERT INTO {log_table} (haex_timestamp, op_type, table_name, row_pks, column_name, old_value)
|
"INSERT INTO {log_table} (id, haex_timestamp, op_type, table_name, row_pks, column_name, old_value)
|
||||||
VALUES (OLD.\"{hlc_col}\", 'DELETE', '{table}', json_object({pk_payload}), '{column}',
|
VALUES ({uuid_fn}(), OLD.\"{hlc_col}\", 'DELETE', '{table}', json_object({pk_payload}), '{column}',
|
||||||
json_object('value', OLD.\"{column}\"));",
|
json_object('value', OLD.\"{column}\"));",
|
||||||
log_table = TABLE_CRDT_LOGS,
|
log_table = TABLE_CRDT_LOGS,
|
||||||
|
uuid_fn = UUID_FUNCTION_NAME,
|
||||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||||
table = table_name,
|
table = table_name,
|
||||||
pk_payload = pk_json_payload,
|
pk_payload = pk_json_payload,
|
||||||
@ -381,13 +367,15 @@ fn generate_delete_trigger_sql(table_name: &str, pks: &[String], cols: &[String]
|
|||||||
// Nur PKs -> minimales Delete Log
|
// Nur PKs -> minimales Delete Log
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut body,
|
&mut body,
|
||||||
"INSERT INTO {log_table} (haex_timestamp, op_type, table_name, row_pks)
|
"INSERT INTO {log_table} (id, haex_timestamp, op_type, table_name, row_pks)
|
||||||
VALUES (OLD.\"{hlc_col}\", 'DELETE', '{table}', json_object({pk_payload}));",
|
VALUES ({uuid_fn}(), OLD.\"{hlc_col}\", 'DELETE', '{table}', json_object({pk_payload}));",
|
||||||
log_table = TABLE_CRDT_LOGS,
|
log_table = TABLE_CRDT_LOGS,
|
||||||
|
uuid_fn = UUID_FUNCTION_NAME,
|
||||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||||
table = table_name,
|
table = table_name,
|
||||||
pk_payload = pk_json_payload
|
pk_payload = pk_json_payload
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let trigger_name = DELETE_TRIGGER_TPL.replace("{TABLE_NAME}", table_name);
|
let trigger_name = DELETE_TRIGGER_TPL.replace("{TABLE_NAME}", table_name);
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
// src-tauri/src/database/core.rs
|
// src-tauri/src/database/core.rs
|
||||||
|
|
||||||
|
use crate::crdt::trigger::UUID_FUNCTION_NAME;
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
use crate::database::DbConnection;
|
use crate::database::DbConnection;
|
||||||
use crate::extension::database::executor::SqlExecutor;
|
use crate::extension::database::executor::SqlExecutor;
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||||
|
use rusqlite::functions::FunctionFlags;
|
||||||
use rusqlite::types::Value as SqlValue;
|
use rusqlite::types::Value as SqlValue;
|
||||||
use rusqlite::{
|
use rusqlite::{
|
||||||
types::{Value as RusqliteValue, ValueRef},
|
types::{Value as RusqliteValue, ValueRef},
|
||||||
@ -13,6 +15,7 @@ use serde_json::Value as JsonValue;
|
|||||||
use sqlparser::ast::{Expr, Query, Select, SetExpr, Statement, TableFactor, TableObject};
|
use sqlparser::ast::{Expr, Query, Select, SetExpr, Statement, TableFactor, TableObject};
|
||||||
use sqlparser::dialect::SQLiteDialect;
|
use sqlparser::dialect::SQLiteDialect;
|
||||||
use sqlparser::parser::Parser;
|
use sqlparser::parser::Parser;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
|
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
|
||||||
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, DatabaseError> {
|
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, DatabaseError> {
|
||||||
@ -34,6 +37,19 @@ pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connectio
|
|||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Register custom UUID function for SQLite triggers
|
||||||
|
conn.create_scalar_function(
|
||||||
|
UUID_FUNCTION_NAME,
|
||||||
|
0,
|
||||||
|
FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
|
||||||
|
|_ctx| {
|
||||||
|
Ok(Uuid::new_v4().to_string())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| DatabaseError::DatabaseError {
|
||||||
|
reason: format!("Failed to register {} function: {}", UUID_FUNCTION_NAME, e),
|
||||||
|
})?;
|
||||||
|
|
||||||
let journal_mode: String = conn
|
let journal_mode: String = conn
|
||||||
.query_row("PRAGMA journal_mode=WAL;", [], |row| row.get(0))
|
.query_row("PRAGMA journal_mode=WAL;", [], |row| row.get(0))
|
||||||
.map_err(|e| DatabaseError::PragmaError {
|
.map_err(|e| DatabaseError::PragmaError {
|
||||||
@ -73,8 +89,15 @@ pub fn parse_single_statement(sql: &str) -> Result<Statement, DatabaseError> {
|
|||||||
/// Utility für SQL-Parsing - parst mehrere SQL-Statements
|
/// Utility für SQL-Parsing - parst mehrere SQL-Statements
|
||||||
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
|
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
|
||||||
let dialect = SQLiteDialect {};
|
let dialect = SQLiteDialect {};
|
||||||
Parser::parse_sql(&dialect, sql).map_err(|e| DatabaseError::ParseError {
|
|
||||||
reason: e.to_string(),
|
// Normalize whitespace: replace multiple whitespaces (including newlines, tabs) with single space
|
||||||
|
let normalized_sql = sql
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
Parser::parse_sql(&dialect, &normalized_sql).map_err(|e| DatabaseError::ParseError {
|
||||||
|
reason: format!("Failed to parse SQL: {}", e),
|
||||||
sql: sql.to_string(),
|
sql: sql.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -236,7 +259,7 @@ pub fn select_with_crdt(
|
|||||||
connection: &DbConnection,
|
connection: &DbConnection,
|
||||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||||
with_connection(&connection, |conn| {
|
with_connection(&connection, |conn| {
|
||||||
SqlExecutor::select_internal(conn, &sql, ¶ms)
|
SqlExecutor::query_select(conn, &sql, ¶ms)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,6 @@ pub struct HaexSettings {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub value: Option<String>,
|
pub value: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub haex_tombstone: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub haex_timestamp: Option<String>,
|
pub haex_timestamp: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,8 +26,7 @@ impl HaexSettings {
|
|||||||
key: row.get(1)?,
|
key: row.get(1)?,
|
||||||
r#type: row.get(2)?,
|
r#type: row.get(2)?,
|
||||||
value: row.get(3)?,
|
value: row.get(3)?,
|
||||||
haex_tombstone: row.get(4)?,
|
haex_timestamp: row.get(4)?,
|
||||||
haex_timestamp: row.get(5)?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,8 +51,6 @@ pub struct HaexExtensions {
|
|||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
pub signature: String,
|
pub signature: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub haex_tombstone: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub haex_timestamp: Option<String>,
|
pub haex_timestamp: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,8 +68,7 @@ impl HaexExtensions {
|
|||||||
enabled: row.get(8)?,
|
enabled: row.get(8)?,
|
||||||
icon: row.get(9)?,
|
icon: row.get(9)?,
|
||||||
signature: row.get(10)?,
|
signature: row.get(10)?,
|
||||||
haex_tombstone: row.get(11)?,
|
haex_timestamp: row.get(11)?,
|
||||||
haex_timestamp: row.get(12)?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,8 +77,7 @@ impl HaexExtensions {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct HaexExtensionPermissions {
|
pub struct HaexExtensionPermissions {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
pub extension_id: String,
|
||||||
pub extension_id: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub resource_type: Option<String>,
|
pub resource_type: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@ -99,8 +92,6 @@ pub struct HaexExtensionPermissions {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub haex_tombstone: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub haex_timestamp: Option<String>,
|
pub haex_timestamp: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,8 +107,7 @@ impl HaexExtensionPermissions {
|
|||||||
status: row.get(6)?,
|
status: row.get(6)?,
|
||||||
created_at: row.get(7)?,
|
created_at: row.get(7)?,
|
||||||
updated_at: row.get(8)?,
|
updated_at: row.get(8)?,
|
||||||
haex_tombstone: row.get(9)?,
|
haex_timestamp: row.get(9)?,
|
||||||
haex_timestamp: row.get(10)?,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,3 +190,51 @@ impl HaexCrdtConfigs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HaexDesktopItems {
|
||||||
|
pub id: String,
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub item_type: String,
|
||||||
|
pub reference_id: String,
|
||||||
|
pub position_x: i64,
|
||||||
|
pub position_y: i64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub haex_timestamp: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HaexDesktopItems {
|
||||||
|
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
id: row.get(0)?,
|
||||||
|
workspace_id: row.get(1)?,
|
||||||
|
item_type: row.get(2)?,
|
||||||
|
reference_id: row.get(3)?,
|
||||||
|
position_x: row.get(4)?,
|
||||||
|
position_y: row.get(5)?,
|
||||||
|
haex_timestamp: row.get(6)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HaexWorkspaces {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub position: i64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub haex_timestamp: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HaexWorkspaces {
|
||||||
|
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
id: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
position: row.get(2)?,
|
||||||
|
haex_timestamp: row.get(3)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
67
src-tauri/src/database/init.rs
Normal file
67
src-tauri/src/database/init.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// src-tauri/src/database/init.rs
|
||||||
|
// Database initialization utilities (trigger setup, etc.)
|
||||||
|
|
||||||
|
use crate::crdt::trigger;
|
||||||
|
use crate::database::error::DatabaseError;
|
||||||
|
use crate::table_names::{
|
||||||
|
TABLE_DESKTOP_ITEMS,
|
||||||
|
TABLE_EXTENSIONS,
|
||||||
|
TABLE_EXTENSION_PERMISSIONS,
|
||||||
|
TABLE_NOTIFICATIONS,
|
||||||
|
TABLE_SETTINGS,
|
||||||
|
TABLE_WORKSPACES,
|
||||||
|
};
|
||||||
|
use rusqlite::{params, Connection};
|
||||||
|
|
||||||
|
/// Liste aller CRDT-Tabellen die Trigger benötigen (ohne Password-Tabellen - die kommen in Extension)
|
||||||
|
const CRDT_TABLES: &[&str] = &[
|
||||||
|
TABLE_SETTINGS,
|
||||||
|
TABLE_EXTENSIONS,
|
||||||
|
TABLE_EXTENSION_PERMISSIONS,
|
||||||
|
TABLE_NOTIFICATIONS,
|
||||||
|
TABLE_WORKSPACES,
|
||||||
|
TABLE_DESKTOP_ITEMS,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Prüft ob Trigger bereits initialisiert wurden und erstellt sie falls nötig
|
||||||
|
///
|
||||||
|
/// Diese Funktion wird beim ersten Öffnen einer Template-DB aufgerufen.
|
||||||
|
/// Sie erstellt alle CRDT-Trigger für die definierten Tabellen und markiert
|
||||||
|
/// die Initialisierung in haex_settings.
|
||||||
|
///
|
||||||
|
/// Bei Migrations (ALTER TABLE) werden Trigger automatisch neu erstellt,
|
||||||
|
/// daher ist kein Versioning nötig.
|
||||||
|
pub fn ensure_triggers_initialized(conn: &mut Connection) -> Result<bool, DatabaseError> {
|
||||||
|
let tx = conn.transaction()?;
|
||||||
|
|
||||||
|
// Check if triggers already initialized
|
||||||
|
let check_sql = format!(
|
||||||
|
"SELECT value FROM {} WHERE key = ? AND type = ?",
|
||||||
|
TABLE_SETTINGS
|
||||||
|
);
|
||||||
|
let initialized: Option<String> = tx
|
||||||
|
.query_row(
|
||||||
|
&check_sql,
|
||||||
|
params!["triggers_initialized", "system"],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
if initialized.is_some() {
|
||||||
|
eprintln!("DEBUG: Triggers already initialized, skipping");
|
||||||
|
tx.commit()?; // Wichtig: Transaktion trotzdem abschließen
|
||||||
|
return Ok(true); // true = war schon initialisiert
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("INFO: Initializing CRDT triggers for database...");
|
||||||
|
|
||||||
|
// Create triggers for all CRDT tables
|
||||||
|
for table_name in CRDT_TABLES {
|
||||||
|
eprintln!(" - Setting up triggers for: {}", table_name);
|
||||||
|
trigger::setup_triggers_for_table(&tx, table_name, false)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit()?;
|
||||||
|
eprintln!("INFO: ✓ CRDT triggers created successfully (flag pending)");
|
||||||
|
Ok(false) // false = wurde gerade initialisiert
|
||||||
|
}
|
||||||
@ -3,11 +3,13 @@
|
|||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod generated;
|
pub mod generated;
|
||||||
|
pub mod init;
|
||||||
|
|
||||||
use crate::crdt::hlc::HlcService;
|
use crate::crdt::hlc::HlcService;
|
||||||
|
use crate::database::core::execute_with_crdt;
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
use crate::extension::database::executor::SqlExecutor;
|
use crate::extension::database::executor::SqlExecutor;
|
||||||
use crate::table_names::TABLE_CRDT_CONFIGS;
|
use crate::table_names::{TABLE_CRDT_CONFIGS, TABLE_SETTINGS};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -18,6 +20,8 @@ use std::time::UNIX_EPOCH;
|
|||||||
use std::{fs, sync::Arc};
|
use std::{fs, sync::Arc};
|
||||||
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
|
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
|
||||||
use tauri_plugin_fs::FsExt;
|
use tauri_plugin_fs::FsExt;
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
use trash;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
|
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
|
||||||
@ -76,7 +80,8 @@ pub fn sql_query_with_crdt(
|
|||||||
|
|
||||||
core::with_connection(&state.db, |conn| {
|
core::with_connection(&state.db, |conn| {
|
||||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||||
let result = SqlExecutor::query_internal(&tx, &hlc_service, &sql, ¶ms)?;
|
let (_modified_tables, result) =
|
||||||
|
SqlExecutor::query_internal(&tx, &hlc_service, &sql, ¶ms)?;
|
||||||
tx.commit().map_err(DatabaseError::from)?;
|
tx.commit().map_err(DatabaseError::from)?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
})
|
})
|
||||||
@ -130,7 +135,6 @@ pub fn get_vaults_directory(app_handle: &AppHandle) -> Result<String, DatabaseEr
|
|||||||
Ok(vaults_dir.to_string_lossy().to_string())
|
Ok(vaults_dir.to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
//#[serde(tag = "type", content = "details")]
|
|
||||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@ -209,7 +213,60 @@ pub fn vault_exists(app_handle: AppHandle, vault_name: String) -> Result<bool, D
|
|||||||
Ok(Path::new(&vault_path).exists())
|
Ok(Path::new(&vault_path).exists())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes a vault database file
|
/// Moves a vault database file to trash (or deletes permanently if trash is unavailable)
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn move_vault_to_trash(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
vault_name: String,
|
||||||
|
) -> Result<String, DatabaseError> {
|
||||||
|
// On Android, trash is not available, so delete permanently
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
println!(
|
||||||
|
"Android platform detected, permanently deleting vault '{}'",
|
||||||
|
vault_name
|
||||||
|
);
|
||||||
|
return delete_vault(app_handle, vault_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On non-Android platforms, try to use trash
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
let vault_path = get_vault_path(&app_handle, &vault_name)?;
|
||||||
|
let vault_shm_path = format!("{}-shm", vault_path);
|
||||||
|
let vault_wal_path = format!("{}-wal", vault_path);
|
||||||
|
|
||||||
|
if !Path::new(&vault_path).exists() {
|
||||||
|
return Err(DatabaseError::IoError {
|
||||||
|
path: vault_path,
|
||||||
|
reason: "Vault does not exist".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to move to trash first (works on desktop systems)
|
||||||
|
let moved_to_trash = trash::delete(&vault_path).is_ok();
|
||||||
|
|
||||||
|
if moved_to_trash {
|
||||||
|
// Also try to move auxiliary files to trash (ignore errors as they might not exist)
|
||||||
|
let _ = trash::delete(&vault_shm_path);
|
||||||
|
let _ = trash::delete(&vault_wal_path);
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"Vault '{}' successfully moved to trash",
|
||||||
|
vault_name
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// Fallback: Permanent deletion if trash fails
|
||||||
|
println!(
|
||||||
|
"Trash not available, falling back to permanent deletion for vault '{}'",
|
||||||
|
vault_name
|
||||||
|
);
|
||||||
|
delete_vault(app_handle, vault_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a vault database file permanently (bypasses trash)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_vault(app_handle: AppHandle, vault_name: String) -> Result<String, DatabaseError> {
|
pub fn delete_vault(app_handle: AppHandle, vault_name: String) -> Result<String, DatabaseError> {
|
||||||
let vault_path = get_vault_path(&app_handle, &vault_name)?;
|
let vault_path = get_vault_path(&app_handle, &vault_name)?;
|
||||||
@ -392,9 +449,6 @@ pub fn open_encrypted_database(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<String, DatabaseError> {
|
) -> Result<String, DatabaseError> {
|
||||||
println!("Opening encrypted database vault_path: {}", vault_path);
|
println!("Opening encrypted database vault_path: {}", vault_path);
|
||||||
|
|
||||||
// Vault-Pfad aus dem Namen ableiten
|
|
||||||
//let vault_path = get_vault_path(&app_handle, &vault_name)?;
|
|
||||||
println!("Resolved vault path: {}", vault_path);
|
println!("Resolved vault path: {}", vault_path);
|
||||||
|
|
||||||
if !Path::new(&vault_path).exists() {
|
if !Path::new(&vault_path).exists() {
|
||||||
@ -417,9 +471,12 @@ fn initialize_session(
|
|||||||
state: &State<'_, AppState>,
|
state: &State<'_, AppState>,
|
||||||
) -> Result<(), DatabaseError> {
|
) -> Result<(), DatabaseError> {
|
||||||
// 1. Establish the raw database connection
|
// 1. Establish the raw database connection
|
||||||
let conn = core::open_and_init_db(path, key, false)?;
|
let mut conn = core::open_and_init_db(path, key, false)?;
|
||||||
|
|
||||||
// 2. Initialize the HLC service
|
// 2. Ensure CRDT triggers are initialized (for template DB)
|
||||||
|
let triggers_were_already_initialized = init::ensure_triggers_initialized(&mut conn)?;
|
||||||
|
|
||||||
|
// 3. Initialize the HLC service
|
||||||
let hlc_service = HlcService::try_initialize(&conn, app_handle).map_err(|e| {
|
let hlc_service = HlcService::try_initialize(&conn, app_handle).map_err(|e| {
|
||||||
// We convert the HlcError into a DatabaseError
|
// We convert the HlcError into a DatabaseError
|
||||||
DatabaseError::ExecutionError {
|
DatabaseError::ExecutionError {
|
||||||
@ -429,16 +486,53 @@ fn initialize_session(
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// 3. Store everything in the global AppState
|
// 4. Store everything in the global AppState
|
||||||
let mut db_guard = state.db.0.lock().map_err(|e| DatabaseError::LockError {
|
let mut db_guard = state.db.0.lock().map_err(|e| DatabaseError::LockError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
// Wichtig: Wir brauchen den db_guard gleich nicht mehr,
|
||||||
|
// da 'execute_with_crdt' 'with_connection' aufruft, was
|
||||||
|
// 'state.db' selbst locken muss.
|
||||||
|
// Wir müssen den Guard freigeben, *bevor* wir 'execute_with_crdt' rufen,
|
||||||
|
// um einen Deadlock zu verhindern.
|
||||||
|
// Aber wir müssen die 'conn' erst hineinbewegen.
|
||||||
*db_guard = Some(conn);
|
*db_guard = Some(conn);
|
||||||
|
drop(db_guard);
|
||||||
|
|
||||||
let mut hlc_guard = state.hlc.lock().map_err(|e| DatabaseError::LockError {
|
let mut hlc_guard = state.hlc.lock().map_err(|e| DatabaseError::LockError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
*hlc_guard = hlc_service;
|
*hlc_guard = hlc_service;
|
||||||
|
|
||||||
|
// WICHTIG: hlc_guard *nicht* freigeben, da 'execute_with_crdt'
|
||||||
|
// eine Referenz auf die Guard erwartet.
|
||||||
|
|
||||||
|
// 5. NEUER SCHRITT: Setze das Flag via CRDT, falls nötig
|
||||||
|
if !triggers_were_already_initialized {
|
||||||
|
eprintln!("INFO: Setting 'triggers_initialized' flag via CRDT...");
|
||||||
|
|
||||||
|
let insert_sql = format!(
|
||||||
|
"INSERT INTO {} (id, key, type, value) VALUES (?, ?, ?, ?)",
|
||||||
|
TABLE_SETTINGS
|
||||||
|
);
|
||||||
|
|
||||||
|
// execute_with_crdt erwartet Vec<JsonValue>, kein params!-Makro
|
||||||
|
let params_vec: Vec<JsonValue> = vec![
|
||||||
|
JsonValue::String(uuid::Uuid::new_v4().to_string()),
|
||||||
|
JsonValue::String("triggers_initialized".to_string()),
|
||||||
|
JsonValue::String("system".to_string()),
|
||||||
|
JsonValue::String("1".to_string()),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Jetzt können wir 'execute_with_crdt' sicher aufrufen,
|
||||||
|
// da der AppState initialisiert ist.
|
||||||
|
execute_with_crdt(
|
||||||
|
insert_sql, params_vec, &state.db, // Das &DbConnection (der Mutex)
|
||||||
|
&hlc_guard, // Die gehaltene MutexGuard
|
||||||
|
)?;
|
||||||
|
|
||||||
|
eprintln!("INFO: ✓ 'triggers_initialized' flag set.");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, E
|
|||||||
use crate::extension::core::types::{copy_directory, Extension, ExtensionSource};
|
use crate::extension::core::types::{copy_directory, Extension, ExtensionSource};
|
||||||
use crate::extension::core::ExtensionPermissions;
|
use crate::extension::core::ExtensionPermissions;
|
||||||
use crate::extension::crypto::ExtensionCrypto;
|
use crate::extension::crypto::ExtensionCrypto;
|
||||||
use crate::extension::database::executor::{PkRemappingContext, SqlExecutor};
|
use crate::extension::database::executor::SqlExecutor;
|
||||||
use crate::extension::error::ExtensionError;
|
use crate::extension::error::ExtensionError;
|
||||||
use crate::extension::permissions::manager::PermissionManager;
|
use crate::extension::permissions::manager::PermissionManager;
|
||||||
use crate::extension::permissions::types::ExtensionPermission;
|
use crate::extension::permissions::types::ExtensionPermission;
|
||||||
@ -66,17 +66,124 @@ impl ExtensionManager {
|
|||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper function to validate path and check for path traversal
|
||||||
|
/// Returns the cleaned path if valid, or None if invalid/not found
|
||||||
|
/// If require_exists is true, returns None if path doesn't exist
|
||||||
|
pub fn validate_path_in_directory(
|
||||||
|
base_dir: &PathBuf,
|
||||||
|
relative_path: &str,
|
||||||
|
require_exists: bool,
|
||||||
|
) -> Result<Option<PathBuf>, ExtensionError> {
|
||||||
|
// Check for path traversal patterns
|
||||||
|
if relative_path.contains("..") {
|
||||||
|
return Err(ExtensionError::SecurityViolation {
|
||||||
|
reason: format!("Path traversal attempt: {}", relative_path),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the path (same logic as in protocol.rs)
|
||||||
|
let clean_path = relative_path
|
||||||
|
.replace('\\', "/")
|
||||||
|
.trim_start_matches('/')
|
||||||
|
.split('/')
|
||||||
|
.filter(|&part| !part.is_empty() && part != "." && part != "..")
|
||||||
|
.collect::<PathBuf>();
|
||||||
|
|
||||||
|
let full_path = base_dir.join(&clean_path);
|
||||||
|
|
||||||
|
// Check if file/directory exists (if required)
|
||||||
|
if require_exists && !full_path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify path is within base directory
|
||||||
|
let canonical_base = base_dir
|
||||||
|
.canonicalize()
|
||||||
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
|
|
||||||
|
if let Ok(canonical_path) = full_path.canonicalize() {
|
||||||
|
if !canonical_path.starts_with(&canonical_base) {
|
||||||
|
return Err(ExtensionError::SecurityViolation {
|
||||||
|
reason: format!("Path outside base directory: {}", relative_path),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Some(canonical_path))
|
||||||
|
} else {
|
||||||
|
// Path doesn't exist yet - still validate it would be within base
|
||||||
|
if full_path.starts_with(&canonical_base) {
|
||||||
|
Ok(Some(full_path))
|
||||||
|
} else {
|
||||||
|
Err(ExtensionError::SecurityViolation {
|
||||||
|
reason: format!("Path outside base directory: {}", relative_path),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates icon path and falls back to favicon.ico if not specified
|
||||||
|
fn validate_and_resolve_icon_path(
|
||||||
|
extension_dir: &PathBuf,
|
||||||
|
haextension_dir: &str,
|
||||||
|
icon_path: Option<&str>,
|
||||||
|
) -> Result<Option<String>, ExtensionError> {
|
||||||
|
// If icon is specified in manifest, validate it
|
||||||
|
if let Some(icon) = icon_path {
|
||||||
|
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, icon, true)? {
|
||||||
|
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
||||||
|
} else {
|
||||||
|
eprintln!("WARNING: Icon path specified in manifest not found: {}", icon);
|
||||||
|
// Continue to fallback logic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback 1: Check haextension/favicon.ico
|
||||||
|
let haextension_favicon = format!("{}/favicon.ico", haextension_dir);
|
||||||
|
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, &haextension_favicon, true)? {
|
||||||
|
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback 2: Check public/favicon.ico
|
||||||
|
if let Some(clean_path) = Self::validate_path_in_directory(extension_dir, "public/favicon.ico", true)? {
|
||||||
|
return Ok(Some(clean_path.to_string_lossy().to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// No icon found
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
/// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest
|
/// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest
|
||||||
fn extract_and_validate_extension(
|
fn extract_and_validate_extension(
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
temp_prefix: &str,
|
temp_prefix: &str,
|
||||||
|
app_handle: &AppHandle,
|
||||||
) -> Result<ExtractedExtension, ExtensionError> {
|
) -> Result<ExtractedExtension, ExtensionError> {
|
||||||
let temp = std::env::temp_dir().join(format!("{}_{}", temp_prefix, uuid::Uuid::new_v4()));
|
// Use app_cache_dir for better Android compatibility
|
||||||
|
let cache_dir = app_handle
|
||||||
|
.path()
|
||||||
|
.app_cache_dir()
|
||||||
|
.map_err(|e| ExtensionError::InstallationFailed {
|
||||||
|
reason: format!("Cannot get app cache dir: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let temp_id = uuid::Uuid::new_v4();
|
||||||
|
let temp = cache_dir.join(format!("{}_{}", temp_prefix, temp_id));
|
||||||
|
let zip_file_path = cache_dir.join(format!("{}_{}_{}.haextension", temp_prefix, temp_id, "temp"));
|
||||||
|
|
||||||
|
// Write bytes to a temporary ZIP file first (important for Android file system)
|
||||||
|
fs::write(&zip_file_path, &bytes).map_err(|e| {
|
||||||
|
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Create extraction directory
|
||||||
fs::create_dir_all(&temp)
|
fs::create_dir_all(&temp)
|
||||||
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?;
|
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?;
|
||||||
|
|
||||||
let mut archive = ZipArchive::new(Cursor::new(bytes)).map_err(|e| {
|
// Open ZIP file from disk (more reliable on Android than from memory)
|
||||||
|
let zip_file = fs::File::open(&zip_file_path).map_err(|e| {
|
||||||
|
ExtensionError::filesystem_with_path(zip_file_path.display().to_string(), e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut archive = ZipArchive::new(zip_file).map_err(|e| {
|
||||||
ExtensionError::InstallationFailed {
|
ExtensionError::InstallationFailed {
|
||||||
reason: format!("Invalid ZIP: {}", e),
|
reason: format!("Invalid ZIP: {}", e),
|
||||||
}
|
}
|
||||||
@ -88,38 +195,54 @@ impl ExtensionManager {
|
|||||||
reason: format!("Cannot extract ZIP: {}", e),
|
reason: format!("Cannot extract ZIP: {}", e),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Check if manifest.json is directly in temp or in a subdirectory
|
// Clean up temporary ZIP file
|
||||||
let manifest_path = temp.join("manifest.json");
|
let _ = fs::remove_file(&zip_file_path);
|
||||||
let actual_dir = if manifest_path.exists() {
|
|
||||||
temp.clone()
|
|
||||||
} else {
|
|
||||||
// manifest.json is in a subdirectory - find it
|
|
||||||
let mut found_dir = None;
|
|
||||||
for entry in fs::read_dir(&temp)
|
|
||||||
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), e))?
|
|
||||||
{
|
|
||||||
let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() && path.join("manifest.json").exists() {
|
|
||||||
found_dir = Some(path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
found_dir.ok_or_else(|| ExtensionError::ManifestError {
|
// Read haextension_dir from config if it exists, otherwise use default
|
||||||
reason: "manifest.json not found in extension archive".to_string(),
|
let config_path = temp.join("haextension.config.json");
|
||||||
})?
|
let haextension_dir = if config_path.exists() {
|
||||||
|
let config_content = std::fs::read_to_string(&config_path)
|
||||||
|
.map_err(|e| ExtensionError::ManifestError {
|
||||||
|
reason: format!("Cannot read haextension.config.json: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let config: serde_json::Value = serde_json::from_str(&config_content)
|
||||||
|
.map_err(|e| ExtensionError::ManifestError {
|
||||||
|
reason: format!("Invalid haextension.config.json: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let dir = config
|
||||||
|
.get("dev")
|
||||||
|
.and_then(|dev| dev.get("haextension_dir"))
|
||||||
|
.and_then(|dir| dir.as_str())
|
||||||
|
.unwrap_or("haextension")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
dir
|
||||||
|
} else {
|
||||||
|
"haextension".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let manifest_path = actual_dir.join("manifest.json");
|
// Validate manifest path using helper function
|
||||||
|
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||||
|
let manifest_path = Self::validate_path_in_directory(&temp, &manifest_relative_path, true)?
|
||||||
|
.ok_or_else(|| ExtensionError::ManifestError {
|
||||||
|
reason: format!("manifest.json not found at {}/manifest.json", haextension_dir),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let actual_dir = temp.clone();
|
||||||
let manifest_content =
|
let manifest_content =
|
||||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||||
reason: format!("Cannot read manifest: {}", e),
|
reason: format!("Cannot read manifest: {}", e),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
let mut manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||||
|
|
||||||
let content_hash = ExtensionCrypto::hash_directory(&actual_dir).map_err(|e| {
|
// Validate and resolve icon path with fallback logic
|
||||||
|
let validated_icon = Self::validate_and_resolve_icon_path(&actual_dir, &haextension_dir, manifest.icon.as_deref())?;
|
||||||
|
manifest.icon = validated_icon;
|
||||||
|
|
||||||
|
let content_hash = ExtensionCrypto::hash_directory(&actual_dir, &manifest_path).map_err(|e| {
|
||||||
ExtensionError::SignatureVerificationFailed {
|
ExtensionError::SignatureVerificationFailed {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
}
|
}
|
||||||
@ -393,9 +516,10 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
pub async fn preview_extension_internal(
|
pub async fn preview_extension_internal(
|
||||||
&self,
|
&self,
|
||||||
|
app_handle: &AppHandle,
|
||||||
file_bytes: Vec<u8>,
|
file_bytes: Vec<u8>,
|
||||||
) -> Result<ExtensionPreview, ExtensionError> {
|
) -> Result<ExtensionPreview, ExtensionError> {
|
||||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview")?;
|
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?;
|
||||||
|
|
||||||
let is_valid_signature = ExtensionCrypto::verify_signature(
|
let is_valid_signature = ExtensionCrypto::verify_signature(
|
||||||
&extracted.manifest.public_key,
|
&extracted.manifest.public_key,
|
||||||
@ -420,7 +544,7 @@ impl ExtensionManager {
|
|||||||
custom_permissions: EditablePermissions,
|
custom_permissions: EditablePermissions,
|
||||||
state: &State<'_, AppState>,
|
state: &State<'_, AppState>,
|
||||||
) -> Result<String, ExtensionError> {
|
) -> Result<String, ExtensionError> {
|
||||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext")?;
|
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext", &app_handle)?;
|
||||||
|
|
||||||
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
|
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
|
||||||
ExtensionCrypto::verify_signature(
|
ExtensionCrypto::verify_signature(
|
||||||
@ -437,6 +561,17 @@ impl ExtensionManager {
|
|||||||
&extracted.manifest.version,
|
&extracted.manifest.version,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// If extension version already exists, remove it completely before installing
|
||||||
|
if extensions_dir.exists() {
|
||||||
|
eprintln!(
|
||||||
|
"Extension version already exists at {}, removing old version",
|
||||||
|
extensions_dir.display()
|
||||||
|
);
|
||||||
|
std::fs::remove_dir_all(&extensions_dir).map_err(|e| {
|
||||||
|
ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
std::fs::create_dir_all(&extensions_dir).map_err(|e| {
|
std::fs::create_dir_all(&extensions_dir).map_err(|e| {
|
||||||
ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e)
|
ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e)
|
||||||
})?;
|
})?;
|
||||||
@ -478,19 +613,13 @@ impl ExtensionManager {
|
|||||||
let hlc_service = hlc_service_guard.clone();
|
let hlc_service = hlc_service_guard.clone();
|
||||||
drop(hlc_service_guard);
|
drop(hlc_service_guard);
|
||||||
|
|
||||||
// Erstelle PK-Remapping Context für die gesamte Transaktion
|
|
||||||
// Dies ermöglicht automatisches FK-Remapping wenn ON CONFLICT bei Extension auftritt
|
|
||||||
let mut pk_context = PkRemappingContext::new();
|
|
||||||
|
|
||||||
// 1. Extension-Eintrag erstellen mit generierter UUID
|
// 1. Extension-Eintrag erstellen mit generierter UUID
|
||||||
// WICHTIG: RETURNING wird vom CRDT-Transformer automatisch hinzugefügt
|
|
||||||
let insert_ext_sql = format!(
|
let insert_ext_sql = format!(
|
||||||
"INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
|
"INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
TABLE_EXTENSIONS
|
TABLE_EXTENSIONS
|
||||||
);
|
);
|
||||||
|
|
||||||
let (_tables, returning_results): (HashSet<String>, Vec<Vec<JsonValue>>) =
|
SqlExecutor::execute_internal_typed(
|
||||||
SqlExecutor::query_internal_typed_with_context(
|
|
||||||
&tx,
|
&tx,
|
||||||
&hlc_service,
|
&hlc_service,
|
||||||
&insert_ext_sql,
|
&insert_ext_sql,
|
||||||
@ -506,27 +635,11 @@ impl ExtensionManager {
|
|||||||
extracted.manifest.homepage,
|
extracted.manifest.homepage,
|
||||||
extracted.manifest.description,
|
extracted.manifest.description,
|
||||||
true, // enabled
|
true, // enabled
|
||||||
|
extracted.manifest.single_instance.unwrap_or(false),
|
||||||
],
|
],
|
||||||
&mut pk_context,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Nutze die tatsächliche ID aus der Datenbank (wichtig bei ON CONFLICT)
|
// 2. Permissions speichern
|
||||||
// Die haex_extensions Tabelle hat einen single-column PK namens "id"
|
|
||||||
let actual_extension_id = returning_results
|
|
||||||
.first() // Holt die erste Zeile (das innere Vec<JsonValue>, z.B. Some(&["uuid-string"]))
|
|
||||||
.and_then(|row_array| row_array.first()) // Holt das erste Element daraus (z.B. Some(&JsonValue::String("uuid-string")))
|
|
||||||
.and_then(|val| val.as_str()) // Konvertiert zu &str (z.B. Some("uuid-string"))
|
|
||||||
.map(|s| s.to_string()) // Konvertiert zu String
|
|
||||||
.unwrap_or_else(|| extension_id.clone()); // Fallback
|
|
||||||
|
|
||||||
eprintln!(
|
|
||||||
"DEBUG: Extension UUID - Generated: {}, Actual from DB: {}",
|
|
||||||
extension_id, actual_extension_id
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Permissions speichern (oder aktualisieren falls schon vorhanden)
|
|
||||||
// Nutze einfaches INSERT - die CRDT-Transformation fügt automatisch ON CONFLICT hinzu
|
|
||||||
// FK-Werte (extension_id) werden automatisch remapped wenn Extension ON CONFLICT hatte
|
|
||||||
let insert_perm_sql = format!(
|
let insert_perm_sql = format!(
|
||||||
"INSERT INTO {} (id, extension_id, resource_type, action, target, constraints, status) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO {} (id, extension_id, resource_type, action, target, constraints, status) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
TABLE_EXTENSION_PERMISSIONS
|
TABLE_EXTENSION_PERMISSIONS
|
||||||
@ -536,7 +649,7 @@ impl ExtensionManager {
|
|||||||
use crate::database::generated::HaexExtensionPermissions;
|
use crate::database::generated::HaexExtensionPermissions;
|
||||||
let db_perm: HaexExtensionPermissions = perm.into();
|
let db_perm: HaexExtensionPermissions = perm.into();
|
||||||
|
|
||||||
SqlExecutor::execute_internal_typed_with_context(
|
SqlExecutor::execute_internal_typed(
|
||||||
&tx,
|
&tx,
|
||||||
&hlc_service,
|
&hlc_service,
|
||||||
&insert_perm_sql,
|
&insert_perm_sql,
|
||||||
@ -549,16 +662,15 @@ impl ExtensionManager {
|
|||||||
db_perm.constraints,
|
db_perm.constraints,
|
||||||
db_perm.status,
|
db_perm.status,
|
||||||
],
|
],
|
||||||
&mut pk_context,
|
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.commit().map_err(DatabaseError::from)?;
|
tx.commit().map_err(DatabaseError::from)?;
|
||||||
Ok(actual_extension_id.clone())
|
Ok(extension_id.clone())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let extension = Extension {
|
let extension = Extension {
|
||||||
id: actual_extension_id.clone(), // Nutze die actual_extension_id aus der Transaktion
|
id: extension_id.clone(),
|
||||||
source: ExtensionSource::Production {
|
source: ExtensionSource::Production {
|
||||||
path: extensions_dir.clone(),
|
path: extensions_dir.clone(),
|
||||||
version: extracted.manifest.version.clone(),
|
version: extracted.manifest.version.clone(),
|
||||||
@ -602,13 +714,12 @@ impl ExtensionManager {
|
|||||||
// Lade alle Daten aus der Datenbank
|
// Lade alle Daten aus der Datenbank
|
||||||
let extensions = with_connection(&state.db, |conn| {
|
let extensions = with_connection(&state.db, |conn| {
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM {}",
|
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance FROM {}",
|
||||||
TABLE_EXTENSIONS
|
TABLE_EXTENSIONS
|
||||||
);
|
);
|
||||||
eprintln!("DEBUG: SQL Query before transformation: {}", sql);
|
eprintln!("DEBUG: SQL Query before transformation: {}", sql);
|
||||||
|
|
||||||
// select_internal gibt jetzt Vec<Vec<JsonValue>> zurück
|
let results = SqlExecutor::query_select(conn, &sql, &[])?;
|
||||||
let results = SqlExecutor::select_internal(conn, &sql, &[])?;
|
|
||||||
eprintln!("DEBUG: Query returned {} results", results.len());
|
eprintln!("DEBUG: Query returned {} results", results.len());
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
@ -635,13 +746,16 @@ impl ExtensionManager {
|
|||||||
})?
|
})?
|
||||||
.to_string(),
|
.to_string(),
|
||||||
author: row[3].as_str().map(String::from),
|
author: row[3].as_str().map(String::from),
|
||||||
entry: row[4].as_str().unwrap_or("index.html").to_string(),
|
entry: row[4].as_str().map(String::from),
|
||||||
icon: row[5].as_str().map(String::from),
|
icon: row[5].as_str().map(String::from),
|
||||||
public_key: row[6].as_str().unwrap_or("").to_string(),
|
public_key: row[6].as_str().unwrap_or("").to_string(),
|
||||||
signature: row[7].as_str().unwrap_or("").to_string(),
|
signature: row[7].as_str().unwrap_or("").to_string(),
|
||||||
permissions: ExtensionPermissions::default(),
|
permissions: ExtensionPermissions::default(),
|
||||||
homepage: row[8].as_str().map(String::from),
|
homepage: row[8].as_str().map(String::from),
|
||||||
description: row[9].as_str().map(String::from),
|
description: row[9].as_str().map(String::from),
|
||||||
|
single_instance: row[11]
|
||||||
|
.as_bool()
|
||||||
|
.or_else(|| row[11].as_i64().map(|v| v != 0)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let enabled = row[10]
|
let enabled = row[10]
|
||||||
@ -675,9 +789,10 @@ impl ExtensionManager {
|
|||||||
&extension_data.manifest.version,
|
&extension_data.manifest.version,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if !extension_path.exists() || !extension_path.join("manifest.json").exists() {
|
// Check if extension directory exists
|
||||||
|
if !extension_path.exists() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"DEBUG: Extension files missing for: {} at {:?}",
|
"DEBUG: Extension directory missing for: {} at {:?}",
|
||||||
extension_id, extension_path
|
extension_id, extension_path
|
||||||
);
|
);
|
||||||
self.missing_extensions
|
self.missing_extensions
|
||||||
@ -694,6 +809,52 @@ impl ExtensionManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read haextension_dir from config if it exists, otherwise use default
|
||||||
|
let config_path = extension_path.join("haextension.config.json");
|
||||||
|
let haextension_dir = if config_path.exists() {
|
||||||
|
match std::fs::read_to_string(&config_path) {
|
||||||
|
Ok(config_content) => {
|
||||||
|
match serde_json::from_str::<serde_json::Value>(&config_content) {
|
||||||
|
Ok(config) => {
|
||||||
|
config
|
||||||
|
.get("dev")
|
||||||
|
.and_then(|dev| dev.get("haextension_dir"))
|
||||||
|
.and_then(|dir| dir.as_str())
|
||||||
|
.unwrap_or("haextension")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
Err(_) => "haextension".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => "haextension".to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"haextension".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate manifest.json path using helper function
|
||||||
|
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||||
|
if Self::validate_path_in_directory(&extension_path, &manifest_relative_path, true)?
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"DEBUG: manifest.json missing or invalid for: {} at {}/manifest.json",
|
||||||
|
extension_id, haextension_dir
|
||||||
|
);
|
||||||
|
self.missing_extensions
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?
|
||||||
|
.push(MissingExtension {
|
||||||
|
id: extension_id.clone(),
|
||||||
|
public_key: extension_data.manifest.public_key.clone(),
|
||||||
|
name: extension_data.manifest.name.clone(),
|
||||||
|
version: extension_data.manifest.version.clone(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
eprintln!("DEBUG: Extension loaded successfully: {}", extension_id);
|
eprintln!("DEBUG: Extension loaded successfully: {}", extension_id);
|
||||||
|
|
||||||
let extension = Extension {
|
let extension = Extension {
|
||||||
|
|||||||
@ -57,13 +57,20 @@ pub struct ExtensionManifest {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
pub entry: String,
|
#[serde(default = "default_entry_value")]
|
||||||
|
pub entry: Option<String>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
pub public_key: String,
|
pub public_key: String,
|
||||||
pub signature: String,
|
pub signature: String,
|
||||||
pub permissions: ExtensionPermissions,
|
pub permissions: ExtensionPermissions,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub single_instance: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_entry_value() -> Option<String> {
|
||||||
|
Some("index.html".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtensionManifest {
|
impl ExtensionManifest {
|
||||||
@ -155,7 +162,6 @@ impl ExtensionPermissions {
|
|||||||
.and_then(|c| serde_json::from_value::<PermissionConstraints>(c.clone()).ok()),
|
.and_then(|c| serde_json::from_value::<PermissionConstraints>(c.clone()).ok()),
|
||||||
status: p.status.clone().unwrap_or(PermissionStatus::Ask),
|
status: p.status.clone().unwrap_or(PermissionStatus::Ask),
|
||||||
haex_timestamp: None,
|
haex_timestamp: None,
|
||||||
haex_tombstone: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,6 +179,8 @@ pub struct ExtensionInfoResponse {
|
|||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
|
pub entry: Option<String>,
|
||||||
|
pub single_instance: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub dev_server_url: Option<String>,
|
pub dev_server_url: Option<String>,
|
||||||
}
|
}
|
||||||
@ -198,6 +206,8 @@ impl ExtensionInfoResponse {
|
|||||||
description: extension.manifest.description.clone(),
|
description: extension.manifest.description.clone(),
|
||||||
homepage: extension.manifest.homepage.clone(),
|
homepage: extension.manifest.homepage.clone(),
|
||||||
icon: extension.manifest.icon.clone(),
|
icon: extension.manifest.icon.clone(),
|
||||||
|
entry: extension.manifest.entry.clone(),
|
||||||
|
single_instance: extension.manifest.single_instance,
|
||||||
dev_server_url,
|
dev_server_url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,28 +4,13 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
// src-tauri/src/extension/crypto.rs
|
// src-tauri/src/extension/crypto.rs
|
||||||
|
use crate::extension::error::ExtensionError;
|
||||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
pub struct ExtensionCrypto;
|
pub struct ExtensionCrypto;
|
||||||
|
|
||||||
impl ExtensionCrypto {
|
impl ExtensionCrypto {
|
||||||
/// Berechnet Hash vom Public Key (wie im SDK)
|
|
||||||
pub fn calculate_key_hash(public_key_hex: &str) -> Result<String, String> {
|
|
||||||
let public_key_bytes =
|
|
||||||
hex::decode(public_key_hex).map_err(|e| format!("Invalid public key hex: {}", e))?;
|
|
||||||
|
|
||||||
let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().unwrap())
|
|
||||||
.map_err(|e| format!("Invalid public key: {}", e))?;
|
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(public_key.as_bytes());
|
|
||||||
let result = hasher.finalize();
|
|
||||||
|
|
||||||
// Ersten 20 Hex-Zeichen (10 Bytes) - wie im SDK
|
|
||||||
Ok(hex::encode(&result[..10]))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verifiziert Extension-Signatur
|
/// Verifiziert Extension-Signatur
|
||||||
pub fn verify_signature(
|
pub fn verify_signature(
|
||||||
public_key_hex: &str,
|
public_key_hex: &str,
|
||||||
@ -50,26 +35,64 @@ impl ExtensionCrypto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Berechnet Hash eines Verzeichnisses (für Verifikation)
|
/// Berechnet Hash eines Verzeichnisses (für Verifikation)
|
||||||
pub fn hash_directory(dir: &Path) -> Result<String, String> {
|
pub fn hash_directory(dir: &Path, manifest_path: &Path) -> Result<String, ExtensionError> {
|
||||||
// 1. Alle Dateipfade rekursiv sammeln
|
// 1. Alle Dateipfade rekursiv sammeln
|
||||||
let mut all_files = Vec::new();
|
let mut all_files = Vec::new();
|
||||||
Self::collect_files_recursively(dir, &mut all_files)
|
Self::collect_files_recursively(dir, &mut all_files)
|
||||||
.map_err(|e| format!("Failed to collect files: {}", e))?;
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
all_files.sort();
|
|
||||||
|
// 2. Konvertiere zu relativen Pfaden für konsistente Sortierung (wie im SDK)
|
||||||
|
let mut relative_files: Vec<(String, PathBuf)> = all_files
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
let relative = path.strip_prefix(dir)
|
||||||
|
.unwrap_or(&path)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
// Normalisiere Pfad-Separatoren zu Unix-Style (/) für plattformübergreifende Konsistenz
|
||||||
|
.replace('\\', "/");
|
||||||
|
(relative, path)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// 3. Sortiere nach relativen Pfaden
|
||||||
|
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
let manifest_path = dir.join("manifest.json");
|
|
||||||
|
|
||||||
// 2. Inhalte der sortierten Dateien hashen
|
// Canonicalize manifest path for comparison (important on Android where symlinks may differ)
|
||||||
for file_path in all_files {
|
// Also ensure the canonical path is still within the allowed directory (security check)
|
||||||
if file_path == manifest_path {
|
let canonical_manifest_path = manifest_path.canonicalize()
|
||||||
|
.unwrap_or_else(|_| manifest_path.to_path_buf());
|
||||||
|
|
||||||
|
// Security: Verify canonical manifest path is still within dir
|
||||||
|
let canonical_dir = dir.canonicalize()
|
||||||
|
.unwrap_or_else(|_| dir.to_path_buf());
|
||||||
|
|
||||||
|
if !canonical_manifest_path.starts_with(&canonical_dir) {
|
||||||
|
return Err(ExtensionError::ManifestError {
|
||||||
|
reason: format!("Manifest path resolves outside of extension directory (potential path traversal)"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Inhalte der sortierten Dateien hashen
|
||||||
|
for (_relative, file_path) in relative_files {
|
||||||
|
// Canonicalize file_path for comparison
|
||||||
|
let canonical_file_path = file_path.canonicalize()
|
||||||
|
.unwrap_or_else(|_| file_path.clone());
|
||||||
|
|
||||||
|
if canonical_file_path == canonical_manifest_path {
|
||||||
// FÜR DIE MANIFEST.JSON:
|
// FÜR DIE MANIFEST.JSON:
|
||||||
let content_str = fs::read_to_string(&file_path)
|
let content_str = fs::read_to_string(&file_path)
|
||||||
.map_err(|e| format!("Cannot read manifest file: {}", e))?;
|
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
|
|
||||||
// Parse zu einem generischen JSON-Wert
|
// Parse zu einem generischen JSON-Wert
|
||||||
let mut manifest: serde_json::Value = serde_json::from_str(&content_str)
|
let mut manifest: serde_json::Value =
|
||||||
.map_err(|e| format!("Cannot parse manifest JSON: {}", e))?;
|
serde_json::from_str(&content_str).map_err(|e| {
|
||||||
|
ExtensionError::ManifestError {
|
||||||
|
reason: format!("Cannot parse manifest JSON: {}", e),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
// Entferne oder leere das Signaturfeld, um den "kanonischen Inhalt" zu erhalten
|
// Entferne oder leere das Signaturfeld, um den "kanonischen Inhalt" zu erhalten
|
||||||
if let Some(obj) = manifest.as_object_mut() {
|
if let Some(obj) = manifest.as_object_mut() {
|
||||||
@ -80,13 +103,23 @@ impl ExtensionCrypto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS)
|
// Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS)
|
||||||
let canonical_manifest_content = serde_json::to_string_pretty(&manifest).unwrap();
|
// serde_json sortiert die Keys automatisch alphabetisch
|
||||||
println!("canonical_manifest_content: {}", canonical_manifest_content);
|
let canonical_manifest_content =
|
||||||
hasher.update(canonical_manifest_content.as_bytes());
|
serde_json::to_string_pretty(&manifest).map_err(|e| {
|
||||||
|
ExtensionError::ManifestError {
|
||||||
|
reason: format!("Failed to serialize manifest: {}", e),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Normalisiere Zeilenenden zu Unix-Style (\n), wie Node.js JSON.stringify es macht
|
||||||
|
// Dies ist wichtig für plattformübergreifende Konsistenz (Desktop vs Android)
|
||||||
|
let normalized_content = canonical_manifest_content.replace("\r\n", "\n");
|
||||||
|
|
||||||
|
hasher.update(normalized_content.as_bytes());
|
||||||
} else {
|
} else {
|
||||||
// FÜR ALLE ANDEREN DATEIEN:
|
// FÜR ALLE ANDEREN DATEIEN:
|
||||||
let content = fs::read(&file_path)
|
let content =
|
||||||
.map_err(|e| format!("Cannot read file {}: {}", file_path.display(), e))?;
|
fs::read(&file_path).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||||
hasher.update(&content);
|
hasher.update(&content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,121 +1,33 @@
|
|||||||
// src-tauri/src/extension/database/executor.rs (neu)
|
// src-tauri/src/extension/database/executor.rs
|
||||||
|
|
||||||
use crate::crdt::hlc::HlcService;
|
use crate::crdt::hlc::HlcService;
|
||||||
use crate::crdt::transformer::CrdtTransformer;
|
use crate::crdt::transformer::CrdtTransformer;
|
||||||
use crate::crdt::trigger;
|
use crate::crdt::trigger;
|
||||||
use crate::database::core::{convert_value_ref_to_json, parse_sql_statements, ValueConverter};
|
use crate::database::core::{convert_value_ref_to_json, parse_sql_statements, ValueConverter};
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
use rusqlite::Connection;
|
|
||||||
use rusqlite::{params_from_iter, types::Value as SqliteValue, ToSql, Transaction};
|
use rusqlite::{params_from_iter, types::Value as SqliteValue, ToSql, Transaction};
|
||||||
use serde_json::{Map, Value as JsonValue};
|
use serde_json::Value as JsonValue;
|
||||||
use sqlparser::ast::{Insert, Statement, TableObject};
|
use sqlparser::ast::Statement;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashSet;
|
||||||
|
|
||||||
/// Repräsentiert PK-Werte für eine Zeile (kann single oder composite key sein)
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
struct PkValues {
|
|
||||||
/// column_name -> value
|
|
||||||
values: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PkValues {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
values: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert(&mut self, column: String, value: String) {
|
|
||||||
self.values.insert(column, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(&self, column: &str) -> Option<&String> {
|
|
||||||
self.values.get(column)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Context für PK-Remapping während einer Transaktion
|
|
||||||
/// Trackt für jede Tabelle: welche PKs sollten eingefügt werden vs. welche sind tatsächlich in der DB
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct PkRemappingContext {
|
|
||||||
/// Für jede Tabelle: Liste von (original_pk_values, actual_pk_values) Mappings
|
|
||||||
/// Wird nur gespeichert wenn original != actual (d.h. ON CONFLICT hat PK geändert)
|
|
||||||
mappings: HashMap<String, Vec<(PkValues, PkValues)>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PkRemappingContext {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fügt ein Mapping für eine Tabelle hinzu, aber nur wenn original != actual
|
|
||||||
/// original und actual sind die PK-Werte vor und nach dem INSERT
|
|
||||||
fn add_mapping(&mut self, table: String, original: PkValues, actual: PkValues) {
|
|
||||||
// Nur speichern wenn tatsächlich unterschiedlich (ON CONFLICT hat stattgefunden)
|
|
||||||
if original != actual {
|
|
||||||
eprintln!(
|
|
||||||
"DEBUG: PK Remapping for table '{}': {:?} -> {:?}",
|
|
||||||
table, original.values, actual.values
|
|
||||||
);
|
|
||||||
self.mappings
|
|
||||||
.entry(table)
|
|
||||||
.or_insert_with(Vec::new)
|
|
||||||
.push((original, actual));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Versucht einen FK-Wert zu remappen
|
|
||||||
/// referenced_table: Die Tabelle auf die der FK zeigt
|
|
||||||
/// referenced_column: Die PK-Spalte in der referenced_table
|
|
||||||
/// value: Der FK-Wert der ersetzt werden soll
|
|
||||||
fn remap_fk_value(
|
|
||||||
&self,
|
|
||||||
referenced_table: &str,
|
|
||||||
referenced_column: &str,
|
|
||||||
value: &str,
|
|
||||||
) -> String {
|
|
||||||
self.mappings
|
|
||||||
.get(referenced_table)
|
|
||||||
.and_then(|mappings| {
|
|
||||||
mappings.iter().find_map(|(original, actual)| {
|
|
||||||
if original.get(referenced_column)? == value {
|
|
||||||
let actual_val = actual.get(referenced_column)?.clone();
|
|
||||||
eprintln!(
|
|
||||||
"DEBUG: FK Remapping for {}.{}: {} -> {}",
|
|
||||||
referenced_table, referenced_column, value, actual_val
|
|
||||||
);
|
|
||||||
Some(actual_val)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// SQL-Executor OHNE Berechtigungsprüfung - für interne Nutzung
|
/// SQL-Executor OHNE Berechtigungsprüfung - für interne Nutzung
|
||||||
pub struct SqlExecutor;
|
pub struct SqlExecutor;
|
||||||
|
|
||||||
impl SqlExecutor {
|
impl SqlExecutor {
|
||||||
/// Führt ein SQL Statement OHNE RETURNING aus (mit CRDT und PK-Remapping)
|
/// Führt ein SQL Statement OHNE RETURNING aus (mit CRDT)
|
||||||
/// Unterstützt automatisches FK-Remapping wenn vorherige INSERTs ON CONFLICT getriggert haben
|
|
||||||
///
|
|
||||||
/// Diese Variante akzeptiert &[&dyn ToSql] direkt (wie von rusqlite::params![] erzeugt)
|
|
||||||
/// Returns: modified_schema_tables
|
/// Returns: modified_schema_tables
|
||||||
pub fn execute_internal_typed_with_context(
|
pub fn execute_internal_typed(
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
hlc_service: &HlcService,
|
hlc_service: &HlcService,
|
||||||
sql: &str,
|
sql: &str,
|
||||||
params: &[&dyn ToSql],
|
params: &[&dyn ToSql],
|
||||||
pk_context: &mut PkRemappingContext,
|
|
||||||
) -> Result<HashSet<String>, DatabaseError> {
|
) -> Result<HashSet<String>, DatabaseError> {
|
||||||
let mut ast_vec = parse_sql_statements(sql)?;
|
let mut ast_vec = parse_sql_statements(sql)?;
|
||||||
|
|
||||||
if ast_vec.len() != 1 {
|
if ast_vec.len() != 1 {
|
||||||
return Err(DatabaseError::ExecutionError {
|
return Err(DatabaseError::ExecutionError {
|
||||||
sql: sql.to_string(),
|
sql: sql.to_string(),
|
||||||
reason: "execute_internal_typed_with_context sollte nur ein einzelnes SQL-Statement erhalten"
|
reason: "execute_internal_typed should only receive a single SQL statement"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
table: None,
|
table: None,
|
||||||
});
|
});
|
||||||
@ -140,85 +52,45 @@ impl SqlExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let sql_str = statement.to_string();
|
let sql_str = statement.to_string();
|
||||||
eprintln!("DEBUG: Transformed SQL (execute path): {}", sql_str);
|
eprintln!("DEBUG: Transformed execute SQL: {}", sql_str);
|
||||||
|
|
||||||
// Spezielle Behandlung für INSERT Statements (mit FK-Remapping, OHNE RETURNING)
|
// Führe Statement aus
|
||||||
if let Statement::Insert(ref insert_stmt) = statement {
|
tx.execute(&sql_str, params)
|
||||||
if let TableObject::TableName(ref table_name) = insert_stmt.table {
|
.map_err(|e| DatabaseError::ExecutionError {
|
||||||
let table_name_str = table_name
|
sql: sql_str.clone(),
|
||||||
.to_string()
|
table: None,
|
||||||
.trim_matches('`')
|
reason: format!("Execute failed: {}", e),
|
||||||
.trim_matches('"')
|
})?;
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// Konvertiere Params zu Vec für Manipulation
|
|
||||||
let mut param_vec = params_to_vec(params, tx)?;
|
|
||||||
|
|
||||||
// Hole Foreign Key Informationen
|
|
||||||
let fk_info = get_fk_info(tx, &table_name_str)?;
|
|
||||||
|
|
||||||
// Remap FK-Werte in params (falls Mappings existieren)
|
|
||||||
remap_fk_params(insert_stmt, &mut param_vec, &fk_info, pk_context)?;
|
|
||||||
|
|
||||||
let param_refs: Vec<&dyn ToSql> =
|
|
||||||
param_vec.iter().map(|v| v as &dyn ToSql).collect();
|
|
||||||
|
|
||||||
let mut stmt = tx
|
|
||||||
.prepare(&sql_str)
|
|
||||||
.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: sql_str.clone(),
|
|
||||||
table: Some(table_name_str.clone()),
|
|
||||||
reason: format!("Prepare failed: {}", e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut rows = stmt
|
|
||||||
.query(params_from_iter(param_refs.iter()))
|
|
||||||
.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: sql_str.clone(),
|
|
||||||
table: Some(table_name_str.clone()),
|
|
||||||
reason: format!("INSERT query execution failed: {}", e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let _ = rows.next()?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Nicht-INSERT Statements normal ausführen
|
|
||||||
tx.execute(&sql_str, params)
|
|
||||||
.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: sql_str.clone(),
|
|
||||||
table: None,
|
|
||||||
reason: format!("Execute failed: {}", e),
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger-Logik für CREATE TABLE
|
// Trigger-Logik für CREATE TABLE
|
||||||
if let Statement::CreateTable(create_table_details) = statement {
|
if let Statement::CreateTable(create_table_details) = statement {
|
||||||
let table_name_str = create_table_details.name.to_string();
|
let raw_name = create_table_details.name.to_string();
|
||||||
|
// Remove quotes from table name
|
||||||
|
let table_name_str = raw_name
|
||||||
|
.trim_matches('"')
|
||||||
|
.trim_matches('`')
|
||||||
|
.to_string();
|
||||||
|
eprintln!("DEBUG: Setting up triggers for table: {}", table_name_str);
|
||||||
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(modified_schema_tables)
|
Ok(modified_schema_tables)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Führt ein SQL Statement MIT RETURNING aus (mit CRDT und PK-Remapping)
|
/// Führt ein SQL Statement MIT RETURNING aus (mit CRDT)
|
||||||
/// Unterstützt automatisches FK-Remapping wenn vorherige INSERTs ON CONFLICT getriggert haben
|
|
||||||
///
|
|
||||||
/// Diese Variante akzeptiert &[&dyn ToSql] direkt (wie von rusqlite::params![] erzeugt)
|
|
||||||
/// Returns: (modified_schema_tables, returning_results)
|
/// Returns: (modified_schema_tables, returning_results)
|
||||||
/// returning_results enthält ALLE RETURNING-Spalten für INSERT/UPDATE/DELETE mit RETURNING
|
pub fn query_internal_typed(
|
||||||
pub fn query_internal_typed_with_context(
|
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
hlc_service: &HlcService,
|
hlc_service: &HlcService,
|
||||||
sql: &str,
|
sql: &str,
|
||||||
params: &[&dyn ToSql],
|
params: &[&dyn ToSql],
|
||||||
pk_context: &mut PkRemappingContext,
|
|
||||||
) -> Result<(HashSet<String>, Vec<Vec<JsonValue>>), DatabaseError> {
|
) -> Result<(HashSet<String>, Vec<Vec<JsonValue>>), DatabaseError> {
|
||||||
let mut ast_vec = parse_sql_statements(sql)?;
|
let mut ast_vec = parse_sql_statements(sql)?;
|
||||||
|
|
||||||
if ast_vec.len() != 1 {
|
if ast_vec.len() != 1 {
|
||||||
return Err(DatabaseError::ExecutionError {
|
return Err(DatabaseError::ExecutionError {
|
||||||
sql: sql.to_string(),
|
sql: sql.to_string(),
|
||||||
reason: "query_internal_typed_with_context sollte nur ein einzelnes SQL-Statement erhalten"
|
reason: "query_internal_typed should only receive a single SQL statement"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
table: None,
|
table: None,
|
||||||
});
|
});
|
||||||
@ -245,493 +117,168 @@ impl SqlExecutor {
|
|||||||
let sql_str = statement.to_string();
|
let sql_str = statement.to_string();
|
||||||
eprintln!("DEBUG: Transformed SQL (with RETURNING): {}", sql_str);
|
eprintln!("DEBUG: Transformed SQL (with RETURNING): {}", sql_str);
|
||||||
|
|
||||||
// Spezielle Behandlung für INSERT Statements (mit PK-Remapping + RETURNING)
|
// Prepare und query ausführen
|
||||||
if let Statement::Insert(ref insert_stmt) = statement {
|
|
||||||
if let TableObject::TableName(ref table_name) = insert_stmt.table {
|
|
||||||
let table_name_str = table_name
|
|
||||||
.to_string()
|
|
||||||
.trim_matches('`')
|
|
||||||
.trim_matches('"')
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// Konvertiere Params zu Vec für Manipulation
|
|
||||||
let mut param_vec = params_to_vec(params, tx)?;
|
|
||||||
|
|
||||||
// Hole Table Schema um PKs und FKs zu identifizieren
|
|
||||||
let table_columns =
|
|
||||||
trigger::get_table_schema(tx, &table_name_str).map_err(|e| {
|
|
||||||
DatabaseError::ExecutionError {
|
|
||||||
sql: format!("PRAGMA table_info('{}')", table_name_str),
|
|
||||||
reason: e.to_string(),
|
|
||||||
table: Some(table_name_str.clone()),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let pk_columns: Vec<String> = table_columns
|
|
||||||
.iter()
|
|
||||||
.filter(|c| c.is_pk)
|
|
||||||
.map(|c| c.name.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Hole Foreign Key Informationen
|
|
||||||
let fk_info = get_fk_info(tx, &table_name_str)?;
|
|
||||||
|
|
||||||
// 1. Extrahiere Original PK-Werte aus params (vor FK-Remapping)
|
|
||||||
let original_pk =
|
|
||||||
extract_pk_values_from_params(insert_stmt, ¶m_vec, &pk_columns)?;
|
|
||||||
|
|
||||||
// 2. Remap FK-Werte in params (falls Mappings existieren)
|
|
||||||
remap_fk_params(insert_stmt, &mut param_vec, &fk_info, pk_context)?;
|
|
||||||
|
|
||||||
// 3. Führe INSERT mit query() aus um RETURNING zu lesen
|
|
||||||
let mut stmt = tx
|
|
||||||
.prepare(&sql_str)
|
|
||||||
.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: sql_str.clone(),
|
|
||||||
table: Some(table_name_str.clone()),
|
|
||||||
reason: e.to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let column_names: Vec<String> = stmt
|
|
||||||
.column_names()
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.collect();
|
|
||||||
let num_columns = column_names.len();
|
|
||||||
|
|
||||||
let param_refs: Vec<&dyn ToSql> =
|
|
||||||
param_vec.iter().map(|v| v as &dyn ToSql).collect();
|
|
||||||
|
|
||||||
let mut rows = stmt
|
|
||||||
.query(params_from_iter(param_refs.iter()))
|
|
||||||
.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: sql_str.clone(),
|
|
||||||
table: Some(table_name_str.clone()),
|
|
||||||
reason: e.to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
|
||||||
|
|
||||||
// 4. Lese ALLE RETURNING Werte und speichere PK-Mapping
|
|
||||||
while let Some(row) = rows.next().map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: sql_str.clone(),
|
|
||||||
table: Some(table_name_str.clone()),
|
|
||||||
reason: e.to_string(),
|
|
||||||
})? {
|
|
||||||
// Extrahiere PK-Werte für PK-Remapping
|
|
||||||
let actual_pk = extract_pk_values_from_row(&row, &pk_columns)?;
|
|
||||||
pk_context.add_mapping(
|
|
||||||
table_name_str.clone(),
|
|
||||||
original_pk.clone(),
|
|
||||||
actual_pk.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Extrahiere ALLE Spalten für RETURNING-Ergebnis
|
|
||||||
let mut row_values: Vec<JsonValue> = Vec::with_capacity(num_columns);
|
|
||||||
|
|
||||||
for i in 0..num_columns {
|
|
||||||
let value_ref =
|
|
||||||
row.get_ref(i)
|
|
||||||
.map_err(|e| DatabaseError::RowProcessingError {
|
|
||||||
reason: format!("Failed to get column {}: {}", i, e),
|
|
||||||
})?;
|
|
||||||
let json_val = convert_value_ref_to_json(value_ref)?;
|
|
||||||
row_values.push(json_val);
|
|
||||||
}
|
|
||||||
result_vec.push(row_values);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok((modified_schema_tables, result_vec));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Für UPDATE/DELETE mit RETURNING: query() verwenden (kein PK-Remapping nötig)
|
|
||||||
let mut stmt = tx
|
let mut stmt = tx
|
||||||
.prepare(&sql_str)
|
.prepare(&sql_str)
|
||||||
.map_err(|e| DatabaseError::PrepareError {
|
.map_err(|e| DatabaseError::ExecutionError {
|
||||||
|
sql: sql_str.clone(),
|
||||||
|
table: None,
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let num_columns = stmt.column_count();
|
let column_names: Vec<String> = stmt
|
||||||
|
.column_names()
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
let num_columns = column_names.len();
|
||||||
|
|
||||||
let mut rows = stmt.query(params).map_err(|e| DatabaseError::QueryError {
|
let mut rows = stmt
|
||||||
reason: e.to_string(),
|
.query(params_from_iter(params.iter()))
|
||||||
})?;
|
.map_err(|e| DatabaseError::ExecutionError {
|
||||||
|
sql: sql_str.clone(),
|
||||||
|
table: None,
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
||||||
|
|
||||||
while let Some(row) = rows.next().map_err(|e| DatabaseError::RowProcessingError {
|
// Lese alle RETURNING Zeilen
|
||||||
reason: format!("Row iteration error: {}", e),
|
while let Some(row) = rows.next().map_err(|e| DatabaseError::ExecutionError {
|
||||||
|
sql: sql_str.clone(),
|
||||||
|
table: None,
|
||||||
|
reason: e.to_string(),
|
||||||
})? {
|
})? {
|
||||||
let mut row_values: Vec<JsonValue> = Vec::with_capacity(num_columns);
|
let mut row_values: Vec<JsonValue> = Vec::new();
|
||||||
|
|
||||||
for i in 0..num_columns {
|
for i in 0..num_columns {
|
||||||
let value_ref = row
|
let value_ref = row.get_ref(i).map_err(|e| DatabaseError::ExecutionError {
|
||||||
.get_ref(i)
|
sql: sql_str.clone(),
|
||||||
.map_err(|e| DatabaseError::RowProcessingError {
|
table: None,
|
||||||
reason: format!("Failed to get column {}: {}", i, e),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
let json_value = convert_value_ref_to_json(value_ref)?;
|
||||||
let json_val = convert_value_ref_to_json(value_ref)?;
|
row_values.push(json_value);
|
||||||
row_values.push(json_val);
|
|
||||||
}
|
}
|
||||||
result_vec.push(row_values);
|
result_vec.push(row_values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger-Logik für CREATE TABLE
|
||||||
|
if let Statement::CreateTable(create_table_details) = statement {
|
||||||
|
let raw_name = create_table_details.name.to_string();
|
||||||
|
// Remove quotes from table name
|
||||||
|
let table_name_str = raw_name
|
||||||
|
.trim_matches('"')
|
||||||
|
.trim_matches('`')
|
||||||
|
.to_string();
|
||||||
|
eprintln!("DEBUG: Setting up triggers for table (RETURNING): {}", table_name_str);
|
||||||
|
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok((modified_schema_tables, result_vec))
|
Ok((modified_schema_tables, result_vec))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Legacy-Methode ohne PK-Remapping Context
|
/// Führt ein einzelnes SQL Statement OHNE Typinformationen aus (JSON params)
|
||||||
pub fn execute_internal_typed(
|
|
||||||
tx: &Transaction,
|
|
||||||
hlc_service: &HlcService,
|
|
||||||
sql: &str,
|
|
||||||
params: &[&dyn ToSql],
|
|
||||||
) -> Result<HashSet<String>, DatabaseError> {
|
|
||||||
let mut context = PkRemappingContext::new();
|
|
||||||
Self::execute_internal_typed_with_context(tx, hlc_service, sql, params, &mut context)
|
|
||||||
}
|
|
||||||
/// Führt SQL aus (mit CRDT-Transformation) - OHNE Permission-Check
|
|
||||||
/// Wrapper um execute_internal_typed für JsonValue-Parameter
|
|
||||||
/// Nutzt PK-Remapping Logik für INSERT mit ON CONFLICT
|
|
||||||
pub fn execute_internal(
|
pub fn execute_internal(
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
hlc_service: &HlcService,
|
hlc_service: &HlcService,
|
||||||
sql: &str,
|
sql: &str,
|
||||||
params: &[JsonValue],
|
params: &[JsonValue],
|
||||||
) -> Result<HashSet<String>, DatabaseError> {
|
) -> Result<HashSet<String>, DatabaseError> {
|
||||||
// Parameter validation
|
let sql_params: Vec<SqliteValue> = params
|
||||||
let total_placeholders = sql.matches('?').count();
|
|
||||||
if total_placeholders != params.len() {
|
|
||||||
return Err(DatabaseError::ParameterMismatchError {
|
|
||||||
expected: total_placeholders,
|
|
||||||
provided: params.len(),
|
|
||||||
sql: sql.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert JsonValue params to SqliteValue
|
|
||||||
let params_converted: Vec<SqliteValue> = params
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(ValueConverter::json_to_rusqlite_value)
|
.map(|v| crate::database::core::ValueConverter::json_to_rusqlite_value(v))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let param_refs: Vec<&dyn ToSql> = sql_params.iter().map(|p| p as &dyn ToSql).collect();
|
||||||
// Convert to &dyn ToSql references
|
|
||||||
let param_refs: Vec<&dyn ToSql> =
|
|
||||||
params_converted.iter().map(|v| v as &dyn ToSql).collect();
|
|
||||||
|
|
||||||
// Call execute_internal_typed (mit PK-Remapping!)
|
|
||||||
Self::execute_internal_typed(tx, hlc_service, sql, ¶m_refs)
|
Self::execute_internal_typed(tx, hlc_service, sql, ¶m_refs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Führt SELECT aus (mit CRDT-Transformation) - OHNE Permission-Check
|
/// Query-Variante (mit RETURNING) OHNE Typinformationen (JSON params)
|
||||||
pub fn select_internal(
|
pub fn query_internal(
|
||||||
conn: &Connection,
|
tx: &Transaction,
|
||||||
|
hlc_service: &HlcService,
|
||||||
sql: &str,
|
sql: &str,
|
||||||
params: &[JsonValue],
|
params: &[JsonValue],
|
||||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
) -> Result<(HashSet<String>, Vec<Vec<JsonValue>>), DatabaseError> {
|
||||||
// Parameter validation
|
let sql_params: Vec<SqliteValue> = params
|
||||||
let total_placeholders = sql.matches('?').count();
|
.iter()
|
||||||
if total_placeholders != params.len() {
|
.map(|v| crate::database::core::ValueConverter::json_to_rusqlite_value(v))
|
||||||
return Err(DatabaseError::ParameterMismatchError {
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
expected: total_placeholders,
|
let param_refs: Vec<&dyn ToSql> = sql_params.iter().map(|p| p as &dyn ToSql).collect();
|
||||||
provided: params.len(),
|
Self::query_internal_typed(tx, hlc_service, sql, ¶m_refs)
|
||||||
sql: sql.to_string(),
|
}
|
||||||
|
|
||||||
|
/// Führt mehrere SQL Statements als Batch aus
|
||||||
|
pub fn execute_batch_internal(
|
||||||
|
tx: &Transaction,
|
||||||
|
hlc_service: &HlcService,
|
||||||
|
sqls: &[String],
|
||||||
|
params: &[Vec<JsonValue>],
|
||||||
|
) -> Result<HashSet<String>, DatabaseError> {
|
||||||
|
if sqls.len() != params.len() {
|
||||||
|
return Err(DatabaseError::ExecutionError {
|
||||||
|
sql: format!("{} statements but {} param sets", sqls.len(), params.len()),
|
||||||
|
reason: "Statement count and parameter count mismatch".to_string(),
|
||||||
|
table: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut all_modified_tables = HashSet::new();
|
||||||
|
|
||||||
|
for (sql, param_set) in sqls.iter().zip(params.iter()) {
|
||||||
|
let modified_tables = Self::execute_internal(tx, hlc_service, sql, param_set)?;
|
||||||
|
all_modified_tables.extend(modified_tables);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(all_modified_tables)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query für SELECT-Statements (read-only, kein CRDT nötig außer Filter)
|
||||||
|
pub fn query_select(
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
sql: &str,
|
||||||
|
params: &[JsonValue],
|
||||||
|
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||||
let mut ast_vec = parse_sql_statements(sql)?;
|
let mut ast_vec = parse_sql_statements(sql)?;
|
||||||
|
|
||||||
if ast_vec.is_empty() {
|
if ast_vec.len() != 1 {
|
||||||
return Ok(vec![]);
|
return Err(DatabaseError::ExecutionError {
|
||||||
|
sql: sql.to_string(),
|
||||||
|
reason: "query_select should only receive a single SELECT statement".to_string(),
|
||||||
|
table: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that all statements are queries
|
|
||||||
for stmt in &ast_vec {
|
|
||||||
if !matches!(stmt, Statement::Query(_)) {
|
|
||||||
return Err(DatabaseError::ExecutionError {
|
|
||||||
sql: sql.to_string(),
|
|
||||||
reason: "Only SELECT statements are allowed".to_string(),
|
|
||||||
table: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sql_params = ValueConverter::convert_params(params)?;
|
|
||||||
|
|
||||||
// Hard Delete: Keine SELECT-Transformation mehr nötig
|
// Hard Delete: Keine SELECT-Transformation mehr nötig
|
||||||
let stmt_to_execute = ast_vec.pop().unwrap();
|
let stmt_to_execute = ast_vec.pop().unwrap();
|
||||||
let transformed_sql = stmt_to_execute.to_string();
|
let transformed_sql = stmt_to_execute.to_string();
|
||||||
|
|
||||||
eprintln!("DEBUG: SELECT (no transformation): {}", transformed_sql);
|
eprintln!("DEBUG: SELECT (no transformation): {}", transformed_sql);
|
||||||
|
|
||||||
|
// Convert JSON params to SQLite values
|
||||||
|
let sql_params: Vec<SqliteValue> = params
|
||||||
|
.iter()
|
||||||
|
.map(|v| crate::database::core::ValueConverter::json_to_rusqlite_value(v))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
let mut prepared_stmt = conn.prepare(&transformed_sql)?;
|
let mut prepared_stmt = conn.prepare(&transformed_sql)?;
|
||||||
|
|
||||||
let num_columns = prepared_stmt.column_count();
|
let num_columns = prepared_stmt.column_count();
|
||||||
|
|
||||||
let mut rows = prepared_stmt
|
let param_refs: Vec<&dyn ToSql> = sql_params.iter().map(|p| p as &dyn ToSql).collect();
|
||||||
.query(params_from_iter(&sql_params[..]))
|
|
||||||
.map_err(|e| DatabaseError::QueryError {
|
|
||||||
reason: e.to_string(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
let mut rows = prepared_stmt.query(params_from_iter(param_refs.iter()))?;
|
||||||
|
|
||||||
while let Some(row) = rows.next().map_err(|e| DatabaseError::RowProcessingError {
|
|
||||||
reason: format!("Row iteration error: {}", e),
|
|
||||||
})? {
|
|
||||||
let mut row_values: Vec<JsonValue> = Vec::with_capacity(num_columns);
|
|
||||||
|
|
||||||
|
let mut result: Vec<Vec<JsonValue>> = Vec::new();
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
let mut row_values: Vec<JsonValue> = Vec::new();
|
||||||
for i in 0..num_columns {
|
for i in 0..num_columns {
|
||||||
let value_ref = row
|
let value_ref = row.get_ref(i)?;
|
||||||
.get_ref(i)
|
let json_value = convert_value_ref_to_json(value_ref)?;
|
||||||
.map_err(|e| DatabaseError::RowProcessingError {
|
row_values.push(json_value);
|
||||||
reason: format!("Failed to get column {}: {}", i, e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let json_val = convert_value_ref_to_json(value_ref)?;
|
|
||||||
row_values.push(json_val);
|
|
||||||
}
|
}
|
||||||
result_vec.push(row_values);
|
result.push(row_values);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result_vec)
|
Ok(result)
|
||||||
}
|
|
||||||
|
|
||||||
/// Führt SQL mit CRDT-Transformation aus und gibt RETURNING-Ergebnisse zurück
|
|
||||||
/// Speziell für INSERT/UPDATE/DELETE mit RETURNING (Drizzle-Integration)
|
|
||||||
/// Nutzt PK-Remapping für INSERT-Operationen
|
|
||||||
pub fn query_internal(
|
|
||||||
tx: &Transaction,
|
|
||||||
hlc_service: &HlcService,
|
|
||||||
sql: &str,
|
|
||||||
params: &[JsonValue],
|
|
||||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
|
||||||
// Parameter validation
|
|
||||||
let total_placeholders = sql.matches('?').count();
|
|
||||||
if total_placeholders != params.len() {
|
|
||||||
return Err(DatabaseError::ParameterMismatchError {
|
|
||||||
expected: total_placeholders,
|
|
||||||
provided: params.len(),
|
|
||||||
sql: sql.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameter konvertieren
|
|
||||||
let params_converted: Vec<SqliteValue> = params
|
|
||||||
.iter()
|
|
||||||
.map(ValueConverter::json_to_rusqlite_value)
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
// Convert to &dyn ToSql references
|
|
||||||
let param_refs: Vec<&dyn ToSql> =
|
|
||||||
params_converted.iter().map(|v| v as &dyn ToSql).collect();
|
|
||||||
|
|
||||||
// Call query_internal_typed_with_context (mit PK-Remapping!)
|
|
||||||
let mut context = PkRemappingContext::new();
|
|
||||||
let (_tables, results) = Self::query_internal_typed_with_context(
|
|
||||||
tx,
|
|
||||||
hlc_service,
|
|
||||||
sql,
|
|
||||||
¶m_refs,
|
|
||||||
&mut context,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(results)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
|
||||||
// Helper-Funktionen für FK-Remapping
|
|
||||||
// =========================
|
|
||||||
|
|
||||||
/// Strukturiert FK-Informationen für einfache Lookups
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct FkInfo {
|
|
||||||
/// column_name -> (referenced_table, referenced_column)
|
|
||||||
mappings: HashMap<String, (String, String)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hole Foreign Key Informationen für eine Tabelle
|
|
||||||
fn get_fk_info(tx: &Transaction, table_name: &str) -> Result<FkInfo, DatabaseError> {
|
|
||||||
// Nutze PRAGMA foreign_key_list um FK-Beziehungen zu holen
|
|
||||||
let sql = format!("PRAGMA foreign_key_list('{}');", table_name);
|
|
||||||
let mut stmt = tx
|
|
||||||
.prepare(&sql)
|
|
||||||
.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: sql.clone(),
|
|
||||||
reason: e.to_string(),
|
|
||||||
table: Some(table_name.to_string()),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut mappings = HashMap::new();
|
|
||||||
let rows = stmt
|
|
||||||
.query_map([], |row| {
|
|
||||||
Ok((
|
|
||||||
row.get::<_, String>("from")?, // FK column in this table
|
|
||||||
row.get::<_, String>("table")?, // referenced table
|
|
||||||
row.get::<_, String>("to")?, // referenced column
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql,
|
|
||||||
reason: e.to_string(),
|
|
||||||
table: Some(table_name.to_string()),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for row in rows {
|
|
||||||
let (from_col, ref_table, ref_col) = row.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: format!("PRAGMA foreign_key_list('{}')", table_name),
|
|
||||||
reason: e.to_string(),
|
|
||||||
table: Some(table_name.to_string()),
|
|
||||||
})?;
|
|
||||||
mappings.insert(from_col, (ref_table, ref_col));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(FkInfo { mappings })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Konvertiert &[&dyn ToSql] zu Vec<SqliteValue> für Manipulation
|
|
||||||
/// Nutzt einen Dummy-Query um die Parameter-Werte zu extrahieren
|
|
||||||
fn params_to_vec(
|
|
||||||
params: &[&dyn ToSql],
|
|
||||||
tx: &Transaction,
|
|
||||||
) -> Result<Vec<SqliteValue>, DatabaseError> {
|
|
||||||
let mut values = Vec::new();
|
|
||||||
|
|
||||||
// Erstelle eine Dummy-Query mit genau so vielen Platzhaltern wie wir Parameter haben
|
|
||||||
// z.B. "SELECT ?, ?, ?"
|
|
||||||
if params.is_empty() {
|
|
||||||
return Ok(values);
|
|
||||||
}
|
|
||||||
|
|
||||||
let placeholders = vec!["?"; params.len()].join(", ");
|
|
||||||
let dummy_sql = format!("SELECT {}", placeholders);
|
|
||||||
|
|
||||||
let mut stmt = tx
|
|
||||||
.prepare(&dummy_sql)
|
|
||||||
.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: dummy_sql.clone(),
|
|
||||||
reason: format!("Failed to prepare dummy query: {}", e),
|
|
||||||
table: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Führe die Query aus und extrahiere die Werte aus der Row
|
|
||||||
let mut rows = stmt
|
|
||||||
.query(params)
|
|
||||||
.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: dummy_sql.clone(),
|
|
||||||
reason: format!("Failed to execute dummy query: {}", e),
|
|
||||||
table: None,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if let Some(row) = rows.next().map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: dummy_sql,
|
|
||||||
reason: format!("Failed to read dummy query result: {}", e),
|
|
||||||
table: None,
|
|
||||||
})? {
|
|
||||||
// Extrahiere alle Spalten-Werte
|
|
||||||
for i in 0..params.len() {
|
|
||||||
let value: SqliteValue = row.get(i).map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: format!("SELECT ..."),
|
|
||||||
reason: format!("Failed to extract value at index {}: {}", i, e),
|
|
||||||
table: None,
|
|
||||||
})?;
|
|
||||||
values.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extrahiert PK-Werte aus den INSERT-Parametern
|
|
||||||
fn extract_pk_values_from_params(
|
|
||||||
insert_stmt: &Insert,
|
|
||||||
params: &[SqliteValue],
|
|
||||||
pk_columns: &[String],
|
|
||||||
) -> Result<PkValues, DatabaseError> {
|
|
||||||
let mut pk_values = PkValues::new();
|
|
||||||
|
|
||||||
// Finde die Positionen der PK-Spalten in der INSERT column list
|
|
||||||
for pk_col in pk_columns {
|
|
||||||
if let Some(pos) = insert_stmt.columns.iter().position(|c| &c.value == pk_col) {
|
|
||||||
// Hole den Parameter-Wert an dieser Position
|
|
||||||
if pos < params.len() {
|
|
||||||
// Konvertiere SqliteValue zu String
|
|
||||||
let value_str = value_to_string(¶ms[pos]);
|
|
||||||
pk_values.insert(pk_col.clone(), value_str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(pk_values)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remapped FK-Werte in den Parametern basierend auf dem PK-Remapping Context
|
|
||||||
fn remap_fk_params(
|
|
||||||
insert_stmt: &Insert,
|
|
||||||
params: &mut Vec<SqliteValue>,
|
|
||||||
fk_info: &FkInfo,
|
|
||||||
pk_context: &PkRemappingContext,
|
|
||||||
) -> Result<(), DatabaseError> {
|
|
||||||
// Für jede FK-Spalte: prüfe ob Remapping nötig ist
|
|
||||||
for (col_name, (ref_table, ref_col)) in &fk_info.mappings {
|
|
||||||
// Finde Position der FK-Spalte in der INSERT column list
|
|
||||||
if let Some(pos) = insert_stmt
|
|
||||||
.columns
|
|
||||||
.iter()
|
|
||||||
.position(|c| &c.value == col_name)
|
|
||||||
{
|
|
||||||
if pos < params.len() {
|
|
||||||
// Hole aktuellen FK-Wert (als String)
|
|
||||||
let current_value = value_to_string(¶ms[pos]);
|
|
||||||
|
|
||||||
// Versuche zu remappen
|
|
||||||
let new_value = pk_context.remap_fk_value(ref_table, ref_col, ¤t_value);
|
|
||||||
|
|
||||||
if new_value != current_value {
|
|
||||||
// Ersetze den Parameter-Wert
|
|
||||||
params[pos] = SqliteValue::Text(new_value);
|
|
||||||
eprintln!(
|
|
||||||
"DEBUG: Remapped FK {}={} to {:?}",
|
|
||||||
col_name, current_value, params[pos]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hilfsfunktion: Konvertiert SqliteValue zu String für Vergleiche
|
|
||||||
fn value_to_string(value: &SqliteValue) -> String {
|
|
||||||
match value {
|
|
||||||
SqliteValue::Null => "NULL".to_string(),
|
|
||||||
SqliteValue::Integer(i) => i.to_string(),
|
|
||||||
SqliteValue::Real(r) => r.to_string(),
|
|
||||||
SqliteValue::Text(s) => s.clone(),
|
|
||||||
SqliteValue::Blob(b) => format!("BLOB({} bytes)", b.len()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extrahiert PK-Werte aus einer RETURNING Row
|
|
||||||
fn extract_pk_values_from_row(
|
|
||||||
row: &rusqlite::Row,
|
|
||||||
pk_columns: &[String],
|
|
||||||
) -> Result<PkValues, DatabaseError> {
|
|
||||||
let mut pk_values = PkValues::new();
|
|
||||||
|
|
||||||
for pk_col in pk_columns.iter() {
|
|
||||||
let value: String =
|
|
||||||
row.get(pk_col.as_str())
|
|
||||||
.map_err(|e| DatabaseError::ExecutionError {
|
|
||||||
sql: "RETURNING clause".to_string(),
|
|
||||||
reason: format!("Failed to extract PK column '{}': {}", pk_col, e),
|
|
||||||
table: None,
|
|
||||||
})?;
|
|
||||||
pk_values.insert(pk_col.clone(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(pk_values)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use crate::crdt::transformer::CrdtTransformer;
|
|||||||
use crate::crdt::trigger;
|
use crate::crdt::trigger;
|
||||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||||
use crate::database::error::DatabaseError;
|
use crate::database::error::DatabaseError;
|
||||||
|
use crate::extension::database::executor::SqlExecutor;
|
||||||
use crate::extension::error::ExtensionError;
|
use crate::extension::error::ExtensionError;
|
||||||
use crate::extension::permissions::validator::SqlPermissionValidator;
|
use crate::extension::permissions::validator::SqlPermissionValidator;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
@ -110,7 +111,7 @@ pub async fn extension_sql_execute(
|
|||||||
public_key: String,
|
public_key: String,
|
||||||
name: String,
|
name: String,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<Vec<String>, ExtensionError> {
|
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||||
// Get extension to retrieve its ID
|
// Get extension to retrieve its ID
|
||||||
let extension = state
|
let extension = state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
@ -129,58 +130,87 @@ pub async fn extension_sql_execute(
|
|||||||
// SQL parsing
|
// SQL parsing
|
||||||
let mut ast_vec = parse_sql_statements(sql)?;
|
let mut ast_vec = parse_sql_statements(sql)?;
|
||||||
|
|
||||||
|
if ast_vec.len() != 1 {
|
||||||
|
return Err(ExtensionError::Database {
|
||||||
|
source: DatabaseError::ExecutionError {
|
||||||
|
sql: sql.to_string(),
|
||||||
|
reason: "extension_sql_execute should only receive a single SQL statement"
|
||||||
|
.to_string(),
|
||||||
|
table: None,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut statement = ast_vec.pop().unwrap();
|
||||||
|
|
||||||
|
// Check if statement has RETURNING clause
|
||||||
|
let has_returning = crate::database::core::statement_has_returning(&statement);
|
||||||
|
|
||||||
// Database operation
|
// Database operation
|
||||||
with_connection(&state.db, |conn| {
|
with_connection(&state.db, |conn| {
|
||||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
let transformer = CrdtTransformer::new();
|
let transformer = CrdtTransformer::new();
|
||||||
let executor = StatementExecutor::new(&tx);
|
|
||||||
|
// Get HLC service reference
|
||||||
|
let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned {
|
||||||
|
reason: "Failed to lock HLC service".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
// Generate HLC timestamp
|
// Generate HLC timestamp
|
||||||
let hlc_timestamp = state
|
let hlc_timestamp = hlc_service
|
||||||
.hlc
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.new_timestamp_and_persist(&tx)
|
.new_timestamp_and_persist(&tx)
|
||||||
.map_err(|e| DatabaseError::HlcError {
|
.map_err(|e| DatabaseError::HlcError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Transform statements
|
// Transform statement
|
||||||
let mut modified_schema_tables = HashSet::new();
|
transformer.transform_execute_statement(&mut statement, &hlc_timestamp)?;
|
||||||
for statement in &mut ast_vec {
|
|
||||||
if let Some(table_name) =
|
|
||||||
transformer.transform_execute_statement(statement, &hlc_timestamp)?
|
|
||||||
{
|
|
||||||
modified_schema_tables.insert(table_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert parameters
|
// Convert parameters to references
|
||||||
let sql_values = ValueConverter::convert_params(¶ms)?;
|
let sql_values = ValueConverter::convert_params(¶ms)?;
|
||||||
|
let param_refs: Vec<&dyn rusqlite::ToSql> = sql_values.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
|
||||||
|
|
||||||
// Execute statements
|
let result = if has_returning {
|
||||||
for statement in ast_vec {
|
// Use query_internal for statements with RETURNING
|
||||||
executor.execute_statement_with_params(&statement, &sql_values)?;
|
let (_, rows) = SqlExecutor::query_internal_typed(&tx, &hlc_service, &statement.to_string(), ¶m_refs)?;
|
||||||
|
rows
|
||||||
|
} else {
|
||||||
|
// Use execute_internal for statements without RETURNING
|
||||||
|
SqlExecutor::execute_internal_typed(&tx, &hlc_service, &statement.to_string(), ¶m_refs)?;
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
if let Statement::CreateTable(create_table_details) = statement {
|
// Handle CREATE TABLE trigger setup
|
||||||
let table_name_str = create_table_details.name.to_string();
|
if let Statement::CreateTable(ref create_table_details) = statement {
|
||||||
println!(
|
// Extract table name and remove quotes (both " and `)
|
||||||
"Table '{}' created by extension, setting up CRDT triggers...",
|
let raw_name = create_table_details.name.to_string();
|
||||||
table_name_str
|
println!("DEBUG: Raw table name from AST: {:?}", raw_name);
|
||||||
);
|
println!("DEBUG: Raw table name chars: {:?}", raw_name.chars().collect::<Vec<_>>());
|
||||||
trigger::setup_triggers_for_table(&tx, &table_name_str, false)?;
|
|
||||||
println!(
|
let table_name_str = raw_name
|
||||||
"Triggers for table '{}' successfully created.",
|
.trim_matches('"')
|
||||||
table_name_str
|
.trim_matches('`')
|
||||||
);
|
.to_string();
|
||||||
}
|
|
||||||
|
println!("DEBUG: Cleaned table name: {:?}", table_name_str);
|
||||||
|
println!("DEBUG: Cleaned table name chars: {:?}", table_name_str.chars().collect::<Vec<_>>());
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Table '{}' created by extension, setting up CRDT triggers...",
|
||||||
|
table_name_str
|
||||||
|
);
|
||||||
|
trigger::setup_triggers_for_table(&tx, &table_name_str, false)?;
|
||||||
|
println!(
|
||||||
|
"Triggers for table '{}' successfully created.",
|
||||||
|
table_name_str
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit transaction
|
// Commit transaction
|
||||||
tx.commit().map_err(DatabaseError::from)?;
|
tx.commit().map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
Ok(modified_schema_tables.into_iter().collect())
|
Ok(result)
|
||||||
})
|
})
|
||||||
.map_err(ExtensionError::from)
|
.map_err(ExtensionError::from)
|
||||||
}
|
}
|
||||||
@ -192,7 +222,7 @@ pub async fn extension_sql_select(
|
|||||||
public_key: String,
|
public_key: String,
|
||||||
name: String,
|
name: String,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<Vec<JsonValue>, ExtensionError> {
|
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||||
// Get extension to retrieve its ID
|
// Get extension to retrieve its ID
|
||||||
let extension = state
|
let extension = state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
@ -229,10 +259,9 @@ pub async fn extension_sql_select(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Database operation
|
// Database operation - return Vec<Vec<JsonValue>> like sql_select_with_crdt
|
||||||
with_connection(&state.db, |conn| {
|
with_connection(&state.db, |conn| {
|
||||||
let sql_params = ValueConverter::convert_params(¶ms)?;
|
let sql_params = ValueConverter::convert_params(¶ms)?;
|
||||||
// Hard Delete: Keine SELECT-Transformation mehr nötig
|
|
||||||
let stmt_to_execute = ast_vec.pop().unwrap();
|
let stmt_to_execute = ast_vec.pop().unwrap();
|
||||||
let transformed_sql = stmt_to_execute.to_string();
|
let transformed_sql = stmt_to_execute.to_string();
|
||||||
|
|
||||||
@ -245,51 +274,34 @@ pub async fn extension_sql_select(
|
|||||||
table: None,
|
table: None,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let column_names: Vec<String> = prepared_stmt
|
let num_columns = prepared_stmt.column_count();
|
||||||
.column_names()
|
let mut rows = prepared_stmt
|
||||||
.into_iter()
|
.query(params_from_iter(sql_params.iter()))
|
||||||
.map(|s| s.to_string())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let rows = prepared_stmt
|
|
||||||
.query_map(params_from_iter(sql_params.iter()), |row| {
|
|
||||||
row_to_json_value(row, &column_names)
|
|
||||||
})
|
|
||||||
.map_err(|e| DatabaseError::QueryError {
|
.map_err(|e| DatabaseError::QueryError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
||||||
for row_result in rows {
|
|
||||||
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError {
|
while let Some(row) = rows.next().map_err(|e| DatabaseError::QueryError {
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?);
|
})? {
|
||||||
|
let mut row_values: Vec<JsonValue> = Vec::new();
|
||||||
|
for i in 0..num_columns {
|
||||||
|
let value_ref = row.get_ref(i).map_err(|e| DatabaseError::QueryError {
|
||||||
|
reason: e.to_string(),
|
||||||
|
})?;
|
||||||
|
let json_value = crate::database::core::convert_value_ref_to_json(value_ref)?;
|
||||||
|
row_values.push(json_value);
|
||||||
|
}
|
||||||
|
result_vec.push(row_values);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(results)
|
Ok(result_vec)
|
||||||
})
|
})
|
||||||
.map_err(ExtensionError::from)
|
.map_err(ExtensionError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Konvertiert eine SQLite-Zeile zu JSON
|
|
||||||
fn row_to_json_value(
|
|
||||||
row: &rusqlite::Row,
|
|
||||||
columns: &[String],
|
|
||||||
) -> Result<JsonValue, rusqlite::Error> {
|
|
||||||
let mut map = serde_json::Map::new();
|
|
||||||
for (i, col_name) in columns.iter().enumerate() {
|
|
||||||
let value = row.get::<usize, rusqlite::types::Value>(i)?;
|
|
||||||
let json_value = match value {
|
|
||||||
rusqlite::types::Value::Null => JsonValue::Null,
|
|
||||||
rusqlite::types::Value::Integer(i) => json!(i),
|
|
||||||
rusqlite::types::Value::Real(f) => json!(f),
|
|
||||||
rusqlite::types::Value::Text(s) => json!(s),
|
|
||||||
rusqlite::types::Value::Blob(blob) => json!(blob.to_vec()),
|
|
||||||
};
|
|
||||||
map.insert(col_name.clone(), json_value);
|
|
||||||
}
|
|
||||||
Ok(JsonValue::Object(map))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validiert Parameter gegen SQL-Platzhalter
|
/// Validiert Parameter gegen SQL-Platzhalter
|
||||||
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/// src-tauri/src/extension/mod.rs
|
/// src-tauri/src/extension/mod.rs
|
||||||
use crate::{
|
use crate::{
|
||||||
extension::{
|
extension::{
|
||||||
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
core::{manager::ExtensionManager, EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||||
error::ExtensionError,
|
error::ExtensionError,
|
||||||
},
|
},
|
||||||
AppState,
|
AppState,
|
||||||
@ -37,7 +37,7 @@ pub async fn get_all_extensions(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<Vec<ExtensionInfoResponse>, String> {
|
) -> Result<Vec<ExtensionInfoResponse>, String> {
|
||||||
// Check if extensions are loaded, if not load them first
|
// Check if extensions are loaded, if not load them first
|
||||||
let needs_loading = {
|
/* let needs_loading = {
|
||||||
let prod_exts = state
|
let prod_exts = state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
.production_extensions
|
.production_extensions
|
||||||
@ -45,15 +45,15 @@ pub async fn get_all_extensions(
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let dev_exts = state.extension_manager.dev_extensions.lock().unwrap();
|
let dev_exts = state.extension_manager.dev_extensions.lock().unwrap();
|
||||||
prod_exts.is_empty() && dev_exts.is_empty()
|
prod_exts.is_empty() && dev_exts.is_empty()
|
||||||
};
|
}; */
|
||||||
|
|
||||||
if needs_loading {
|
/* if needs_loading { */
|
||||||
state
|
state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
.load_installed_extensions(&app_handle, &state)
|
.load_installed_extensions(&app_handle, &state)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to load extensions: {:?}", e))?;
|
.map_err(|e| format!("Failed to load extensions: {:?}", e))?;
|
||||||
}
|
/* } */
|
||||||
|
|
||||||
let mut extensions = Vec::new();
|
let mut extensions = Vec::new();
|
||||||
|
|
||||||
@ -82,12 +82,13 @@ pub async fn get_all_extensions(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn preview_extension(
|
pub async fn preview_extension(
|
||||||
|
app_handle: AppHandle,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
file_bytes: Vec<u8>,
|
file_bytes: Vec<u8>,
|
||||||
) -> Result<ExtensionPreview, ExtensionError> {
|
) -> Result<ExtensionPreview, ExtensionError> {
|
||||||
state
|
state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
.preview_extension_internal(file_bytes)
|
.preview_extension_internal(&app_handle, file_bytes)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,13 +194,7 @@ pub async fn remove_extension(
|
|||||||
) -> Result<(), ExtensionError> {
|
) -> Result<(), ExtensionError> {
|
||||||
state
|
state
|
||||||
.extension_manager
|
.extension_manager
|
||||||
.remove_extension_internal(
|
.remove_extension_internal(&app_handle, &public_key, &name, &version, &state)
|
||||||
&app_handle,
|
|
||||||
&public_key,
|
|
||||||
&name,
|
|
||||||
&version,
|
|
||||||
&state,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +218,16 @@ pub fn is_extension_installed(
|
|||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct HaextensionConfig {
|
struct HaextensionConfig {
|
||||||
dev: DevConfig,
|
dev: DevConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
keys: KeysConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug, Default)]
|
||||||
|
struct KeysConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
public_key_path: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
private_key_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
@ -231,6 +236,8 @@ struct DevConfig {
|
|||||||
port: u16,
|
port: u16,
|
||||||
#[serde(default = "default_host")]
|
#[serde(default = "default_host")]
|
||||||
host: String,
|
host: String,
|
||||||
|
#[serde(default = "default_haextension_dir")]
|
||||||
|
haextension_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_port() -> u16 {
|
fn default_port() -> u16 {
|
||||||
@ -241,10 +248,14 @@ fn default_host() -> String {
|
|||||||
"localhost".to_string()
|
"localhost".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_haextension_dir() -> String {
|
||||||
|
"haextension".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a dev server is reachable by making a simple HTTP request
|
/// Check if a dev server is reachable by making a simple HTTP request
|
||||||
async fn check_dev_server_health(url: &str) -> bool {
|
async fn check_dev_server_health(url: &str) -> bool {
|
||||||
use tauri_plugin_http::reqwest;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tauri_plugin_http::reqwest;
|
||||||
|
|
||||||
// Try to connect with a short timeout
|
// Try to connect with a short timeout
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
@ -276,29 +287,28 @@ pub async fn load_dev_extension(
|
|||||||
|
|
||||||
let extension_path_buf = PathBuf::from(&extension_path);
|
let extension_path_buf = PathBuf::from(&extension_path);
|
||||||
|
|
||||||
// 1. Read haextension.json to get dev server config
|
// 1. Read haextension.config.json to get dev server config and haextension directory
|
||||||
let config_path = extension_path_buf.join("haextension.json");
|
let config_path = extension_path_buf.join("haextension.config.json");
|
||||||
let (host, port) = if config_path.exists() {
|
let (host, port, haextension_dir) = if config_path.exists() {
|
||||||
let config_content = std::fs::read_to_string(&config_path).map_err(|e| {
|
let config_content =
|
||||||
ExtensionError::ValidationError {
|
std::fs::read_to_string(&config_path).map_err(|e| ExtensionError::ValidationError {
|
||||||
reason: format!("Failed to read haextension.json: {}", e),
|
reason: format!("Failed to read haextension.config.json: {}", e),
|
||||||
}
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
let config: HaextensionConfig = serde_json::from_str(&config_content).map_err(|e| {
|
let config: HaextensionConfig =
|
||||||
ExtensionError::ValidationError {
|
serde_json::from_str(&config_content).map_err(|e| ExtensionError::ValidationError {
|
||||||
reason: format!("Failed to parse haextension.json: {}", e),
|
reason: format!("Failed to parse haextension.config.json: {}", e),
|
||||||
}
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
(config.dev.host, config.dev.port)
|
(config.dev.host, config.dev.port, config.dev.haextension_dir)
|
||||||
} else {
|
} else {
|
||||||
// Default values if config doesn't exist
|
// Default values if config doesn't exist
|
||||||
(default_host(), default_port())
|
(default_host(), default_port(), default_haextension_dir())
|
||||||
};
|
};
|
||||||
|
|
||||||
let dev_server_url = format!("http://{}:{}", host, port);
|
let dev_server_url = format!("http://{}:{}", host, port);
|
||||||
eprintln!("📡 Dev server URL: {}", dev_server_url);
|
eprintln!("📡 Dev server URL: {}", dev_server_url);
|
||||||
|
eprintln!("📁 Haextension directory: {}", haextension_dir);
|
||||||
|
|
||||||
// 1.5. Check if dev server is running
|
// 1.5. Check if dev server is running
|
||||||
if !check_dev_server_health(&dev_server_url).await {
|
if !check_dev_server_health(&dev_server_url).await {
|
||||||
@ -311,35 +321,30 @@ pub async fn load_dev_extension(
|
|||||||
}
|
}
|
||||||
eprintln!("✅ Dev server is reachable");
|
eprintln!("✅ Dev server is reachable");
|
||||||
|
|
||||||
// 2. Build path to manifest: <extension_path>/haextension/manifest.json
|
// 2. Validate and build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
||||||
let manifest_path = extension_path_buf.join("haextension").join("manifest.json");
|
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||||
|
let manifest_path = ExtensionManager::validate_path_in_directory(
|
||||||
// Check if manifest exists
|
&extension_path_buf,
|
||||||
if !manifest_path.exists() {
|
&manifest_relative_path,
|
||||||
return Err(ExtensionError::ManifestError {
|
true,
|
||||||
reason: format!(
|
)?
|
||||||
"Manifest not found at: {}. Make sure you run 'npx @haexhub/sdk init' first.",
|
.ok_or_else(|| ExtensionError::ManifestError {
|
||||||
manifest_path.display()
|
reason: format!(
|
||||||
),
|
"Manifest not found at: {}/manifest.json. Make sure you run 'npx @haexhub/sdk init' first.",
|
||||||
});
|
haextension_dir
|
||||||
}
|
),
|
||||||
|
})?;
|
||||||
|
|
||||||
// 3. Read and parse manifest
|
// 3. Read and parse manifest
|
||||||
let manifest_content = std::fs::read_to_string(&manifest_path).map_err(|e| {
|
let manifest_content =
|
||||||
ExtensionError::ManifestError {
|
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||||
reason: format!("Failed to read manifest: {}", e),
|
reason: format!("Failed to read manifest: {}", e),
|
||||||
}
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||||
|
|
||||||
// 4. Generate a unique ID for dev extension: dev_<public_key_first_8>_<name>
|
// 4. Generate a unique ID for dev extension: dev_<public_key>_<name>
|
||||||
let key_prefix = manifest
|
let extension_id = format!("dev_{}_{}", manifest.public_key, manifest.name);
|
||||||
.public_key
|
|
||||||
.chars()
|
|
||||||
.take(8)
|
|
||||||
.collect::<String>();
|
|
||||||
let extension_id = format!("dev_{}_{}", key_prefix, manifest.name);
|
|
||||||
|
|
||||||
// 5. Check if dev extension already exists (allow reload)
|
// 5. Check if dev extension already exists (allow reload)
|
||||||
if let Some(existing) = state
|
if let Some(existing) = state
|
||||||
@ -387,13 +392,11 @@ pub fn remove_dev_extension(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<(), ExtensionError> {
|
) -> Result<(), ExtensionError> {
|
||||||
// Only remove from dev_extensions, not production_extensions
|
// Only remove from dev_extensions, not production_extensions
|
||||||
let mut dev_exts = state
|
let mut dev_exts = state.extension_manager.dev_extensions.lock().map_err(|e| {
|
||||||
.extension_manager
|
ExtensionError::MutexPoisoned {
|
||||||
.dev_extensions
|
|
||||||
.lock()
|
|
||||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
|
||||||
reason: e.to_string(),
|
reason: e.to_string(),
|
||||||
})?;
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
// Find and remove by public_key and name
|
// Find and remove by public_key and name
|
||||||
let to_remove = dev_exts
|
let to_remove = dev_exts
|
||||||
@ -406,10 +409,7 @@ pub fn remove_dev_extension(
|
|||||||
eprintln!("✅ Dev extension removed: {}", name);
|
eprintln!("✅ Dev extension removed: {}", name);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ExtensionError::NotFound {
|
Err(ExtensionError::NotFound { public_key, name })
|
||||||
public_key,
|
|
||||||
name,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -197,6 +197,30 @@ impl PermissionManager {
|
|||||||
action: Action,
|
action: Action,
|
||||||
table_name: &str,
|
table_name: &str,
|
||||||
) -> Result<(), ExtensionError> {
|
) -> Result<(), ExtensionError> {
|
||||||
|
// Remove quotes from table name if present (from SDK's getTableName())
|
||||||
|
let clean_table_name = table_name.trim_matches('"');
|
||||||
|
|
||||||
|
// Auto-allow: Extensions have full access to their own tables
|
||||||
|
// Table format: {publicKey}__{extensionName}__{tableName}
|
||||||
|
// Extension ID format: dev_{publicKey}_{extensionName} or {publicKey}_{extensionName}
|
||||||
|
|
||||||
|
// Get the extension to check if this is its own table
|
||||||
|
let extension = app_state
|
||||||
|
.extension_manager
|
||||||
|
.get_extension(extension_id)
|
||||||
|
.ok_or_else(|| ExtensionError::ValidationError {
|
||||||
|
reason: format!("Extension with ID {} not found", extension_id),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Build expected table prefix: {publicKey}__{extensionName}__
|
||||||
|
let expected_prefix = format!("{}__{}__", extension.manifest.public_key, extension.manifest.name);
|
||||||
|
|
||||||
|
if clean_table_name.starts_with(&expected_prefix) {
|
||||||
|
// This is the extension's own table - auto-allow
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not own table - check explicit permissions
|
||||||
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
let permissions = Self::get_permissions(app_state, extension_id).await?;
|
||||||
|
|
||||||
let has_permission = permissions
|
let has_permission = permissions
|
||||||
@ -205,7 +229,7 @@ impl PermissionManager {
|
|||||||
.filter(|perm| perm.resource_type == ResourceType::Db)
|
.filter(|perm| perm.resource_type == ResourceType::Db)
|
||||||
.filter(|perm| perm.action == action) // action ist nicht mehr Option
|
.filter(|perm| perm.action == action) // action ist nicht mehr Option
|
||||||
.any(|perm| {
|
.any(|perm| {
|
||||||
if perm.target != "*" && perm.target != table_name {
|
if perm.target != "*" && perm.target != clean_table_name {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
|||||||
@ -165,8 +165,6 @@ pub struct ExtensionPermission {
|
|||||||
pub constraints: Option<PermissionConstraints>,
|
pub constraints: Option<PermissionConstraints>,
|
||||||
pub status: PermissionStatus,
|
pub status: PermissionStatus,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub haex_tombstone: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub haex_timestamp: Option<String>,
|
pub haex_timestamp: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,9 +339,9 @@ impl From<&ExtensionPermission> for crate::database::generated::HaexExtensionPer
|
|||||||
fn from(perm: &ExtensionPermission) -> Self {
|
fn from(perm: &ExtensionPermission) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: perm.id.clone(),
|
id: perm.id.clone(),
|
||||||
extension_id: Some(perm.extension_id.clone()),
|
extension_id: perm.extension_id.clone(),
|
||||||
resource_type: Some(perm.resource_type.as_str().to_string()),
|
resource_type: Some(perm.resource_type.as_str().to_string()),
|
||||||
action: Some(perm.action.as_str()),
|
action: Some(perm.action.as_str().to_string()),
|
||||||
target: Some(perm.target.clone()),
|
target: Some(perm.target.clone()),
|
||||||
constraints: perm
|
constraints: perm
|
||||||
.constraints
|
.constraints
|
||||||
@ -352,7 +350,6 @@ impl From<&ExtensionPermission> for crate::database::generated::HaexExtensionPer
|
|||||||
status: perm.status.as_str().to_string(),
|
status: perm.status.as_str().to_string(),
|
||||||
created_at: None,
|
created_at: None,
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
haex_tombstone: perm.haex_tombstone,
|
|
||||||
haex_timestamp: perm.haex_timestamp.clone(),
|
haex_timestamp: perm.haex_timestamp.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -382,13 +379,12 @@ impl From<crate::database::generated::HaexExtensionPermissions> for ExtensionPer
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: db_perm.id,
|
id: db_perm.id,
|
||||||
extension_id: db_perm.extension_id.unwrap_or_default(),
|
extension_id: db_perm.extension_id,
|
||||||
resource_type,
|
resource_type,
|
||||||
action,
|
action,
|
||||||
target: db_perm.target.unwrap_or_default(),
|
target: db_perm.target.unwrap_or_default(),
|
||||||
constraints,
|
constraints,
|
||||||
status,
|
status,
|
||||||
haex_tombstone: db_perm.haex_tombstone,
|
|
||||||
haex_timestamp: db_perm.haex_timestamp,
|
haex_timestamp: db_perm.haex_timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,7 @@ pub fn run() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
database::create_encrypted_database,
|
database::create_encrypted_database,
|
||||||
database::delete_vault,
|
database::delete_vault,
|
||||||
|
database::move_vault_to_trash,
|
||||||
database::list_vaults,
|
database::list_vaults,
|
||||||
database::open_encrypted_database,
|
database::open_encrypted_database,
|
||||||
database::sql_execute_with_crdt,
|
database::sql_execute_with_crdt,
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "haex-hub",
|
"productName": "haex-hub",
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"identifier": "space.haex.hub",
|
"identifier": "space.haex.hub",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
"devUrl": "http://localhost:3003",
|
"devUrl": "http://localhost:3003",
|
||||||
"beforeBuildCommand": "pnpm generate",
|
"beforeBuildCommand": "pnpm generate",
|
||||||
"frontendDist": "../dist"
|
"frontendDist": "../.output/public"
|
||||||
},
|
},
|
||||||
|
|
||||||
"app": {
|
"app": {
|
||||||
@ -25,7 +25,8 @@
|
|||||||
"'self'",
|
"'self'",
|
||||||
"http://tauri.localhost",
|
"http://tauri.localhost",
|
||||||
"haex-extension:",
|
"haex-extension:",
|
||||||
"'wasm-unsafe-eval'"
|
"'wasm-unsafe-eval'",
|
||||||
|
"'unsafe-inline'"
|
||||||
],
|
],
|
||||||
"style-src": [
|
"style-src": [
|
||||||
"'self'",
|
"'self'",
|
||||||
|
|||||||
@ -3,6 +3,8 @@ export default defineAppConfig({
|
|||||||
colors: {
|
colors: {
|
||||||
primary: 'sky',
|
primary: 'sky',
|
||||||
secondary: 'fuchsia',
|
secondary: 'fuchsia',
|
||||||
|
warning: 'yellow',
|
||||||
|
danger: 'red',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<UApp :locale="locales[locale]">
|
<UApp :locale="locales[locale]">
|
||||||
<div data-vaul-drawer-wrapper>
|
<div data-vaul-drawer-wrapper>
|
||||||
<NuxtLayout>
|
<NuxtPage />
|
||||||
<NuxtPage />
|
|
||||||
</NuxtLayout>
|
|
||||||
</div>
|
</div>
|
||||||
</UApp>
|
</UApp>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -13,6 +13,46 @@
|
|||||||
[disabled] {
|
[disabled] {
|
||||||
@apply cursor-not-allowed;
|
@apply cursor-not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Define safe-area-insets as CSS custom properties for JavaScript access */
|
||||||
|
:root {
|
||||||
|
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
||||||
|
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
||||||
|
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verhindere Scrolling auf html und body */
|
||||||
|
html {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100dvh;
|
||||||
|
height: 100vh; /* Fallback */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#__nuxt {
|
||||||
|
/* Volle Höhe des body */
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
/* Safe-Area Paddings auf root element - damit ALLES davon profitiert */
|
||||||
|
padding-top: var(--safe-area-inset-top);
|
||||||
|
padding-bottom: var(--safe-area-inset-bottom);
|
||||||
|
padding-left: var(--safe-area-inset-left);
|
||||||
|
padding-right: var(--safe-area-inset-right);
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
|
|||||||
61
src/components/haex/debug/overlay.vue
Normal file
61
src/components/haex/debug/overlay.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="data"
|
||||||
|
class="fixed top-2 right-2 bg-black/90 text-white text-xs p-3 rounded-lg shadow-2xl max-w-sm z-[9999] backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-start gap-3 mb-2">
|
||||||
|
<span class="font-bold text-sm">{{ title }}</span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button
|
||||||
|
class="bg-white/20 hover:bg-white/30 px-2 py-1 rounded text-xs transition-colors"
|
||||||
|
@click="copyToClipboardAsync"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="dismissible"
|
||||||
|
class="bg-white/20 hover:bg-white/30 px-2 py-1 rounded text-xs transition-colors"
|
||||||
|
@click="handleDismiss"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre class="text-xs whitespace-pre-wrap font-mono overflow-auto max-h-96">{{ formattedData }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
data: Record<string, any> | null
|
||||||
|
title?: string
|
||||||
|
dismissible?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
title: 'Debug Info',
|
||||||
|
dismissible: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
dismiss: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const formattedData = computed(() => {
|
||||||
|
if (!props.data) return ''
|
||||||
|
return JSON.stringify(props.data, null, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
const copyToClipboardAsync = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(formattedData.value)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy debug info:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDismiss = () => {
|
||||||
|
emit('dismiss')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<div
|
<div
|
||||||
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"
|
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"
|
||||||
></div>
|
/>
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
Loading extension...
|
Loading extension...
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
id: string
|
id: string
|
||||||
itemType: 'extension' | 'file' | 'folder'
|
itemType: DesktopItemType
|
||||||
referenceId: string
|
referenceId: string
|
||||||
initialX: number
|
initialX: number
|
||||||
initialY: number
|
initialY: number
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="desktopEl"
|
ref="desktopEl"
|
||||||
class="w-full h-full relative overflow-hidden isolate"
|
class="absolute inset-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
<Swiper
|
<Swiper
|
||||||
:modules="[SwiperNavigation]"
|
:modules="[SwiperNavigation]"
|
||||||
@ -10,14 +10,13 @@
|
|||||||
:initial-slide="currentWorkspaceIndex"
|
:initial-slide="currentWorkspaceIndex"
|
||||||
:speed="300"
|
:speed="300"
|
||||||
:touch-angle="45"
|
:touch-angle="45"
|
||||||
:threshold="10"
|
|
||||||
:no-swiping="true"
|
:no-swiping="true"
|
||||||
no-swiping-class="no-swipe"
|
no-swiping-class="no-swipe"
|
||||||
:allow-touch-move="allowSwipe"
|
:allow-touch-move="allowSwipe"
|
||||||
class="w-full h-full"
|
class="h-full w-full"
|
||||||
|
direction="vertical"
|
||||||
@swiper="onSwiperInit"
|
@swiper="onSwiperInit"
|
||||||
@slide-change="onSlideChange"
|
@slide-change="onSlideChange"
|
||||||
direction="vertical"
|
|
||||||
>
|
>
|
||||||
<SwiperSlide
|
<SwiperSlide
|
||||||
v-for="workspace in workspaces"
|
v-for="workspace in workspaces"
|
||||||
@ -25,9 +24,11 @@
|
|||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-full h-full relative isolate"
|
class="w-full h-full relative"
|
||||||
@click.self.stop="handleDesktopClick"
|
@click.self.stop="handleDesktopClick"
|
||||||
@mousedown.left.self="handleAreaSelectStart"
|
@mousedown.left.self="handleAreaSelectStart"
|
||||||
|
@dragover.prevent="handleDragOver"
|
||||||
|
@drop.prevent="handleDrop($event, workspace.id)"
|
||||||
>
|
>
|
||||||
<!-- Grid Pattern Background -->
|
<!-- Grid Pattern Background -->
|
||||||
<div
|
<div
|
||||||
@ -40,18 +41,16 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Snap Dropzones (only visible when window drag near edge) -->
|
<!-- Snap Dropzones (only visible when window drag near edge) -->
|
||||||
<Transition name="fade">
|
|
||||||
<div
|
<div
|
||||||
v-if="showLeftSnapZone"
|
class="absolute left-0 top-0 bottom-0 border-blue-500 pointer-events-none backdrop-blur-sm z-50 transition-all duration-500 ease-in-out"
|
||||||
class="absolute left-0 top-0 bottom-0 w-1/2 bg-blue-500/20 border-2 border-blue-500 pointer-events-none backdrop-blur-sm z-40"
|
:class="showLeftSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'"
|
||||||
/>
|
/>
|
||||||
</Transition>
|
|
||||||
<Transition name="fade">
|
<div
|
||||||
<div
|
class="absolute right-0 top-0 bottom-0 border-blue-500 pointer-events-none backdrop-blur-sm z-50 transition-all duration-500 ease-in-out"
|
||||||
v-if="showRightSnapZone"
|
:class="showRightSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'"
|
||||||
class="absolute right-0 top-0 bottom-0 w-1/2 bg-blue-500/20 border-2 border-blue-500 pointer-events-none backdrop-blur-sm z-40"
|
/>
|
||||||
/>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- Area Selection Box -->
|
<!-- Area Selection Box -->
|
||||||
<div
|
<div
|
||||||
@ -79,83 +78,93 @@
|
|||||||
|
|
||||||
<!-- Windows for this workspace -->
|
<!-- Windows for this workspace -->
|
||||||
<template
|
<template
|
||||||
v-for="(window, index) in getWorkspaceWindows(workspace.id)"
|
v-for="window in getWorkspaceWindows(workspace.id)"
|
||||||
:key="window.id"
|
:key="window.id"
|
||||||
>
|
>
|
||||||
<!-- Wrapper for Overview Mode Click/Drag -->
|
<!-- Overview Mode: Teleport to window preview -->
|
||||||
<div
|
<Teleport
|
||||||
v-if="false"
|
v-if="
|
||||||
:style="
|
windowManager.showWindowOverview &&
|
||||||
getOverviewWindowGridStyle(
|
overviewWindowState.has(window.id)
|
||||||
index,
|
|
||||||
getWorkspaceWindows(workspace.id).length,
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
class="absolute cursor-pointer group"
|
:to="`#window-preview-${window.id}`"
|
||||||
:draggable="true"
|
|
||||||
@dragstart="handleOverviewWindowDragStart($event, window.id)"
|
|
||||||
@dragend="handleOverviewWindowDragEnd"
|
|
||||||
@click="handleOverviewWindowClick(window.id)"
|
|
||||||
>
|
>
|
||||||
<!-- Overlay for click/drag events (prevents interaction with window content) -->
|
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 z-[100] bg-transparent group-hover:ring-4 group-hover:ring-purple-500 rounded-xl transition-all"
|
class="absolute origin-top-left"
|
||||||
/>
|
:style="{
|
||||||
|
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
||||||
<HaexWindow
|
width: `${overviewWindowState.get(window.id)!.width}px`,
|
||||||
:id="window.id"
|
height: `${overviewWindowState.get(window.id)!.height}px`,
|
||||||
:title="window.title"
|
}"
|
||||||
:icon="window.icon"
|
|
||||||
:initial-x="window.x"
|
|
||||||
:initial-y="window.y"
|
|
||||||
:initial-width="window.width"
|
|
||||||
:initial-height="window.height"
|
|
||||||
:is-active="windowManager.isWindowActive(window.id)"
|
|
||||||
:source-x="window.sourceX"
|
|
||||||
:source-y="window.sourceY"
|
|
||||||
:source-width="window.sourceWidth"
|
|
||||||
:source-height="window.sourceHeight"
|
|
||||||
:is-opening="window.isOpening"
|
|
||||||
:is-closing="window.isClosing"
|
|
||||||
class="no-swipe pointer-events-none"
|
|
||||||
@close="windowManager.closeWindow(window.id)"
|
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
|
||||||
@activate="windowManager.activateWindow(window.id)"
|
|
||||||
@position-changed="
|
|
||||||
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
|
|
||||||
"
|
|
||||||
@size-changed="
|
|
||||||
(width, height) =>
|
|
||||||
windowManager.updateWindowSize(window.id, width, height)
|
|
||||||
"
|
|
||||||
@drag-start="handleWindowDragStart(window.id)"
|
|
||||||
@drag-end="handleWindowDragEnd"
|
|
||||||
>
|
>
|
||||||
{{ window }}
|
<HaexWindow
|
||||||
<!-- System Window: Render Vue Component -->
|
v-show="
|
||||||
<component
|
windowManager.showWindowOverview || !window.isMinimized
|
||||||
:is="getSystemWindowComponent(window.sourceId)"
|
"
|
||||||
v-if="window.type === 'system'"
|
:id="window.id"
|
||||||
/>
|
v-model:x="overviewWindowState.get(window.id)!.x"
|
||||||
|
v-model:y="overviewWindowState.get(window.id)!.y"
|
||||||
|
v-model:width="overviewWindowState.get(window.id)!.width"
|
||||||
|
v-model:height="overviewWindowState.get(window.id)!.height"
|
||||||
|
:title="window.title"
|
||||||
|
:icon="window.icon"
|
||||||
|
:is-active="windowManager.isWindowActive(window.id)"
|
||||||
|
:source-x="window.sourceX"
|
||||||
|
:source-y="window.sourceY"
|
||||||
|
:source-width="window.sourceWidth"
|
||||||
|
:source-height="window.sourceHeight"
|
||||||
|
:is-opening="window.isOpening"
|
||||||
|
:is-closing="window.isClosing"
|
||||||
|
:warning-level="
|
||||||
|
window.type === 'extension' &&
|
||||||
|
availableExtensions.find(
|
||||||
|
(ext) => ext.id === window.sourceId,
|
||||||
|
)?.devServerUrl
|
||||||
|
? 'warning'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
class="no-swipe"
|
||||||
|
@close="windowManager.closeWindow(window.id)"
|
||||||
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
|
@activate="windowManager.activateWindow(window.id)"
|
||||||
|
@position-changed="
|
||||||
|
(x, y) =>
|
||||||
|
windowManager.updateWindowPosition(window.id, x, y)
|
||||||
|
"
|
||||||
|
@size-changed="
|
||||||
|
(width, height) =>
|
||||||
|
windowManager.updateWindowSize(window.id, width, height)
|
||||||
|
"
|
||||||
|
@drag-start="handleWindowDragStart(window.id)"
|
||||||
|
@drag-end="handleWindowDragEnd"
|
||||||
|
>
|
||||||
|
<!-- System Window: Render Vue Component -->
|
||||||
|
<component
|
||||||
|
:is="getSystemWindowComponent(window.sourceId)"
|
||||||
|
v-if="window.type === 'system'"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Extension Window: Render iFrame -->
|
<!-- Extension Window: Render iFrame -->
|
||||||
<HaexDesktopExtensionFrame
|
<HaexDesktopExtensionFrame
|
||||||
v-else
|
v-else
|
||||||
:extension-id="window.sourceId"
|
:extension-id="window.sourceId"
|
||||||
:window-id="window.id"
|
:window-id="window.id"
|
||||||
/>
|
/>
|
||||||
</HaexWindow>
|
</HaexWindow>
|
||||||
</div>
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
<!-- Normal Mode (non-overview) -->
|
<!-- Desktop Mode: Render directly in workspace -->
|
||||||
<HaexWindow
|
<HaexWindow
|
||||||
|
v-else
|
||||||
|
v-show="windowManager.showWindowOverview || !window.isMinimized"
|
||||||
:id="window.id"
|
:id="window.id"
|
||||||
|
v-model:x="window.x"
|
||||||
|
v-model:y="window.y"
|
||||||
|
v-model:width="window.width"
|
||||||
|
v-model:height="window.height"
|
||||||
:title="window.title"
|
:title="window.title"
|
||||||
:icon="window.icon"
|
:icon="window.icon"
|
||||||
:initial-x="window.x"
|
|
||||||
:initial-y="window.y"
|
|
||||||
:initial-width="window.width"
|
|
||||||
:initial-height="window.height"
|
|
||||||
:is-active="windowManager.isWindowActive(window.id)"
|
:is-active="windowManager.isWindowActive(window.id)"
|
||||||
:source-x="window.sourceX"
|
:source-x="window.sourceX"
|
||||||
:source-y="window.sourceY"
|
:source-y="window.sourceY"
|
||||||
@ -163,6 +172,13 @@
|
|||||||
:source-height="window.sourceHeight"
|
:source-height="window.sourceHeight"
|
||||||
:is-opening="window.isOpening"
|
:is-opening="window.isOpening"
|
||||||
:is-closing="window.isClosing"
|
:is-closing="window.isClosing"
|
||||||
|
:warning-level="
|
||||||
|
window.type === 'extension' &&
|
||||||
|
availableExtensions.find((ext) => ext.id === window.sourceId)
|
||||||
|
?.devServerUrl
|
||||||
|
? 'warning'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
class="no-swipe"
|
class="no-swipe"
|
||||||
@close="windowManager.closeWindow(window.id)"
|
@close="windowManager.closeWindow(window.id)"
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
@ -195,53 +211,8 @@
|
|||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
|
||||||
<!-- Workspace Drawer -->
|
<!-- Window Overview Modal -->
|
||||||
<UDrawer
|
<HaexWindowOverview />
|
||||||
v-model:open="isOverviewMode"
|
|
||||||
direction="left"
|
|
||||||
:dismissible="false"
|
|
||||||
:overlay="false"
|
|
||||||
:modal="false"
|
|
||||||
should-scale-background
|
|
||||||
set-background-color-on-scale
|
|
||||||
title="Workspaces"
|
|
||||||
description="Workspaces"
|
|
||||||
>
|
|
||||||
<template #content>
|
|
||||||
<div class="p-6 h-full overflow-y-auto">
|
|
||||||
<UButton
|
|
||||||
block
|
|
||||||
trailing-icon="mdi-close"
|
|
||||||
class="text-2xl font-bold ext-gray-900 dark:text-white mb-4"
|
|
||||||
@click="isOverviewMode = false"
|
|
||||||
>
|
|
||||||
Workspaces
|
|
||||||
</UButton>
|
|
||||||
|
|
||||||
<!-- Workspace Cards -->
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<HaexWorkspaceCard
|
|
||||||
v-for="workspace in workspaces"
|
|
||||||
:key="workspace.id"
|
|
||||||
:workspace
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add New Workspace Button -->
|
|
||||||
<UButton
|
|
||||||
block
|
|
||||||
variant="outline"
|
|
||||||
class="mt-6"
|
|
||||||
@click="handleAddWorkspace"
|
|
||||||
>
|
|
||||||
<template #leading>
|
|
||||||
<UIcon name="i-heroicons-plus" />
|
|
||||||
</template>
|
|
||||||
New Workspace
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</UDrawer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -252,17 +223,12 @@ import type { Swiper as SwiperType } from 'swiper'
|
|||||||
import 'swiper/css'
|
import 'swiper/css'
|
||||||
import 'swiper/css/navigation'
|
import 'swiper/css/navigation'
|
||||||
|
|
||||||
import { eq } from 'drizzle-orm'
|
|
||||||
import { haexDesktopItems } from '~~/src-tauri/database/schemas'
|
|
||||||
|
|
||||||
const SwiperNavigation = Navigation
|
const SwiperNavigation = Navigation
|
||||||
|
|
||||||
const desktopStore = useDesktopStore()
|
const desktopStore = useDesktopStore()
|
||||||
const extensionsStore = useExtensionsStore()
|
const extensionsStore = useExtensionsStore()
|
||||||
const windowManager = useWindowManagerStore()
|
const windowManager = useWindowManagerStore()
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
|
||||||
const { currentVault } = storeToRefs(useVaultStore())
|
|
||||||
const { desktopItems } = storeToRefs(desktopStore)
|
const { desktopItems } = storeToRefs(desktopStore)
|
||||||
const { availableExtensions } = storeToRefs(extensionsStore)
|
const { availableExtensions } = storeToRefs(extensionsStore)
|
||||||
const {
|
const {
|
||||||
@ -315,7 +281,6 @@ const currentDraggedReferenceId = ref<string>()
|
|||||||
|
|
||||||
// Window drag state for snap zones
|
// Window drag state for snap zones
|
||||||
const isWindowDragging = ref(false)
|
const isWindowDragging = ref(false)
|
||||||
const currentDraggingWindowId = ref<string | null>(null)
|
|
||||||
const snapEdgeThreshold = 50 // pixels from edge to show snap zone
|
const snapEdgeThreshold = 50 // pixels from edge to show snap zone
|
||||||
|
|
||||||
// Computed visibility for snap zones (uses mouseX from above)
|
// Computed visibility for snap zones (uses mouseX from above)
|
||||||
@ -329,37 +294,29 @@ const showRightSnapZone = computed(() => {
|
|||||||
return mouseX.value >= viewportWidth - snapEdgeThreshold
|
return mouseX.value >= viewportWidth - snapEdgeThreshold
|
||||||
})
|
})
|
||||||
|
|
||||||
// Dropzone refs
|
|
||||||
/* const removeDropzoneEl = ref<HTMLElement>()
|
|
||||||
const uninstallDropzoneEl = ref<HTMLElement>() */
|
|
||||||
|
|
||||||
// Setup dropzones with VueUse
|
|
||||||
/* const { isOverDropZone: isOverRemoveZone } = useDropZone(removeDropzoneEl, {
|
|
||||||
onDrop: () => {
|
|
||||||
if (currentDraggedItemId.value) {
|
|
||||||
handleRemoveFromDesktop(currentDraggedItemId.value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}) */
|
|
||||||
|
|
||||||
/* const { isOverDropZone: isOverUninstallZone } = useDropZone(uninstallDropzoneEl, {
|
|
||||||
onDrop: () => {
|
|
||||||
if (currentDraggedItemType.value && currentDraggedReferenceId.value) {
|
|
||||||
handleUninstall(currentDraggedItemType.value, currentDraggedReferenceId.value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}) */
|
|
||||||
|
|
||||||
// Get icons for a specific workspace
|
// Get icons for a specific workspace
|
||||||
const getWorkspaceIcons = (workspaceId: string) => {
|
const getWorkspaceIcons = (workspaceId: string) => {
|
||||||
return desktopItems.value
|
return desktopItems.value
|
||||||
.filter((item) => item.workspaceId === workspaceId)
|
.filter((item) => item.workspaceId === workspaceId)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
|
if (item.itemType === 'system') {
|
||||||
|
const systemWindow = windowManager
|
||||||
|
.getAllSystemWindows()
|
||||||
|
.find((win) => win.id === item.referenceId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
label: systemWindow?.name || 'Unknown',
|
||||||
|
icon: systemWindow?.icon || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (item.itemType === 'extension') {
|
if (item.itemType === 'extension') {
|
||||||
const extension = availableExtensions.value.find(
|
const extension = availableExtensions.value.find(
|
||||||
(ext) => ext.id === item.referenceId,
|
(ext) => ext.id === item.referenceId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.log('found ext', extension)
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
label: extension?.name || 'Unknown',
|
label: extension?.name || 'Unknown',
|
||||||
@ -393,11 +350,9 @@ const getWorkspaceIcons = (workspaceId: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get windows for a specific workspace
|
// Get windows for a specific workspace (including minimized for teleport)
|
||||||
const getWorkspaceWindows = (workspaceId: string) => {
|
const getWorkspaceWindows = (workspaceId: string) => {
|
||||||
return windowManager.windows.filter(
|
return windowManager.windows.filter((w) => w.workspaceId === workspaceId)
|
||||||
(w) => w.workspaceId === workspaceId && !w.isMinimized,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Vue Component for system window
|
// Get Vue Component for system window
|
||||||
@ -431,26 +386,50 @@ const handleDragEnd = async () => {
|
|||||||
allowSwipe.value = true // Re-enable Swiper after drag
|
allowSwipe.value = true // Re-enable Swiper after drag
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move desktop item to different workspace
|
// Handle drag over for launcher items
|
||||||
const moveItemToWorkspace = async (
|
const handleDragOver = (event: DragEvent) => {
|
||||||
itemId: string,
|
if (!event.dataTransfer) return
|
||||||
targetWorkspaceId: string,
|
|
||||||
) => {
|
// Check if this is a launcher item
|
||||||
const item = desktopItems.value.find((i) => i.id === itemId)
|
if (event.dataTransfer.types.includes('application/haex-launcher-item')) {
|
||||||
if (!item) return
|
event.dataTransfer.dropEffect = 'copy'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drop for launcher items
|
||||||
|
const handleDrop = async (event: DragEvent, workspaceId: string) => {
|
||||||
|
if (!event.dataTransfer) return
|
||||||
|
|
||||||
|
const launcherItemData = event.dataTransfer.getData(
|
||||||
|
'application/haex-launcher-item',
|
||||||
|
)
|
||||||
|
if (!launcherItemData) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!currentVault.value?.drizzle) return
|
const item = JSON.parse(launcherItemData) as {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: string
|
||||||
|
type: 'system' | 'extension'
|
||||||
|
}
|
||||||
|
|
||||||
await currentVault.value.drizzle
|
// Get drop position relative to desktop
|
||||||
.update(haexDesktopItems)
|
const desktopRect = (
|
||||||
.set({ workspaceId: targetWorkspaceId })
|
event.currentTarget as HTMLElement
|
||||||
.where(eq(haexDesktopItems.id, itemId))
|
).getBoundingClientRect()
|
||||||
|
const x = Math.max(0, event.clientX - desktopRect.left - 32) // Center icon (64px / 2)
|
||||||
|
const y = Math.max(0, event.clientY - desktopRect.top - 32)
|
||||||
|
|
||||||
// Update local state
|
// Create desktop icon on the specific workspace
|
||||||
item.workspaceId = targetWorkspaceId
|
await desktopStore.addDesktopItemAsync(
|
||||||
|
item.type as DesktopItemType,
|
||||||
|
item.id,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
workspaceId,
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Verschieben des Items:', error)
|
console.error('Failed to create desktop icon:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,30 +449,51 @@ const handleDesktopClick = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleWindowDragStart = (windowId: string) => {
|
const handleWindowDragStart = (windowId: string) => {
|
||||||
|
console.log('[Desktop] handleWindowDragStart:', windowId)
|
||||||
isWindowDragging.value = true
|
isWindowDragging.value = true
|
||||||
currentDraggingWindowId.value = windowId
|
windowManager.draggingWindowId = windowId // Set in store for workspace cards
|
||||||
|
console.log(
|
||||||
|
'[Desktop] draggingWindowId set to:',
|
||||||
|
windowManager.draggingWindowId,
|
||||||
|
)
|
||||||
allowSwipe.value = false // Disable Swiper during window drag
|
allowSwipe.value = false // Disable Swiper during window drag
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWindowDragEnd = async () => {
|
const handleWindowDragEnd = async () => {
|
||||||
// Window handles snapping itself, we just need to cleanup state
|
console.log('[Desktop] handleWindowDragEnd')
|
||||||
|
|
||||||
|
// Check if window should snap to left or right
|
||||||
|
const draggingWindowId = windowManager.draggingWindowId
|
||||||
|
|
||||||
|
if (draggingWindowId) {
|
||||||
|
if (showLeftSnapZone.value) {
|
||||||
|
// Snap to left half
|
||||||
|
windowManager.updateWindowPosition(draggingWindowId, 0, 0)
|
||||||
|
windowManager.updateWindowSize(
|
||||||
|
draggingWindowId,
|
||||||
|
viewportWidth.value / 2,
|
||||||
|
viewportHeight.value,
|
||||||
|
)
|
||||||
|
} else if (showRightSnapZone.value) {
|
||||||
|
// Snap to right half
|
||||||
|
windowManager.updateWindowPosition(
|
||||||
|
draggingWindowId,
|
||||||
|
viewportWidth.value / 2,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
windowManager.updateWindowSize(
|
||||||
|
draggingWindowId,
|
||||||
|
viewportWidth.value / 2,
|
||||||
|
viewportHeight.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isWindowDragging.value = false
|
isWindowDragging.value = false
|
||||||
currentDraggingWindowId.value = null
|
windowManager.draggingWindowId = null // Clear from store
|
||||||
allowSwipe.value = true // Re-enable Swiper after drag
|
allowSwipe.value = true // Re-enable Swiper after drag
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move window to different workspace
|
|
||||||
const moveWindowToWorkspace = async (
|
|
||||||
windowId: string,
|
|
||||||
targetWorkspaceId: string,
|
|
||||||
) => {
|
|
||||||
const window = windowManager.windows.find((w) => w.id === windowId)
|
|
||||||
if (!window) return
|
|
||||||
|
|
||||||
// Update window's workspaceId
|
|
||||||
window.workspaceId = targetWorkspaceId
|
|
||||||
}
|
|
||||||
|
|
||||||
// Area selection handlers
|
// Area selection handlers
|
||||||
const handleAreaSelectStart = (e: MouseEvent) => {
|
const handleAreaSelectStart = (e: MouseEvent) => {
|
||||||
if (!desktopEl.value) return
|
if (!desktopEl.value) return
|
||||||
@ -568,24 +568,7 @@ const onSlideChange = (swiper: SwiperType) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workspace control handlers
|
/* const handleRemoveWorkspace = async () => {
|
||||||
const handleAddWorkspace = async () => {
|
|
||||||
await workspaceStore.addWorkspaceAsync()
|
|
||||||
// Swiper will auto-slide to new workspace because we switch in addWorkspaceAsync
|
|
||||||
nextTick(() => {
|
|
||||||
if (swiperInstance.value) {
|
|
||||||
swiperInstance.value.slideTo(workspaces.value.length - 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSwitchToWorkspace = (index: number) => {
|
|
||||||
if (swiperInstance.value) {
|
|
||||||
swiperInstance.value.slideTo(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveWorkspace = async () => {
|
|
||||||
if (!currentWorkspace.value || workspaces.value.length <= 1) return
|
if (!currentWorkspace.value || workspaces.value.length <= 1) return
|
||||||
|
|
||||||
const currentIndex = currentWorkspaceIndex.value
|
const currentIndex = currentWorkspaceIndex.value
|
||||||
@ -600,13 +583,6 @@ const handleRemoveWorkspace = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drawer handlers
|
|
||||||
const handleSwitchToWorkspaceFromDrawer = (index: number) => {
|
|
||||||
handleSwitchToWorkspace(index)
|
|
||||||
// Close drawer after switch
|
|
||||||
isOverviewMode.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDropWindowOnWorkspace = async (
|
const handleDropWindowOnWorkspace = async (
|
||||||
event: DragEvent,
|
event: DragEvent,
|
||||||
targetWorkspaceId: string,
|
targetWorkspaceId: string,
|
||||||
@ -616,116 +592,65 @@ const handleDropWindowOnWorkspace = async (
|
|||||||
if (windowId) {
|
if (windowId) {
|
||||||
await moveWindowToWorkspace(windowId, targetWorkspaceId)
|
await moveWindowToWorkspace(windowId, targetWorkspaceId)
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
|
|
||||||
// Overview Mode: Calculate grid positions and scale for windows
|
// Overview Mode: Calculate grid positions and scale for windows
|
||||||
const getOverviewWindowGridStyle = (index: number, totalWindows: number) => {
|
// Calculate preview dimensions for window overview
|
||||||
if (!viewportWidth.value || !viewportHeight.value) {
|
const MIN_PREVIEW_WIDTH = 300 // 50% increase from 200
|
||||||
return {}
|
const MAX_PREVIEW_WIDTH = 600 // 50% increase from 400
|
||||||
}
|
const MIN_PREVIEW_HEIGHT = 225 // 50% increase from 150
|
||||||
|
const MAX_PREVIEW_HEIGHT = 450 // 50% increase from 300
|
||||||
|
|
||||||
// Determine grid layout based on number of windows
|
// Store window state for overview (position only, size stays original)
|
||||||
let cols = 1
|
const overviewWindowState = ref(
|
||||||
let rows = 1
|
new Map<
|
||||||
|
string,
|
||||||
|
{ x: number; y: number; width: number; height: number; scale: number }
|
||||||
|
>(),
|
||||||
|
)
|
||||||
|
|
||||||
if (totalWindows === 1) {
|
// Calculate scale and card dimensions for each window
|
||||||
cols = 1
|
watch(
|
||||||
rows = 1
|
() => windowManager.showWindowOverview,
|
||||||
} else if (totalWindows === 2) {
|
(isOpen) => {
|
||||||
cols = 2
|
if (isOpen) {
|
||||||
rows = 1
|
// Wait for the Overview modal to mount and create the teleport targets
|
||||||
} else if (totalWindows <= 4) {
|
nextTick(() => {
|
||||||
cols = 2
|
windowManager.windows.forEach((window) => {
|
||||||
rows = 2
|
const scaleX = MAX_PREVIEW_WIDTH / window.width
|
||||||
} else if (totalWindows <= 6) {
|
const scaleY = MAX_PREVIEW_HEIGHT / window.height
|
||||||
cols = 3
|
const scale = Math.min(scaleX, scaleY, 1)
|
||||||
rows = 2
|
|
||||||
} else if (totalWindows <= 9) {
|
|
||||||
cols = 3
|
|
||||||
rows = 3
|
|
||||||
} else {
|
|
||||||
cols = 4
|
|
||||||
rows = Math.ceil(totalWindows / 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate grid cell position
|
// Ensure minimum card size
|
||||||
const col = index % cols
|
const scaledWidth = window.width * scale
|
||||||
const row = Math.floor(index / cols)
|
const scaledHeight = window.height * scale
|
||||||
|
|
||||||
// Padding and gap
|
let finalScale = scale
|
||||||
const padding = 40 // px from viewport edges
|
if (scaledWidth < MIN_PREVIEW_WIDTH) {
|
||||||
const gap = 30 // px between windows
|
finalScale = MIN_PREVIEW_WIDTH / window.width
|
||||||
|
}
|
||||||
|
if (scaledHeight < MIN_PREVIEW_HEIGHT) {
|
||||||
|
finalScale = Math.max(
|
||||||
|
finalScale,
|
||||||
|
MIN_PREVIEW_HEIGHT / window.height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Available space
|
overviewWindowState.value.set(window.id, {
|
||||||
const availableWidth = viewportWidth.value - padding * 2 - gap * (cols - 1)
|
x: 0,
|
||||||
const availableHeight = viewportHeight.value - padding * 2 - gap * (rows - 1)
|
y: 0,
|
||||||
|
width: window.width,
|
||||||
// Cell dimensions
|
height: window.height,
|
||||||
const cellWidth = availableWidth / cols
|
scale: finalScale,
|
||||||
const cellHeight = availableHeight / rows
|
})
|
||||||
|
})
|
||||||
// Window aspect ratio (assume 16:9 or use actual window dimensions)
|
})
|
||||||
const windowAspectRatio = 16 / 9
|
} else {
|
||||||
|
// Clear state when overview is closed
|
||||||
// Calculate scale to fit window in cell
|
overviewWindowState.value.clear()
|
||||||
const targetWidth = cellWidth
|
}
|
||||||
const targetHeight = cellHeight
|
},
|
||||||
const targetAspect = targetWidth / targetHeight
|
)
|
||||||
|
|
||||||
let scale = 0.25 // Default scale
|
|
||||||
let scaledWidth = 800 * scale
|
|
||||||
let scaledHeight = 600 * scale
|
|
||||||
|
|
||||||
if (targetAspect > windowAspectRatio) {
|
|
||||||
// Cell is wider than window aspect ratio - fit by height
|
|
||||||
scaledHeight = Math.min(targetHeight, 600 * 0.4)
|
|
||||||
scale = scaledHeight / 600
|
|
||||||
scaledWidth = 800 * scale
|
|
||||||
} else {
|
|
||||||
// Cell is taller than window aspect ratio - fit by width
|
|
||||||
scaledWidth = Math.min(targetWidth, 800 * 0.4)
|
|
||||||
scale = scaledWidth / 800
|
|
||||||
scaledHeight = 600 * scale
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate position to center window in cell
|
|
||||||
const cellX = padding + col * (cellWidth + gap)
|
|
||||||
const cellY = padding + row * (cellHeight + gap)
|
|
||||||
|
|
||||||
// Center window in cell
|
|
||||||
const x = cellX + (cellWidth - scaledWidth) / 2
|
|
||||||
const y = cellY + (cellHeight - scaledHeight) / 2
|
|
||||||
|
|
||||||
return {
|
|
||||||
transform: `scale(${scale})`,
|
|
||||||
transformOrigin: 'top left',
|
|
||||||
left: `${x / scale}px`,
|
|
||||||
top: `${y / scale}px`,
|
|
||||||
width: '800px',
|
|
||||||
height: '600px',
|
|
||||||
zIndex: 91,
|
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overview Mode handlers
|
|
||||||
const handleOverviewWindowClick = (windowId: string) => {
|
|
||||||
// Activate the window
|
|
||||||
windowManager.activateWindow(windowId)
|
|
||||||
// Close overview mode
|
|
||||||
isOverviewMode.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOverviewWindowDragStart = (event: DragEvent, windowId: string) => {
|
|
||||||
if (event.dataTransfer) {
|
|
||||||
event.dataTransfer.effectAllowed = 'move'
|
|
||||||
event.dataTransfer.setData('windowId', windowId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOverviewWindowDragEnd = () => {
|
|
||||||
// Cleanup after drag
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable Swiper in overview mode
|
// Disable Swiper in overview mode
|
||||||
watch(isOverviewMode, (newValue) => {
|
watch(isOverviewMode, (newValue) => {
|
||||||
|
|||||||
@ -1,480 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="windowEl"
|
|
||||||
:style="windowStyle"
|
|
||||||
:class="[
|
|
||||||
'absolute backdrop-blur-xl rounded-xl shadow-2xl overflow-hidden',
|
|
||||||
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600',
|
|
||||||
'flex flex-col',
|
|
||||||
|
|
||||||
isActive ? 'z-50' : 'z-10',
|
|
||||||
]"
|
|
||||||
@mousedown="handleActivate"
|
|
||||||
>
|
|
||||||
<!-- Window Titlebar -->
|
|
||||||
<div
|
|
||||||
ref="titlebarEl"
|
|
||||||
class="grid grid-cols-3 items-center px-3 py-1 bg-white/80 dark:bg-gray-800/80 border-b border-gray-200/50 dark:border-gray-700/50 cursor-move select-none touch-none"
|
|
||||||
@dblclick="handleMaximize"
|
|
||||||
>
|
|
||||||
<!-- Left: Icon -->
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<img
|
|
||||||
v-if="icon"
|
|
||||||
:src="icon"
|
|
||||||
:alt="title"
|
|
||||||
class="w-5 h-5 object-contain flex-shrink-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Center: Title -->
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<span
|
|
||||||
class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-full"
|
|
||||||
>
|
|
||||||
{{ title }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right: Window Controls -->
|
|
||||||
<div class="flex items-center gap-1 justify-end">
|
|
||||||
<button
|
|
||||||
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
|
|
||||||
@click.stop="handleMinimize"
|
|
||||||
>
|
|
||||||
<UIcon
|
|
||||||
name="i-heroicons-minus"
|
|
||||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
|
|
||||||
@click.stop="handleMaximize"
|
|
||||||
>
|
|
||||||
<UIcon
|
|
||||||
:name="
|
|
||||||
isMaximized
|
|
||||||
? 'i-heroicons-arrows-pointing-in'
|
|
||||||
: 'i-heroicons-arrows-pointing-out'
|
|
||||||
"
|
|
||||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-8 h-8 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 flex items-center justify-center transition-colors group"
|
|
||||||
@click.stop="handleClose"
|
|
||||||
>
|
|
||||||
<UIcon
|
|
||||||
name="i-heroicons-x-mark"
|
|
||||||
class="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-red-600 dark:group-hover:text-red-400"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Window Content -->
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'flex-1 overflow-auto relative ',
|
|
||||||
isDragging || isResizing ? 'pointer-events-none select-none' : '',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Resize Handles -->
|
|
||||||
<template v-if="!isMaximized">
|
|
||||||
<div
|
|
||||||
class="absolute top-0 left-0 w-2 h-2 cursor-nw-resize shrink-0"
|
|
||||||
@mousedown.left.stop="handleResizeStart('nw', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="absolute top-0 right-0 w-2 h-2 cursor-ne-resize shrink-0"
|
|
||||||
@mousedown.left.stop="handleResizeStart('ne', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="absolute bottom-0 left-0 w-2 h-2 cursor-sw-resize shrink-0"
|
|
||||||
@mousedown.left.stop="handleResizeStart('sw', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="absolute bottom-0 right-0 w-2 h-2 cursor-se-resize shrink-0"
|
|
||||||
@mousedown.left.stop="handleResizeStart('se', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="absolute top-0 left-2 right-2 h-2 cursor-n-resize shrink-0"
|
|
||||||
@mousedown.left.stop="handleResizeStart('n', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="absolute bottom-0 left-2 right-2 h-2 cursor-s-resize shrink-0"
|
|
||||||
@mousedown.left.stop="handleResizeStart('s', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="absolute left-0 top-2 bottom-2 w-2 cursor-w-resize bg-red-300 overflow-visible"
|
|
||||||
@mousedown.left.stop="handleResizeStart('w', $event)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="absolute right-0 top-2 bottom-2 w-2 cursor-e-resize shrink-0"
|
|
||||||
@mousedown.left.stop="handleResizeStart('e', $event)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
icon?: string | null
|
|
||||||
initialX?: number
|
|
||||||
initialY?: number
|
|
||||||
initialWidth?: number
|
|
||||||
initialHeight?: number
|
|
||||||
isActive?: boolean
|
|
||||||
sourceX?: number
|
|
||||||
sourceY?: number
|
|
||||||
sourceWidth?: number
|
|
||||||
sourceHeight?: number
|
|
||||||
isOpening?: boolean
|
|
||||||
isClosing?: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
close: []
|
|
||||||
minimize: []
|
|
||||||
activate: []
|
|
||||||
positionChanged: [x: number, y: number]
|
|
||||||
sizeChanged: [width: number, height: number]
|
|
||||||
dragStart: []
|
|
||||||
dragEnd: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const windowEl = ref<HTMLElement>()
|
|
||||||
const titlebarEl = useTemplateRef('titlebarEl')
|
|
||||||
|
|
||||||
// Inject viewport size from parent desktop
|
|
||||||
const viewportSize = inject<{
|
|
||||||
width: Ref<number>
|
|
||||||
height: Ref<number>
|
|
||||||
}>('viewportSize')
|
|
||||||
|
|
||||||
// Window state
|
|
||||||
const x = ref(props.initialX ?? 100)
|
|
||||||
const y = ref(props.initialY ?? 100)
|
|
||||||
const width = ref(props.initialWidth ?? 800)
|
|
||||||
const height = ref(props.initialHeight ?? 600)
|
|
||||||
const isMaximized = ref(false) // Don't start maximized
|
|
||||||
|
|
||||||
// Store initial position/size for restore
|
|
||||||
const preMaximizeState = ref({
|
|
||||||
x: props.initialX ?? 100,
|
|
||||||
y: props.initialY ?? 100,
|
|
||||||
width: props.initialWidth ?? 800,
|
|
||||||
height: props.initialHeight ?? 600,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Dragging state
|
|
||||||
const isDragging = ref(false)
|
|
||||||
const dragStartX = ref(0)
|
|
||||||
const dragStartY = ref(0)
|
|
||||||
|
|
||||||
// Resizing state
|
|
||||||
const isResizing = ref(false)
|
|
||||||
const resizeDirection = ref<string>('')
|
|
||||||
const resizeStartX = ref(0)
|
|
||||||
const resizeStartY = ref(0)
|
|
||||||
const resizeStartWidth = ref(0)
|
|
||||||
const resizeStartHeight = ref(0)
|
|
||||||
const resizeStartPosX = ref(0)
|
|
||||||
const resizeStartPosY = ref(0)
|
|
||||||
|
|
||||||
// Snap settings
|
|
||||||
const snapEdgeThreshold = 50 // pixels from edge to trigger snap
|
|
||||||
const { x: mouseX } = useMouse()
|
|
||||||
|
|
||||||
// Setup drag with useDrag composable (supports mouse + touch)
|
|
||||||
useDrag(
|
|
||||||
({ movement: [mx, my], first, last }) => {
|
|
||||||
if (isMaximized.value) return
|
|
||||||
|
|
||||||
if (first) {
|
|
||||||
// Drag started - save initial position
|
|
||||||
isDragging.value = true
|
|
||||||
dragStartX.value = x.value
|
|
||||||
dragStartY.value = y.value
|
|
||||||
emit('dragStart')
|
|
||||||
return // Don't update position on first event
|
|
||||||
}
|
|
||||||
|
|
||||||
if (last) {
|
|
||||||
// Drag ended - apply snapping
|
|
||||||
isDragging.value = false
|
|
||||||
|
|
||||||
const viewportBounds = getViewportBounds()
|
|
||||||
if (viewportBounds) {
|
|
||||||
const viewportWidth = viewportBounds.width
|
|
||||||
const viewportHeight = viewportBounds.height
|
|
||||||
|
|
||||||
if (mouseX.value <= snapEdgeThreshold) {
|
|
||||||
// Snap to left half
|
|
||||||
x.value = 0
|
|
||||||
y.value = 0
|
|
||||||
width.value = viewportWidth / 2
|
|
||||||
height.value = viewportHeight
|
|
||||||
isMaximized.value = false
|
|
||||||
} else if (mouseX.value >= viewportWidth - snapEdgeThreshold) {
|
|
||||||
// Snap to right half
|
|
||||||
x.value = viewportWidth / 2
|
|
||||||
y.value = 0
|
|
||||||
width.value = viewportWidth / 2
|
|
||||||
height.value = viewportHeight
|
|
||||||
isMaximized.value = false
|
|
||||||
} else {
|
|
||||||
// Normal snap back to viewport
|
|
||||||
snapToViewport()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('positionChanged', x.value, y.value)
|
|
||||||
emit('sizeChanged', width.value, height.value)
|
|
||||||
emit('dragEnd')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dragging (not first, not last)
|
|
||||||
const newX = dragStartX.value + mx
|
|
||||||
const newY = dragStartY.value + my
|
|
||||||
|
|
||||||
// Apply constraints during drag
|
|
||||||
const constrained = constrainToViewportDuringDrag(newX, newY)
|
|
||||||
x.value = constrained.x
|
|
||||||
y.value = constrained.y
|
|
||||||
},
|
|
||||||
{
|
|
||||||
domTarget: titlebarEl,
|
|
||||||
eventOptions: { passive: false },
|
|
||||||
pointer: { touch: true },
|
|
||||||
drag: {
|
|
||||||
threshold: 10, // 10px threshold prevents accidental drags and improves performance
|
|
||||||
filterTaps: true, // Filter out taps (clicks) vs drags
|
|
||||||
delay: 0, // No delay for immediate response
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const windowStyle = computed(() => {
|
|
||||||
const baseStyle: Record<string, string> = {}
|
|
||||||
|
|
||||||
// Opening animation: start from icon position
|
|
||||||
if (
|
|
||||||
props.isOpening &&
|
|
||||||
props.sourceX !== undefined &&
|
|
||||||
props.sourceY !== undefined
|
|
||||||
) {
|
|
||||||
baseStyle.left = `${props.sourceX}px`
|
|
||||||
baseStyle.top = `${props.sourceY}px`
|
|
||||||
baseStyle.width = `${props.sourceWidth || 100}px`
|
|
||||||
baseStyle.height = `${props.sourceHeight || 100}px`
|
|
||||||
baseStyle.opacity = '0'
|
|
||||||
baseStyle.transform = 'scale(0.3)'
|
|
||||||
}
|
|
||||||
// Closing animation: shrink to icon position
|
|
||||||
else if (
|
|
||||||
props.isClosing &&
|
|
||||||
props.sourceX !== undefined &&
|
|
||||||
props.sourceY !== undefined
|
|
||||||
) {
|
|
||||||
baseStyle.left = `${props.sourceX}px`
|
|
||||||
baseStyle.top = `${props.sourceY}px`
|
|
||||||
baseStyle.width = `${props.sourceWidth || 100}px`
|
|
||||||
baseStyle.height = `${props.sourceHeight || 100}px`
|
|
||||||
baseStyle.opacity = '0'
|
|
||||||
baseStyle.transform = 'scale(0.3)'
|
|
||||||
}
|
|
||||||
// Normal state
|
|
||||||
else if (isMaximized.value) {
|
|
||||||
baseStyle.left = '0px'
|
|
||||||
baseStyle.top = '0px'
|
|
||||||
baseStyle.width = '100%'
|
|
||||||
baseStyle.height = '100%'
|
|
||||||
baseStyle.borderRadius = '0'
|
|
||||||
baseStyle.opacity = '1'
|
|
||||||
baseStyle.transform = 'scale(1)'
|
|
||||||
} else {
|
|
||||||
baseStyle.left = `${x.value}px`
|
|
||||||
baseStyle.top = `${y.value}px`
|
|
||||||
baseStyle.width = `${width.value}px`
|
|
||||||
baseStyle.height = `${height.value}px`
|
|
||||||
baseStyle.opacity = '1'
|
|
||||||
baseStyle.transform = 'scale(1)'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performance optimization: hint browser about transforms
|
|
||||||
if (isDragging.value || isResizing.value) {
|
|
||||||
baseStyle.willChange = 'transform, width, height'
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseStyle
|
|
||||||
})
|
|
||||||
|
|
||||||
const getViewportBounds = () => {
|
|
||||||
// Use reactive viewport size from parent if available
|
|
||||||
if (viewportSize) {
|
|
||||||
return {
|
|
||||||
width: viewportSize.width.value,
|
|
||||||
height: viewportSize.height.value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to parent element measurement
|
|
||||||
if (!windowEl.value?.parentElement) return null
|
|
||||||
|
|
||||||
const parent = windowEl.value.parentElement
|
|
||||||
return {
|
|
||||||
width: parent.clientWidth,
|
|
||||||
height: parent.clientHeight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const constrainToViewportDuringDrag = (newX: number, newY: number) => {
|
|
||||||
const bounds = getViewportBounds()
|
|
||||||
if (!bounds) return { x: newX, y: newY }
|
|
||||||
|
|
||||||
const windowWidth = width.value
|
|
||||||
const windowHeight = height.value
|
|
||||||
|
|
||||||
// Allow max 1/3 of window to go outside viewport during drag
|
|
||||||
const maxOffscreenX = windowWidth / 3
|
|
||||||
const maxOffscreenY = windowHeight / 3
|
|
||||||
|
|
||||||
const maxX = bounds.width - windowWidth + maxOffscreenX
|
|
||||||
const minX = -maxOffscreenX
|
|
||||||
const maxY = bounds.height - windowHeight + maxOffscreenY
|
|
||||||
const minY = -maxOffscreenY
|
|
||||||
|
|
||||||
const constrainedX = Math.max(minX, Math.min(maxX, newX))
|
|
||||||
const constrainedY = Math.max(minY, Math.min(maxY, newY))
|
|
||||||
|
|
||||||
return { x: constrainedX, y: constrainedY }
|
|
||||||
}
|
|
||||||
|
|
||||||
const constrainToViewportFully = (
|
|
||||||
newX: number,
|
|
||||||
newY: number,
|
|
||||||
newWidth?: number,
|
|
||||||
newHeight?: number,
|
|
||||||
) => {
|
|
||||||
const bounds = getViewportBounds()
|
|
||||||
if (!bounds) return { x: newX, y: newY }
|
|
||||||
|
|
||||||
const windowWidth = newWidth ?? width.value
|
|
||||||
const windowHeight = newHeight ?? height.value
|
|
||||||
|
|
||||||
// Keep entire window within viewport
|
|
||||||
const maxX = bounds.width - windowWidth
|
|
||||||
const minX = 0
|
|
||||||
const maxY = bounds.height - windowHeight
|
|
||||||
const minY = 0
|
|
||||||
|
|
||||||
const constrainedX = Math.max(minX, Math.min(maxX, newX))
|
|
||||||
const constrainedY = Math.max(minY, Math.min(maxY, newY))
|
|
||||||
|
|
||||||
return { x: constrainedX, y: constrainedY }
|
|
||||||
}
|
|
||||||
|
|
||||||
const snapToViewport = () => {
|
|
||||||
const bounds = getViewportBounds()
|
|
||||||
if (!bounds) return
|
|
||||||
|
|
||||||
const constrained = constrainToViewportFully(x.value, y.value)
|
|
||||||
x.value = constrained.x
|
|
||||||
y.value = constrained.y
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleActivate = () => {
|
|
||||||
emit('activate')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMinimize = () => {
|
|
||||||
emit('minimize')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMaximize = () => {
|
|
||||||
if (isMaximized.value) {
|
|
||||||
// Restore
|
|
||||||
x.value = preMaximizeState.value.x
|
|
||||||
y.value = preMaximizeState.value.y
|
|
||||||
width.value = preMaximizeState.value.width
|
|
||||||
height.value = preMaximizeState.value.height
|
|
||||||
isMaximized.value = false
|
|
||||||
} else {
|
|
||||||
// Maximize
|
|
||||||
preMaximizeState.value = {
|
|
||||||
x: x.value,
|
|
||||||
y: y.value,
|
|
||||||
width: width.value,
|
|
||||||
height: height.value,
|
|
||||||
}
|
|
||||||
isMaximized.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Window resizing
|
|
||||||
const handleResizeStart = (direction: string, e: MouseEvent) => {
|
|
||||||
isResizing.value = true
|
|
||||||
resizeDirection.value = direction
|
|
||||||
resizeStartX.value = e.clientX
|
|
||||||
resizeStartY.value = e.clientY
|
|
||||||
resizeStartWidth.value = width.value
|
|
||||||
resizeStartHeight.value = height.value
|
|
||||||
resizeStartPosX.value = x.value
|
|
||||||
resizeStartPosY.value = y.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global mouse move handler (for resizing only, dragging handled by useDrag)
|
|
||||||
useEventListener(window, 'mousemove', (e: MouseEvent) => {
|
|
||||||
if (isResizing.value) {
|
|
||||||
const deltaX = e.clientX - resizeStartX.value
|
|
||||||
const deltaY = e.clientY - resizeStartY.value
|
|
||||||
|
|
||||||
const dir = resizeDirection.value
|
|
||||||
|
|
||||||
// Handle width changes
|
|
||||||
if (dir.includes('e')) {
|
|
||||||
width.value = Math.max(300, resizeStartWidth.value + deltaX)
|
|
||||||
} else if (dir.includes('w')) {
|
|
||||||
const newWidth = Math.max(300, resizeStartWidth.value - deltaX)
|
|
||||||
const widthDiff = resizeStartWidth.value - newWidth
|
|
||||||
x.value = resizeStartPosX.value + widthDiff
|
|
||||||
width.value = newWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle height changes
|
|
||||||
if (dir.includes('s')) {
|
|
||||||
height.value = Math.max(200, resizeStartHeight.value + deltaY)
|
|
||||||
} else if (dir.includes('n')) {
|
|
||||||
const newHeight = Math.max(200, resizeStartHeight.value - deltaY)
|
|
||||||
const heightDiff = resizeStartHeight.value - newHeight
|
|
||||||
y.value = resizeStartPosY.value + heightDiff
|
|
||||||
height.value = newHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Global mouse up handler (for resizing only, dragging handled by useDrag)
|
|
||||||
useEventListener(window, 'mouseup', () => {
|
|
||||||
if (isResizing.value) {
|
|
||||||
globalThis.getSelection()?.removeAllRanges()
|
|
||||||
isResizing.value = false
|
|
||||||
|
|
||||||
// Snap back to viewport after resize ends
|
|
||||||
snapToViewport()
|
|
||||||
|
|
||||||
emit('positionChanged', x.value, y.value)
|
|
||||||
emit('sizeChanged', width.value, height.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@ -89,7 +89,11 @@ const removeExtensionAsync = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await extensionStore.removeExtensionAsync(extension.id, extension.version)
|
await extensionStore.removeExtensionAsync(
|
||||||
|
extension.publicKey,
|
||||||
|
extension.name,
|
||||||
|
extension.version,
|
||||||
|
)
|
||||||
await extensionStore.loadExtensionsAsync()
|
await extensionStore.loadExtensionsAsync()
|
||||||
|
|
||||||
add({
|
add({
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div
|
<div
|
||||||
v-if="preview?.manifest.icon"
|
v-if="preview?.manifest.icon"
|
||||||
class="w-16 h-16 flex-shrink-0"
|
class="w-16 h-16 shrink-0"
|
||||||
>
|
>
|
||||||
<UIcon
|
<UIcon
|
||||||
:name="preview.manifest.icon"
|
:name="preview.manifest.icon"
|
||||||
@ -184,7 +184,6 @@ const shellPermissions = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const permissionAccordionItems = computed(() => {
|
const permissionAccordionItems = computed(() => {
|
||||||
const items = []
|
const items = []
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<UPopover v-model:open="open">
|
<UDrawer
|
||||||
|
v-model:open="open"
|
||||||
|
direction="right"
|
||||||
|
:title="t('launcher.title')"
|
||||||
|
:description="t('launcher.description')"
|
||||||
|
:ui="{
|
||||||
|
content: 'w-dvw max-w-md sm:max-w-fit',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<UButton
|
<UButton
|
||||||
icon="material-symbols:apps"
|
icon="material-symbols:apps"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
@ -9,48 +17,75 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<ul class="p-4 max-h-96 grid grid-cols-3 gap-2 overflow-scroll">
|
<div class="p-4 h-full overflow-y-auto">
|
||||||
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
|
<div class="flex flex-wrap">
|
||||||
<UiButton
|
<!-- All launcher items (system windows + enabled extensions, alphabetically sorted) -->
|
||||||
v-for="item in launcherItems"
|
<UContextMenu
|
||||||
:key="item.id"
|
v-for="item in launcherItems"
|
||||||
square
|
:key="item.id"
|
||||||
size="xl"
|
:items="getContextMenuItems(item)"
|
||||||
variant="ghost"
|
>
|
||||||
:ui="{
|
<UiButton
|
||||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible',
|
square
|
||||||
leadingIcon: 'size-10',
|
size="lg"
|
||||||
label: 'w-full',
|
variant="ghost"
|
||||||
}"
|
:ui="{
|
||||||
:icon="item.icon"
|
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible cursor-grab active:cursor-grabbing',
|
||||||
:label="item.name"
|
leadingIcon: 'size-10',
|
||||||
:tooltip="item.name"
|
label: 'w-full',
|
||||||
@click="openItem(item)"
|
}"
|
||||||
/>
|
:icon="item.icon"
|
||||||
|
:label="item.name"
|
||||||
|
:tooltip="item.name"
|
||||||
|
draggable="true"
|
||||||
|
@click="openItem(item)"
|
||||||
|
@dragstart="handleDragStart($event, item)"
|
||||||
|
@dragend="handleDragEnd"
|
||||||
|
/>
|
||||||
|
</UContextMenu>
|
||||||
|
|
||||||
<!-- Disabled Extensions (grayed out) -->
|
<!-- Disabled Extensions (grayed out) -->
|
||||||
<UiButton
|
<UiButton
|
||||||
v-for="extension in disabledExtensions"
|
v-for="extension in disabledExtensions"
|
||||||
:key="extension.id"
|
:key="extension.id"
|
||||||
square
|
square
|
||||||
size="xl"
|
size="xl"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:disabled="true"
|
:disabled="true"
|
||||||
:ui="{
|
:ui="{
|
||||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible opacity-40',
|
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible opacity-40',
|
||||||
leadingIcon: 'size-10',
|
leadingIcon: 'size-10',
|
||||||
label: 'w-full',
|
label: 'w-full',
|
||||||
}"
|
}"
|
||||||
:icon="extension.icon || 'i-heroicons-puzzle-piece-solid'"
|
:icon="extension.icon || 'i-heroicons-puzzle-piece-solid'"
|
||||||
:label="extension.name"
|
:label="extension.name"
|
||||||
:tooltip="`${extension.name} (${t('disabled')})`"
|
:tooltip="`${extension.name} (${t('disabled')})`"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UPopover>
|
</UDrawer>
|
||||||
|
|
||||||
|
<!-- Uninstall Confirmation Dialog -->
|
||||||
|
<UiDialogConfirm
|
||||||
|
v-model:open="showUninstallDialog"
|
||||||
|
:title="t('uninstall.confirm.title')"
|
||||||
|
:description="
|
||||||
|
t('uninstall.confirm.description', {
|
||||||
|
name: extensionToUninstall?.name || '',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:confirm-label="t('uninstall.confirm.button')"
|
||||||
|
confirm-icon="i-heroicons-trash"
|
||||||
|
@confirm="confirmUninstall"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
const extensionStore = useExtensionsStore()
|
const extensionStore = useExtensionsStore()
|
||||||
const windowManagerStore = useWindowManagerStore()
|
const windowManagerStore = useWindowManagerStore()
|
||||||
|
|
||||||
@ -58,6 +93,10 @@ const { t } = useI18n()
|
|||||||
|
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
|
|
||||||
|
// Uninstall dialog state
|
||||||
|
const showUninstallDialog = ref(false)
|
||||||
|
const extensionToUninstall = ref<LauncherItem | null>(null)
|
||||||
|
|
||||||
// Unified launcher item type
|
// Unified launcher item type
|
||||||
interface LauncherItem {
|
interface LauncherItem {
|
||||||
id: string
|
id: string
|
||||||
@ -119,14 +158,123 @@ const openItem = async (item: LauncherItem) => {
|
|||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uninstall extension - shows confirmation dialog first
|
||||||
|
const uninstallExtension = async (item: LauncherItem) => {
|
||||||
|
extensionToUninstall.value = item
|
||||||
|
showUninstallDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm uninstall - actually removes the extension
|
||||||
|
const confirmUninstall = async () => {
|
||||||
|
if (!extensionToUninstall.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const extension = extensionStore.availableExtensions.find(
|
||||||
|
(ext) => ext.id === extensionToUninstall.value!.id,
|
||||||
|
)
|
||||||
|
if (!extension) return
|
||||||
|
|
||||||
|
// Close all windows of this extension first
|
||||||
|
const extensionWindows = windowManagerStore.windows.filter(
|
||||||
|
(win) => win.type === 'extension' && win.sourceId === extension.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const win of extensionWindows) {
|
||||||
|
windowManagerStore.closeWindow(win.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uninstall the extension
|
||||||
|
await extensionStore.removeExtensionAsync(
|
||||||
|
extension.publicKey,
|
||||||
|
extension.name,
|
||||||
|
extension.version,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Refresh available extensions list
|
||||||
|
await extensionStore.loadExtensionsAsync()
|
||||||
|
|
||||||
|
// Close dialog and reset state
|
||||||
|
showUninstallDialog.value = false
|
||||||
|
extensionToUninstall.value = null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to uninstall extension:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get context menu items for launcher item
|
||||||
|
const getContextMenuItems = (item: LauncherItem) => {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: t('contextMenu.open'),
|
||||||
|
icon: 'i-heroicons-arrow-top-right-on-square',
|
||||||
|
onSelect: () => openItem(item),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Add uninstall option for extensions
|
||||||
|
if (item.type === 'extension') {
|
||||||
|
items.push({
|
||||||
|
label: t('contextMenu.uninstall'),
|
||||||
|
icon: 'i-heroicons-trash',
|
||||||
|
onSelect: () => uninstallExtension(item),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag & Drop handling
|
||||||
|
const handleDragStart = (event: DragEvent, item: LauncherItem) => {
|
||||||
|
if (!event.dataTransfer) return
|
||||||
|
|
||||||
|
// Store the launcher item data
|
||||||
|
event.dataTransfer.effectAllowed = 'copy'
|
||||||
|
event.dataTransfer.setData(
|
||||||
|
'application/haex-launcher-item',
|
||||||
|
JSON.stringify(item),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set drag image (optional - uses default if not set)
|
||||||
|
const dragImage = event.target as HTMLElement
|
||||||
|
if (dragImage) {
|
||||||
|
event.dataTransfer.setDragImage(dragImage, 20, 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
// Cleanup if needed
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
de:
|
de:
|
||||||
disabled: Deaktiviert
|
disabled: Deaktiviert
|
||||||
marketplace: Marketplace
|
marketplace: Marketplace
|
||||||
|
launcher:
|
||||||
|
title: App Launcher
|
||||||
|
description: Wähle eine App zum Öffnen
|
||||||
|
contextMenu:
|
||||||
|
open: Öffnen
|
||||||
|
uninstall: Deinstallieren
|
||||||
|
uninstall:
|
||||||
|
confirm:
|
||||||
|
title: Erweiterung deinstallieren
|
||||||
|
description: Möchtest du wirklich "{name}" deinstallieren? Diese Aktion kann nicht rückgängig gemacht werden.
|
||||||
|
button: Deinstallieren
|
||||||
|
|
||||||
en:
|
en:
|
||||||
disabled: Disabled
|
disabled: Disabled
|
||||||
marketplace: Marketplace
|
marketplace: Marketplace
|
||||||
|
launcher:
|
||||||
|
title: App Launcher
|
||||||
|
description: Select an app to open
|
||||||
|
contextMenu:
|
||||||
|
open: Open
|
||||||
|
uninstall: Uninstall
|
||||||
|
uninstall:
|
||||||
|
confirm:
|
||||||
|
title: Uninstall Extension
|
||||||
|
description: Do you really want to uninstall "{name}"? This action cannot be undone.
|
||||||
|
button: Uninstall
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
:checked="Object.values(read).at(0)"
|
:checked="Object.values(read).at(0)"
|
||||||
/>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text text-base"
|
class="label-text text-base"
|
||||||
:for="Object.keys(read).at(0)"
|
:for="Object.keys(read).at(0)"
|
||||||
@ -42,7 +42,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
:checked="Object.values(write).at(0)"
|
:checked="Object.values(write).at(0)"
|
||||||
/>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text text-base"
|
class="label-text text-base"
|
||||||
:for="Object.keys(write).at(0)"
|
:for="Object.keys(write).at(0)"
|
||||||
@ -69,7 +69,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
:checked="Object.values(create).at(0)"
|
:checked="Object.values(create).at(0)"
|
||||||
/>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text text-base"
|
class="label-text text-base"
|
||||||
:for="Object.keys(create).at(0)"
|
:for="Object.keys(create).at(0)"
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
:checked="Object.values(read).at(0)"
|
:checked="Object.values(read).at(0)"
|
||||||
/>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text text-base"
|
class="label-text text-base"
|
||||||
:for="Object.keys(read).at(0)"
|
:for="Object.keys(read).at(0)"
|
||||||
@ -41,7 +41,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
:checked="Object.values(write).at(0)"
|
:checked="Object.values(write).at(0)"
|
||||||
/>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text text-base"
|
class="label-text text-base"
|
||||||
:for="Object.keys(write).at(0)"
|
:for="Object.keys(write).at(0)"
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
:checked="Object.values(access).at(0)"
|
:checked="Object.values(access).at(0)"
|
||||||
/>
|
>
|
||||||
<label
|
<label
|
||||||
class="label-text text-base"
|
class="label-text text-base"
|
||||||
:for="Object.keys(access).at(0)"
|
:for="Object.keys(access).at(0)"
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<!-- Icon -->
|
<!-- Icon -->
|
||||||
<div class="flex-shrink-0">
|
<div class="shrink-0">
|
||||||
<div
|
<div
|
||||||
v-if="extension.icon"
|
v-if="extension.icon"
|
||||||
class="w-16 h-16 rounded-lg bg-primary/10 flex items-center justify-center"
|
class="w-16 h-16 rounded-lg bg-primary/10 flex items-center justify-center"
|
||||||
|
|||||||
@ -33,8 +33,8 @@
|
|||||||
:label="t('extension.installFromFile')"
|
:label="t('extension.installFromFile')"
|
||||||
icon="i-heroicons-arrow-up-tray"
|
icon="i-heroicons-arrow-up-tray"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
@click="onSelectExtensionAsync"
|
|
||||||
block
|
block
|
||||||
|
@click="onSelectExtensionAsync"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-full"></div>
|
<div class="h-full"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
<UiDialogConfirm
|
<UiDialogConfirm
|
||||||
:confirm-label="t('create')"
|
:confirm-label="t('create')"
|
||||||
@confirm="onCreateAsync"
|
@confirm="onCreateAsync"
|
||||||
|
:description="t('description')"
|
||||||
>
|
>
|
||||||
<UiButton
|
<UiButton
|
||||||
:label="t('vault.create')"
|
:label="t('vault.create')"
|
||||||
@ -55,7 +56,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { vaultSchema } from './schema'
|
import { vaultSchema } from './schema'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n({
|
||||||
|
useScope: 'local',
|
||||||
|
})
|
||||||
|
|
||||||
const vault = reactive<{
|
const vault = reactive<{
|
||||||
name: string
|
name: string
|
||||||
@ -118,6 +121,7 @@ de:
|
|||||||
name: HaexVault
|
name: HaexVault
|
||||||
title: Neue {haexvault} erstellen
|
title: Neue {haexvault} erstellen
|
||||||
create: Erstellen
|
create: Erstellen
|
||||||
|
description: Erstelle eine neue Vault für deine Daten
|
||||||
|
|
||||||
en:
|
en:
|
||||||
vault:
|
vault:
|
||||||
@ -127,4 +131,5 @@ en:
|
|||||||
name: HaexVault
|
name: HaexVault
|
||||||
title: Create new {haexvault}
|
title: Create new {haexvault}
|
||||||
create: Create
|
create: Create
|
||||||
|
description: Create a new vault for your data
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
:description="vault.path || path"
|
:description="vault.path || path"
|
||||||
@confirm="onOpenDatabase"
|
@confirm="onOpenDatabase"
|
||||||
>
|
>
|
||||||
<!-- <UiButton
|
<UiButton
|
||||||
:label="t('vault.open')"
|
:label="t('vault.open')"
|
||||||
:ui="{
|
:ui="{
|
||||||
base: 'px-3 py-2',
|
base: 'px-3 py-2',
|
||||||
@ -14,8 +14,7 @@
|
|||||||
size="xl"
|
size="xl"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
block
|
block
|
||||||
@click.stop="onLoadDatabase"
|
/>
|
||||||
/> -->
|
|
||||||
|
|
||||||
<template #title>
|
<template #title>
|
||||||
<i18n-t
|
<i18n-t
|
||||||
@ -59,7 +58,9 @@ const props = defineProps<{
|
|||||||
path?: string
|
path?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n({
|
||||||
|
useScope: 'local',
|
||||||
|
})
|
||||||
|
|
||||||
const vault = reactive<{
|
const vault = reactive<{
|
||||||
name: string
|
name: string
|
||||||
@ -100,9 +101,6 @@ const vault = reactive<{
|
|||||||
}
|
}
|
||||||
} */
|
} */
|
||||||
|
|
||||||
const { syncLocaleAsync, syncThemeAsync, syncVaultNameAsync } =
|
|
||||||
useVaultSettingsStore()
|
|
||||||
|
|
||||||
const check = ref(false)
|
const check = ref(false)
|
||||||
|
|
||||||
const initVault = () => {
|
const initVault = () => {
|
||||||
@ -156,15 +154,17 @@ const onOpenDatabase = async () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
await Promise.allSettled([
|
|
||||||
syncLocaleAsync(),
|
|
||||||
syncThemeAsync(),
|
|
||||||
syncVaultNameAsync(),
|
|
||||||
])
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
open.value = false
|
open.value = false
|
||||||
console.error('handleError', error, typeof error)
|
if (error?.details?.reason === 'file is not a database') {
|
||||||
add({ color: 'error', description: `${error}` })
|
add({
|
||||||
|
color: 'error',
|
||||||
|
title: t('error.password.title'),
|
||||||
|
description: t('error.password.description'),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
add({ color: 'error', description: JSON.stringify(error) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -178,7 +178,9 @@ de:
|
|||||||
open: Vault öffnen
|
open: Vault öffnen
|
||||||
description: Öffne eine vorhandene Vault
|
description: Öffne eine vorhandene Vault
|
||||||
error:
|
error:
|
||||||
open: Vault konnte nicht geöffnet werden. \n Vermutlich ist das Passwort falsch
|
password:
|
||||||
|
title: Vault konnte nicht geöffnet werden
|
||||||
|
description: Bitte üperprüfe das Passwort
|
||||||
|
|
||||||
en:
|
en:
|
||||||
open: Unlock
|
open: Unlock
|
||||||
@ -188,5 +190,7 @@ en:
|
|||||||
vault:
|
vault:
|
||||||
open: Open Vault
|
open: Open Vault
|
||||||
error:
|
error:
|
||||||
open: Vault couldn't be opened. \n The password is probably wrong
|
password:
|
||||||
|
title: Vault couldn't be opened
|
||||||
|
description: Please check your password
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
83
src/components/haex/window/button.vue
Normal file
83
src/components/haex/window/button.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<UTooltip :text="tooltip">
|
||||||
|
<button
|
||||||
|
class="size-8 shrink-0 rounded-lg flex justify-center transition-colors group"
|
||||||
|
:class="variantClasses.buttonClass"
|
||||||
|
@click="(e) => $emit('click', e)"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
:name="icon"
|
||||||
|
class="size-4 text-gray-600 dark:text-gray-400"
|
||||||
|
:class="variantClasses.iconClass"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</UTooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
variant: 'close' | 'maximize' | 'minimize'
|
||||||
|
isMaximized?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits(['click'])
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
switch (props.variant) {
|
||||||
|
case 'close':
|
||||||
|
return 'i-heroicons-x-mark'
|
||||||
|
case 'maximize':
|
||||||
|
return props.isMaximized
|
||||||
|
? 'i-heroicons-arrows-pointing-in'
|
||||||
|
: 'i-heroicons-arrows-pointing-out'
|
||||||
|
default:
|
||||||
|
return 'i-heroicons-minus'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const variantClasses = computed(() => {
|
||||||
|
if (props.variant === 'close') {
|
||||||
|
return {
|
||||||
|
iconClass: 'group-hover:text-error',
|
||||||
|
buttonClass: 'hover:bg-error/30 items-center',
|
||||||
|
}
|
||||||
|
} else if (props.variant === 'maximize') {
|
||||||
|
return {
|
||||||
|
iconClass: 'group-hover:text-warning',
|
||||||
|
buttonClass: 'hover:bg-warning/30 items-center',
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
iconClass: 'group-hover:text-success',
|
||||||
|
buttonClass: 'hover:bg-success/30 items-end pb-1',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const tooltip = computed(() => {
|
||||||
|
switch (props.variant) {
|
||||||
|
case 'close':
|
||||||
|
return t('close')
|
||||||
|
case 'maximize':
|
||||||
|
return props.isMaximized ? t('shrink') : t('maximize')
|
||||||
|
default:
|
||||||
|
return t('minimize')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
de:
|
||||||
|
close: Schließen
|
||||||
|
maximize: Maximieren
|
||||||
|
shrink: Verkleinern
|
||||||
|
minimize: Minimieren
|
||||||
|
|
||||||
|
en:
|
||||||
|
close: Close
|
||||||
|
maximize: Maximize
|
||||||
|
shrink: Shrink
|
||||||
|
minimize: Minimize
|
||||||
|
</i18n>
|
||||||
@ -3,11 +3,17 @@
|
|||||||
ref="windowEl"
|
ref="windowEl"
|
||||||
:style="windowStyle"
|
:style="windowStyle"
|
||||||
:class="[
|
:class="[
|
||||||
'absolute bg-default/80 backdrop-blur-xl rounded-xl shadow-2xl overflow-hidden isolate',
|
'absolute bg-default/80 backdrop-blur-xl rounded-lg shadow-xl overflow-hidden',
|
||||||
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600 ',
|
'transition-all ease-out duration-600',
|
||||||
'flex flex-col @container',
|
'flex flex-col @container',
|
||||||
{ 'select-none': isResizingOrDragging },
|
{ 'select-none': isResizingOrDragging },
|
||||||
isActive ? 'z-50' : 'z-10',
|
isActive ? 'z-20' : 'z-10',
|
||||||
|
// Border colors based on warning level
|
||||||
|
warningLevel === 'warning'
|
||||||
|
? 'border-2 border-warning-500'
|
||||||
|
: warningLevel === 'danger'
|
||||||
|
? 'border-2 border-danger-500'
|
||||||
|
: 'border border-gray-200 dark:border-gray-700',
|
||||||
]"
|
]"
|
||||||
@mousedown="handleActivate"
|
@mousedown="handleActivate"
|
||||||
>
|
>
|
||||||
@ -23,7 +29,7 @@
|
|||||||
v-if="icon"
|
v-if="icon"
|
||||||
:src="icon"
|
:src="icon"
|
||||||
:alt="title"
|
:alt="title"
|
||||||
class="w-5 h-5 object-contain flex-shrink-0"
|
class="w-5 h-5 object-contain shrink-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -38,37 +44,21 @@
|
|||||||
|
|
||||||
<!-- Right: Window Controls -->
|
<!-- Right: Window Controls -->
|
||||||
<div class="flex items-center gap-1 justify-end">
|
<div class="flex items-center gap-1 justify-end">
|
||||||
<button
|
<HaexWindowButton
|
||||||
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
|
variant="minimize"
|
||||||
@click.stop="handleMinimize"
|
@click.stop="handleMinimize"
|
||||||
>
|
/>
|
||||||
<UIcon
|
|
||||||
name="i-heroicons-minus"
|
<HaexWindowButton
|
||||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
:is-maximized
|
||||||
/>
|
variant="maximize"
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
|
|
||||||
@click.stop="handleMaximize"
|
@click.stop="handleMaximize"
|
||||||
>
|
/>
|
||||||
<UIcon
|
|
||||||
:name="
|
<HaexWindowButton
|
||||||
isMaximized
|
variant="close"
|
||||||
? 'i-heroicons-arrows-pointing-in'
|
|
||||||
: 'i-heroicons-arrows-pointing-out'
|
|
||||||
"
|
|
||||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w-8 h-8 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 flex items-center justify-center transition-colors group"
|
|
||||||
@click.stop="handleClose"
|
@click.stop="handleClose"
|
||||||
>
|
/>
|
||||||
<UIcon
|
|
||||||
name="i-heroicons-x-mark"
|
|
||||||
class="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-red-600 dark:group-hover:text-red-400"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -95,10 +85,6 @@ const props = defineProps<{
|
|||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
icon?: string | null
|
icon?: string | null
|
||||||
initialX?: number
|
|
||||||
initialY?: number
|
|
||||||
initialWidth?: number
|
|
||||||
initialHeight?: number
|
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
sourceX?: number
|
sourceX?: number
|
||||||
sourceY?: number
|
sourceY?: number
|
||||||
@ -106,6 +92,7 @@ const props = defineProps<{
|
|||||||
sourceHeight?: number
|
sourceHeight?: number
|
||||||
isOpening?: boolean
|
isOpening?: boolean
|
||||||
isClosing?: boolean
|
isClosing?: boolean
|
||||||
|
warningLevel?: 'warning' | 'danger' // Warning indicator (e.g., dev extension, dangerous permissions)
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -118,6 +105,12 @@ const emit = defineEmits<{
|
|||||||
dragEnd: []
|
dragEnd: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Use defineModel for x, y, width, height
|
||||||
|
const x = defineModel<number>('x', { default: 100 })
|
||||||
|
const y = defineModel<number>('y', { default: 100 })
|
||||||
|
const width = defineModel<number>('width', { default: 800 })
|
||||||
|
const height = defineModel<number>('height', { default: 600 })
|
||||||
|
|
||||||
const windowEl = useTemplateRef('windowEl')
|
const windowEl = useTemplateRef('windowEl')
|
||||||
const titlebarEl = useTemplateRef('titlebarEl')
|
const titlebarEl = useTemplateRef('titlebarEl')
|
||||||
|
|
||||||
@ -126,20 +119,14 @@ const viewportSize = inject<{
|
|||||||
width: Ref<number>
|
width: Ref<number>
|
||||||
height: Ref<number>
|
height: Ref<number>
|
||||||
}>('viewportSize')
|
}>('viewportSize')
|
||||||
|
|
||||||
// Window state
|
|
||||||
const x = ref(props.initialX ?? 100)
|
|
||||||
const y = ref(props.initialY ?? 100)
|
|
||||||
const width = ref(props.initialWidth ?? 800)
|
|
||||||
const height = ref(props.initialHeight ?? 600)
|
|
||||||
const isMaximized = ref(false) // Don't start maximized
|
const isMaximized = ref(false) // Don't start maximized
|
||||||
|
|
||||||
// Store initial position/size for restore
|
// Store initial position/size for restore
|
||||||
const preMaximizeState = ref({
|
const preMaximizeState = ref({
|
||||||
x: props.initialX ?? 100,
|
x: x.value,
|
||||||
y: props.initialY ?? 100,
|
y: y.value,
|
||||||
width: props.initialWidth ?? 800,
|
width: width.value,
|
||||||
height: props.initialHeight ?? 600,
|
height: height.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Dragging state
|
// Dragging state
|
||||||
@ -161,10 +148,6 @@ const isResizingOrDragging = computed(
|
|||||||
() => isResizing.value || isDragging.value,
|
() => isResizing.value || isDragging.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Snap settings
|
|
||||||
const snapEdgeThreshold = 50 // pixels from edge to trigger snap
|
|
||||||
const { x: mouseX } = useMouse()
|
|
||||||
|
|
||||||
// Setup drag with useDrag composable (supports mouse + touch)
|
// Setup drag with useDrag composable (supports mouse + touch)
|
||||||
useDrag(
|
useDrag(
|
||||||
({ movement: [mx, my], first, last }) => {
|
({ movement: [mx, my], first, last }) => {
|
||||||
@ -180,34 +163,8 @@ useDrag(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (last) {
|
if (last) {
|
||||||
// Drag ended - apply snapping
|
// Drag ended
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
|
|
||||||
const viewportBounds = getViewportBounds()
|
|
||||||
if (viewportBounds) {
|
|
||||||
const viewportWidth = viewportBounds.width
|
|
||||||
const viewportHeight = viewportBounds.height
|
|
||||||
|
|
||||||
if (mouseX.value <= snapEdgeThreshold) {
|
|
||||||
// Snap to left half
|
|
||||||
x.value = 0
|
|
||||||
y.value = 0
|
|
||||||
width.value = viewportWidth / 2
|
|
||||||
height.value = viewportHeight
|
|
||||||
isMaximized.value = false
|
|
||||||
} else if (mouseX.value >= viewportWidth - snapEdgeThreshold) {
|
|
||||||
// Snap to right half
|
|
||||||
x.value = viewportWidth / 2
|
|
||||||
y.value = 0
|
|
||||||
width.value = viewportWidth / 2
|
|
||||||
height.value = viewportHeight
|
|
||||||
isMaximized.value = false
|
|
||||||
} else {
|
|
||||||
// Normal snap back to viewport
|
|
||||||
snapToViewport()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
globalThis.getSelection()?.removeAllRanges()
|
globalThis.getSelection()?.removeAllRanges()
|
||||||
emit('positionChanged', x.value, y.value)
|
emit('positionChanged', x.value, y.value)
|
||||||
emit('sizeChanged', width.value, height.value)
|
emit('sizeChanged', width.value, height.value)
|
||||||
@ -229,7 +186,6 @@ useDrag(
|
|||||||
eventOptions: { passive: false },
|
eventOptions: { passive: false },
|
||||||
pointer: { touch: true },
|
pointer: { touch: true },
|
||||||
drag: {
|
drag: {
|
||||||
threshold: 10, // 10px threshold prevents accidental drags and improves performance
|
|
||||||
filterTaps: true, // Filter out taps (clicks) vs drags
|
filterTaps: true, // Filter out taps (clicks) vs drags
|
||||||
delay: 0, // No delay for immediate response
|
delay: 0, // No delay for immediate response
|
||||||
},
|
},
|
||||||
@ -265,22 +221,18 @@ const windowStyle = computed(() => {
|
|||||||
baseStyle.opacity = '0'
|
baseStyle.opacity = '0'
|
||||||
baseStyle.transform = 'scale(0.3)'
|
baseStyle.transform = 'scale(0.3)'
|
||||||
}
|
}
|
||||||
// Normal state
|
// Normal state (maximized windows now use actual pixel dimensions)
|
||||||
else if (isMaximized.value) {
|
else {
|
||||||
baseStyle.left = '0px'
|
|
||||||
baseStyle.top = '0px'
|
|
||||||
baseStyle.width = '100%'
|
|
||||||
baseStyle.height = '100%'
|
|
||||||
baseStyle.borderRadius = '0'
|
|
||||||
baseStyle.opacity = '1'
|
|
||||||
//baseStyle.transform = 'scale(1)'
|
|
||||||
} else {
|
|
||||||
baseStyle.left = `${x.value}px`
|
baseStyle.left = `${x.value}px`
|
||||||
baseStyle.top = `${y.value}px`
|
baseStyle.top = `${y.value}px`
|
||||||
baseStyle.width = `${width.value}px`
|
baseStyle.width = `${width.value}px`
|
||||||
baseStyle.height = `${height.value}px`
|
baseStyle.height = `${height.value}px`
|
||||||
baseStyle.opacity = '1'
|
baseStyle.opacity = '1'
|
||||||
//baseStyle.transform = 'scale(1)'
|
|
||||||
|
// Remove border-radius when maximized
|
||||||
|
if (isMaximized.value) {
|
||||||
|
baseStyle.borderRadius = '0'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Performance optimization: hint browser about transforms
|
// Performance optimization: hint browser about transforms
|
||||||
@ -318,38 +270,18 @@ const constrainToViewportDuringDrag = (newX: number, newY: number) => {
|
|||||||
const windowWidth = width.value
|
const windowWidth = width.value
|
||||||
const windowHeight = height.value
|
const windowHeight = height.value
|
||||||
|
|
||||||
// Allow max 1/3 of window to go outside viewport during drag
|
// Allow sides and bottom to go out more
|
||||||
const maxOffscreenX = windowWidth / 3
|
const maxOffscreenX = windowWidth / 3
|
||||||
const maxOffscreenY = windowHeight / 3
|
const maxOffscreenBottom = windowHeight / 3
|
||||||
|
|
||||||
|
// For X axis: allow 1/3 to go outside on both sides
|
||||||
const maxX = bounds.width - windowWidth + maxOffscreenX
|
const maxX = bounds.width - windowWidth + maxOffscreenX
|
||||||
const minX = -maxOffscreenX
|
const minX = -maxOffscreenX
|
||||||
const maxY = bounds.height - windowHeight + maxOffscreenY
|
|
||||||
const minY = -maxOffscreenY
|
|
||||||
|
|
||||||
const constrainedX = Math.max(minX, Math.min(maxX, newX))
|
// For Y axis: HARD constraint at top (y=0), never allow window to go above header
|
||||||
const constrainedY = Math.max(minY, Math.min(maxY, newY))
|
|
||||||
|
|
||||||
return { x: constrainedX, y: constrainedY }
|
|
||||||
}
|
|
||||||
|
|
||||||
const constrainToViewportFully = (
|
|
||||||
newX: number,
|
|
||||||
newY: number,
|
|
||||||
newWidth?: number,
|
|
||||||
newHeight?: number,
|
|
||||||
) => {
|
|
||||||
const bounds = getViewportBounds()
|
|
||||||
if (!bounds) return { x: newX, y: newY }
|
|
||||||
|
|
||||||
const windowWidth = newWidth ?? width.value
|
|
||||||
const windowHeight = newHeight ?? height.value
|
|
||||||
|
|
||||||
// Keep entire window within viewport
|
|
||||||
const maxX = bounds.width - windowWidth
|
|
||||||
const minX = 0
|
|
||||||
const maxY = bounds.height - windowHeight
|
|
||||||
const minY = 0
|
const minY = 0
|
||||||
|
// Bottom: allow 1/3 to go outside
|
||||||
|
const maxY = bounds.height - windowHeight + maxOffscreenBottom
|
||||||
|
|
||||||
const constrainedX = Math.max(minX, Math.min(maxX, newX))
|
const constrainedX = Math.max(minX, Math.min(maxX, newX))
|
||||||
const constrainedY = Math.max(minY, Math.min(maxY, newY))
|
const constrainedY = Math.max(minY, Math.min(maxY, newY))
|
||||||
@ -357,15 +289,6 @@ const constrainToViewportFully = (
|
|||||||
return { x: constrainedX, y: constrainedY }
|
return { x: constrainedX, y: constrainedY }
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapToViewport = () => {
|
|
||||||
const bounds = getViewportBounds()
|
|
||||||
if (!bounds) return
|
|
||||||
|
|
||||||
const constrained = constrainToViewportFully(x.value, y.value)
|
|
||||||
x.value = constrained.x
|
|
||||||
y.value = constrained.y
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleActivate = () => {
|
const handleActivate = () => {
|
||||||
emit('activate')
|
emit('activate')
|
||||||
}
|
}
|
||||||
@ -387,14 +310,45 @@ const handleMaximize = () => {
|
|||||||
height.value = preMaximizeState.value.height
|
height.value = preMaximizeState.value.height
|
||||||
isMaximized.value = false
|
isMaximized.value = false
|
||||||
} else {
|
} else {
|
||||||
// Maximize
|
// Maximize - set position and size to viewport dimensions
|
||||||
preMaximizeState.value = {
|
preMaximizeState.value = {
|
||||||
x: x.value,
|
x: x.value,
|
||||||
y: y.value,
|
y: y.value,
|
||||||
width: width.value,
|
width: width.value,
|
||||||
height: height.value,
|
height: height.value,
|
||||||
}
|
}
|
||||||
isMaximized.value = true
|
|
||||||
|
// Get viewport bounds (desktop container, already excludes header)
|
||||||
|
const bounds = getViewportBounds()
|
||||||
|
|
||||||
|
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
||||||
|
// Get safe-area-insets from CSS variables for debug
|
||||||
|
const safeAreaTop = parseFloat(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
|
'--safe-area-inset-top',
|
||||||
|
) || '0',
|
||||||
|
)
|
||||||
|
const safeAreaBottom = parseFloat(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
|
'--safe-area-inset-bottom',
|
||||||
|
) || '0',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Desktop container uses 'absolute inset-0' which stretches over full viewport
|
||||||
|
// bounds.height = full viewport height (includes header area + safe-areas)
|
||||||
|
// We need to calculate available space properly
|
||||||
|
|
||||||
|
// Get header height from UI store (measured reactively in layout)
|
||||||
|
const uiStore = useUiStore()
|
||||||
|
const headerHeight = uiStore.headerHeight
|
||||||
|
|
||||||
|
x.value = 0
|
||||||
|
y.value = 0 // Start below header and status bar
|
||||||
|
width.value = bounds.width
|
||||||
|
// Height: viewport - header - both safe-areas
|
||||||
|
height.value = bounds.height - headerHeight - safeAreaTop - safeAreaBottom
|
||||||
|
isMaximized.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,8 +356,30 @@ const handleMaximize = () => {
|
|||||||
const handleResizeStart = (direction: string, e: MouseEvent | TouchEvent) => {
|
const handleResizeStart = (direction: string, e: MouseEvent | TouchEvent) => {
|
||||||
isResizing.value = true
|
isResizing.value = true
|
||||||
resizeDirection.value = direction
|
resizeDirection.value = direction
|
||||||
resizeStartX.value = e.clientX
|
let clientX: number
|
||||||
resizeStartY.value = e.clientY
|
let clientY: number
|
||||||
|
|
||||||
|
if ('touches' in e) {
|
||||||
|
// Es ist ein TouchEvent
|
||||||
|
const touch = e.touches[0] // Hole den ersten Touch
|
||||||
|
|
||||||
|
// Prüfe, ob 'touch' existiert (ist undefined, wenn e.touches leer ist)
|
||||||
|
if (touch) {
|
||||||
|
clientX = touch.clientX
|
||||||
|
clientY = touch.clientY
|
||||||
|
} else {
|
||||||
|
// Ungültiges Start-Event (kein Finger). Abbruch.
|
||||||
|
isResizing.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Es ist ein MouseEvent
|
||||||
|
clientX = e.clientX
|
||||||
|
clientY = e.clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeStartX.value = clientX
|
||||||
|
resizeStartY.value = clientY
|
||||||
resizeStartWidth.value = width.value
|
resizeStartWidth.value = width.value
|
||||||
resizeStartHeight.value = height.value
|
resizeStartHeight.value = height.value
|
||||||
resizeStartPosX.value = x.value
|
resizeStartPosX.value = x.value
|
||||||
@ -446,9 +422,6 @@ useEventListener(window, 'mouseup', () => {
|
|||||||
globalThis.getSelection()?.removeAllRanges()
|
globalThis.getSelection()?.removeAllRanges()
|
||||||
isResizing.value = false
|
isResizing.value = false
|
||||||
|
|
||||||
// Snap back to viewport after resize ends
|
|
||||||
snapToViewport()
|
|
||||||
|
|
||||||
emit('positionChanged', x.value, y.value)
|
emit('positionChanged', x.value, y.value)
|
||||||
emit('sizeChanged', width.value, height.value)
|
emit('sizeChanged', width.value, height.value)
|
||||||
}
|
}
|
||||||
|
|||||||
222
src/components/haex/window/overview.vue
Normal file
222
src/components/haex/window/overview.vue
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
<template>
|
||||||
|
<UDrawer
|
||||||
|
v-model:open="localShowWindowOverview"
|
||||||
|
direction="bottom"
|
||||||
|
:title="t('modal.title')"
|
||||||
|
:description="t('modal.description')"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="h-full overflow-y-auto p-6 justify-center flex">
|
||||||
|
<!-- Window Thumbnails Flex Layout -->
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="windows.length > 0"
|
||||||
|
class="flex flex-wrap gap-6 justify-center-safe items-start"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="window in windows"
|
||||||
|
:key="window.id"
|
||||||
|
class="relative group cursor-pointer"
|
||||||
|
>
|
||||||
|
<!-- Window Title Bar -->
|
||||||
|
<div class="flex items-center gap-3 mb-2 px-2">
|
||||||
|
<UIcon
|
||||||
|
v-if="window.icon"
|
||||||
|
:name="window.icon"
|
||||||
|
class="size-5 shrink-0"
|
||||||
|
/>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-semibold text-sm truncate">
|
||||||
|
{{ window.title }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- Minimized Badge -->
|
||||||
|
<UBadge
|
||||||
|
v-if="window.isMinimized"
|
||||||
|
color="info"
|
||||||
|
size="xs"
|
||||||
|
:title="t('minimized')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scaled Window Preview Container / Teleport Target -->
|
||||||
|
<div
|
||||||
|
:id="`window-preview-${window.id}`"
|
||||||
|
class="relative bg-gray-100 dark:bg-gray-900 rounded-xl overflow-hidden border-2 border-gray-200 dark:border-gray-700 group-hover:border-primary-500 transition-all shadow-lg"
|
||||||
|
:style="getCardStyle(window)"
|
||||||
|
@click="handleRestoreAndActivateWindow(window.id)"
|
||||||
|
>
|
||||||
|
<!-- Hover Overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-primary-500/10 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
name="i-heroicons-window"
|
||||||
|
class="size-16 mb-4 shrink-0"
|
||||||
|
/>
|
||||||
|
<p class="text-lg font-medium">No windows open</p>
|
||||||
|
<p class="text-sm">
|
||||||
|
Open an extension or system window to see it here
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const windowManager = useWindowManagerStore()
|
||||||
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
|
||||||
|
const { showWindowOverview, windows } = storeToRefs(windowManager)
|
||||||
|
|
||||||
|
// Local computed for two-way binding with UModal
|
||||||
|
const localShowWindowOverview = computed({
|
||||||
|
get: () => showWindowOverview.value,
|
||||||
|
set: (value) => {
|
||||||
|
showWindowOverview.value = value
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRestoreAndActivateWindow = (windowId: string) => {
|
||||||
|
const window = windowManager.windows.find((w) => w.id === windowId)
|
||||||
|
if (!window) return
|
||||||
|
|
||||||
|
// Switch to the workspace where this window is located
|
||||||
|
if (window.workspaceId) {
|
||||||
|
workspaceStore.slideToWorkspace(window.workspaceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If window is minimized, restore it first
|
||||||
|
if (window.isMinimized) {
|
||||||
|
windowManager.restoreWindow(windowId)
|
||||||
|
} else {
|
||||||
|
// If not minimized, just activate it
|
||||||
|
windowManager.activateWindow(windowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the overview
|
||||||
|
localShowWindowOverview.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store original window sizes and positions to restore after overview closes
|
||||||
|
const originalWindowState = ref<
|
||||||
|
Map<string, { width: number; height: number; x: number; y: number }>
|
||||||
|
>(new Map())
|
||||||
|
|
||||||
|
// Min/Max dimensions for preview cards
|
||||||
|
const MIN_PREVIEW_WIDTH = 300
|
||||||
|
const MAX_PREVIEW_WIDTH = 600
|
||||||
|
const MIN_PREVIEW_HEIGHT = 225
|
||||||
|
const MAX_PREVIEW_HEIGHT = 450
|
||||||
|
|
||||||
|
// Calculate card size and scale based on window dimensions
|
||||||
|
const getCardStyle = (window: (typeof windows.value)[0]) => {
|
||||||
|
const scaleX = MAX_PREVIEW_WIDTH / window.width
|
||||||
|
const scaleY = MAX_PREVIEW_HEIGHT / window.height
|
||||||
|
const scale = Math.min(scaleX, scaleY, 1) // Never scale up, only down
|
||||||
|
|
||||||
|
// Calculate scaled dimensions
|
||||||
|
const scaledWidth = window.width * scale
|
||||||
|
const scaledHeight = window.height * scale
|
||||||
|
|
||||||
|
// Ensure minimum card size
|
||||||
|
let finalScale = scale
|
||||||
|
if (scaledWidth < MIN_PREVIEW_WIDTH) {
|
||||||
|
finalScale = MIN_PREVIEW_WIDTH / window.width
|
||||||
|
}
|
||||||
|
if (scaledHeight < MIN_PREVIEW_HEIGHT) {
|
||||||
|
finalScale = Math.max(finalScale, MIN_PREVIEW_HEIGHT / window.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardWidth = window.width * finalScale
|
||||||
|
const cardHeight = window.height * finalScale
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: `${cardWidth}px`,
|
||||||
|
height: `${cardHeight}px`,
|
||||||
|
'--window-scale': finalScale, // CSS variable for scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for overview closing to restore windows
|
||||||
|
watch(localShowWindowOverview, async (isOpen, wasOpen) => {
|
||||||
|
if (!isOpen && wasOpen) {
|
||||||
|
console.log('[WindowOverview] Overview closed, restoring windows...')
|
||||||
|
|
||||||
|
// Restore original window state
|
||||||
|
for (const window of windows.value) {
|
||||||
|
const originalState = originalWindowState.value.get(window.id)
|
||||||
|
if (originalState) {
|
||||||
|
console.log(
|
||||||
|
`[WindowOverview] Restoring window ${window.id} to:`,
|
||||||
|
originalState,
|
||||||
|
)
|
||||||
|
|
||||||
|
windowManager.updateWindowSize(
|
||||||
|
window.id,
|
||||||
|
originalState.width,
|
||||||
|
originalState.height,
|
||||||
|
)
|
||||||
|
windowManager.updateWindowPosition(
|
||||||
|
window.id,
|
||||||
|
originalState.x,
|
||||||
|
originalState.y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
originalWindowState.value.clear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for overview opening to store original state
|
||||||
|
watch(
|
||||||
|
() => localShowWindowOverview.value && windows.value.length,
|
||||||
|
(shouldStore) => {
|
||||||
|
if (shouldStore && originalWindowState.value.size === 0) {
|
||||||
|
console.log('[WindowOverview] Storing original window states...')
|
||||||
|
|
||||||
|
for (const window of windows.value) {
|
||||||
|
console.log(`[WindowOverview] Window ${window.id}:`, {
|
||||||
|
originalSize: { width: window.width, height: window.height },
|
||||||
|
originalPos: { x: window.x, y: window.y },
|
||||||
|
})
|
||||||
|
|
||||||
|
originalWindowState.value.set(window.id, {
|
||||||
|
width: window.width,
|
||||||
|
height: window.height,
|
||||||
|
x: window.x,
|
||||||
|
y: window.y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<i18n lang="yaml">
|
||||||
|
de:
|
||||||
|
modal:
|
||||||
|
title: Fensterübersicht
|
||||||
|
description: Übersicht aller offenen Fenster auf allen Workspaces
|
||||||
|
|
||||||
|
minimized: Minimiert
|
||||||
|
|
||||||
|
en:
|
||||||
|
modal:
|
||||||
|
title: Window Overview
|
||||||
|
description: Overview of all open windows on all workspaces
|
||||||
|
|
||||||
|
minimized: Minimized
|
||||||
|
</i18n>
|
||||||
@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<UCard
|
<UCard
|
||||||
class="cursor-pointer transition-all h-32 w-72 shrink-0 group duration-500"
|
ref="cardEl"
|
||||||
|
class="cursor-pointer transition-all h-32 w-72 shrink-0 group duration-500 rounded-lg"
|
||||||
:class="[
|
:class="[
|
||||||
workspace.id === currentWorkspace?.id
|
workspace.id === currentWorkspace?.id
|
||||||
? 'ring-2 ring-secondary bg-secondary/10'
|
? 'ring-2 ring-secondary bg-secondary/10'
|
||||||
: 'hover:ring-2 hover:ring-gray-300',
|
: 'hover:ring-2 hover:ring-gray-300',
|
||||||
|
isDragOver ? 'ring-4 ring-primary bg-primary/20 scale-105' : '',
|
||||||
]"
|
]"
|
||||||
@click="workspaceStore.slideToWorkspace(workspace.id)"
|
@click="workspaceStore.slideToWorkspace(workspace.id)"
|
||||||
>
|
>
|
||||||
@ -27,9 +29,70 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{ workspace: IWorkspace }>()
|
const props = defineProps<{ workspace: IWorkspace }>()
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
const windowManager = useWindowManagerStore()
|
||||||
|
|
||||||
const { currentWorkspace } = storeToRefs(workspaceStore)
|
const { currentWorkspace } = storeToRefs(workspaceStore)
|
||||||
|
|
||||||
|
const cardEl = useTemplateRef('cardEl')
|
||||||
|
const isDragOver = ref(false)
|
||||||
|
|
||||||
|
// Use mouse position to detect if over card
|
||||||
|
const { x: mouseX, y: mouseY } = useMouse()
|
||||||
|
|
||||||
|
// Check if mouse is over this card while dragging
|
||||||
|
watchEffect(() => {
|
||||||
|
if (!windowManager.draggingWindowId || !cardEl.value?.$el) {
|
||||||
|
isDragOver.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get card bounding box
|
||||||
|
const rect = cardEl.value.$el.getBoundingClientRect()
|
||||||
|
|
||||||
|
// Check if mouse is within card bounds
|
||||||
|
const isOver =
|
||||||
|
mouseX.value >= rect.left &&
|
||||||
|
mouseX.value <= rect.right &&
|
||||||
|
mouseY.value >= rect.top &&
|
||||||
|
mouseY.value <= rect.bottom
|
||||||
|
|
||||||
|
isDragOver.value = isOver
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle drop when drag ends - check BEFORE draggingWindowId is cleared
|
||||||
|
let wasOverThisCard = false
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (isDragOver.value && windowManager.draggingWindowId) {
|
||||||
|
wasOverThisCard = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => windowManager.draggingWindowId,
|
||||||
|
(newValue, oldValue) => {
|
||||||
|
// Drag ended (from something to null)
|
||||||
|
if (oldValue && !newValue && wasOverThisCard) {
|
||||||
|
console.log(
|
||||||
|
'[WorkspaceCard] Drop detected! Moving window to workspace:',
|
||||||
|
props.workspace.name,
|
||||||
|
)
|
||||||
|
const window = windowManager.windows.find((w) => w.id === oldValue)
|
||||||
|
if (window) {
|
||||||
|
window.workspaceId = props.workspace.id
|
||||||
|
window.x = 0
|
||||||
|
window.y = 0
|
||||||
|
// Switch to the workspace after dropping
|
||||||
|
//workspaceStore.slideToWorkspace(props.workspace.id)
|
||||||
|
}
|
||||||
|
wasOverThisCard = false
|
||||||
|
} else if (!newValue) {
|
||||||
|
// Drag ended but not over this card
|
||||||
|
wasOverThisCard = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
28
src/components/ui/button/context.vue
Normal file
28
src/components/ui/button/context.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<UContextMenu :items="contextMenuItems">
|
||||||
|
<UiButton
|
||||||
|
v-bind="$attrs"
|
||||||
|
@click="$emit('click', $event)"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(_, slotName) in $slots"
|
||||||
|
#[slotName]="slotProps"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
:name="slotName"
|
||||||
|
v-bind="slotProps"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UiButton>
|
||||||
|
</UContextMenu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ContextMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
contextMenuItems: ContextMenuItem[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{ click: [Event] }>()
|
||||||
|
</script>
|
||||||
@ -4,11 +4,10 @@
|
|||||||
<UButton
|
<UButton
|
||||||
class="pointer-events-auto"
|
class="pointer-events-auto"
|
||||||
v-bind="{
|
v-bind="{
|
||||||
...{ size: isSmallScreen ? 'lg' : 'md' },
|
|
||||||
...buttonProps,
|
...buttonProps,
|
||||||
...$attrs,
|
...$attrs,
|
||||||
}"
|
}"
|
||||||
@click="(e) => $emit('click', e)"
|
@click="$emit('click', $event)"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(_, slotName) in $slots"
|
v-for="(_, slotName) in $slots"
|
||||||
|
|||||||
@ -11,10 +11,6 @@ const { availableThemes, currentTheme } = storeToRefs(useUiStore())
|
|||||||
|
|
||||||
const emit = defineEmits<{ select: [string] }>()
|
const emit = defineEmits<{ select: [string] }>()
|
||||||
|
|
||||||
watchImmediate(availableThemes, () =>
|
|
||||||
console.log('availableThemes', availableThemes),
|
|
||||||
)
|
|
||||||
|
|
||||||
const items = computed<DropdownMenuItem[]>(() =>
|
const items = computed<DropdownMenuItem[]>(() =>
|
||||||
availableThemes?.value.map((theme) => ({
|
availableThemes?.value.map((theme) => ({
|
||||||
...theme,
|
...theme,
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
:title="t('pick')"
|
:title="t('pick')"
|
||||||
class="top-0 left-0 absolute size-0"
|
class="top-0 left-0 absolute size-0"
|
||||||
type="color"
|
type="color"
|
||||||
/>
|
>
|
||||||
|
|
||||||
<UiTooltip :tooltip="t('reset')">
|
<UiTooltip :tooltip="t('reset')">
|
||||||
<UiButton
|
<UiButton
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
<UDropdownMenu
|
<UDropdownMenu
|
||||||
:items="icons"
|
:items="icons"
|
||||||
class="btn"
|
class="btn"
|
||||||
@select="(newIcon) => (iconName = newIcon)"
|
|
||||||
:read_only
|
:read_only
|
||||||
|
@select="(newIcon) => (iconName = newIcon)"
|
||||||
>
|
>
|
||||||
<template #activator>
|
<template #activator>
|
||||||
<Icon :name="iconName ? iconName : defaultIcon || icons.at(0)" />
|
<Icon :name="iconName ? iconName : defaultIcon || icons.at(0)" />
|
||||||
@ -12,8 +12,8 @@
|
|||||||
<template #items="{ items }">
|
<template #items="{ items }">
|
||||||
<div class="grid grid-cols-6 -ml-2">
|
<div class="grid grid-cols-6 -ml-2">
|
||||||
<li
|
<li
|
||||||
class="dropdown-item"
|
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
|
class="dropdown-item"
|
||||||
@click="read_only ? '' : (iconName = item)"
|
@click="read_only ? '' : (iconName = item)"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
<button
|
<button
|
||||||
:id
|
:id
|
||||||
class="advance-select-toogle flex justify-between grow p-3"
|
class="advance-select-toogle flex justify-between grow p-3"
|
||||||
@click.prevent="toogleMenu"
|
|
||||||
:disabled="read_only"
|
:disabled="read_only"
|
||||||
|
@click.prevent="toogleMenu"
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
name="value"
|
name="value"
|
||||||
@ -18,9 +18,9 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click.prevent="toogleMenu"
|
|
||||||
class="flex items-center p-2 hover:shadow rounded-md hover:bg-primary hover:text-base-content"
|
class="flex items-center p-2 hover:shadow rounded-md hover:bg-primary hover:text-base-content"
|
||||||
:disabled="read_only"
|
:disabled="read_only"
|
||||||
|
@click.prevent="toogleMenu"
|
||||||
>
|
>
|
||||||
<i class="i-[material-symbols--keyboard-arrow-down] size-4" />
|
<i class="i-[material-symbols--keyboard-arrow-down] size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
// composables/extensionContextBroadcast.ts
|
|
||||||
// NOTE: This composable is deprecated. Use tabsStore.broadcastToAllTabs() instead.
|
|
||||||
// Keeping for backwards compatibility.
|
|
||||||
|
|
||||||
import { getExtensionWindow } from './extensionMessageHandler'
|
|
||||||
|
|
||||||
export const useExtensionContextBroadcast = () => {
|
|
||||||
// Globaler State für Extension IDs statt IFrames
|
|
||||||
const extensionIds = useState<Set<string>>(
|
|
||||||
'extension-ids',
|
|
||||||
() => new Set(),
|
|
||||||
)
|
|
||||||
|
|
||||||
const registerExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => {
|
|
||||||
extensionIds.value.add(extensionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const unregisterExtensionIframe = (_iframe: HTMLIFrameElement, extensionId: string) => {
|
|
||||||
extensionIds.value.delete(extensionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const broadcastContextChange = (context: {
|
|
||||||
theme: string
|
|
||||||
locale: string
|
|
||||||
platform: string
|
|
||||||
}) => {
|
|
||||||
const message = {
|
|
||||||
type: 'context.changed',
|
|
||||||
data: { context },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionIds.value.forEach((extensionId) => {
|
|
||||||
const win = getExtensionWindow(extensionId)
|
|
||||||
if (win) {
|
|
||||||
win.postMessage(message, '*')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const broadcastSearchRequest = (query: string, requestId: string) => {
|
|
||||||
const message = {
|
|
||||||
type: 'search.request',
|
|
||||||
data: {
|
|
||||||
query: { query, limit: 10 },
|
|
||||||
requestId,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
extensionIds.value.forEach((extensionId) => {
|
|
||||||
const win = getExtensionWindow(extensionId)
|
|
||||||
if (win) {
|
|
||||||
win.postMessage(message, '*')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
registerExtensionIframe,
|
|
||||||
unregisterExtensionIframe,
|
|
||||||
broadcastContextChange,
|
|
||||||
broadcastSearchRequest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -166,20 +166,18 @@ const registerGlobalMessageHandler = () => {
|
|||||||
try {
|
try {
|
||||||
let result: unknown
|
let result: unknown
|
||||||
|
|
||||||
if (request.method.startsWith('extension.')) {
|
if (request.method.startsWith('haextension.context.')) {
|
||||||
result = await handleExtensionMethodAsync(request, instance.extension)
|
|
||||||
} else if (request.method.startsWith('db.')) {
|
|
||||||
result = await handleDatabaseMethodAsync(request, instance.extension)
|
|
||||||
} else if (request.method.startsWith('fs.')) {
|
|
||||||
result = await handleFilesystemMethodAsync(request, instance.extension)
|
|
||||||
} else if (request.method.startsWith('http.')) {
|
|
||||||
result = await handleHttpMethodAsync(request, instance.extension)
|
|
||||||
} else if (request.method.startsWith('permissions.')) {
|
|
||||||
result = await handlePermissionsMethodAsync(request, instance.extension)
|
|
||||||
} else if (request.method.startsWith('context.')) {
|
|
||||||
result = await handleContextMethodAsync(request)
|
result = await handleContextMethodAsync(request)
|
||||||
} else if (request.method.startsWith('storage.')) {
|
} else if (request.method.startsWith('haextension.storage.')) {
|
||||||
result = await handleStorageMethodAsync(request, instance)
|
result = await handleStorageMethodAsync(request, instance)
|
||||||
|
} else if (request.method.startsWith('haextension.db.')) {
|
||||||
|
result = await handleDatabaseMethodAsync(request, instance.extension)
|
||||||
|
} else if (request.method.startsWith('haextension.fs.')) {
|
||||||
|
result = await handleFilesystemMethodAsync(request, instance.extension)
|
||||||
|
} else if (request.method.startsWith('haextension.http.')) {
|
||||||
|
result = await handleHttpMethodAsync(request, instance.extension)
|
||||||
|
} else if (request.method.startsWith('haextension.permissions.')) {
|
||||||
|
result = await handlePermissionsMethodAsync(request, instance.extension)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown method: ${request.method}`)
|
throw new Error(`Unknown method: ${request.method}`)
|
||||||
}
|
}
|
||||||
@ -328,30 +326,27 @@ export const getExtensionWindow = (extensionId: string): Window | undefined => {
|
|||||||
return getAllInstanceWindows(extensionId)[0]
|
return getAllInstanceWindows(extensionId)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// Broadcast context changes to all extension instances
|
||||||
// Extension Methods
|
export const broadcastContextToAllExtensions = (context: {
|
||||||
// ==========================================
|
theme: string
|
||||||
|
locale: string
|
||||||
|
platform?: string
|
||||||
|
}) => {
|
||||||
|
const message = {
|
||||||
|
type: 'haextension.context.changed',
|
||||||
|
data: { context },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
async function handleExtensionMethodAsync(
|
console.log('[ExtensionHandler] Broadcasting context to all extensions:', context)
|
||||||
request: ExtensionRequest,
|
|
||||||
extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr
|
// Send to all registered extension windows
|
||||||
) {
|
for (const [_, instance] of iframeRegistry.entries()) {
|
||||||
switch (request.method) {
|
const win = windowIdToWindowMap.get(instance.windowId)
|
||||||
case 'extension.getInfo': {
|
if (win) {
|
||||||
const info = (await invoke('get_extension_info', {
|
console.log('[ExtensionHandler] Sending context to:', instance.extension.name, instance.windowId)
|
||||||
publicKey: extension.publicKey,
|
win.postMessage(message, '*')
|
||||||
name: extension.name,
|
|
||||||
})) as Record<string, unknown>
|
|
||||||
// Override allowedOrigin with the actual window origin
|
|
||||||
// This fixes the dev-mode issue where Rust returns "tauri://localhost"
|
|
||||||
// but the actual origin is "http://localhost:3003"
|
|
||||||
return {
|
|
||||||
...info,
|
|
||||||
allowedOrigin: window.location.origin,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
throw new Error(`Unknown extension method: ${request.method}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,11 +364,12 @@ async function handleDatabaseMethodAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'db.query': {
|
case 'haextension.db.query': {
|
||||||
const rows = await invoke<unknown[]>('extension_sql_select', {
|
const rows = await invoke<unknown[]>('extension_sql_select', {
|
||||||
sql: params.query || '',
|
sql: params.query || '',
|
||||||
params: params.params || [],
|
params: params.params || [],
|
||||||
extensionId: extension.id,
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -383,21 +379,22 @@ async function handleDatabaseMethodAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'db.execute': {
|
case 'haextension.db.execute': {
|
||||||
await invoke<string[]>('extension_sql_execute', {
|
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
||||||
sql: params.query || '',
|
sql: params.query || '',
|
||||||
params: params.params || [],
|
params: params.params || [],
|
||||||
extensionId: extension.id,
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: [],
|
rows,
|
||||||
rowsAffected: 1,
|
rowsAffected: 1,
|
||||||
lastInsertId: undefined,
|
lastInsertId: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'db.transaction': {
|
case 'haextension.db.transaction': {
|
||||||
const statements =
|
const statements =
|
||||||
(request.params as { statements?: string[] }).statements || []
|
(request.params as { statements?: string[] }).statements || []
|
||||||
|
|
||||||
@ -405,7 +402,8 @@ async function handleDatabaseMethodAsync(
|
|||||||
await invoke('extension_sql_execute', {
|
await invoke('extension_sql_execute', {
|
||||||
sql: stmt,
|
sql: stmt,
|
||||||
params: [],
|
params: [],
|
||||||
extensionId: extension.id,
|
publicKey: extension.publicKey,
|
||||||
|
name: extension.name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,7 +465,7 @@ async function handlePermissionsMethodAsync(
|
|||||||
|
|
||||||
async function handleContextMethodAsync(request: ExtensionRequest) {
|
async function handleContextMethodAsync(request: ExtensionRequest) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'context.get':
|
case 'haextension.context.get':
|
||||||
if (!contextGetters) {
|
if (!contextGetters) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Context not initialized. Make sure useExtensionMessageHandler is called in a component.',
|
'Context not initialized. Make sure useExtensionMessageHandler is called in a component.',
|
||||||
@ -499,25 +497,25 @@ async function handleStorageMethodAsync(
|
|||||||
)
|
)
|
||||||
|
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'storage.getItem': {
|
case 'haextension.storage.getItem': {
|
||||||
const key = request.params.key as string
|
const key = request.params.key as string
|
||||||
return localStorage.getItem(storageKey + key)
|
return localStorage.getItem(storageKey + key)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'storage.setItem': {
|
case 'haextension.storage.setItem': {
|
||||||
const key = request.params.key as string
|
const key = request.params.key as string
|
||||||
const value = request.params.value as string
|
const value = request.params.value as string
|
||||||
localStorage.setItem(storageKey + key, value)
|
localStorage.setItem(storageKey + key, value)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'storage.removeItem': {
|
case 'haextension.storage.removeItem': {
|
||||||
const key = request.params.key as string
|
const key = request.params.key as string
|
||||||
localStorage.removeItem(storageKey + key)
|
localStorage.removeItem(storageKey + key)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'storage.clear': {
|
case 'haextension.storage.clear': {
|
||||||
// Remove only instance-specific keys
|
// Remove only instance-specific keys
|
||||||
const keys = Object.keys(localStorage).filter((k) =>
|
const keys = Object.keys(localStorage).filter((k) =>
|
||||||
k.startsWith(storageKey),
|
k.startsWith(storageKey),
|
||||||
@ -526,7 +524,7 @@ async function handleStorageMethodAsync(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'storage.keys': {
|
case 'haextension.storage.keys': {
|
||||||
// Return only instance-specific keys (without prefix)
|
// Return only instance-specific keys (without prefix)
|
||||||
const keys = Object.keys(localStorage)
|
const keys = Object.keys(localStorage)
|
||||||
.filter((k) => k.startsWith(storageKey))
|
.filter((k) => k.startsWith(storageKey))
|
||||||
|
|||||||
@ -14,12 +14,20 @@ export function useAndroidBackButton() {
|
|||||||
|
|
||||||
// Track navigation history manually
|
// Track navigation history manually
|
||||||
router.afterEach((to, from) => {
|
router.afterEach((to, from) => {
|
||||||
console.log('[AndroidBack] Navigation:', { to: to.path, from: from.path, stackSize: historyStack.value.length })
|
console.log('[AndroidBack] Navigation:', {
|
||||||
|
to: to.path,
|
||||||
|
from: from.path,
|
||||||
|
stackSize: historyStack.value.length,
|
||||||
|
})
|
||||||
|
|
||||||
// If navigating forward (new page)
|
// If navigating forward (new page)
|
||||||
if (from.path && to.path !== from.path && !historyStack.value.includes(to.path)) {
|
if (
|
||||||
|
from.path &&
|
||||||
|
to.path !== from.path &&
|
||||||
|
!historyStack.value.includes(to.path)
|
||||||
|
) {
|
||||||
historyStack.value.push(from.path)
|
historyStack.value.push(from.path)
|
||||||
console.log('[AndroidBack] Added to stack:', from.path, 'Stack:', historyStack.value)
|
//console.log('[AndroidBack] Added to stack:', from.path, 'Stack:', historyStack.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -31,7 +39,10 @@ export function useAndroidBackButton() {
|
|||||||
|
|
||||||
// Listen to close requested event (triggered by Android back button)
|
// Listen to close requested event (triggered by Android back button)
|
||||||
unlisten = await appWindow.onCloseRequested(async (event) => {
|
unlisten = await appWindow.onCloseRequested(async (event) => {
|
||||||
console.log('[AndroidBack] Back button pressed, stack size:', historyStack.value.length)
|
console.log(
|
||||||
|
'[AndroidBack] Back button pressed, stack size:',
|
||||||
|
historyStack.value.length,
|
||||||
|
)
|
||||||
|
|
||||||
// Check if we have history
|
// Check if we have history
|
||||||
if (historyStack.value.length > 0) {
|
if (historyStack.value.length > 0) {
|
||||||
@ -40,7 +51,10 @@ export function useAndroidBackButton() {
|
|||||||
|
|
||||||
// Remove current page from stack
|
// Remove current page from stack
|
||||||
historyStack.value.pop()
|
historyStack.value.pop()
|
||||||
console.log('[AndroidBack] Going back, new stack size:', historyStack.value.length)
|
console.log(
|
||||||
|
'[AndroidBack] Going back, new stack size:',
|
||||||
|
historyStack.value.length,
|
||||||
|
)
|
||||||
|
|
||||||
// Navigate back in router
|
// Navigate back in router
|
||||||
router.back()
|
router.back()
|
||||||
|
|||||||
1
src/database/index.ts
Normal file
1
src/database/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * as schema from './schemas'
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'
|
import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'
|
||||||
import tableNames from '../tableNames.json'
|
import tableNames from '~/database/tableNames.json'
|
||||||
|
|
||||||
export const haexCrdtLogs = sqliteTable(
|
export const haexCrdtLogs = sqliteTable(
|
||||||
tableNames.haex.crdt.logs.name,
|
tableNames.haex.crdt.logs.name,
|
||||||
{
|
{
|
||||||
id: text()
|
id: text()
|
||||||
.primaryKey()
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.primaryKey(),
|
||||||
haexTimestamp: text(tableNames.haex.crdt.logs.columns.haexTimestamp),
|
haexTimestamp: text(tableNames.haex.crdt.logs.columns.haexTimestamp),
|
||||||
tableName: text(tableNames.haex.crdt.logs.columns.tableName),
|
tableName: text(tableNames.haex.crdt.logs.columns.tableName),
|
||||||
rowPks: text(tableNames.haex.crdt.logs.columns.rowPks, { mode: 'json' }),
|
rowPks: text(tableNames.haex.crdt.logs.columns.rowPks, { mode: 'json' }),
|
||||||
@ -33,8 +33,8 @@ export const haexCrdtSnapshots = sqliteTable(
|
|||||||
tableNames.haex.crdt.snapshots.name,
|
tableNames.haex.crdt.snapshots.name,
|
||||||
{
|
{
|
||||||
snapshotId: text(tableNames.haex.crdt.snapshots.columns.snapshotId)
|
snapshotId: text(tableNames.haex.crdt.snapshots.columns.snapshotId)
|
||||||
.primaryKey()
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.primaryKey(),
|
||||||
created: text(),
|
created: text(),
|
||||||
epochHlc: text(tableNames.haex.crdt.snapshots.columns.epochHlc),
|
epochHlc: text(tableNames.haex.crdt.snapshots.columns.epochHlc),
|
||||||
locationUrl: text(tableNames.haex.crdt.snapshots.columns.locationUrl),
|
locationUrl: text(tableNames.haex.crdt.snapshots.columns.locationUrl),
|
||||||
@ -45,8 +45,6 @@ export const haexCrdtSnapshots = sqliteTable(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const haexCrdtConfigs = sqliteTable(tableNames.haex.crdt.configs.name, {
|
export const haexCrdtConfigs = sqliteTable(tableNames.haex.crdt.configs.name, {
|
||||||
key: text()
|
key: text().primaryKey(),
|
||||||
.primaryKey()
|
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
|
||||||
value: text(),
|
value: text(),
|
||||||
})
|
})
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { sql } from 'drizzle-orm'
|
import { sql } from 'drizzle-orm'
|
||||||
import {
|
import {
|
||||||
|
check,
|
||||||
integer,
|
integer,
|
||||||
sqliteTable,
|
sqliteTable,
|
||||||
text,
|
text,
|
||||||
@ -7,34 +8,32 @@ import {
|
|||||||
type AnySQLiteColumn,
|
type AnySQLiteColumn,
|
||||||
type SQLiteColumnBuilderBase,
|
type SQLiteColumnBuilderBase,
|
||||||
} from 'drizzle-orm/sqlite-core'
|
} from 'drizzle-orm/sqlite-core'
|
||||||
import tableNames from '../tableNames.json'
|
import tableNames from '~/database/tableNames.json'
|
||||||
|
|
||||||
// Helper function to add common CRDT columns (haexTombstone and haexTimestamp)
|
const crdtColumnNames = {
|
||||||
|
haexTimestamp: 'haex_timestamp',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to add common CRDT columns ( haexTimestamp)
|
||||||
export const withCrdtColumns = <
|
export const withCrdtColumns = <
|
||||||
T extends Record<string, SQLiteColumnBuilderBase>,
|
T extends Record<string, SQLiteColumnBuilderBase>,
|
||||||
>(
|
>(
|
||||||
columns: T,
|
columns: T,
|
||||||
columnNames: { haexTombstone: string; haexTimestamp: string },
|
|
||||||
) => ({
|
) => ({
|
||||||
...columns,
|
...columns,
|
||||||
haexTombstone: integer(columnNames.haexTombstone, { mode: 'boolean' }),
|
haexTimestamp: text(crdtColumnNames.haexTimestamp),
|
||||||
haexTimestamp: text(columnNames.haexTimestamp),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const haexSettings = sqliteTable(
|
export const haexSettings = sqliteTable(
|
||||||
tableNames.haex.settings.name,
|
tableNames.haex.settings.name,
|
||||||
{
|
withCrdtColumns({
|
||||||
id: text()
|
id: text()
|
||||||
.primaryKey()
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.primaryKey(),
|
||||||
key: text(),
|
key: text(),
|
||||||
type: text(),
|
type: text(),
|
||||||
value: text(),
|
value: text(),
|
||||||
haexTombstone: integer(tableNames.haex.settings.columns.haexTombstone, {
|
}),
|
||||||
mode: 'boolean',
|
|
||||||
}),
|
|
||||||
haexTimestamp: text(tableNames.haex.settings.columns.haexTimestamp),
|
|
||||||
},
|
|
||||||
(table) => [unique().on(table.key, table.type, table.value)],
|
(table) => [unique().on(table.key, table.type, table.value)],
|
||||||
)
|
)
|
||||||
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
||||||
@ -42,25 +41,22 @@ export type SelectHaexSettings = typeof haexSettings.$inferSelect
|
|||||||
|
|
||||||
export const haexExtensions = sqliteTable(
|
export const haexExtensions = sqliteTable(
|
||||||
tableNames.haex.extensions.name,
|
tableNames.haex.extensions.name,
|
||||||
{
|
withCrdtColumns({
|
||||||
id: text()
|
id: text()
|
||||||
.primaryKey()
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.primaryKey(),
|
||||||
public_key: text().notNull(),
|
public_key: text().notNull(),
|
||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
version: text().notNull(),
|
version: text().notNull(),
|
||||||
author: text(),
|
author: text(),
|
||||||
description: text(),
|
description: text(),
|
||||||
entry: text().notNull().default('index.html'),
|
entry: text().default('index.html'),
|
||||||
homepage: text(),
|
homepage: text(),
|
||||||
enabled: integer({ mode: 'boolean' }).default(true),
|
enabled: integer({ mode: 'boolean' }).default(true),
|
||||||
icon: text(),
|
icon: text(),
|
||||||
signature: text().notNull(),
|
signature: text().notNull(),
|
||||||
haexTombstone: integer(tableNames.haex.extensions.columns.haexTombstone, {
|
single_instance: integer({ mode: 'boolean' }).default(false),
|
||||||
mode: 'boolean',
|
}),
|
||||||
}),
|
|
||||||
haexTimestamp: text(tableNames.haex.extensions.columns.haexTimestamp),
|
|
||||||
},
|
|
||||||
(table) => [
|
(table) => [
|
||||||
// UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren
|
// UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren
|
||||||
unique().on(table.public_key, table.name),
|
unique().on(table.public_key, table.name),
|
||||||
@ -71,13 +67,11 @@ export type SelectHaexExtensions = typeof haexExtensions.$inferSelect
|
|||||||
|
|
||||||
export const haexExtensionPermissions = sqliteTable(
|
export const haexExtensionPermissions = sqliteTable(
|
||||||
tableNames.haex.extension_permissions.name,
|
tableNames.haex.extension_permissions.name,
|
||||||
{
|
withCrdtColumns({
|
||||||
id: text()
|
id: text()
|
||||||
.primaryKey()
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.primaryKey(),
|
||||||
extensionId: text(
|
extensionId: text(tableNames.haex.extension_permissions.columns.extensionId)
|
||||||
tableNames.haex.extension_permissions.columns.extensionId,
|
|
||||||
)
|
|
||||||
.notNull()
|
.notNull()
|
||||||
.references((): AnySQLiteColumn => haexExtensions.id, {
|
.references((): AnySQLiteColumn => haexExtensions.id, {
|
||||||
onDelete: 'cascade',
|
onDelete: 'cascade',
|
||||||
@ -95,14 +89,7 @@ export const haexExtensionPermissions = sqliteTable(
|
|||||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
||||||
() => new Date(),
|
() => new Date(),
|
||||||
),
|
),
|
||||||
haexTombstone: integer(
|
}),
|
||||||
tableNames.haex.extension_permissions.columns.haexTombstone,
|
|
||||||
{ mode: 'boolean' },
|
|
||||||
),
|
|
||||||
haexTimestamp: text(
|
|
||||||
tableNames.haex.extension_permissions.columns.haexTimestamp,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
(table) => [
|
(table) => [
|
||||||
unique().on(
|
unique().on(
|
||||||
table.extensionId,
|
table.extensionId,
|
||||||
@ -119,8 +106,10 @@ export type SelecthaexExtensionPermissions =
|
|||||||
|
|
||||||
export const haexNotifications = sqliteTable(
|
export const haexNotifications = sqliteTable(
|
||||||
tableNames.haex.notifications.name,
|
tableNames.haex.notifications.name,
|
||||||
{
|
withCrdtColumns({
|
||||||
id: text().primaryKey(),
|
id: text()
|
||||||
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
|
.primaryKey(),
|
||||||
alt: text(),
|
alt: text(),
|
||||||
date: text(),
|
date: text(),
|
||||||
icon: text(),
|
icon: text(),
|
||||||
@ -132,30 +121,23 @@ export const haexNotifications = sqliteTable(
|
|||||||
type: text({
|
type: text({
|
||||||
enum: ['error', 'success', 'warning', 'info', 'log'],
|
enum: ['error', 'success', 'warning', 'info', 'log'],
|
||||||
}).notNull(),
|
}).notNull(),
|
||||||
haexTombstone: integer(
|
}),
|
||||||
tableNames.haex.notifications.columns.haexTombstone,
|
|
||||||
{ mode: 'boolean' },
|
|
||||||
),
|
|
||||||
haexTimestamp: text(tableNames.haex.notifications.columns.haexTimestamp),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
export type InsertHaexNotifications = typeof haexNotifications.$inferInsert
|
export type InsertHaexNotifications = typeof haexNotifications.$inferInsert
|
||||||
export type SelectHaexNotifications = typeof haexNotifications.$inferSelect
|
export type SelectHaexNotifications = typeof haexNotifications.$inferSelect
|
||||||
|
|
||||||
export const haexWorkspaces = sqliteTable(
|
export const haexWorkspaces = sqliteTable(
|
||||||
tableNames.haex.workspaces.name,
|
tableNames.haex.workspaces.name,
|
||||||
withCrdtColumns(
|
withCrdtColumns({
|
||||||
{
|
id: text(tableNames.haex.workspaces.columns.id)
|
||||||
id: text(tableNames.haex.workspaces.columns.id)
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.primaryKey()
|
.primaryKey(),
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
deviceId: text(tableNames.haex.workspaces.columns.deviceId).notNull(),
|
||||||
name: text(tableNames.haex.workspaces.columns.name).notNull(),
|
name: text(tableNames.haex.workspaces.columns.name).notNull(),
|
||||||
position: integer(tableNames.haex.workspaces.columns.position)
|
position: integer(tableNames.haex.workspaces.columns.position)
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
},
|
}),
|
||||||
tableNames.haex.workspaces.columns,
|
|
||||||
),
|
|
||||||
(table) => [unique().on(table.position)],
|
(table) => [unique().on(table.position)],
|
||||||
)
|
)
|
||||||
export type InsertHaexWorkspaces = typeof haexWorkspaces.$inferInsert
|
export type InsertHaexWorkspaces = typeof haexWorkspaces.$inferInsert
|
||||||
@ -163,29 +145,37 @@ export type SelectHaexWorkspaces = typeof haexWorkspaces.$inferSelect
|
|||||||
|
|
||||||
export const haexDesktopItems = sqliteTable(
|
export const haexDesktopItems = sqliteTable(
|
||||||
tableNames.haex.desktop_items.name,
|
tableNames.haex.desktop_items.name,
|
||||||
withCrdtColumns(
|
withCrdtColumns({
|
||||||
{
|
id: text(tableNames.haex.desktop_items.columns.id)
|
||||||
id: text(tableNames.haex.desktop_items.columns.id)
|
.$defaultFn(() => crypto.randomUUID())
|
||||||
.primaryKey()
|
.primaryKey(),
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
workspaceId: text(tableNames.haex.desktop_items.columns.workspaceId)
|
||||||
workspaceId: text(tableNames.haex.desktop_items.columns.workspaceId)
|
.notNull()
|
||||||
.notNull()
|
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
|
||||||
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
|
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
|
||||||
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
|
enum: ['system', 'extension', 'file', 'folder'],
|
||||||
enum: ['extension', 'file', 'folder'],
|
}).notNull(),
|
||||||
}).notNull(),
|
// Für Extensions (wenn itemType = 'extension')
|
||||||
referenceId: text(
|
extensionId: text(
|
||||||
tableNames.haex.desktop_items.columns.referenceId,
|
tableNames.haex.desktop_items.columns.extensionId,
|
||||||
).notNull(), // extensionId für extensions, filePath für files/folders
|
).references((): AnySQLiteColumn => haexExtensions.id, {
|
||||||
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
|
onDelete: 'cascade',
|
||||||
.notNull()
|
}),
|
||||||
.default(0),
|
// Für System Windows (wenn itemType = 'system')
|
||||||
positionY: integer(tableNames.haex.desktop_items.columns.positionY)
|
systemWindowId: text(tableNames.haex.desktop_items.columns.systemWindowId),
|
||||||
.notNull()
|
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
|
||||||
.default(0),
|
.notNull()
|
||||||
},
|
.default(0),
|
||||||
tableNames.haex.desktop_items.columns,
|
positionY: integer(tableNames.haex.desktop_items.columns.positionY)
|
||||||
),
|
.notNull()
|
||||||
|
.default(0),
|
||||||
|
}),
|
||||||
|
(table) => [
|
||||||
|
check(
|
||||||
|
'item_reference',
|
||||||
|
sql`(${table.itemType} = 'extension' AND ${table.extensionId} IS NOT NULL AND ${table.systemWindowId} IS NULL) OR (${table.itemType} = 'system' AND ${table.systemWindowId} IS NOT NULL AND ${table.extensionId} IS NULL) OR (${table.itemType} = 'file' AND ${table.systemWindowId} IS NOT NULL AND ${table.extensionId} IS NULL) OR (${table.itemType} = 'folder' AND ${table.systemWindowId} IS NOT NULL AND ${table.extensionId} IS NULL)`,
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
export type InsertHaexDesktopItems = typeof haexDesktopItems.$inferInsert
|
export type InsertHaexDesktopItems = typeof haexDesktopItems.$inferInsert
|
||||||
export type SelectHaexDesktopItems = typeof haexDesktopItems.$inferSelect
|
export type SelectHaexDesktopItems = typeof haexDesktopItems.$inferSelect
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user