mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-19 15:20:51 +01:00
Compare commits
52 Commits
fb577a8699
...
v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 922ae539ba | |||
| 3d020e7dcf | |||
| f70e924cc3 | |||
| 9ea057e943 | |||
| e268947593 | |||
| df97a3cb8b | |||
| 57fb496fca | |||
| 2b8f1781f3 | |||
| a291619f63 | |||
| 033c9135c6 | |||
| 5d6acfef93 | |||
| f006927d1a | |||
| fa3348a5ad | |||
| c8c3a5c73f | |||
| 225835e5d1 | |||
| fc841f238b |
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
@ -25,3 +25,7 @@ dist-ssr
|
||||
.nuxt
|
||||
src-tauri/target
|
||||
nogit*
|
||||
.claude
|
||||
.output
|
||||
target
|
||||
CLAUDE.md
|
||||
193
README.md
193
README.md
@ -1,81 +1,144 @@
|
||||
# HaexHub - The European "Everything App"
|
||||
# 🧩 HaexHub – The European “Everything App”
|
||||
|
||||
## Vision
|
||||
## 🌍 Vision
|
||||
|
||||
Today, we undoubtedly find ourselves in the computer age. Almost everyone owns at least one computer, often even more. Most probably have at least a smartphone and a standard PC. On each of these devices (Desktop PC, Laptop, Tablet, Smartphone) and systems (Windows, macOS, Linux (all flavors), Android, iOS), there are various programs and data, which can be highly individual and sensitive. Unfortunately, interoperability between these devices and systems often proves difficult, sometimes even impossible, for a multitude of reasons. On one hand, there are the system providers themselves (like Microsoft, Apple, Google), who often design their systems to make it as easy as possible for users to enter their ecosystems, but place many hurdles in the way when users wish to leave again. The golden cage, as we say in Germany, or walled garden. However, it's not just the system providers per se who make cross-device and cross-system work difficult. Another problem lies with the software manufacturers/providers. Since it is already challenging and above all resource-intensive (time, money, and technical know-how) to provide a good and "secure" product for one device class and/or system, it's not uncommon for a program to be developed (initially) for only one platform. So, there might be a program for Windows or Apple, but not for Linux, or only in one distribution/package format. Or there might be an app for iOS and/or Android, but not for the PC. This is partly due to the fact that it would simply be too complex to develop and, especially, maintain a product for multiple systems and devices (simultaneously). This effort is almost insurmountable, particularly for startups, small businesses, and individual open-source developers working on their passion projects in their spare time.
|
||||
Let's not even start talking about application distribution. For each platform, you end up with a separate build pipeline that builds, tests, signs, packages the application into the appropriate format (msi, exe, deb, flatpak, snap, AppImage, Apk, etc.), and delivers it to the corresponding store (AppStore, PlayStore, Windows Store, and the various repositories of Linux distributions). This is a huge cascade of tasks that especially causes problems for small companies (at least if you want to serve ALL platforms simultaneously).
|
||||
Wouldn't it be nice if there were a simple(r) way for developers to develop and build their application just once and then be able to serve ALL\* devices and systems? PWAs were already on the right track, but there is often a lack of more in-depth access to system resources, such as file or console access.
|
||||
HaexHub gives any web application/PWA superpowers.
|
||||
Extensions can be used to add any functions to HaexHub, whereby almost any access to the underlying system is possible, provided that the necessary authorizations have been granted by the user beforehand.
|
||||
We are living in the **computer age** — nearly everyone owns multiple devices: a smartphone, a laptop, perhaps even a desktop PC or tablet.
|
||||
Each of these runs its own **operating system** — Windows, macOS, Linux, Android, iOS — and hosts a unique mix of **apps and data**.
|
||||
|
||||
\*In principle, the approach presented here allows an application to run on all devices and systems. However, some applications might still only be usable on certain devices or systems. For example, if an application absolutely requires an NFC device, which is typically not found on a desktop PC, then this application will probably only work on mobile devices. Or if an application requires system-specific interfaces or programs, such as the Registry on Windows or systemd on Linux, then this application will naturally only work where these dependencies are found. However, developers who create their applications without such dependencies can immediately serve all devices and systems.
|
||||
Unfortunately, **interoperability** between these devices is often poor or even impossible.
|
||||
The reasons are many:
|
||||
|
||||
## Enter HaexHub
|
||||
- **Platform lock-in**: Vendors like Microsoft, Apple, or Google design systems that make it easy to _enter_ their ecosystem but difficult to _leave_.
|
||||
- **Fragmented software development**: Developers face high technical and financial hurdles to support multiple platforms at once.
|
||||
|
||||
HaexHub provides a framework that makes it incredibly easy for the community and any developer to build extensions (web applications), which can then be easily integrated into HaexHub by users. Each extension is essentially a web application that can be loaded, executed, customized, and deleted at runtime. Each extension is confined within an IFrame, communicating with HaexHub via APIs using postMessage. HaexHub, in turn, checks these requests for the necessary permissions, executes or rejects the command, and returns a possible response to the caller – ideally, the correct result.
|
||||
Since these are purely web applications, they are initially subject to the same limitations as any other web application in any "normal" browser worldwide. Fun Fact: Extensions in HaexHub are even more restricted than that. While a "normal" web application can, for example, load additional resources (JavaScript, CSS, images, ads) (assuming CORS allows it), this is initially not possible with a HaexHub extension. Everything the extension needs to be able to do must be specified as a permission in a manifest and approved by the user before (potentially) dangerous actions are executed on the host. And loading external resources is already considered such a risk from Tauri's (and my) perspective, as it can severely compromise the user's privacy.
|
||||
With the appropriate permissions, however, an extension can do almost anything possible on a computer. Thus, unlike a "normal" web application, an extension can directly access the host's file system, execute other applications and commands, make/manipulate/block web requests, or access the SQLite database. To use these interfaces, each extension must declare the corresponding permissions in a manifest, which must then be approved by the user. Otherwise, no access to the host system is possible. Extensions can be added and removed at runtime. Since the extension runs in an IFrame, it cannot cause much damage without the appropriate permissions. It would be a pure web application where routing within the application is possible (WebHistoryHash). However, as soon as it tries to load external resources, regardless of whether they are local from the host or from any server on the World Wide Web, the extension is on its own without permission.
|
||||
Technically, for example, it would pose no problem to make the host system's shell available to extensions. This could give Visual Studio Code in the browser superpowers. While a web version of Visual Studio Code already exists, its usability is limited. For instance, it's not possible to directly access the shell or the file system, which significantly hinders file management. And since no commands or applications can be executed on the host, it's (unfortunately) practically useless for developers. Visual Studio Code as a HaexHub extension could be used like a native application. And thanks to HaexHub's permission concept, it can be controlled with fine granularity which extension is allowed to execute what and how, and what is not. An extension with such power over the host, which can be both advantageous and disadvantageous for the user, should naturally be handled with particular care. It would probably not be a good idea to grant this permission to any advertising and data tracking services.
|
||||
Creating and maintaining one secure, high-quality app for _all_ systems can be almost impossible — especially for small teams, startups, and indie developers.
|
||||
|
||||
The framework itself provides a platform that will be available on all common devices (Desktop PC, Laptop, Tablet, Smartphone) and systems (Windows, macOS, Linux (all flavors), Android, iOS). All extensions can then be used on all supported devices and systems (provided there are no dependencies in the extension that are only available on specific devices or systems, like NFC, Google Pay, etc.).
|
||||
All user and extension data can be securely stored and used in the locally encrypted SQLite database. To enable comfortable use of the database across multiple devices and systems, there will be a synchronization server that allows the database to be synchronized conflict-free across devices and systems. This server can, of course, also be self-hosted, ensuring the user is never dependent on a single provider.
|
||||
Furthermore, the data can be encrypted beforehand, making it unreadable by third parties.
|
||||
And then there’s **distribution**: each platform requires its own build, packaging, signing, and publishing process.
|
||||
What if you could build your app **once** and deploy it **everywhere**?
|
||||
|
||||
HaexHub is a cross-platform, local-first, open-source application that prioritizes user privacy, security, and digital sovereignty. The goal is for the user to have control over their data at all times and be able to independently decide what they want to disclose to whom. Additionally, they should be able to adjust this decision at any time.
|
||||
Through the possibility of extensions, HaexHub is also almost infinitely expandable. What Visual Studio Code is for text editors/IDEs, HaexHub will be for (web) applications and even has the potential to become the European counterpart to WeChat (the "everything app"). However, without a central authority controlling everything.
|
||||
> **HaexHub** makes that possible — giving every web app or PWA **superpowers**.
|
||||
|
||||
But first things first.
|
||||
With HaexHub, developers can extend functionality via **extensions** that run securely inside the app, with carefully controlled permissions for accessing system features (files, shell, database, etc.).
|
||||
|
||||
## Technical Foundations
|
||||
---
|
||||
|
||||
The technical foundation of the project is Tauri. This framework makes it possible to provide native applications for all common devices (Desktops, Laptops, Tablets, Smartphones) and systems (Windows, Linux, macOS, Android, iOS) with the same codebase. Tauri is comparable to Electron (the technical basis for Visual Studio Code, for example), but the applications created with it are significantly smaller because Tauri uses the native rendering engine of the respective platform (WebView2 (Windows), WKWebView (macOS), WebKitGTK (Linux)) and does not bundle a (customized Chromium) browser, as is the case with Electron. Furthermore, Tauri offers significant advantages over Electron in terms of security and resource efficiency. There is also a sophisticated permission system, which effectively shields the frontend from the host. All access to the host system is only possible with the appropriate permission. This permission concept is also used for the (HaexHub) extensions, thereby ensuring the security of third-party extensions as well.
|
||||
## 🚀 Enter HaexHub
|
||||
|
||||
The project follows a strict local-first approach. This means that HaexHub can fundamentally be used without any form of online account or internet access. The extensions are also stored locally and can be used offline, provided, of course, that the extension itself can function without the internet. A messenger extension will likely make limited sense without internet access. An image viewer or text editor, however, should work fine without the internet.
|
||||
All user data can be persistently stored and used in a locally encrypted SQLite database, even across extensions, with the appropriate permissions, of course. Unlike many other applications that call themselves local-first, this project implements this approach more consistently. Most applications claiming to be local-first often aren't truly so. The data usually resides (unencrypted) on a backend server and is merely "cached" to varying degrees in the frontend. While this allows these applications to be used offline for a while, the usage is either restricted (read-only in Bitwarden, for example) or the persistence is temporary at best. Most approaches, like this project, use an SQLite (or similar) database in the frontend to achieve offline capability, but this is usually implemented in a browser via IndexedDB or OPFS. Examples include [powersync](https://www.powersync.com/) , [evolu](https://www.evolu.dev/), or [electricSql](https://electric-sql.com/). The problem here is that such persistence is never truly permanent, as the operating system and/or browser can decide when to free up storage. For instance, it's common for Apple to clear the storage of web applications that haven't been used for over a week. As long as the user's data is still present in the backend, this is only moderately tragic, as the "source of truth" residing there can be synchronized back to the frontend at any time. However, this always requires an online account and internet access. Furthermore, with these approaches, the user cannot simply copy their data onto a USB stick and take it with them to use on a completely different computer (perhaps where only intranet is available).
|
||||
Moreover, all these approaches are subject to the limitations of the respective browser. The limitation on persistent storage is particularly noteworthy here. All browsers have strict limits, which is why this approach is not suitable for all requirements. Since HaexHub stores data not in the browser, but in a real SQLite database on the hard drive, it is only subject to the hardware limitations of the host system (or USB stick/storage medium).
|
||||
HaexHub provides a **framework** for building and running modular, sandboxed **web extensions** — web apps that run in an isolated environment but can communicate securely with the host.
|
||||
|
||||
With HaexHub, all user and extension data can be permanently stored in the local and encrypted database without requiring an online account. However, to make the user's data conveniently and securely available on multiple devices, there will be a synchronization service to synchronize the database state across the user's various devices and systems. The user can, of course, also host this service themselves on their (local) systems or servers. The database state is thus temporarily stored on a (third-party) server and can be synchronized from there with other instances of the local SQLite database. To further enhance data security, the user can also encrypt the data before sending it to the backend, making it unreadable by third parties. This will likely be enabled by default, but it can also be turned off, as there are legitimate use cases where it might be disadvantageous or undesirable. Particularly in corporate or government environments, it could be problematic if all user (employee) data were stored encrypted on the company servers. If the employee becomes unavailable (resignation, accident, death) and their database password (or the encryption key stored in the database) is unknown, there would be no way to access this data.
|
||||
Since this use case should also be considered, backend encryption will be optional.
|
||||
Each extension:
|
||||
|
||||
As HaexHub is ultimately a kind of distributed and federated system, there is no (single) authority that could control everything. Unless the user truly has only one instance of their database (perhaps on a USB stick) and always carries it with them. Part of HaexHub's charm, however, is that the user can have multiple instances of their SQLite database on multiple devices and systems without having to worry about how the correct data (source of truth) gets from A to B and B to A.
|
||||
To make this possible and to synchronize even conflicting data states of the SQLite database, HaexHub uses Conflict-free Replicated Data Types (CRDTs). This will make it possible to merge multiple conflicting data states, even if they are encrypted.
|
||||
- Runs inside an **IFrame**.
|
||||
- Uses **postMessage APIs** to communicate with HaexHub.
|
||||
- Declares required **permissions** in a manifest file.
|
||||
- Can be added or removed at runtime.
|
||||
|
||||
## Extensions
|
||||
Without explicit permission, extensions cannot access the file system, network, or external resources — ensuring **privacy and security** by default.
|
||||
Once granted, however, extensions can unlock full desktop-like capabilities:
|
||||
access files, execute commands, or interact with SQLite databases.
|
||||
|
||||
The real highlight of HaexHub, however, lies in its extensions. All end-user functionality will ultimately be provided through extensions. There will be (official/core) extensions and third-party extensions. One of the first (official) extensions will be a password manager, for example, but a file synchronization service is also planned.
|
||||
Each extension is essentially just a web application\* loaded into an IFrame. This keeps all extensions well isolated (sandboxed) from the main application (HaexHub) and the user's host system, ensuring the user's security and privacy. Of course, as with any application, a degree of trust must be placed in the extension developer that they are genuinely only doing what they claim to do. HaexHub is ingenious, but it can't perform magic.
|
||||
Each extension must declare the permissions it requires in a manifest, which must then be accepted by the user. This ensures that each extension can only access the resources (file system, web requests, database access, etc.) for which it has the appropriate permissions.
|
||||
Imagine a **web-based VS Code** that can directly access your local shell and file system — something that current web IDEs can’t do.
|
||||
With HaexHub’s permission model, such power is possible, but **always under user control**.
|
||||
|
||||
In principle, any (existing) web application could be integrated and run within HaexHub. Technically, each extension is just a web application, but with significantly more capabilities. Traditional web applications are restricted by the (justified) limitations of a browser. For example, a web application cannot simply access the host system's file system or manipulate network traffic. And for good reasons. With HaexHub, however, these limitations do not exist. A (HaexHub) extension can indeed access the file system if it has the corresponding permission. This opens up almost unlimited application possibilities, making the term "everything app" seem not so far-fetched. In a future iteration, a browser and later a payment option (GNU Taler?!) are planned to be added, so it could truly become a fully-fledged counterpart to WeChat. However, these aspects are not considered in the first iteration of the application.
|
||||
By providing extensions, HaexHub can truly be enhanced arbitrarily. Extension developers could use simple tools (Vite application) to immediately provide their functionality for all devices and systems and utilize the provided ecosystem, without the developer having to deal with the peculiarities of each system for development and distribution. (Provided, of course, they don't rely on dependencies that only exist on specific systems or devices).
|
||||
Extensions can also access the data of other extensions (e.g., via the SQLite database) and build upon it (with appropriate permission, naturally).
|
||||
I want to outline this with a concrete example. The first official extension will be a password manager.
|
||||
This will be a Nuxt/Vue application. The password manager's manifest will request permission to create a few tables and to read from and write to them. The extension then provides a nice UI for creating and managing login credentials, similar to existing password managers. Each entry can also be tagged, which could later be used by other extensions.
|
||||
For example, entries tagged "E-Mail" could be created, which could then be used by an email client extension to automatically connect to mail servers.
|
||||
Any other extension could access specific entries in the password database (or other extensions' data) to easily provide its service.
|
||||
But of course, each extension can also create its own tables as needed for its specific use case.
|
||||
HaexHub takes care of secure storage and, if configured, conflict-free synchronization.
|
||||
Each user can expand their HaexHub with the individual functionality they need. And since all settings for these extensions can be stored in the SQLite database, they can be easily and seamlessly synchronized and used across multiple devices. The user only needs to set up their extensions once on one device and can then use them on all other devices and systems without further action.
|
||||
HaexHub itself is **cross-platform** and runs on:
|
||||
|
||||
Another example of an extension would be file synchronization, which will also be a core extension.
|
||||
This extension allows users to easily synchronize their files across different devices and systems. It can be configured on each device which files and folders should be synchronized and how. For instance, one might want to upload pictures and videos from their smartphone to an S3 bucket/Google Drive/Dropbox and their desktop PC. However, one probably doesn't want all pictures from the S3 bucket/Google Drive/Dropbox/Desktop to be synchronized back to the smartphone. All these configurations will again be stored in the SQLite database and, where possible, synchronized across all devices and systems.
|
||||
- 💻 Windows, macOS, Linux
|
||||
- 📱 Android, iOS
|
||||
- 🧠 Desktops, laptops, tablets, smartphones
|
||||
|
||||
Further examples of extensions include calendars, (collaborative) document management, contacts, messengers, and in the distant future, a browser and payment service (GNU Taler perhaps?!).
|
||||
All user and extension data is stored in a **locally encrypted SQLite database**.
|
||||
To sync across devices, HaexHub can connect to a **synchronization server** — which you can even **self-host** for maximum independence.
|
||||
|
||||
\*Fundamentally, any bundler (Vite, Webpack, Rollup, etc.) and any frontend framework (Vue, React, Angular, Svelte, plain HTML) should be usable. The crucial part is that it's a JS bundle. However, initially, the focus will primarily be on Vite and Vue to demonstrate the general feasibility first.
|
||||
> 🛡️ HaexHub is built on the principles of **privacy, security, and digital sovereignty**.
|
||||
|
||||
## Preperation
|
||||
The user is always in control of their data — deciding what to share, and with whom.
|
||||
|
||||
install:
|
||||
---
|
||||
|
||||
- [nodejs/nvm](https://nodejs.org/en/download)
|
||||
- [tauri](https://v2.tauri.app/start/prerequisites/)
|
||||
- [rust](https://v2.tauri.app/start/prerequisites/#rust)
|
||||
- [android-studio](https://developer.android.com/studio?hl=de)
|
||||
- webkit2gtk + GTK3
|
||||
## 🧠 Technical Foundations
|
||||
|
||||
HaexHub is powered by **[Tauri](https://v2.tauri.app/)** — a secure, efficient framework for building native apps from web technologies.
|
||||
|
||||
Unlike Electron (used by apps like VS Code), Tauri:
|
||||
|
||||
- Uses **native rendering engines** (WebView2, WKWebView, WebKitGTK)
|
||||
- Produces **smaller, faster apps**
|
||||
- Enforces **strong sandboxing and permission models**
|
||||
|
||||
HaexHub builds upon Tauri’s security features, extending them to third-party extensions.
|
||||
|
||||
### 🏡 Local-first by Design
|
||||
|
||||
HaexHub follows a **strict local-first architecture**:
|
||||
|
||||
- Works **offline** without accounts or internet.
|
||||
- Stores data locally in **encrypted SQLite**.
|
||||
- Uses **CRDTs (Conflict-free Replicated Data Types)** for safe synchronization across devices — even with encrypted data.
|
||||
|
||||
Unlike many “local-first” apps, HaexHub doesn’t just cache data in the browser.
|
||||
Your data truly resides **on your disk**, not under a browser’s limited storage policy.
|
||||
|
||||
Optionally, HaexHub can sync databases via a backend service — self-hosted or external — with optional **end-to-end encryption**.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Extensions
|
||||
|
||||
Extensions are the heart of HaexHub.
|
||||
|
||||
Everything the user interacts with — from password management to file syncing — will be implemented as **extensions**.
|
||||
|
||||
There are two types:
|
||||
|
||||
- **Official/Core Extensions**
|
||||
- **Third-Party Extensions**
|
||||
|
||||
Each extension is a **web app** bundled via your preferred frontend stack:
|
||||
|
||||
> Vue, React, Svelte, Angular, Vite, Webpack, Rollup — you name it.
|
||||
|
||||
### 🔐 Example: Password Manager
|
||||
|
||||
A first official extension will be a **Password Manager**, built with **Vue/Nuxt**:
|
||||
|
||||
- Declares database permissions via its manifest.
|
||||
- Manages login credentials locally in encrypted SQLite.
|
||||
- Can tag entries (e.g. “Email”) for use by other extensions — such as an email client.
|
||||
|
||||
### 🗂 Example: File Synchronization
|
||||
|
||||
Another planned core extension will handle **file synchronization**:
|
||||
|
||||
- Syncs files/folders between devices and cloud providers (e.g. S3, Google Drive, Dropbox).
|
||||
- Lets users define sync rules per device.
|
||||
- Stores configuration securely in the local database.
|
||||
|
||||
### 💬 Future Extensions
|
||||
|
||||
- Calendar & Contacts
|
||||
- Collaborative document management
|
||||
- Messenger
|
||||
- Browser & Payment Services (e.g., GNU Taler integration)
|
||||
|
||||
With this modular design, HaexHub can evolve into a true **European alternative to WeChat** — but open, federated, and privacy-first.
|
||||
|
||||
---
|
||||
|
||||
## 🧰 Installation & Setup
|
||||
|
||||
### 📦 Prerequisites
|
||||
|
||||
Install the following dependencies:
|
||||
|
||||
- [Node.js / nvm](https://nodejs.org/en/download)
|
||||
- [Tauri](https://v2.tauri.app/start/prerequisites/)
|
||||
- [Rust](https://v2.tauri.app/start/prerequisites/#rust)
|
||||
- [Android Studio](https://developer.android.com/studio?hl=de)
|
||||
- WebKit2GTK + GTK3
|
||||
|
||||
#### 🐧 Debian / Ubuntu
|
||||
|
||||
```bash
|
||||
# debian/ubuntu
|
||||
sudo apt update
|
||||
sudo apt install \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
@ -84,8 +147,9 @@ sudo apt install \
|
||||
librsvg2-dev
|
||||
```
|
||||
|
||||
#### 🦊 Fedora
|
||||
|
||||
```bash
|
||||
# fedora
|
||||
sudo dnf install \
|
||||
webkit2gtk4.1-devel \
|
||||
gtk3-devel \
|
||||
@ -93,11 +157,24 @@ sudo dnf install \
|
||||
librsvg2-devel
|
||||
```
|
||||
|
||||
- port 3003 needs to be open/free or you need to adjust it in `nuxt.config.ts` AND `src-tauri/tauri.conf.json`
|
||||
#### ⚙️ Development
|
||||
|
||||
```
|
||||
Make sure port 3003 is available (or adjust it in `nuxt.config.ts` and `src-tauri/tauri.conf.json`).
|
||||
|
||||
```bash
|
||||
git clone https://github.com/haexhub/haex-vault.git
|
||||
cd haex-vault
|
||||
pnpm i
|
||||
pnpm install
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
#### 🧭 Summary
|
||||
|
||||
HaexHub aims to:
|
||||
|
||||
- Simplify cross-platform app development
|
||||
- Empower users with local-first privacy
|
||||
- Enable developers to create modular, permissioned extensions
|
||||
- Bridge the gap between web and native worlds
|
||||
|
||||
HaexHub is the foundation for a decentralized, privacy-friendly, European “everything app.”
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
//import tailwindcss from '@tailwindcss/vite'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
@ -7,7 +7,16 @@ export default defineNuxtConfig({
|
||||
|
||||
srcDir: './src',
|
||||
|
||||
alias: {
|
||||
'@bindings': fileURLToPath(
|
||||
new URL('./src-tauri/bindings', import.meta.url),
|
||||
),
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
viewport: 'width=device-width, initial-scale=1.0, viewport-fit=cover',
|
||||
},
|
||||
pageTransition: {
|
||||
name: 'fade',
|
||||
},
|
||||
@ -20,7 +29,6 @@ export default defineNuxtConfig({
|
||||
'@vueuse/nuxt',
|
||||
'@nuxt/icon',
|
||||
'@nuxt/eslint',
|
||||
//"@nuxt/image",
|
||||
'@nuxt/fonts',
|
||||
'@nuxt/ui',
|
||||
],
|
||||
@ -33,6 +41,20 @@ export default defineNuxtConfig({
|
||||
'pages/**',
|
||||
'types/**',
|
||||
],
|
||||
presets: [
|
||||
{
|
||||
from: '@vueuse/gesture',
|
||||
imports: [
|
||||
'useDrag',
|
||||
'useGesture',
|
||||
'useHover',
|
||||
'useMove',
|
||||
'usePinch',
|
||||
'useScroll',
|
||||
'useWheel',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
css: ['./assets/css/main.css'],
|
||||
@ -72,6 +94,8 @@ export default defineNuxtConfig({
|
||||
redirectOn: 'root', // recommended
|
||||
},
|
||||
types: 'composition',
|
||||
|
||||
vueI18n: './i18n.config.ts',
|
||||
},
|
||||
|
||||
zodI18n: {
|
||||
@ -84,8 +108,7 @@ export default defineNuxtConfig({
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
haexVault: {
|
||||
lastVaultFileName: 'lastVaults.json',
|
||||
instanceFileName: 'instance.json',
|
||||
deviceFileName: 'device.json',
|
||||
defaultVaultName: 'HaexHub',
|
||||
},
|
||||
},
|
||||
@ -99,7 +122,6 @@ export default defineNuxtConfig({
|
||||
},
|
||||
|
||||
vite: {
|
||||
//plugins: [tailwindcss()],
|
||||
// Better support for Tauri CLI output
|
||||
clearScreen: false,
|
||||
// Enable environment variables
|
||||
|
||||
79
package.json
79
package.json
@ -1,62 +1,67 @@
|
||||
{
|
||||
"name": "tauri-app",
|
||||
"name": "haex-hub",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"tauri": "tauri",
|
||||
"tauri:build:debug": "tauri build --debug",
|
||||
"drizzle:generate": "drizzle-kit generate",
|
||||
"drizzle:migrate": "drizzle-kit migrate",
|
||||
"eslint:fix": "eslint --fix"
|
||||
"eslint:fix": "eslint --fix",
|
||||
"generate:rust-types": "tsx ./src-tauri/database/generate-rust-types.ts",
|
||||
"generate:ts-types": "cd src-tauri && cargo test",
|
||||
"generate": "nuxt generate",
|
||||
"postinstall": "nuxt prepare",
|
||||
"preview": "nuxt preview",
|
||||
"tauri:build:debug": "tauri build --debug",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/eslint": "1.9.0",
|
||||
"@nuxt/fonts": "0.11.4",
|
||||
"@nuxt/icon": "2.0.0",
|
||||
"@nuxt/ui": "4.0.0",
|
||||
"@nuxt/ui": "4.1.0",
|
||||
"@nuxtjs/i18n": "10.0.6",
|
||||
"@pinia/nuxt": "^0.11.1",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-http": "2.5.2",
|
||||
"@pinia/nuxt": "^0.11.2",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@tauri-apps/api": "^2.9.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-notification": "2.3.1",
|
||||
"@tauri-apps/plugin-opener": "^2.3.0",
|
||||
"@tauri-apps/plugin-os": "^2.2.2",
|
||||
"@tauri-apps/plugin-sql": "2.3.0",
|
||||
"@tauri-apps/plugin-store": "^2.2.1",
|
||||
"@tauri-apps/plugin-opener": "^2.5.2",
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@tauri-apps/plugin-store": "^2.4.1",
|
||||
"@vueuse/components": "^13.9.0",
|
||||
"@vueuse/core": "^13.4.0",
|
||||
"@vueuse/nuxt": "^13.4.0",
|
||||
"drizzle-orm": "^0.44.2",
|
||||
"eslint": "^9.34.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"nuxt": "^4.0.3",
|
||||
"nuxt-zod-i18n": "^1.12.0",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"vue": "^3.5.20",
|
||||
"vue-router": "^4.5.1",
|
||||
"zod": "4.1.5"
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"@vueuse/gesture": "^2.0.0",
|
||||
"@vueuse/nuxt": "^13.9.0",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"eslint": "^9.38.0",
|
||||
"nuxt-zod-i18n": "^1.12.1",
|
||||
"swiper": "^12.0.3",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.2.351",
|
||||
"@iconify-json/hugeicons": "^1.2.17",
|
||||
"@iconify-json/lucide": "^1.2.71",
|
||||
"@iconify/json": "^2.2.401",
|
||||
"@iconify/tailwind4": "^1.0.6",
|
||||
"@libsql/client": "^0.15.15",
|
||||
"@tauri-apps/cli": "^2.5.0",
|
||||
"@tauri-apps/cli": "^2.9.1",
|
||||
"@types/node": "^24.9.1",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"@vue/compiler-sfc": "^3.5.17",
|
||||
"drizzle-kit": "^0.31.2",
|
||||
"globals": "^16.2.0",
|
||||
"@vue/compiler-sfc": "^3.5.22",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"globals": "^16.4.0",
|
||||
"nuxt": "^4.2.0",
|
||||
"prettier": "3.6.2",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "^5.8.3",
|
||||
"tsx": "^4.20.6",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "7.1.3",
|
||||
"vue-tsc": "3.0.6"
|
||||
},
|
||||
|
||||
4592
pnpm-lock.yaml
generated
4592
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1318
src-tauri/Cargo.lock
generated
1318
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "haex-hub"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
@ -16,40 +16,41 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = "1.0.145"
|
||||
|
||||
tauri-build = { version = "2.2", features = [] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
[dependencies]
|
||||
rusqlite = { version = "0.37.0", features = [
|
||||
"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"] }
|
||||
[dependencies]
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||
base64 = "0.22"
|
||||
ed25519-dalek = "2.1"
|
||||
fs_extra = "1.3.0"
|
||||
hex = "0.4"
|
||||
lazy_static = "1.5"
|
||||
mime = "0.3"
|
||||
mime_guess = "2.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
sha2 = "0.10.9"
|
||||
sqlparser = { version = "0.59.0", features = ["visitor"] }
|
||||
tauri = { version = "2.8.5", features = ["protocol-asset", "devtools"] }
|
||||
tauri-plugin-dialog = "2.4.0"
|
||||
tauri = { version = "2.9.1", features = ["protocol-asset", "devtools"] }
|
||||
tauri-plugin-dialog = "2.4.2"
|
||||
tauri-plugin-fs = "2.4.0"
|
||||
tauri-plugin-http = "2.5.2"
|
||||
tauri-plugin-notification = "2.3.1"
|
||||
tauri-plugin-opener = "2.5.0"
|
||||
tauri-plugin-os = "2.3"
|
||||
tauri-plugin-persisted-scope = "2.3.2"
|
||||
tauri-plugin-store = "2.4.0"
|
||||
tauri-plugin-http = "2.5.4"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
tauri-plugin-opener = "2.5.2"
|
||||
tauri-plugin-os = "2.3.2"
|
||||
tauri-plugin-persisted-scope = "2.3.4"
|
||||
tauri-plugin-store = "2.4.1"
|
||||
thiserror = "2.0.17"
|
||||
ts-rs = "11.0.1"
|
||||
uhlc = "0.8"
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
zip = "5.1.1"
|
||||
ts-rs = { version = "11.1.0", features = ["serde-compat"] }
|
||||
uhlc = "0.8.2"
|
||||
url = "2.5.7"
|
||||
uuid = { version = "1.18.1", features = ["v4"] }
|
||||
zip = "6.0.0"
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
trash = "5.2.0"
|
||||
rusqlite = { version = "0.37.0", features = ["load_extension", "bundled-sqlcipher-vendored-openssl", "functions"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
rusqlite = { version = "0.37.0", features = ["load_extension", "bundled-sqlcipher-vendored-openssl", "functions"] }
|
||||
|
||||
10
src-tauri/bindings/Action.ts
Normal file
10
src-tauri/bindings/Action.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DbAction } from "./DbAction";
|
||||
import type { FsAction } from "./FsAction";
|
||||
import type { HttpAction } from "./HttpAction";
|
||||
import type { ShellAction } from "./ShellAction";
|
||||
|
||||
/**
|
||||
* Ein typsicherer Container, der die spezifische Aktion für einen Ressourcentyp enthält.
|
||||
*/
|
||||
export type Action = { "Database": DbAction } | { "Filesystem": FsAction } | { "Http": HttpAction } | { "Shell": ShellAction };
|
||||
3
src-tauri/bindings/DatabaseError.ts
Normal file
3
src-tauri/bindings/DatabaseError.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type DatabaseError = { "type": "ParseError", "details": { reason: string, sql: string, } } | { "type": "ParameterMismatchError", "details": { expected: number, provided: number, sql: string, } } | { "type": "NoTableError", "details": { sql: string, } } | { "type": "StatementError", "details": { reason: string, } } | { "type": "PrepareError", "details": { reason: string, } } | { "type": "DatabaseError", "details": { reason: string, } } | { "type": "ExecutionError", "details": { sql: string, reason: string, table: string | null, } } | { "type": "TransactionError", "details": { reason: string, } } | { "type": "UnsupportedStatement", "details": { reason: string, sql: string, } } | { "type": "HlcError", "details": { reason: string, } } | { "type": "LockError", "details": { reason: string, } } | { "type": "ConnectionError", "details": { reason: string, } } | { "type": "SerializationError", "details": { reason: string, } } | { "type": "PermissionError", "details": { extension_id: string, operation: string | null, resource: string | null, reason: string, } } | { "type": "QueryError", "details": { reason: string, } } | { "type": "RowProcessingError", "details": { reason: string, } } | { "type": "MutexPoisoned", "details": { reason: string, } } | { "type": "ConnectionFailed", "details": { path: string, reason: string, } } | { "type": "PragmaError", "details": { pragma: string, reason: string, } } | { "type": "PathResolutionError", "details": { reason: string, } } | { "type": "IoError", "details": { path: string, reason: string, } } | { "type": "CrdtSetup", "details": string };
|
||||
6
src-tauri/bindings/DbAction.ts
Normal file
6
src-tauri/bindings/DbAction.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Definiert Aktionen, die auf eine Datenbank angewendet werden können.
|
||||
*/
|
||||
export type DbAction = "read" | "readWrite" | "create" | "delete" | "alterDrop";
|
||||
3
src-tauri/bindings/DbConstraints.ts
Normal file
3
src-tauri/bindings/DbConstraints.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type DbConstraints = { where_clause: string | null, columns: Array<string> | null, limit: number | null, };
|
||||
3
src-tauri/bindings/ExtensionInfoResponse.ts
Normal file
3
src-tauri/bindings/ExtensionInfoResponse.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type 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, };
|
||||
4
src-tauri/bindings/ExtensionManifest.ts
Normal file
4
src-tauri/bindings/ExtensionManifest.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// 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";
|
||||
|
||||
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, };
|
||||
7
src-tauri/bindings/ExtensionPermissions.ts
Normal file
7
src-tauri/bindings/ExtensionPermissions.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PermissionEntry } from "./PermissionEntry";
|
||||
|
||||
/**
|
||||
* Definiert die einheitliche Struktur für alle Berechtigungsarten im Manifest und UI.
|
||||
*/
|
||||
export type ExtensionPermissions = { database: Array<PermissionEntry> | null, filesystem: Array<PermissionEntry> | null, http: Array<PermissionEntry> | null, shell: Array<PermissionEntry> | null, };
|
||||
5
src-tauri/bindings/ExtensionPreview.ts
Normal file
5
src-tauri/bindings/ExtensionPreview.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ExtensionManifest } from "./ExtensionManifest";
|
||||
import type { ExtensionPermissions } from "./ExtensionPermissions";
|
||||
|
||||
export type ExtensionPreview = { manifest: ExtensionManifest, is_valid_signature: boolean, editable_permissions: ExtensionPermissions, };
|
||||
6
src-tauri/bindings/FsAction.ts
Normal file
6
src-tauri/bindings/FsAction.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Definiert Aktionen, die auf das Dateisystem angewendet werden können.
|
||||
*/
|
||||
export type FsAction = "read" | "readWrite";
|
||||
3
src-tauri/bindings/FsConstraints.ts
Normal file
3
src-tauri/bindings/FsConstraints.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FsConstraints = { max_file_size: bigint | null, allowed_extensions: Array<string> | null, recursive: boolean | null, };
|
||||
6
src-tauri/bindings/HttpAction.ts
Normal file
6
src-tauri/bindings/HttpAction.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Definiert Aktionen (HTTP-Methoden), die auf HTTP-Anfragen angewendet werden können.
|
||||
*/
|
||||
export type HttpAction = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "*";
|
||||
4
src-tauri/bindings/HttpConstraints.ts
Normal file
4
src-tauri/bindings/HttpConstraints.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { RateLimit } from "./RateLimit";
|
||||
|
||||
export type HttpConstraints = { methods: Array<string> | null, rate_limit: RateLimit | null, };
|
||||
7
src-tauri/bindings/PermissionConstraints.ts
Normal file
7
src-tauri/bindings/PermissionConstraints.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DbConstraints } from "./DbConstraints";
|
||||
import type { FsConstraints } from "./FsConstraints";
|
||||
import type { HttpConstraints } from "./HttpConstraints";
|
||||
import type { ShellConstraints } from "./ShellConstraints";
|
||||
|
||||
export type PermissionConstraints = DbConstraints | FsConstraints | HttpConstraints | ShellConstraints;
|
||||
19
src-tauri/bindings/PermissionEntry.ts
Normal file
19
src-tauri/bindings/PermissionEntry.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PermissionStatus } from "./PermissionStatus";
|
||||
|
||||
/**
|
||||
* Repräsentiert einen einzelnen Berechtigungseintrag im Manifest und im UI-Modell.
|
||||
*/
|
||||
export type PermissionEntry = { target: string,
|
||||
/**
|
||||
* Die auszuführende Aktion (z.B. "read", "read_write", "GET", "execute").
|
||||
*/
|
||||
operation?: string | null,
|
||||
/**
|
||||
* Optionale, spezifische Einschränkungen für diese Berechtigung.
|
||||
*/
|
||||
constraints?: Record<string, unknown>,
|
||||
/**
|
||||
* Der Status der Berechtigung (wird nur im UI-Modell verwendet).
|
||||
*/
|
||||
status?: PermissionStatus | null, };
|
||||
3
src-tauri/bindings/PermissionStatus.ts
Normal file
3
src-tauri/bindings/PermissionStatus.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PermissionStatus = "ask" | "granted" | "denied";
|
||||
3
src-tauri/bindings/RateLimit.ts
Normal file
3
src-tauri/bindings/RateLimit.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RateLimit = { requests: number, per_minutes: number, };
|
||||
3
src-tauri/bindings/ResourceType.ts
Normal file
3
src-tauri/bindings/ResourceType.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ResourceType = "fs" | "http" | "db" | "shell";
|
||||
6
src-tauri/bindings/ShellAction.ts
Normal file
6
src-tauri/bindings/ShellAction.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Definiert Aktionen, die auf Shell-Befehle angewendet werden können.
|
||||
*/
|
||||
export type ShellAction = "execute";
|
||||
3
src-tauri/bindings/ShellConstraints.ts
Normal file
3
src-tauri/bindings/ShellConstraints.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ShellConstraints = { allowed_subcommands: Array<string> | null, allowed_flags: Array<string> | null, forbidden_args: Array<string> | null, };
|
||||
3
src-tauri/bindings/VaultInfo.ts
Normal file
3
src-tauri/bindings/VaultInfo.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type VaultInfo = { name: string, lastAccess: bigint, path: string, };
|
||||
@ -1,106 +1,7 @@
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Write};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Schema {
|
||||
haex: Haex,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Haex {
|
||||
settings: String,
|
||||
extensions: String,
|
||||
extension_permissions: String,
|
||||
notifications: String,
|
||||
passwords: Passwords,
|
||||
crdt: Crdt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Passwords {
|
||||
groups: String,
|
||||
group_items: String,
|
||||
item_details: String,
|
||||
item_key_values: String,
|
||||
item_histories: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Crdt {
|
||||
logs: String,
|
||||
snapshots: String,
|
||||
configs: String,
|
||||
}
|
||||
mod generator;
|
||||
|
||||
fn main() {
|
||||
// Pfad zur Eingabe-JSON und zur Ausgabe-Rust-Datei festlegen.
|
||||
// `OUT_DIR` ist ein spezielles Verzeichnis, das Cargo für generierte Dateien bereitstellt.
|
||||
let schema_path = Path::new("database/tableNames.json");
|
||||
let out_dir =
|
||||
env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt. Führen Sie dies mit Cargo aus.");
|
||||
let dest_path = Path::new(&out_dir).join("tableNames.rs");
|
||||
|
||||
// --- 2. JSON-Datei lesen und mit serde parsen ---
|
||||
let file = File::open(&schema_path).expect("Konnte tableNames.json nicht öffnen");
|
||||
let reader = BufReader::new(file);
|
||||
let schema: Schema =
|
||||
serde_json::from_reader(reader).expect("Konnte tableNames.json nicht parsen");
|
||||
let haex = schema.haex;
|
||||
|
||||
// --- 3. Den zu generierenden Rust-Code als String erstellen ---
|
||||
// Wir verwenden das `format!`-Makro, um die Werte aus den geparsten Structs
|
||||
// in einen vordefinierten Code-Template-String einzufügen.
|
||||
// Das `r#""#`-Format erlaubt uns, mehrzeilige Strings mit Anführungszeichen zu verwenden.
|
||||
let code = format!(
|
||||
r#"
|
||||
// HINWEIS: Diese Datei wurde automatisch von build.rs generiert.
|
||||
// Manuelle Änderungen werden bei der nächsten Kompilierung überschrieben!
|
||||
|
||||
pub const TABLE_SETTINGS: &str = "{settings}";
|
||||
pub const TABLE_EXTENSIONS: &str = "{extensions}";
|
||||
pub const TABLE_EXTENSION_PERMISSIONS: &str = "{extension_permissions}";
|
||||
pub const TABLE_NOTIFICATIONS: &str = "{notifications}";
|
||||
|
||||
// Passwords
|
||||
pub const TABLE_PASSWORDS_GROUPS: &str = "{pw_groups}";
|
||||
pub const TABLE_PASSWORDS_GROUP_ITEMS: &str = "{pw_group_items}";
|
||||
pub const TABLE_PASSWORDS_ITEM_DETAILS: &str = "{pw_item_details}";
|
||||
pub const TABLE_PASSWORDS_ITEM_KEY_VALUES: &str = "{pw_item_key_values}";
|
||||
pub const TABLE_PASSWORDS_ITEM_HISTORIES: &str = "{pw_item_histories}";
|
||||
|
||||
// CRDT
|
||||
pub const TABLE_CRDT_LOGS: &str = "{crdt_logs}";
|
||||
pub const TABLE_CRDT_SNAPSHOTS: &str = "{crdt_snapshots}";
|
||||
pub const TABLE_CRDT_CONFIGS: &str = "{crdt_configs}";
|
||||
|
||||
"#,
|
||||
// Hier werden die Werte aus dem `haex`-Struct in die Platzhalter oben eingesetzt.
|
||||
settings = haex.settings,
|
||||
extensions = haex.extensions,
|
||||
extension_permissions = haex.extension_permissions,
|
||||
notifications = haex.notifications,
|
||||
pw_groups = haex.passwords.groups,
|
||||
pw_group_items = haex.passwords.group_items,
|
||||
pw_item_details = haex.passwords.item_details,
|
||||
pw_item_key_values = haex.passwords.item_key_values,
|
||||
pw_item_histories = haex.passwords.item_histories,
|
||||
crdt_logs = haex.crdt.logs,
|
||||
crdt_snapshots = haex.crdt.snapshots,
|
||||
crdt_configs = haex.crdt.configs
|
||||
);
|
||||
|
||||
// --- 4. Den generierten Code in die Zieldatei schreiben ---
|
||||
let mut f = File::create(&dest_path).expect("Konnte die Zieldatei nicht erstellen");
|
||||
f.write_all(code.as_bytes())
|
||||
.expect("Konnte nicht in die Zieldatei schreiben");
|
||||
|
||||
// --- 5. Cargo anweisen, das Skript erneut auszuführen, wenn sich die JSON-Datei ändert ---
|
||||
// Diese Zeile ist extrem wichtig für eine reibungslose Entwicklung! Ohne sie
|
||||
// würde Cargo Änderungen an der JSON-Datei nicht bemerken.
|
||||
println!("cargo:rerun-if-changed=database/tableNames.json");
|
||||
|
||||
tauri_build::build()
|
||||
generator::table_names::generate_table_names();
|
||||
generator::rust_types::generate_rust_types();
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
200
src-tauri/database/generate-rust-types.ts
Normal file
200
src-tauri/database/generate-rust-types.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { writeFileSync, mkdirSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import tablesNames from './tableNames.json'
|
||||
import { schema } from './index'
|
||||
import { getTableColumns } from 'drizzle-orm'
|
||||
import type { AnySQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
interface Column {
|
||||
name: string
|
||||
rustType: string
|
||||
isOptional: boolean
|
||||
}
|
||||
|
||||
function drizzleToRustType(colDef: AnySQLiteColumn): {
|
||||
rustType: string
|
||||
isOptional: boolean
|
||||
} {
|
||||
let baseType = 'String'
|
||||
let isOptional = !colDef.notNull
|
||||
|
||||
if (colDef.columnType === 'SQLiteText') {
|
||||
if ('mode' in colDef && colDef.mode === 'json') {
|
||||
baseType = 'serde_json::Value'
|
||||
} else {
|
||||
baseType = 'String'
|
||||
}
|
||||
} else if (colDef.columnType === 'SQLiteInteger') {
|
||||
baseType = 'i64'
|
||||
} else if (colDef.columnType === 'SQLiteBoolean') {
|
||||
baseType = 'bool'
|
||||
} else if (colDef.columnType === 'SQLiteReal') {
|
||||
baseType = 'f64'
|
||||
} else if (colDef.columnType === 'SQLiteBlob') {
|
||||
baseType = 'Vec<u8>'
|
||||
}
|
||||
|
||||
// Drizzle verwendet 'primary' für den Primärschlüssel-Status
|
||||
if (colDef.primary) {
|
||||
isOptional = false
|
||||
}
|
||||
|
||||
return { rustType: baseType, isOptional }
|
||||
}
|
||||
|
||||
function extractColumns(table: SQLiteTable): Column[] {
|
||||
const columns: Column[] = []
|
||||
|
||||
// getTableColumns gibt ein Record<string, AnySQLiteColumn> zurück
|
||||
const tableColumns = getTableColumns(table)
|
||||
|
||||
// Object.values gibt uns ein Array vom Typ AnySQLiteColumn[]
|
||||
for (const colDef of Object.values(tableColumns)) {
|
||||
// Die relevanten Infos stehen im 'config' Property der Spalte.
|
||||
// TypeScript kennt den Typ von 'config' bereits!
|
||||
const { rustType, isOptional } = drizzleToRustType(colDef)
|
||||
|
||||
columns.push({
|
||||
name: colDef.name,
|
||||
rustType: isOptional ? `Option<${rustType}>` : rustType,
|
||||
isOptional,
|
||||
})
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
function toSnakeCase(str: string): string {
|
||||
return str.replace(/[A-Z]/g, (letter, index) =>
|
||||
index === 0 ? letter.toLowerCase() : `_${letter.toLowerCase()}`,
|
||||
)
|
||||
}
|
||||
|
||||
function toPascalCase(str: string): string {
|
||||
console.log('toPascalCase:', str)
|
||||
return str
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join('')
|
||||
}
|
||||
|
||||
const RUST_KEYWORDS = new Set([
|
||||
'type',
|
||||
'struct',
|
||||
'enum',
|
||||
'pub',
|
||||
'use',
|
||||
'as',
|
||||
'crate',
|
||||
'super',
|
||||
'self',
|
||||
'let',
|
||||
'mut',
|
||||
])
|
||||
|
||||
function generateStruct(name: string, columns: Column[]): string {
|
||||
let structName = toPascalCase(name)
|
||||
|
||||
if (RUST_KEYWORDS.has(structName.toLowerCase())) {
|
||||
structName = `r#${structName}`
|
||||
}
|
||||
|
||||
// --- Teil 1: Struct-Definition ---
|
||||
let code = `#[derive(Debug, Clone, Serialize, Deserialize)]\n`
|
||||
code += `#[serde(rename_all = "camelCase")]\n`
|
||||
code += `pub struct ${structName} {\n`
|
||||
|
||||
for (const col of columns) {
|
||||
let fieldName = toSnakeCase(col.name)
|
||||
|
||||
// Prüfen, ob der Name ein Keyword ist
|
||||
if (RUST_KEYWORDS.has(fieldName)) {
|
||||
fieldName = `r#${fieldName}`
|
||||
}
|
||||
|
||||
if (col.isOptional) {
|
||||
code += ` #[serde(skip_serializing_if = "Option::is_none")]\n`
|
||||
}
|
||||
// Wichtig: #[serde(rename = "...")] hinzufügen, falls der Feldname geändert wurde!
|
||||
if (fieldName.startsWith('r#')) {
|
||||
const originalName = fieldName.substring(2)
|
||||
code += ` #[serde(rename = "${originalName}")]\n`
|
||||
}
|
||||
code += ` pub ${fieldName}: ${col.rustType},\n`
|
||||
}
|
||||
|
||||
code += `}\n\n`
|
||||
|
||||
// --- Teil 2: Impl-Block ---
|
||||
code += `impl ${structName} {\n`
|
||||
code += ` pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {\n`
|
||||
code += ` Ok(Self {\n`
|
||||
|
||||
columns.forEach((col, idx) => {
|
||||
let fieldName = toSnakeCase(col.name)
|
||||
if (RUST_KEYWORDS.has(fieldName)) {
|
||||
fieldName = `r#${fieldName}`
|
||||
}
|
||||
code += ` ${fieldName}: row.get(${idx})?,\n`
|
||||
})
|
||||
|
||||
code += ` })\n`
|
||||
code += ` }\n`
|
||||
code += `}\n\n`
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
function main() {
|
||||
let output = `// Auto-generated from Drizzle schema
|
||||
// DO NOT EDIT MANUALLY
|
||||
// Run 'pnpm generate:rust-types' to regenerate
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
`
|
||||
|
||||
const schemas = [
|
||||
{ name: tablesNames.haex.settings.name, table: schema.haexSettings },
|
||||
{ name: tablesNames.haex.extensions.name, table: schema.haexExtensions },
|
||||
{
|
||||
name: tablesNames.haex.extension_permissions.name,
|
||||
table: schema.haexExtensionPermissions,
|
||||
},
|
||||
{ name: tablesNames.haex.crdt.logs.name, table: schema.haexCrdtLogs },
|
||||
{
|
||||
name: tablesNames.haex.crdt.snapshots.name,
|
||||
table: schema.haexCrdtSnapshots,
|
||||
},
|
||||
{ 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) {
|
||||
console.log(`\n=== Processing table: ${name} ===`)
|
||||
const columns = extractColumns(table)
|
||||
console.log(`Found ${columns.length} columns`)
|
||||
|
||||
if (columns.length > 0) {
|
||||
output += generateStruct(name, columns)
|
||||
}
|
||||
}
|
||||
|
||||
const outputPath = join(__dirname, '../src/database/generated.rs')
|
||||
mkdirSync(dirname(outputPath), { recursive: true })
|
||||
writeFileSync(outputPath, output, 'utf-8')
|
||||
|
||||
console.log('\n✅ Rust types generated:', outputPath)
|
||||
}
|
||||
|
||||
main()
|
||||
@ -1,6 +1,6 @@
|
||||
import { drizzle } from 'drizzle-orm/sqlite-proxy' // Adapter für Query Building ohne direkte Verbindung
|
||||
import * as schema from './schemas/haex' // Importiere alles aus deiner Schema-Datei
|
||||
|
||||
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.
|
||||
@ -19,5 +19,3 @@ const dummyExecutor = async (
|
||||
// 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
|
||||
|
||||
104
src-tauri/database/migrations/0000_dashing_night_nurse.sql
Normal file
104
src-tauri/database/migrations/0000_dashing_night_nurse.sql
Normal file
@ -0,0 +1,104 @@
|
||||
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_desktop_items` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`workspace_id` text NOT NULL,
|
||||
`item_type` text NOT NULL,
|
||||
`extension_id` text,
|
||||
`system_window_id` text,
|
||||
`position_x` integer DEFAULT 0 NOT NULL,
|
||||
`position_y` integer DEFAULT 0 NOT NULL,
|
||||
`haex_timestamp` text,
|
||||
FOREIGN KEY (`workspace_id`) REFERENCES `haex_workspaces`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`extension_id`) REFERENCES `haex_extensions`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
CONSTRAINT "item_reference" CHECK(("haex_desktop_items"."item_type" = 'extension' AND "haex_desktop_items"."extension_id" IS NOT NULL AND "haex_desktop_items"."system_window_id" IS NULL) OR ("haex_desktop_items"."item_type" = 'system' AND "haex_desktop_items"."system_window_id" IS NOT NULL AND "haex_desktop_items"."extension_id" IS NULL) OR ("haex_desktop_items"."item_type" = 'file' AND "haex_desktop_items"."system_window_id" IS NOT NULL AND "haex_desktop_items"."extension_id" IS NULL) OR ("haex_desktop_items"."item_type" = 'folder' AND "haex_desktop_items"."system_window_id" IS NOT NULL AND "haex_desktop_items"."extension_id" IS NULL))
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `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_timestamp` text,
|
||||
FOREIGN KEY (`extension_id`) REFERENCES `haex_extensions`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> 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,
|
||||
`public_key` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`version` text NOT NULL,
|
||||
`author` text,
|
||||
`description` text,
|
||||
`entry` text DEFAULT 'index.html',
|
||||
`homepage` text,
|
||||
`enabled` integer DEFAULT true,
|
||||
`icon` text,
|
||||
`signature` text NOT NULL,
|
||||
`single_instance` integer DEFAULT false,
|
||||
`haex_timestamp` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `haex_extensions_public_key_name_unique` ON `haex_extensions` (`public_key`,`name`);--> 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_timestamp` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `haex_settings` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`key` text,
|
||||
`type` text,
|
||||
`value` text,
|
||||
`haex_timestamp` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `haex_settings_key_type_value_unique` ON `haex_settings` (`key`,`type`,`value`);--> statement-breakpoint
|
||||
CREATE TABLE `haex_workspaces` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`device_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`position` integer DEFAULT 0 NOT NULL,
|
||||
`haex_timestamp` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `haex_workspaces_position_unique` ON `haex_workspaces` (`position`);
|
||||
@ -1,26 +0,0 @@
|
||||
CREATE TABLE `haex_extensions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`author` text,
|
||||
`enabled` integer,
|
||||
`name` text,
|
||||
`url` text,
|
||||
`version` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `haex_extensions_permissions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`extension_id` text,
|
||||
`resource` text,
|
||||
`operation` text,
|
||||
`path` text,
|
||||
FOREIGN KEY (`extension_id`) REFERENCES `haex_extensions`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `haex_extensions_permissions_extension_id_resource_operation_path_unique` ON `haex_extensions_permissions` (`extension_id`,`resource`,`operation`,`path`);--> statement-breakpoint
|
||||
CREATE TABLE `haex_settings` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`key` text,
|
||||
`value_text` text,
|
||||
`value_json` text,
|
||||
`value_number` numeric
|
||||
);
|
||||
@ -1,7 +0,0 @@
|
||||
CREATE TABLE `testTable` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`author` text,
|
||||
`test` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `icon` text;
|
||||
@ -1,4 +0,0 @@
|
||||
ALTER TABLE `haex_settings` RENAME COLUMN "value_text" TO "value";--> statement-breakpoint
|
||||
DROP TABLE `testTable`;--> statement-breakpoint
|
||||
ALTER TABLE `haex_settings` DROP COLUMN `value_json`;--> statement-breakpoint
|
||||
ALTER TABLE `haex_settings` DROP COLUMN `value_number`;
|
||||
@ -1,11 +0,0 @@
|
||||
CREATE TABLE `haex_notofications` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`title` text,
|
||||
`text` text,
|
||||
`type` text NOT NULL,
|
||||
`read` integer,
|
||||
`date` text,
|
||||
`image` text,
|
||||
`alt` text,
|
||||
`icon` text
|
||||
);
|
||||
@ -1 +0,0 @@
|
||||
ALTER TABLE `haex_notofications` RENAME TO `haex_notifications`;
|
||||
@ -1,50 +0,0 @@
|
||||
CREATE TABLE `haex_passwords_group_items` (
|
||||
`group_id` text,
|
||||
`item_id` text,
|
||||
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_items`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `haex_passwords_groups` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text,
|
||||
`icon` text,
|
||||
`order` integer,
|
||||
`color` text,
|
||||
`parent_id` text,
|
||||
FOREIGN KEY (`parent_id`) REFERENCES `haex_passwords_groups`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> 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),
|
||||
FOREIGN KEY (`item_id`) REFERENCES `haex_passwords_items`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `haex_passwords_items` (
|
||||
`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
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `haex_passwords_items_key_values` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`item_id` text,
|
||||
`key` text,
|
||||
`value` text,
|
||||
FOREIGN KEY (`item_id`) REFERENCES `haex_passwords_items`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `haex_notifications` ADD `source` text;
|
||||
@ -1,5 +0,0 @@
|
||||
ALTER TABLE `haex_extensions_permissions` ADD `created_at` text DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions_permissions` ADD `updated_at` integer;--> statement-breakpoint
|
||||
ALTER TABLE `haex_passwords_groups` ADD `created_at` text DEFAULT (CURRENT_TIMESTAMP);--> statement-breakpoint
|
||||
ALTER TABLE `haex_passwords_groups` ADD `updated_at` integer;--> statement-breakpoint
|
||||
ALTER TABLE `haex_passwords_items_key_values` ADD `updated_at` integer;
|
||||
@ -1 +0,0 @@
|
||||
ALTER TABLE `haex_passwords_groups` ADD `description` text;
|
||||
@ -1,40 +0,0 @@
|
||||
ALTER TABLE `haex_passwords_items` RENAME TO `haex_passwords_item_details`;--> statement-breakpoint
|
||||
ALTER TABLE `haex_passwords_items_key_values` RENAME TO `haex_passwords_item_key_values`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_haex_passwords_item_key_values` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`item_id` text,
|
||||
`key` text,
|
||||
`value` text,
|
||||
`updated_at` integer,
|
||||
FOREIGN KEY (`item_id`) REFERENCES `haex_passwords_item_details`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_haex_passwords_item_key_values`("id", "item_id", "key", "value", "updated_at") SELECT "id", "item_id", "key", "value", "updated_at" 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`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||
CREATE TABLE `__new_haex_passwords_group_items` (
|
||||
`group_id` text,
|
||||
`item_id` text,
|
||||
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
|
||||
INSERT INTO `__new_haex_passwords_group_items`("group_id", "item_id") SELECT "group_id", "item_id" 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_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),
|
||||
FOREIGN KEY (`item_id`) REFERENCES `haex_passwords_item_details`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_haex_passwords_item_history`("id", "item_id", "changed_property", "old_value", "new_value", "created_at") SELECT "id", "item_id", "changed_property", "old_value", "new_value", "created_at" 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`;
|
||||
@ -1 +0,0 @@
|
||||
ALTER TABLE `haex_settings` ADD `type` text;
|
||||
@ -1,32 +0,0 @@
|
||||
CREATE TABLE `haex_crdt_logs` (
|
||||
`hlc_timestamp` text PRIMARY KEY NOT NULL,
|
||||
`table_name` text,
|
||||
`row_pks` text,
|
||||
`op_type` text,
|
||||
`column_name` text,
|
||||
`new_value` text,
|
||||
`old_value` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `haex_crdt_settings` (
|
||||
`type` text PRIMARY KEY NOT NULL,
|
||||
`value` text
|
||||
);
|
||||
--> 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
|
||||
ALTER TABLE `haex_extensions` ADD `haex_tombstone` integer;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions_permissions` ADD `haex_tombstone` integer;--> statement-breakpoint
|
||||
ALTER TABLE `haex_notifications` ADD `haex_tombstone` integer;--> statement-breakpoint
|
||||
ALTER TABLE `haex_passwords_group_items` ADD `haex_tombstone` integer;--> statement-breakpoint
|
||||
ALTER TABLE `haex_passwords_groups` ADD `haex_tombstone` integer;--> statement-breakpoint
|
||||
ALTER TABLE `haex_passwords_item_details` ADD `haex_tombstone` integer;--> statement-breakpoint
|
||||
ALTER TABLE `haex_passwords_item_history` ADD `haex_tombstone` integer;--> statement-breakpoint
|
||||
ALTER TABLE `haex_passwords_item_key_values` ADD `haex_tombstone` integer;--> statement-breakpoint
|
||||
ALTER TABLE `haex_settings` ADD `haex_tombstone` integer;
|
||||
@ -1,21 +0,0 @@
|
||||
ALTER TABLE `haex_crdt_settings` RENAME TO `haex_crdt_configs`;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions_permissions` RENAME TO `haex_extension_permissions`;--> statement-breakpoint
|
||||
ALTER TABLE `haex_crdt_configs` RENAME COLUMN "type" TO "key";--> statement-breakpoint
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_haex_extension_permissions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`extension_id` text,
|
||||
`resource` text,
|
||||
`operation` text,
|
||||
`path` text,
|
||||
`created_at` text DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_at` integer,
|
||||
`haex_tombstone` integer,
|
||||
FOREIGN KEY (`extension_id`) REFERENCES `haex_extensions`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_haex_extension_permissions`("id", "extension_id", "resource", "operation", "path", "created_at", "updated_at", "haex_tombstone") SELECT "id", "extension_id", "resource", "operation", "path", "created_at", "updated_at", "haex_tombstone" 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
|
||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `haex_extension_permissions_extension_id_resource_operation_path_unique` ON `haex_extension_permissions` (`extension_id`,`resource`,`operation`,`path`);
|
||||
@ -1,15 +0,0 @@
|
||||
ALTER TABLE `haex_extension_permissions` RENAME COLUMN "resource" TO "resource_type";--> statement-breakpoint
|
||||
ALTER TABLE `haex_extension_permissions` RENAME COLUMN "operation" TO "action";--> statement-breakpoint
|
||||
ALTER TABLE `haex_extension_permissions` RENAME COLUMN "path" TO "target";--> statement-breakpoint
|
||||
DROP INDEX `haex_extension_permissions_extension_id_resource_operation_path_unique`;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extension_permissions` ADD `constraints` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extension_permissions` ADD `status` text DEFAULT 'denied' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extension_permissions` ADD `haex_timestamp` text;--> 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
|
||||
ALTER TABLE `haex_extensions` ADD `description` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `entry` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `homepage` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `public_key` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `signature` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_extensions` ADD `haex_timestamp` text;--> statement-breakpoint
|
||||
ALTER TABLE `haex_settings` ADD `haex_timestamp` text;
|
||||
@ -1,49 +1,21 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "fc5a7c9d-4846-4120-a762-cc2ea00504b9",
|
||||
"id": "8dc25226-70f9-4d2e-89d4-f3a6b2bdf58d",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"haex_extensions": {
|
||||
"name": "haex_extensions",
|
||||
"haex_crdt_configs": {
|
||||
"name": "haex_crdt_configs",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"author": {
|
||||
"name": "author",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
@ -56,8 +28,235 @@
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_permissions",
|
||||
"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
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"item_type": {
|
||||
"name": "item_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"extension_id": {
|
||||
"name": "extension_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"system_window_id": {
|
||||
"name": "system_window_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"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_timestamp": {
|
||||
"name": "haex_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_desktop_items_workspace_id_haex_workspaces_id_fk": {
|
||||
"name": "haex_desktop_items_workspace_id_haex_workspaces_id_fk",
|
||||
"tableFrom": "haex_desktop_items",
|
||||
"tableTo": "haex_workspaces",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"haex_desktop_items_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_desktop_items_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_desktop_items",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {
|
||||
"item_reference": {
|
||||
"name": "item_reference",
|
||||
"value": "(\"haex_desktop_items\".\"item_type\" = 'extension' AND \"haex_desktop_items\".\"extension_id\" IS NOT NULL AND \"haex_desktop_items\".\"system_window_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'system' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'file' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL) OR (\"haex_desktop_items\".\"item_type\" = 'folder' AND \"haex_desktop_items\".\"system_window_id\" IS NOT NULL AND \"haex_desktop_items\".\"extension_id\" IS NULL)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"haex_extension_permissions": {
|
||||
"name": "haex_extension_permissions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
@ -70,25 +269,62 @@
|
||||
"name": "extension_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"resource": {
|
||||
"name": "resource",
|
||||
"resource_type": {
|
||||
"name": "resource_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"action": {
|
||||
"name": "action",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"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_timestamp": {
|
||||
"name": "haex_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
@ -96,21 +332,21 @@
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"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",
|
||||
"operation",
|
||||
"path"
|
||||
"resource_type",
|
||||
"action",
|
||||
"target"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"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"
|
||||
@ -118,7 +354,7 @@
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
@ -126,6 +362,206 @@
|
||||
"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": false,
|
||||
"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
|
||||
},
|
||||
"single_instance": {
|
||||
"name": "single_instance",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 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_timestamp": {
|
||||
"name": "haex_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_settings": {
|
||||
"name": "haex_settings",
|
||||
"columns": {
|
||||
@ -143,29 +579,93 @@
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value_text": {
|
||||
"name": "value_text",
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value_json": {
|
||||
"name": "value_json",
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value_number": {
|
||||
"name": "value_number",
|
||||
"type": "numeric",
|
||||
"haex_timestamp": {
|
||||
"name": "haex_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"indexes": {
|
||||
"haex_settings_key_type_value_unique": {
|
||||
"name": "haex_settings_key_type_value_unique",
|
||||
"columns": [
|
||||
"key",
|
||||
"type",
|
||||
"value"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_workspaces": {
|
||||
"name": "haex_workspaces",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"device_id": {
|
||||
"name": "device_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"position": {
|
||||
"name": "position",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"haex_timestamp": {
|
||||
"name": "haex_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_workspaces_position_unique": {
|
||||
"name": "haex_workspaces_position_unique",
|
||||
"columns": [
|
||||
"position"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
|
||||
@ -1,223 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "6fb5396b-9f87-4fb5-87a2-22d4eecaa11e",
|
||||
"prevId": "fc5a7c9d-4846-4120-a762-cc2ea00504b9",
|
||||
"tables": {
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"value_text": {
|
||||
"name": "value_text",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value_json": {
|
||||
"name": "value_json",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value_number": {
|
||||
"name": "value_number",
|
||||
"type": "numeric",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"testTable": {
|
||||
"name": "testTable",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"author": {
|
||||
"name": "author",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"test": {
|
||||
"name": "test",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,180 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "ea3507ca-77bc-4f3c-a605-8426614f5803",
|
||||
"prevId": "6fb5396b-9f87-4fb5-87a2-22d4eecaa11e",
|
||||
"tables": {
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {
|
||||
"\"haex_settings\".\"value_text\"": "\"haex_settings\".\"value\""
|
||||
}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,251 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "5f413421-18a5-4c1b-9c5b-99f574b10126",
|
||||
"prevId": "ea3507ca-77bc-4f3c-a605-8426614f5803",
|
||||
"tables": {
|
||||
"haex_extensions": {
|
||||
"name": "haex_extensions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"author": {
|
||||
"name": "author",
|
||||
"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
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_notofications": {
|
||||
"name": "haex_notofications",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"text": {
|
||||
"name": "text",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"read": {
|
||||
"name": "read",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"date": {
|
||||
"name": "date",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"alt": {
|
||||
"name": "alt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"icon": {
|
||||
"name": "icon",
|
||||
"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
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,253 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "7aaac460-00b5-4387-bef9-b189297cefb3",
|
||||
"prevId": "5f413421-18a5-4c1b-9c5b-99f574b10126",
|
||||
"tables": {
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {
|
||||
"\"haex_notofications\"": "\"haex_notifications\""
|
||||
},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,583 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "fd079acd-3b5f-4fb7-97e2-d6641620f393",
|
||||
"prevId": "7aaac460-00b5-4387-bef9-b189297cefb3",
|
||||
"tables": {
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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_items_id_fk": {
|
||||
"name": "haex_passwords_group_items_item_id_haex_passwords_items_id_fk",
|
||||
"tableFrom": "haex_passwords_group_items",
|
||||
"tableTo": "haex_passwords_items",
|
||||
"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
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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_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)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_passwords_item_history_item_id_haex_passwords_items_id_fk": {
|
||||
"name": "haex_passwords_item_history_item_id_haex_passwords_items_id_fk",
|
||||
"tableFrom": "haex_passwords_item_history",
|
||||
"tableTo": "haex_passwords_items",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_items": {
|
||||
"name": "haex_passwords_items",
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_items_key_values": {
|
||||
"name": "haex_passwords_items_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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_passwords_items_key_values_item_id_haex_passwords_items_id_fk": {
|
||||
"name": "haex_passwords_items_key_values_item_id_haex_passwords_items_id_fk",
|
||||
"tableFrom": "haex_passwords_items_key_values",
|
||||
"tableTo": "haex_passwords_items",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,620 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "76878f8b-9a30-4fd2-9a7b-d1a85874b1ab",
|
||||
"prevId": "fd079acd-3b5f-4fb7-97e2-d6641620f393",
|
||||
"tables": {
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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_items_id_fk": {
|
||||
"name": "haex_passwords_group_items_item_id_haex_passwords_items_id_fk",
|
||||
"tableFrom": "haex_passwords_group_items",
|
||||
"tableTo": "haex_passwords_items",
|
||||
"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
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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_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)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_passwords_item_history_item_id_haex_passwords_items_id_fk": {
|
||||
"name": "haex_passwords_item_history_item_id_haex_passwords_items_id_fk",
|
||||
"tableFrom": "haex_passwords_item_history",
|
||||
"tableTo": "haex_passwords_items",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_items": {
|
||||
"name": "haex_passwords_items",
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_items_key_values": {
|
||||
"name": "haex_passwords_items_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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_passwords_items_key_values_item_id_haex_passwords_items_id_fk": {
|
||||
"name": "haex_passwords_items_key_values_item_id_haex_passwords_items_id_fk",
|
||||
"tableFrom": "haex_passwords_items_key_values",
|
||||
"tableTo": "haex_passwords_items",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,627 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "47f309cf-dabd-4f19-b87a-ed73d0e97781",
|
||||
"prevId": "76878f8b-9a30-4fd2-9a7b-d1a85874b1ab",
|
||||
"tables": {
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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_items_id_fk": {
|
||||
"name": "haex_passwords_group_items_item_id_haex_passwords_items_id_fk",
|
||||
"tableFrom": "haex_passwords_group_items",
|
||||
"tableTo": "haex_passwords_items",
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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_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)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_passwords_item_history_item_id_haex_passwords_items_id_fk": {
|
||||
"name": "haex_passwords_item_history_item_id_haex_passwords_items_id_fk",
|
||||
"tableFrom": "haex_passwords_item_history",
|
||||
"tableTo": "haex_passwords_items",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_items": {
|
||||
"name": "haex_passwords_items",
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_passwords_items_key_values": {
|
||||
"name": "haex_passwords_items_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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"haex_passwords_items_key_values_item_id_haex_passwords_items_id_fk": {
|
||||
"name": "haex_passwords_items_key_values_item_id_haex_passwords_items_id_fk",
|
||||
"tableFrom": "haex_passwords_items_key_values",
|
||||
"tableTo": "haex_passwords_items",
|
||||
"columnsFrom": [
|
||||
"item_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,630 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "c4edecb8-6aef-49e2-8498-0c4b74653c75",
|
||||
"prevId": "47f309cf-dabd-4f19-b87a-ed73d0e97781",
|
||||
"tables": {
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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)"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {
|
||||
"\"haex_passwords_items\"": "\"haex_passwords_item_details\"",
|
||||
"\"haex_passwords_items_key_values\"": "\"haex_passwords_item_key_values\""
|
||||
},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,634 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "c3a688c3-9537-4aa8-be95-a8f55546caf1",
|
||||
"prevId": "c4edecb8-6aef-49e2-8498-0c4b74653c75",
|
||||
"tables": {
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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)"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,825 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "288d577f-f9c8-44e8-964e-da1fa062aff9",
|
||||
"prevId": "c3a688c3-9537-4aa8-be95-a8f55546caf1",
|
||||
"tables": {
|
||||
"haex_crdt_logs": {
|
||||
"name": "haex_crdt_logs",
|
||||
"columns": {
|
||||
"hlc_timestamp": {
|
||||
"name": "hlc_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"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": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_crdt_settings": {
|
||||
"name": "haex_crdt_settings",
|
||||
"columns": {
|
||||
"type": {
|
||||
"name": "type",
|
||||
"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_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_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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"haex_extensions_permissions": {
|
||||
"name": "haex_extensions_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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"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": {
|
||||
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
|
||||
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
|
||||
"tableFrom": "haex_extensions_permissions",
|
||||
"tableTo": "haex_extensions",
|
||||
"columnsFrom": [
|
||||
"extension_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,830 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "c8c0825d-c435-4a42-986a-a4f70e7f9e8b",
|
||||
"prevId": "288d577f-f9c8-44e8-964e-da1fa062aff9",
|
||||
"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": {
|
||||
"hlc_timestamp": {
|
||||
"name": "hlc_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"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": {},
|
||||
"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": {
|
||||
"name": "resource",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"operation": {
|
||||
"name": "operation",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"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": {
|
||||
"haex_extension_permissions_extension_id_resource_operation_path_unique": {
|
||||
"name": "haex_extension_permissions_extension_id_resource_operation_path_unique",
|
||||
"columns": [
|
||||
"extension_id",
|
||||
"resource",
|
||||
"operation",
|
||||
"path"
|
||||
],
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {
|
||||
"\"haex_crdt_settings\"": "\"haex_crdt_configs\"",
|
||||
"\"haex_extensions_permissions\"": "\"haex_extension_permissions\""
|
||||
},
|
||||
"columns": {
|
||||
"\"haex_crdt_configs\".\"type\"": "\"haex_crdt_configs\".\"key\""
|
||||
}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -1,900 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "e4f45178-afcf-46e9-9a07-79b7bca88d88",
|
||||
"prevId": "c8c0825d-c435-4a42-986a-a4f70e7f9e8b",
|
||||
"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": {
|
||||
"hlc_timestamp": {
|
||||
"name": "hlc_timestamp",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"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": {},
|
||||
"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": {
|
||||
"\"haex_extension_permissions\".\"resource\"": "\"haex_extension_permissions\".\"resource_type\"",
|
||||
"\"haex_extension_permissions\".\"operation\"": "\"haex_extension_permissions\".\"action\"",
|
||||
"\"haex_extension_permissions\".\"path\"": "\"haex_extension_permissions\".\"target\""
|
||||
}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -5,92 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1742903332283,
|
||||
"tag": "0000_zippy_scourge",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1746281577722,
|
||||
"tag": "0001_wealthy_thaddeus_ross",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1747583956679,
|
||||
"tag": "0002_married_bushwacker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1748873820060,
|
||||
"tag": "0003_familiar_doctor_faustus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "6",
|
||||
"when": 1748982377354,
|
||||
"tag": "0004_wooden_lockheed",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1749073296353,
|
||||
"tag": "0005_wooden_nuke",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "6",
|
||||
"when": 1749128243104,
|
||||
"tag": "0006_complete_martin_li",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "6",
|
||||
"when": 1749244165094,
|
||||
"tag": "0007_daffy_tusk",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "6",
|
||||
"when": 1749727958231,
|
||||
"tag": "0008_faulty_mercury",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "6",
|
||||
"when": 1750158916787,
|
||||
"tag": "0009_curved_selene",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "6",
|
||||
"when": 1756377828646,
|
||||
"tag": "0010_deep_war_machine",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "6",
|
||||
"when": 1757968140525,
|
||||
"tag": "0011_illegal_thor_girl",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "6",
|
||||
"when": 1759362109283,
|
||||
"tag": "0012_special_gwen_stacy",
|
||||
"when": 1761821821609,
|
||||
"tag": "0000_dashing_night_nurse",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,27 +1,52 @@
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'
|
||||
import tableNames from '../tableNames.json'
|
||||
|
||||
export const haexCrdtLogs = sqliteTable(tableNames.haex.crdt.logs, {
|
||||
hlc_timestamp: text().primaryKey(),
|
||||
table_name: text(),
|
||||
row_pks: text({ mode: 'json' }),
|
||||
op_type: text({ enum: ['INSERT', 'UPDATE', 'DELETE'] }),
|
||||
column_name: text(),
|
||||
new_value: text({ mode: 'json' }),
|
||||
old_value: text({ mode: 'json' }),
|
||||
})
|
||||
export const haexCrdtLogs = sqliteTable(
|
||||
tableNames.haex.crdt.logs.name,
|
||||
{
|
||||
id: text()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
haexTimestamp: text(tableNames.haex.crdt.logs.columns.haexTimestamp),
|
||||
tableName: text(tableNames.haex.crdt.logs.columns.tableName),
|
||||
rowPks: text(tableNames.haex.crdt.logs.columns.rowPks, { mode: 'json' }),
|
||||
opType: text(tableNames.haex.crdt.logs.columns.opType, {
|
||||
enum: ['INSERT', 'UPDATE', 'DELETE'],
|
||||
}),
|
||||
columnName: text(tableNames.haex.crdt.logs.columns.columnName),
|
||||
newValue: text(tableNames.haex.crdt.logs.columns.newValue, {
|
||||
mode: 'json',
|
||||
}),
|
||||
oldValue: text(tableNames.haex.crdt.logs.columns.oldValue, {
|
||||
mode: 'json',
|
||||
}),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_haex_timestamp').on(table.haexTimestamp),
|
||||
index('idx_table_row').on(table.tableName, table.rowPks),
|
||||
],
|
||||
)
|
||||
export type InsertHaexCrdtLogs = typeof haexCrdtLogs.$inferInsert
|
||||
export type SelectHaexCrdtLogs = typeof haexCrdtLogs.$inferSelect
|
||||
|
||||
export const haexCrdtSnapshots = sqliteTable(tableNames.haex.crdt.snapshots, {
|
||||
snapshot_id: text().primaryKey(),
|
||||
created: text(),
|
||||
epoch_hlc: text(),
|
||||
location_url: text(),
|
||||
file_size_bytes: integer(),
|
||||
})
|
||||
export const haexCrdtSnapshots = sqliteTable(
|
||||
tableNames.haex.crdt.snapshots.name,
|
||||
{
|
||||
snapshotId: text(tableNames.haex.crdt.snapshots.columns.snapshotId)
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
created: text(),
|
||||
epochHlc: text(tableNames.haex.crdt.snapshots.columns.epochHlc),
|
||||
locationUrl: text(tableNames.haex.crdt.snapshots.columns.locationUrl),
|
||||
fileSizeBytes: integer(
|
||||
tableNames.haex.crdt.snapshots.columns.fileSizeBytes,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
export const haexCrdtConfigs = sqliteTable(tableNames.haex.crdt.configs, {
|
||||
key: text().primaryKey(),
|
||||
export const haexCrdtConfigs = sqliteTable(tableNames.haex.crdt.configs.name, {
|
||||
key: text()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
value: text(),
|
||||
})
|
||||
|
||||
@ -1,50 +1,78 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import {
|
||||
check,
|
||||
integer,
|
||||
sqliteTable,
|
||||
text,
|
||||
unique,
|
||||
type AnySQLiteColumn,
|
||||
type SQLiteColumnBuilderBase,
|
||||
} from 'drizzle-orm/sqlite-core'
|
||||
import tableNames from '../tableNames.json'
|
||||
import { crdtColumnNames } from '.'
|
||||
|
||||
export const haexSettings = sqliteTable(tableNames.haex.settings, {
|
||||
id: text().primaryKey(),
|
||||
key: text(),
|
||||
type: text(),
|
||||
value: text(),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
haex_timestamp: text(),
|
||||
// Helper function to add common CRDT columns ( haexTimestamp)
|
||||
export const withCrdtColumns = <
|
||||
T extends Record<string, SQLiteColumnBuilderBase>,
|
||||
>(
|
||||
columns: T,
|
||||
) => ({
|
||||
...columns,
|
||||
haexTimestamp: text(crdtColumnNames.haexTimestamp),
|
||||
})
|
||||
|
||||
export const haexSettings = sqliteTable(
|
||||
tableNames.haex.settings.name,
|
||||
withCrdtColumns({
|
||||
id: text()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
key: text(),
|
||||
type: text(),
|
||||
value: text(),
|
||||
}),
|
||||
(table) => [unique().on(table.key, table.type, table.value)],
|
||||
)
|
||||
export type InsertHaexSettings = typeof haexSettings.$inferInsert
|
||||
export type SelectHaexSettings = typeof haexSettings.$inferSelect
|
||||
|
||||
export const haexExtensions = sqliteTable(tableNames.haex.extensions, {
|
||||
id: text().primaryKey(),
|
||||
author: text(),
|
||||
description: text(),
|
||||
entry: text(),
|
||||
homepage: text(),
|
||||
enabled: integer({ mode: 'boolean' }),
|
||||
icon: text(),
|
||||
name: text(),
|
||||
public_key: text(),
|
||||
signature: text(),
|
||||
url: text(),
|
||||
version: text(),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
haex_timestamp: text(),
|
||||
})
|
||||
export const haexExtensions = sqliteTable(
|
||||
tableNames.haex.extensions.name,
|
||||
withCrdtColumns({
|
||||
id: text()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
public_key: text().notNull(),
|
||||
name: text().notNull(),
|
||||
version: text().notNull(),
|
||||
author: text(),
|
||||
description: text(),
|
||||
entry: text().default('index.html'),
|
||||
homepage: text(),
|
||||
enabled: integer({ mode: 'boolean' }).default(true),
|
||||
icon: text(),
|
||||
signature: text().notNull(),
|
||||
single_instance: integer({ mode: 'boolean' }).default(false),
|
||||
}),
|
||||
(table) => [
|
||||
// UNIQUE constraint: Pro Developer (public_key) kann nur eine Extension mit diesem Namen existieren
|
||||
unique().on(table.public_key, table.name),
|
||||
],
|
||||
)
|
||||
export type InsertHaexExtensions = typeof haexExtensions.$inferInsert
|
||||
export type SelectHaexExtensions = typeof haexExtensions.$inferSelect
|
||||
|
||||
export const haexExtensionPermissions = sqliteTable(
|
||||
tableNames.haex.extension_permissions,
|
||||
{
|
||||
id: text().primaryKey(),
|
||||
extensionId: text('extension_id').references(
|
||||
(): AnySQLiteColumn => haexExtensions.id,
|
||||
),
|
||||
tableNames.haex.extension_permissions.name,
|
||||
withCrdtColumns({
|
||||
id: text()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
extensionId: text(tableNames.haex.extension_permissions.columns.extensionId)
|
||||
.notNull()
|
||||
.references((): AnySQLiteColumn => haexExtensions.id, {
|
||||
onDelete: 'cascade',
|
||||
}),
|
||||
resourceType: text('resource_type', {
|
||||
enum: ['fs', 'http', 'db', 'shell'],
|
||||
}),
|
||||
@ -58,9 +86,7 @@ export const haexExtensionPermissions = sqliteTable(
|
||||
updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate(
|
||||
() => new Date(),
|
||||
),
|
||||
haexTombstone: integer('haex_tombstone', { mode: 'boolean' }),
|
||||
haexTimestamp: text('haex_timestamp'),
|
||||
},
|
||||
}),
|
||||
(table) => [
|
||||
unique().on(
|
||||
table.extensionId,
|
||||
@ -74,3 +100,77 @@ export type InserthaexExtensionPermissions =
|
||||
typeof haexExtensionPermissions.$inferInsert
|
||||
export type SelecthaexExtensionPermissions =
|
||||
typeof haexExtensionPermissions.$inferSelect
|
||||
|
||||
export const haexNotifications = sqliteTable(
|
||||
tableNames.haex.notifications.name,
|
||||
withCrdtColumns({
|
||||
id: text().primaryKey(),
|
||||
alt: text(),
|
||||
date: text(),
|
||||
icon: text(),
|
||||
image: text(),
|
||||
read: integer({ mode: 'boolean' }),
|
||||
source: text(),
|
||||
text: text(),
|
||||
title: text(),
|
||||
type: text({
|
||||
enum: ['error', 'success', 'warning', 'info', 'log'],
|
||||
}).notNull(),
|
||||
}),
|
||||
)
|
||||
export type InsertHaexNotifications = typeof haexNotifications.$inferInsert
|
||||
export type SelectHaexNotifications = typeof haexNotifications.$inferSelect
|
||||
|
||||
export const haexWorkspaces = sqliteTable(
|
||||
tableNames.haex.workspaces.name,
|
||||
withCrdtColumns({
|
||||
id: text(tableNames.haex.workspaces.columns.id)
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
deviceId: text(tableNames.haex.workspaces.columns.deviceId).notNull(),
|
||||
name: text(tableNames.haex.workspaces.columns.name).notNull(),
|
||||
position: integer(tableNames.haex.workspaces.columns.position)
|
||||
.notNull()
|
||||
.default(0),
|
||||
}),
|
||||
(table) => [unique().on(table.position)],
|
||||
)
|
||||
export type InsertHaexWorkspaces = typeof haexWorkspaces.$inferInsert
|
||||
export type SelectHaexWorkspaces = typeof haexWorkspaces.$inferSelect
|
||||
|
||||
export const haexDesktopItems = sqliteTable(
|
||||
tableNames.haex.desktop_items.name,
|
||||
withCrdtColumns({
|
||||
id: text(tableNames.haex.desktop_items.columns.id)
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
workspaceId: text(tableNames.haex.desktop_items.columns.workspaceId)
|
||||
.notNull()
|
||||
.references(() => haexWorkspaces.id, { onDelete: 'cascade' }),
|
||||
itemType: text(tableNames.haex.desktop_items.columns.itemType, {
|
||||
enum: ['system', 'extension', 'file', 'folder'],
|
||||
}).notNull(),
|
||||
// Für Extensions (wenn itemType = 'extension')
|
||||
extensionId: text(
|
||||
tableNames.haex.desktop_items.columns.extensionId,
|
||||
).references((): AnySQLiteColumn => haexExtensions.id, {
|
||||
onDelete: 'cascade',
|
||||
}),
|
||||
// Für System Windows (wenn itemType = 'system')
|
||||
systemWindowId: text(tableNames.haex.desktop_items.columns.systemWindowId),
|
||||
positionX: integer(tableNames.haex.desktop_items.columns.positionX)
|
||||
.notNull()
|
||||
.default(0),
|
||||
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 SelectHaexDesktopItems = typeof haexDesktopItems.$inferSelect
|
||||
|
||||
5
src-tauri/database/schemas/index.ts
Normal file
5
src-tauri/database/schemas/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const crdtColumnNames = {
|
||||
haexTimestamp: 'haex_timestamp',
|
||||
}
|
||||
export * from './crdt'
|
||||
export * from './haex'
|
||||
@ -1,130 +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 haexNotifications = sqliteTable(tableNames.haex.notifications, {
|
||||
id: text().primaryKey(),
|
||||
alt: text(),
|
||||
date: text(),
|
||||
icon: text(),
|
||||
image: text(),
|
||||
read: integer({ mode: 'boolean' }),
|
||||
source: text(),
|
||||
text: text(),
|
||||
title: text(),
|
||||
type: text({
|
||||
enum: ['error', 'success', 'warning', 'info', 'log'],
|
||||
}).notNull(),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
})
|
||||
export type InsertHaexNotifications = typeof haexNotifications.$inferInsert
|
||||
export type SelectHaexNotifications = typeof haexNotifications.$inferSelect
|
||||
|
||||
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').references(
|
||||
(): AnySQLiteColumn => haexPasswordsItemDetails.id,
|
||||
),
|
||||
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').references(
|
||||
(): AnySQLiteColumn => haexPasswordsItemDetails.id,
|
||||
),
|
||||
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,
|
||||
),
|
||||
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').references(
|
||||
(): AnySQLiteColumn => haexPasswordsGroups.id,
|
||||
),
|
||||
itemId: text('item_id').references(
|
||||
(): AnySQLiteColumn => haexPasswordsItemDetails.id,
|
||||
),
|
||||
haex_tombstone: integer({ mode: 'boolean' }),
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.itemId, table.groupId] })],
|
||||
)
|
||||
export type InsertHaexPasswordsGroupItems =
|
||||
typeof haexPasswordsGroupItems.$inferInsert
|
||||
export type SelectHaexPasswordsGroupItems =
|
||||
typeof haexPasswordsGroupItems.$inferSelect
|
||||
@ -1,20 +1,126 @@
|
||||
{
|
||||
"haex": {
|
||||
"settings": "haex_settings",
|
||||
"extensions": "haex_extensions",
|
||||
"extension_permissions": "haex_extension_permissions",
|
||||
"notifications": "haex_notifications",
|
||||
"passwords": {
|
||||
"groups": "haex_passwords_groups",
|
||||
"group_items": "haex_passwords_group_items",
|
||||
"item_details": "haex_passwords_item_details",
|
||||
"item_key_values": "haex_passwords_item_key_values",
|
||||
"item_histories": "haex_passwords_item_history"
|
||||
"settings": {
|
||||
"name": "haex_settings",
|
||||
"columns": {
|
||||
"id": "id",
|
||||
"key": "key",
|
||||
"type": "type",
|
||||
"value": "value",
|
||||
|
||||
"haexTimestamp": "haex_timestamp"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"name": "haex_extensions",
|
||||
"columns": {
|
||||
"id": "id",
|
||||
"author": "author",
|
||||
"description": "description",
|
||||
"entry": "entry",
|
||||
"homepage": "homepage",
|
||||
"enabled": "enabled",
|
||||
"icon": "icon",
|
||||
"name": "name",
|
||||
"public_key": "public_key",
|
||||
"signature": "signature",
|
||||
"url": "url",
|
||||
"version": "version",
|
||||
|
||||
"haexTimestamp": "haex_timestamp"
|
||||
}
|
||||
},
|
||||
"extension_permissions": {
|
||||
"name": "haex_extension_permissions",
|
||||
"columns": {
|
||||
"id": "id",
|
||||
"extensionId": "extension_id",
|
||||
"resourceType": "resource_type",
|
||||
"action": "action",
|
||||
"target": "target",
|
||||
"constraints": "constraints",
|
||||
"status": "status",
|
||||
"createdAt": "created_at",
|
||||
"updateAt": "updated_at",
|
||||
|
||||
"haexTimestamp": "haex_timestamp"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"name": "haex_notifications",
|
||||
"columns": {
|
||||
"id": "id",
|
||||
"alt": "alt",
|
||||
"date": "date",
|
||||
"icon": "icon",
|
||||
"image": "image",
|
||||
"read": "read",
|
||||
"source": "source",
|
||||
"text": "text",
|
||||
"title": "title",
|
||||
"type": "type",
|
||||
|
||||
"haexTimestamp": "haex_timestamp"
|
||||
}
|
||||
},
|
||||
"workspaces": {
|
||||
"name": "haex_workspaces",
|
||||
"columns": {
|
||||
"id": "id",
|
||||
"deviceId": "device_id",
|
||||
"name": "name",
|
||||
"position": "position",
|
||||
"createdAt": "created_at",
|
||||
|
||||
"haexTimestamp": "haex_timestamp"
|
||||
}
|
||||
},
|
||||
"desktop_items": {
|
||||
"name": "haex_desktop_items",
|
||||
"columns": {
|
||||
"id": "id",
|
||||
"workspaceId": "workspace_id",
|
||||
"itemType": "item_type",
|
||||
"extensionId": "extension_id",
|
||||
"systemWindowId": "system_window_id",
|
||||
"positionX": "position_x",
|
||||
"positionY": "position_y",
|
||||
|
||||
"haexTimestamp": "haex_timestamp"
|
||||
}
|
||||
},
|
||||
|
||||
"crdt": {
|
||||
"logs": "haex_crdt_logs",
|
||||
"snapshots": "haex_crdt_snapshots",
|
||||
"configs": "haex_crdt_configs"
|
||||
"logs": {
|
||||
"name": "haex_crdt_logs",
|
||||
"columns": {
|
||||
"id": "id",
|
||||
"haexTimestamp": "haex_timestamp",
|
||||
"tableName": "table_name",
|
||||
"rowPks": "row_pks",
|
||||
"opType": "op_type",
|
||||
"columnName": "column_name",
|
||||
"newValue": "new_value",
|
||||
"oldValue": "old_value"
|
||||
}
|
||||
},
|
||||
"snapshots": {
|
||||
"name": "haex_crdt_snapshots",
|
||||
"columns": {
|
||||
"snapshotId": "snapshot_id",
|
||||
"created": "created",
|
||||
"epochHlc": "epoch_hlc",
|
||||
"locationUrl": "location_url",
|
||||
"fileSizeBytes": "file_size_bytes"
|
||||
}
|
||||
},
|
||||
"configs": {
|
||||
"name": "haex_crdt_configs",
|
||||
"columns": {
|
||||
"key": "key",
|
||||
"value": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -24,6 +24,23 @@ android {
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
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 {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
@ -43,6 +60,12 @@ android {
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
|
||||
// Sign with release config if available
|
||||
val releaseSigningConfig = signingConfigs.getByName("release")
|
||||
if (releaseSigningConfig.storeFile != null) {
|
||||
signingConfig = releaseSigningConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
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."
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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.",
|
||||
@ -2270,12 +2270,6 @@
|
||||
"Identifier": {
|
||||
"description": "Permission identifier",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default permissions for the plugin",
|
||||
"type": "string",
|
||||
"const": "android-fs:default",
|
||||
"markdownDescription": "Default permissions for the plugin"
|
||||
},
|
||||
{
|
||||
"description": "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`",
|
||||
"type": "string",
|
||||
@ -2283,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`"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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.",
|
||||
@ -2330,12 +2324,24 @@
|
||||
"const": "core:app:allow-name",
|
||||
"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.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@ -2402,12 +2408,24 @@
|
||||
"const": "core:app:deny-name",
|
||||
"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.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@ -5547,10 +5565,10 @@
|
||||
"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",
|
||||
"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.",
|
||||
|
||||
@ -1400,10 +1400,10 @@
|
||||
"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",
|
||||
"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.",
|
||||
@ -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`"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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.",
|
||||
@ -2324,12 +2324,24 @@
|
||||
"const": "core:app:allow-name",
|
||||
"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.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@ -2396,12 +2408,24 @@
|
||||
"const": "core:app:deny-name",
|
||||
"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.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@ -5541,10 +5565,10 @@
|
||||
"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",
|
||||
"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.",
|
||||
|
||||
@ -1400,10 +1400,10 @@
|
||||
"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",
|
||||
"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.",
|
||||
@ -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`"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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.",
|
||||
@ -2324,12 +2324,24 @@
|
||||
"const": "core:app:allow-name",
|
||||
"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.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@ -2396,12 +2408,24 @@
|
||||
"const": "core:app:deny-name",
|
||||
"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.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@ -5541,10 +5565,10 @@
|
||||
"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",
|
||||
"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.",
|
||||
|
||||
@ -1400,10 +1400,10 @@
|
||||
"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",
|
||||
"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.",
|
||||
@ -2270,12 +2270,6 @@
|
||||
"Identifier": {
|
||||
"description": "Permission identifier",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Default permissions for the plugin",
|
||||
"type": "string",
|
||||
"const": "android-fs:default",
|
||||
"markdownDescription": "Default permissions for the plugin"
|
||||
},
|
||||
{
|
||||
"description": "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`",
|
||||
"type": "string",
|
||||
@ -2283,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`"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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.",
|
||||
@ -2330,12 +2324,24 @@
|
||||
"const": "core:app:allow-name",
|
||||
"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.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@ -2402,12 +2408,24 @@
|
||||
"const": "core:app:deny-name",
|
||||
"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.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@ -5547,10 +5565,10 @@
|
||||
"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",
|
||||
"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.",
|
||||
|
||||
3
src-tauri/generator/mod.rs
Normal file
3
src-tauri/generator/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
// build/mod.rs
|
||||
pub mod rust_types;
|
||||
pub mod table_names;
|
||||
24
src-tauri/generator/rust_types.rs
Normal file
24
src-tauri/generator/rust_types.rs
Normal file
@ -0,0 +1,24 @@
|
||||
// src-tauri/src/build/rust_types.rs
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn generate_rust_types() {
|
||||
// Prüfe ob die generierte Datei vom TypeScript-Script existiert
|
||||
let generated_path = Path::new("src/database/generated.rs");
|
||||
|
||||
if !generated_path.exists() {
|
||||
eprintln!("⚠️ Warning: src/database/generated.rs not found!");
|
||||
eprintln!(" Run 'pnpm generate:rust-types' first.");
|
||||
|
||||
// Erstelle eine leere Datei als Fallback
|
||||
fs::write(
|
||||
generated_path,
|
||||
"// Run 'pnpm generate:rust-types' to generate this file\n",
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed=src/database/generated.rs");
|
||||
println!("cargo:rerun-if-changed=src/database/schemas/crdt.ts");
|
||||
println!("cargo:rerun-if-changed=src/database/schemas/haex.ts");
|
||||
}
|
||||
118
src-tauri/generator/table_names.rs
Normal file
118
src-tauri/generator/table_names.rs
Normal file
@ -0,0 +1,118 @@
|
||||
// src-tarui/src/build/table_names.rs
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Write};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Schema {
|
||||
haex: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TableDefinition {
|
||||
name: String,
|
||||
columns: HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub fn generate_table_names() {
|
||||
let out_dir = env::var("OUT_DIR").expect("OUT_DIR ist nicht gesetzt.");
|
||||
println!("Generiere Tabellennamen nach {}", out_dir);
|
||||
let schema_path = Path::new("database/tableNames.json");
|
||||
let dest_path = Path::new(&out_dir).join("tableNames.rs");
|
||||
|
||||
let file = File::open(&schema_path).expect("Konnte tableNames.json nicht öffnen");
|
||||
let reader = BufReader::new(file);
|
||||
let schema: Schema =
|
||||
serde_json::from_reader(reader).expect("Konnte tableNames.json nicht parsen");
|
||||
|
||||
let mut code = String::from(
|
||||
r#"
|
||||
// ==================================================================
|
||||
// HINWEIS: Diese Datei wurde automatisch von build.rs generiert.
|
||||
// Manuelle Änderungen werden bei der nächsten Kompilierung überschrieben!
|
||||
// ==================================================================
|
||||
|
||||
"#,
|
||||
);
|
||||
|
||||
// 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 ---
|
||||
let mut f = File::create(&dest_path).expect("Konnte Zieldatei nicht erstellen");
|
||||
f.write_all(code.as_bytes())
|
||||
.expect("Konnte nicht in Zieldatei schreiben");
|
||||
|
||||
println!("cargo:rerun-if-changed=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
|
||||
}
|
||||
99
src-tauri/src/crdt/insert_transformer.rs
Normal file
99
src-tauri/src/crdt/insert_transformer.rs
Normal file
@ -0,0 +1,99 @@
|
||||
// src-tauri/src/crdt/insert_transformer.rs
|
||||
// INSERT-spezifische CRDT-Transformationen (ON CONFLICT, RETURNING)
|
||||
|
||||
use crate::crdt::trigger::HLC_TIMESTAMP_COLUMN;
|
||||
use crate::database::error::DatabaseError;
|
||||
use sqlparser::ast::{Expr, Ident, Insert, SelectItem, SetExpr, Value};
|
||||
use uhlc::Timestamp;
|
||||
|
||||
/// Helper-Struct für INSERT-Transformationen
|
||||
pub struct InsertTransformer {
|
||||
hlc_timestamp_column: &'static str,
|
||||
}
|
||||
|
||||
impl InsertTransformer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
hlc_timestamp_column: HLC_TIMESTAMP_COLUMN,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_or_add_column(columns: &mut Vec<Ident>, col_name: &'static str) -> usize {
|
||||
match columns.iter().position(|c| c.value == col_name) {
|
||||
Some(index) => index, // Gefunden! Gib Index zurück.
|
||||
None => {
|
||||
// Nicht gefunden! Hinzufügen.
|
||||
columns.push(Ident::new(col_name));
|
||||
columns.len() - 1 // Der Index des gerade hinzugefügten Elements
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wenn der Index == der Länge ist, wird der Wert stattdessen gepusht.
|
||||
fn set_or_push_value(row: &mut Vec<Expr>, index: usize, value: Expr) {
|
||||
if index < row.len() {
|
||||
// Spalte war vorhanden, Wert (wahrscheinlich `?` oder NULL) ersetzen
|
||||
row[index] = value;
|
||||
} else {
|
||||
// Spalte war nicht vorhanden, Wert hinzufügen
|
||||
row.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_or_push_projection(projection: &mut Vec<SelectItem>, index: usize, value: Expr) {
|
||||
let item = SelectItem::UnnamedExpr(value);
|
||||
if index < projection.len() {
|
||||
projection[index] = item;
|
||||
} else {
|
||||
projection.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformiert INSERT-Statements (fügt HLC-Timestamp hinzu)
|
||||
/// Hard Delete: Kein ON CONFLICT mehr nötig - gelöschte Einträge sind wirklich weg
|
||||
pub fn transform_insert(
|
||||
&self,
|
||||
insert_stmt: &mut Insert,
|
||||
timestamp: &Timestamp,
|
||||
) -> Result<(), DatabaseError> {
|
||||
// Add haex_timestamp column if not exists
|
||||
let hlc_col_index =
|
||||
Self::find_or_add_column(&mut insert_stmt.columns, self.hlc_timestamp_column);
|
||||
|
||||
// ON CONFLICT Logik komplett entfernt!
|
||||
// Bei Hard Deletes gibt es keine Tombstone-Einträge mehr zu reaktivieren
|
||||
// UNIQUE Constraint Violations sind echte Fehler
|
||||
|
||||
match insert_stmt.source.as_mut() {
|
||||
Some(query) => match &mut *query.body {
|
||||
SetExpr::Values(values) => {
|
||||
for row in &mut values.rows {
|
||||
let hlc_value =
|
||||
Expr::Value(Value::SingleQuotedString(timestamp.to_string()).into());
|
||||
|
||||
Self::set_or_push_value(row, hlc_col_index, hlc_value);
|
||||
}
|
||||
}
|
||||
SetExpr::Select(select) => {
|
||||
let hlc_value =
|
||||
Expr::Value(Value::SingleQuotedString(timestamp.to_string()).into());
|
||||
|
||||
Self::set_or_push_projection(&mut select.projection, hlc_col_index, hlc_value);
|
||||
}
|
||||
_ => {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
sql: insert_stmt.to_string(),
|
||||
reason: "INSERT with unsupported source type".to_string(),
|
||||
});
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
reason: "INSERT statement has no source".to_string(),
|
||||
sql: insert_stmt.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
pub mod hlc;
|
||||
pub mod insert_transformer;
|
||||
//pub mod query_transformer;
|
||||
pub mod transformer;
|
||||
pub mod trigger;
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
use crate::crdt::trigger::{HLC_TIMESTAMP_COLUMN, TOMBSTONE_COLUMN};
|
||||
// src-tauri/src/crdt/transformer.rs
|
||||
|
||||
use crate::crdt::insert_transformer::InsertTransformer;
|
||||
use crate::crdt::trigger::HLC_TIMESTAMP_COLUMN;
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::table_names::{TABLE_CRDT_CONFIGS, TABLE_CRDT_LOGS};
|
||||
use sqlparser::ast::{
|
||||
Assignment, AssignmentTarget, BinaryOperator, ColumnDef, DataType, Expr, Ident, Insert,
|
||||
ObjectName, ObjectNamePart, SelectItem, SetExpr, Statement, TableFactor, TableObject, Value,
|
||||
Assignment, AssignmentTarget, ColumnDef, DataType, Expr, Ident, ObjectName, ObjectNamePart,
|
||||
Statement, TableFactor, TableObject, Value,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
@ -12,46 +15,14 @@ use uhlc::Timestamp;
|
||||
/// Konfiguration für CRDT-Spalten
|
||||
#[derive(Clone)]
|
||||
struct CrdtColumns {
|
||||
tombstone: &'static str,
|
||||
hlc_timestamp: &'static str,
|
||||
}
|
||||
|
||||
impl CrdtColumns {
|
||||
const DEFAULT: Self = Self {
|
||||
tombstone: TOMBSTONE_COLUMN,
|
||||
hlc_timestamp: HLC_TIMESTAMP_COLUMN,
|
||||
};
|
||||
|
||||
/// Erstellt einen Tombstone-Filter für eine Tabelle
|
||||
fn create_tombstone_filter(&self, table_alias: Option<&str>) -> Expr {
|
||||
let column_expr = match table_alias {
|
||||
Some(alias) => {
|
||||
// Qualifizierte Referenz: alias.tombstone
|
||||
Expr::CompoundIdentifier(vec![Ident::new(alias), Ident::new(self.tombstone)])
|
||||
}
|
||||
None => {
|
||||
// Einfache Referenz: tombstone
|
||||
Expr::Identifier(Ident::new(self.tombstone))
|
||||
}
|
||||
};
|
||||
|
||||
Expr::BinaryOp {
|
||||
left: Box::new(column_expr),
|
||||
op: BinaryOperator::NotEq,
|
||||
right: Box::new(Expr::Value(Value::Number("1".to_string(), false).into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Erstellt eine Tombstone-Zuweisung für UPDATE/DELETE
|
||||
fn create_tombstone_assignment(&self) -> Assignment {
|
||||
Assignment {
|
||||
target: AssignmentTarget::ColumnName(ObjectName(vec![ObjectNamePart::Identifier(
|
||||
Ident::new(self.tombstone),
|
||||
)])),
|
||||
value: Expr::Value(Value::Number("1".to_string(), false).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Erstellt eine HLC-Zuweisung für UPDATE/DELETE
|
||||
fn create_hlc_assignment(&self, timestamp: &Timestamp) -> Assignment {
|
||||
Assignment {
|
||||
@ -64,13 +35,6 @@ impl CrdtColumns {
|
||||
|
||||
/// Fügt CRDT-Spalten zu einer Tabellendefinition hinzu
|
||||
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) {
|
||||
columns.push(ColumnDef {
|
||||
name: Ident::new(self.hlc_timestamp),
|
||||
@ -110,14 +74,61 @@ impl CrdtTransformer {
|
||||
Cow::Owned(name_str.trim_matches('`').trim_matches('"').to_string())
|
||||
}
|
||||
|
||||
pub fn transform_select_statement(&self, stmt: &mut Statement) -> Result<(), DatabaseError> {
|
||||
// =================================================================
|
||||
// ÖFFENTLICHE API-METHODEN
|
||||
// =================================================================
|
||||
|
||||
pub fn transform_execute_statement_with_table_info(
|
||||
&self,
|
||||
stmt: &mut Statement,
|
||||
hlc_timestamp: &Timestamp,
|
||||
) -> Result<Option<String>, DatabaseError> {
|
||||
match stmt {
|
||||
Statement::Query(query) => self.transform_query_recursive(query),
|
||||
// Fange alle anderen Fälle ab und gib einen Fehler zurück
|
||||
_ => Err(DatabaseError::UnsupportedStatement {
|
||||
sql: stmt.to_string(),
|
||||
reason: "This operation only accepts SELECT statements.".to_string(),
|
||||
}),
|
||||
Statement::CreateTable(create_table) => {
|
||||
if self.is_crdt_sync_table(&create_table.name) {
|
||||
self.columns
|
||||
.add_to_table_definition(&mut create_table.columns);
|
||||
Ok(Some(
|
||||
self.normalize_table_name(&create_table.name).into_owned(),
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Statement::Insert(insert_stmt) => {
|
||||
if let TableObject::TableName(name) = &insert_stmt.table {
|
||||
if self.is_crdt_sync_table(name) {
|
||||
// Hard Delete: Kein Schema-Lookup mehr nötig (kein ON CONFLICT)
|
||||
let insert_transformer = InsertTransformer::new();
|
||||
insert_transformer.transform_insert(insert_stmt, hlc_timestamp)?;
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Statement::Update {
|
||||
table, assignments, ..
|
||||
} => {
|
||||
if let TableFactor::Table { name, .. } = &table.relation {
|
||||
if self.is_crdt_sync_table(name) {
|
||||
assignments.push(self.columns.create_hlc_assignment(hlc_timestamp));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Statement::Delete(_del_stmt) => {
|
||||
// Hard Delete - keine Transformation!
|
||||
// DELETE bleibt DELETE
|
||||
// BEFORE DELETE Trigger schreiben die Logs
|
||||
Ok(None)
|
||||
}
|
||||
Statement::AlterTable { name, .. } => {
|
||||
if self.is_crdt_sync_table(name) {
|
||||
Ok(Some(self.normalize_table_name(name).into_owned()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,7 +152,9 @@ impl CrdtTransformer {
|
||||
Statement::Insert(insert_stmt) => {
|
||||
if let TableObject::TableName(name) = &insert_stmt.table {
|
||||
if self.is_crdt_sync_table(name) {
|
||||
self.transform_insert(insert_stmt, hlc_timestamp)?;
|
||||
// Hard Delete: Keine ON CONFLICT Logik mehr nötig
|
||||
let insert_transformer = InsertTransformer::new();
|
||||
insert_transformer.transform_insert(insert_stmt, hlc_timestamp)?;
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
@ -156,18 +169,10 @@ impl CrdtTransformer {
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Statement::Delete(del_stmt) => {
|
||||
if let Some(table_name) = self.extract_table_name_from_delete(del_stmt) {
|
||||
if self.is_crdt_sync_table(&table_name) {
|
||||
self.transform_delete_to_update(stmt, hlc_timestamp)?;
|
||||
}
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(DatabaseError::UnsupportedStatement {
|
||||
sql: del_stmt.to_string(),
|
||||
reason: "DELETE from non-table source or multiple tables".to_string(),
|
||||
})
|
||||
}
|
||||
Statement::Delete(_del_stmt) => {
|
||||
// Hard Delete - keine Transformation!
|
||||
// DELETE bleibt DELETE
|
||||
Ok(None)
|
||||
}
|
||||
Statement::AlterTable { name, .. } => {
|
||||
if self.is_crdt_sync_table(name) {
|
||||
@ -179,606 +184,4 @@ impl CrdtTransformer {
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformiert Query-Statements (fügt Tombstone-Filter hinzu)
|
||||
fn transform_query_recursive(
|
||||
&self,
|
||||
query: &mut sqlparser::ast::Query,
|
||||
) -> Result<(), DatabaseError> {
|
||||
self.add_tombstone_filters_recursive(&mut query.body)
|
||||
}
|
||||
|
||||
/// Rekursive Behandlung aller SetExpr-Typen mit vollständiger Subquery-Unterstützung
|
||||
fn add_tombstone_filters_recursive(&self, set_expr: &mut SetExpr) -> Result<(), DatabaseError> {
|
||||
match set_expr {
|
||||
SetExpr::Select(select) => {
|
||||
self.add_tombstone_filters_to_select(select)?;
|
||||
|
||||
// Transformiere auch Subqueries in Projektionen
|
||||
for projection in &mut select.projection {
|
||||
match projection {
|
||||
SelectItem::UnnamedExpr(expr) | SelectItem::ExprWithAlias { expr, .. } => {
|
||||
self.transform_expression_subqueries(expr)?;
|
||||
}
|
||||
_ => {} // Wildcard projections ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
// Transformiere Subqueries in WHERE
|
||||
if let Some(where_clause) = &mut select.selection {
|
||||
self.transform_expression_subqueries(where_clause)?;
|
||||
}
|
||||
|
||||
// Transformiere Subqueries in GROUP BY
|
||||
match &mut select.group_by {
|
||||
sqlparser::ast::GroupByExpr::All(_) => {
|
||||
// GROUP BY ALL - keine Expressions zu transformieren
|
||||
}
|
||||
sqlparser::ast::GroupByExpr::Expressions(exprs, _) => {
|
||||
for group_expr in exprs {
|
||||
self.transform_expression_subqueries(group_expr)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transformiere Subqueries in HAVING
|
||||
if let Some(having) = &mut select.having {
|
||||
self.transform_expression_subqueries(having)?;
|
||||
}
|
||||
}
|
||||
SetExpr::SetOperation { left, right, .. } => {
|
||||
self.add_tombstone_filters_recursive(left)?;
|
||||
self.add_tombstone_filters_recursive(right)?;
|
||||
}
|
||||
SetExpr::Query(query) => {
|
||||
self.add_tombstone_filters_recursive(&mut query.body)?;
|
||||
}
|
||||
SetExpr::Values(values) => {
|
||||
// Transformiere auch Subqueries in Values-Listen
|
||||
for row in &mut values.rows {
|
||||
for expr in row {
|
||||
self.transform_expression_subqueries(expr)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {} // Andere Fälle
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transformiert Subqueries innerhalb von Expressions
|
||||
fn transform_expression_subqueries(&self, expr: &mut Expr) -> Result<(), DatabaseError> {
|
||||
match expr {
|
||||
// Einfache Subqueries
|
||||
Expr::Subquery(query) => {
|
||||
self.add_tombstone_filters_recursive(&mut query.body)?;
|
||||
}
|
||||
// EXISTS Subqueries
|
||||
Expr::Exists { subquery, .. } => {
|
||||
self.add_tombstone_filters_recursive(&mut subquery.body)?;
|
||||
}
|
||||
// IN Subqueries
|
||||
Expr::InSubquery {
|
||||
expr: left_expr,
|
||||
subquery,
|
||||
..
|
||||
} => {
|
||||
self.transform_expression_subqueries(left_expr)?;
|
||||
self.add_tombstone_filters_recursive(&mut subquery.body)?;
|
||||
}
|
||||
// ANY/ALL Subqueries
|
||||
Expr::AnyOp { left, right, .. } | Expr::AllOp { left, right, .. } => {
|
||||
self.transform_expression_subqueries(left)?;
|
||||
self.transform_expression_subqueries(right)?;
|
||||
}
|
||||
// Binäre Operationen
|
||||
Expr::BinaryOp { left, right, .. } => {
|
||||
self.transform_expression_subqueries(left)?;
|
||||
self.transform_expression_subqueries(right)?;
|
||||
}
|
||||
// Unäre Operationen
|
||||
Expr::UnaryOp {
|
||||
expr: inner_expr, ..
|
||||
} => {
|
||||
self.transform_expression_subqueries(inner_expr)?;
|
||||
}
|
||||
// Verschachtelte Ausdrücke
|
||||
Expr::Nested(nested) => {
|
||||
self.transform_expression_subqueries(nested)?;
|
||||
}
|
||||
// CASE-Ausdrücke
|
||||
Expr::Case {
|
||||
operand,
|
||||
conditions,
|
||||
else_result,
|
||||
..
|
||||
} => {
|
||||
if let Some(op) = operand {
|
||||
self.transform_expression_subqueries(op)?;
|
||||
}
|
||||
for case_when in conditions {
|
||||
self.transform_expression_subqueries(&mut case_when.condition)?;
|
||||
self.transform_expression_subqueries(&mut case_when.result)?;
|
||||
}
|
||||
if let Some(else_res) = else_result {
|
||||
self.transform_expression_subqueries(else_res)?;
|
||||
}
|
||||
}
|
||||
// Funktionsaufrufe
|
||||
Expr::Function(func) => match &mut func.args {
|
||||
sqlparser::ast::FunctionArguments::List(sqlparser::ast::FunctionArgumentList {
|
||||
args,
|
||||
..
|
||||
}) => {
|
||||
for arg in args {
|
||||
if let sqlparser::ast::FunctionArg::Unnamed(
|
||||
sqlparser::ast::FunctionArgExpr::Expr(expr),
|
||||
) = arg
|
||||
{
|
||||
self.transform_expression_subqueries(expr)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
// BETWEEN
|
||||
Expr::Between {
|
||||
expr: main_expr,
|
||||
low,
|
||||
high,
|
||||
..
|
||||
} => {
|
||||
self.transform_expression_subqueries(main_expr)?;
|
||||
self.transform_expression_subqueries(low)?;
|
||||
self.transform_expression_subqueries(high)?;
|
||||
}
|
||||
// IN Liste
|
||||
Expr::InList {
|
||||
expr: main_expr,
|
||||
list,
|
||||
..
|
||||
} => {
|
||||
self.transform_expression_subqueries(main_expr)?;
|
||||
for list_expr in list {
|
||||
self.transform_expression_subqueries(list_expr)?;
|
||||
}
|
||||
}
|
||||
// IS NULL/IS NOT NULL
|
||||
Expr::IsNull(inner) | Expr::IsNotNull(inner) => {
|
||||
self.transform_expression_subqueries(inner)?;
|
||||
}
|
||||
// Andere Expression-Typen benötigen keine Transformation
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fügt Tombstone-Filter zu SELECT-Statements hinzu (nur wenn nicht explizit in WHERE gesetzt)
|
||||
fn add_tombstone_filters_to_select(
|
||||
&self,
|
||||
select: &mut sqlparser::ast::Select,
|
||||
) -> Result<(), DatabaseError> {
|
||||
// Sammle alle CRDT-Tabellen mit ihren Aliasen
|
||||
let mut crdt_tables = Vec::new();
|
||||
for twj in &select.from {
|
||||
if let TableFactor::Table { name, alias, .. } = &twj.relation {
|
||||
if self.is_crdt_sync_table(name) {
|
||||
let table_alias = alias.as_ref().map(|a| a.name.value.as_str());
|
||||
crdt_tables.push((name.clone(), table_alias));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if crdt_tables.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Prüfe, welche Tombstone-Spalten bereits in der WHERE-Klausel referenziert werden
|
||||
let explicitly_filtered_tables = if let Some(where_clause) = &select.selection {
|
||||
self.find_explicitly_filtered_tombstone_tables(where_clause, &crdt_tables)
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
|
||||
// Erstelle Filter nur für Tabellen, die noch nicht explizit gefiltert werden
|
||||
let mut tombstone_filters = Vec::new();
|
||||
for (table_name, table_alias) in crdt_tables {
|
||||
let table_name_string = table_name.to_string();
|
||||
let table_key = table_alias.unwrap_or(&table_name_string);
|
||||
if !explicitly_filtered_tables.contains(table_key) {
|
||||
tombstone_filters.push(self.columns.create_tombstone_filter(table_alias));
|
||||
}
|
||||
}
|
||||
|
||||
// Füge die automatischen Filter hinzu
|
||||
if !tombstone_filters.is_empty() {
|
||||
let combined_filter = tombstone_filters
|
||||
.into_iter()
|
||||
.reduce(|acc, expr| Expr::BinaryOp {
|
||||
left: Box::new(acc),
|
||||
op: BinaryOperator::And,
|
||||
right: Box::new(expr),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match &mut select.selection {
|
||||
Some(existing) => {
|
||||
*existing = Expr::BinaryOp {
|
||||
left: Box::new(existing.clone()),
|
||||
op: BinaryOperator::And,
|
||||
right: Box::new(combined_filter),
|
||||
};
|
||||
}
|
||||
None => {
|
||||
select.selection = Some(combined_filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Findet alle Tabellen, die bereits explizit Tombstone-Filter in der WHERE-Klausel haben
|
||||
fn find_explicitly_filtered_tombstone_tables(
|
||||
&self,
|
||||
where_expr: &Expr,
|
||||
crdt_tables: &[(ObjectName, Option<&str>)],
|
||||
) -> HashSet<String> {
|
||||
let mut filtered_tables = HashSet::new();
|
||||
self.scan_expression_for_tombstone_references(
|
||||
where_expr,
|
||||
crdt_tables,
|
||||
&mut filtered_tables,
|
||||
);
|
||||
filtered_tables
|
||||
}
|
||||
|
||||
/// Rekursiv durchsucht einen Expression-Baum nach Tombstone-Spalten-Referenzen
|
||||
fn scan_expression_for_tombstone_references(
|
||||
&self,
|
||||
expr: &Expr,
|
||||
crdt_tables: &[(ObjectName, Option<&str>)],
|
||||
filtered_tables: &mut HashSet<String>,
|
||||
) {
|
||||
match expr {
|
||||
// Einfache Spaltenreferenz: tombstone = ?
|
||||
Expr::Identifier(ident) => {
|
||||
if ident.value == self.columns.tombstone {
|
||||
// Wenn keine Tabelle spezifiziert ist und es nur eine CRDT-Tabelle gibt
|
||||
if crdt_tables.len() == 1 {
|
||||
let table_name_str = crdt_tables[0].0.to_string();
|
||||
let table_key = crdt_tables[0].1.unwrap_or(&table_name_str);
|
||||
filtered_tables.insert(table_key.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Qualifizierte Spaltenreferenz: table.tombstone = ? oder alias.tombstone = ?
|
||||
Expr::CompoundIdentifier(idents) => {
|
||||
if idents.len() == 2 && idents[1].value == self.columns.tombstone {
|
||||
let table_ref = &idents[0].value;
|
||||
|
||||
// Prüfe, ob es eine unserer CRDT-Tabellen ist (nach Name oder Alias)
|
||||
for (table_name, alias) in crdt_tables {
|
||||
let table_name_str = table_name.to_string();
|
||||
if table_ref == &table_name_str || alias.map_or(false, |a| a == table_ref) {
|
||||
filtered_tables.insert(table_ref.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Binäre Operationen: AND, OR, etc.
|
||||
Expr::BinaryOp { left, right, .. } => {
|
||||
self.scan_expression_for_tombstone_references(left, crdt_tables, filtered_tables);
|
||||
self.scan_expression_for_tombstone_references(right, crdt_tables, filtered_tables);
|
||||
}
|
||||
// Unäre Operationen: NOT, etc.
|
||||
Expr::UnaryOp { expr, .. } => {
|
||||
self.scan_expression_for_tombstone_references(expr, crdt_tables, filtered_tables);
|
||||
}
|
||||
// Verschachtelte Ausdrücke
|
||||
Expr::Nested(nested) => {
|
||||
self.scan_expression_for_tombstone_references(nested, crdt_tables, filtered_tables);
|
||||
}
|
||||
// IN-Klauseln
|
||||
Expr::InList { expr, .. } => {
|
||||
self.scan_expression_for_tombstone_references(expr, crdt_tables, filtered_tables);
|
||||
}
|
||||
// BETWEEN-Klauseln
|
||||
Expr::Between { expr, .. } => {
|
||||
self.scan_expression_for_tombstone_references(expr, crdt_tables, filtered_tables);
|
||||
}
|
||||
// IS NULL/IS NOT NULL
|
||||
Expr::IsNull(expr) | Expr::IsNotNull(expr) => {
|
||||
self.scan_expression_for_tombstone_references(expr, crdt_tables, filtered_tables);
|
||||
}
|
||||
// Funktionsaufrufe - KORRIGIERT
|
||||
Expr::Function(func) => {
|
||||
match &func.args {
|
||||
sqlparser::ast::FunctionArguments::List(
|
||||
sqlparser::ast::FunctionArgumentList { args, .. },
|
||||
) => {
|
||||
for arg in args {
|
||||
if let sqlparser::ast::FunctionArg::Unnamed(
|
||||
sqlparser::ast::FunctionArgExpr::Expr(expr),
|
||||
) = arg
|
||||
{
|
||||
self.scan_expression_for_tombstone_references(
|
||||
expr,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {} // Andere FunctionArguments-Varianten ignorieren
|
||||
}
|
||||
}
|
||||
// CASE-Ausdrücke - KORRIGIERT
|
||||
Expr::Case {
|
||||
operand,
|
||||
conditions,
|
||||
else_result,
|
||||
..
|
||||
} => {
|
||||
if let Some(op) = operand {
|
||||
self.scan_expression_for_tombstone_references(op, crdt_tables, filtered_tables);
|
||||
}
|
||||
for case_when in conditions {
|
||||
self.scan_expression_for_tombstone_references(
|
||||
&case_when.condition,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
);
|
||||
self.scan_expression_for_tombstone_references(
|
||||
&case_when.result,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
);
|
||||
}
|
||||
if let Some(else_res) = else_result {
|
||||
self.scan_expression_for_tombstone_references(
|
||||
else_res,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Subqueries mit vollständiger Unterstützung
|
||||
Expr::Subquery(query) => {
|
||||
self.transform_query_recursive_for_tombstone_analysis(
|
||||
query,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
// EXISTS/NOT EXISTS Subqueries
|
||||
Expr::Exists { subquery, .. } => {
|
||||
self.transform_query_recursive_for_tombstone_analysis(
|
||||
subquery,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
// IN/NOT IN Subqueries
|
||||
Expr::InSubquery { expr, subquery, .. } => {
|
||||
self.scan_expression_for_tombstone_references(expr, crdt_tables, filtered_tables);
|
||||
self.transform_query_recursive_for_tombstone_analysis(
|
||||
subquery,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
// ANY/ALL Subqueries
|
||||
Expr::AnyOp { left, right, .. } | Expr::AllOp { left, right, .. } => {
|
||||
self.scan_expression_for_tombstone_references(left, crdt_tables, filtered_tables);
|
||||
self.scan_expression_for_tombstone_references(right, crdt_tables, filtered_tables);
|
||||
}
|
||||
// Andere Expression-Typen ignorieren wir für jetzt
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Analysiert eine Subquery und sammelt Tombstone-Referenzen
|
||||
fn transform_query_recursive_for_tombstone_analysis(
|
||||
&self,
|
||||
query: &sqlparser::ast::Query,
|
||||
crdt_tables: &[(ObjectName, Option<&str>)],
|
||||
filtered_tables: &mut HashSet<String>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
self.analyze_set_expr_for_tombstone_references(&query.body, crdt_tables, filtered_tables)
|
||||
}
|
||||
|
||||
/// Rekursiv analysiert SetExpr für Tombstone-Referenzen
|
||||
fn analyze_set_expr_for_tombstone_references(
|
||||
&self,
|
||||
set_expr: &SetExpr,
|
||||
crdt_tables: &[(ObjectName, Option<&str>)],
|
||||
filtered_tables: &mut HashSet<String>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
match set_expr {
|
||||
SetExpr::Select(select) => {
|
||||
// Analysiere WHERE-Klausel
|
||||
if let Some(where_clause) = &select.selection {
|
||||
self.scan_expression_for_tombstone_references(
|
||||
where_clause,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
);
|
||||
}
|
||||
|
||||
// Analysiere alle Projektionen (können auch Subqueries enthalten)
|
||||
for projection in &select.projection {
|
||||
match projection {
|
||||
SelectItem::UnnamedExpr(expr) | SelectItem::ExprWithAlias { expr, .. } => {
|
||||
self.scan_expression_for_tombstone_references(
|
||||
expr,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
);
|
||||
}
|
||||
_ => {} // Wildcard projections ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
// Analysiere GROUP BY
|
||||
match &select.group_by {
|
||||
sqlparser::ast::GroupByExpr::All(_) => {
|
||||
// GROUP BY ALL - keine Expressions zu analysieren
|
||||
}
|
||||
sqlparser::ast::GroupByExpr::Expressions(exprs, _) => {
|
||||
for group_expr in exprs {
|
||||
self.scan_expression_for_tombstone_references(
|
||||
group_expr,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analysiere HAVING
|
||||
if let Some(having) = &select.having {
|
||||
self.scan_expression_for_tombstone_references(
|
||||
having,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
);
|
||||
}
|
||||
}
|
||||
SetExpr::SetOperation { left, right, .. } => {
|
||||
self.analyze_set_expr_for_tombstone_references(left, crdt_tables, filtered_tables)?;
|
||||
self.analyze_set_expr_for_tombstone_references(
|
||||
right,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
)?;
|
||||
}
|
||||
SetExpr::Query(query) => {
|
||||
self.analyze_set_expr_for_tombstone_references(
|
||||
&query.body,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
)?;
|
||||
}
|
||||
SetExpr::Values(values) => {
|
||||
// Analysiere Values-Listen
|
||||
for row in &values.rows {
|
||||
for expr in row {
|
||||
self.scan_expression_for_tombstone_references(
|
||||
expr,
|
||||
crdt_tables,
|
||||
filtered_tables,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {} // Andere Varianten
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transformiert INSERT-Statements (fügt HLC-Timestamp hinzu)
|
||||
fn transform_insert(
|
||||
&self,
|
||||
insert_stmt: &mut Insert,
|
||||
timestamp: &Timestamp,
|
||||
) -> Result<(), DatabaseError> {
|
||||
insert_stmt
|
||||
.columns
|
||||
.push(Ident::new(self.columns.hlc_timestamp));
|
||||
|
||||
match insert_stmt.source.as_mut() {
|
||||
Some(query) => match &mut *query.body {
|
||||
SetExpr::Values(values) => {
|
||||
for row in &mut values.rows {
|
||||
row.push(Expr::Value(
|
||||
Value::SingleQuotedString(timestamp.to_string()).into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
SetExpr::Select(select) => {
|
||||
let hlc_expr =
|
||||
Expr::Value(Value::SingleQuotedString(timestamp.to_string()).into());
|
||||
select.projection.push(SelectItem::UnnamedExpr(hlc_expr));
|
||||
}
|
||||
_ => {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
sql: insert_stmt.to_string(),
|
||||
reason: "INSERT with unsupported source type".to_string(),
|
||||
});
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
reason: "INSERT statement has no source".to_string(),
|
||||
sql: insert_stmt.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transformiert DELETE zu UPDATE (soft delete)
|
||||
fn transform_delete_to_update(
|
||||
&self,
|
||||
stmt: &mut Statement,
|
||||
timestamp: &Timestamp,
|
||||
) -> Result<(), DatabaseError> {
|
||||
if let Statement::Delete(del_stmt) = stmt {
|
||||
let table_to_update = match &del_stmt.from {
|
||||
sqlparser::ast::FromTable::WithFromKeyword(from)
|
||||
| sqlparser::ast::FromTable::WithoutKeyword(from) => {
|
||||
if from.len() == 1 {
|
||||
from[0].clone()
|
||||
} else {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
reason: "DELETE with multiple tables not supported".to_string(),
|
||||
sql: stmt.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let assignments = vec![
|
||||
self.columns.create_tombstone_assignment(),
|
||||
self.columns.create_hlc_assignment(timestamp),
|
||||
];
|
||||
|
||||
*stmt = Statement::Update {
|
||||
table: table_to_update,
|
||||
assignments,
|
||||
from: None,
|
||||
selection: del_stmt.selection.clone(),
|
||||
returning: None,
|
||||
or: None,
|
||||
limit: None,
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellennamen aus DELETE-Statement
|
||||
fn extract_table_name_from_delete(
|
||||
&self,
|
||||
del_stmt: &sqlparser::ast::Delete,
|
||||
) -> Option<ObjectName> {
|
||||
let tables = match &del_stmt.from {
|
||||
sqlparser::ast::FromTable::WithFromKeyword(from)
|
||||
| sqlparser::ast::FromTable::WithoutKeyword(from) => from,
|
||||
};
|
||||
|
||||
if tables.len() == 1 {
|
||||
if let TableFactor::Table { name, .. } = &tables[0].relation {
|
||||
Some(name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,20 +9,17 @@ use ts_rs::TS;
|
||||
// Der "z_"-Präfix soll sicherstellen, dass diese Trigger als Letzte ausgeführt werden
|
||||
const INSERT_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_insert";
|
||||
const UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update";
|
||||
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";
|
||||
|
||||
/// Name der custom UUID-Generierungs-Funktion (registriert in database::core::open_and_init_db)
|
||||
pub const UUID_FUNCTION_NAME: &str = "gen_uuid";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CrdtSetupError {
|
||||
/// Kapselt einen Fehler, der von der rusqlite-Bibliothek kommt.
|
||||
DatabaseError(rusqlite::Error),
|
||||
/// Die Tabelle hat keine Tombstone-Spalte, was eine CRDT-Voraussetzung ist.
|
||||
TombstoneColumnMissing {
|
||||
table_name: String,
|
||||
column_name: String,
|
||||
},
|
||||
HlcColumnMissing {
|
||||
table_name: String,
|
||||
column_name: String,
|
||||
@ -36,14 +33,6 @@ impl Display for CrdtSetupError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
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 {
|
||||
table_name,
|
||||
column_name,
|
||||
@ -78,14 +67,14 @@ pub enum TriggerSetupResult {
|
||||
TableNotFound,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ColumnInfo {
|
||||
name: String,
|
||||
is_pk: bool,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ColumnInfo {
|
||||
pub name: String,
|
||||
pub is_pk: bool,
|
||||
}
|
||||
|
||||
impl ColumnInfo {
|
||||
fn from_row(row: &Row) -> RusqliteResult<Self> {
|
||||
pub fn from_row(row: &Row) -> RusqliteResult<Self> {
|
||||
Ok(ColumnInfo {
|
||||
name: row.get("name")?,
|
||||
is_pk: row.get::<_, i64>("pk")? > 0,
|
||||
@ -94,7 +83,8 @@ impl ColumnInfo {
|
||||
}
|
||||
|
||||
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.
|
||||
@ -109,13 +99,6 @@ pub fn setup_triggers_for_table(
|
||||
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) {
|
||||
return Err(CrdtSetupError::HlcColumnMissing {
|
||||
table_name: table_name.to_string(),
|
||||
@ -137,12 +120,13 @@ pub fn setup_triggers_for_table(
|
||||
|
||||
let cols_to_track: Vec<String> = columns
|
||||
.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())
|
||||
.collect();
|
||||
|
||||
let insert_trigger_sql = generate_insert_trigger_sql(table_name, &pks, &cols_to_track);
|
||||
let update_trigger_sql = generate_update_trigger_sql(table_name, &pks, &cols_to_track);
|
||||
let delete_trigger_sql = generate_delete_trigger_sql(table_name, &pks, &cols_to_track);
|
||||
|
||||
if recreate {
|
||||
drop_triggers_for_table(&tx, table_name)?;
|
||||
@ -150,12 +134,13 @@ pub fn setup_triggers_for_table(
|
||||
|
||||
tx.execute_batch(&insert_trigger_sql)?;
|
||||
tx.execute_batch(&update_trigger_sql)?;
|
||||
tx.execute_batch(&delete_trigger_sql)?;
|
||||
|
||||
Ok(TriggerSetupResult::Success)
|
||||
}
|
||||
|
||||
/// Holt das Schema für eine gegebene Tabelle.
|
||||
fn get_table_schema(conn: &Connection, table_name: &str) -> RusqliteResult<Vec<ColumnInfo>> {
|
||||
pub fn get_table_schema(conn: &Connection, table_name: &str) -> RusqliteResult<Vec<ColumnInfo>> {
|
||||
if !is_safe_identifier(table_name) {
|
||||
return Err(rusqlite::Error::InvalidParameterName(format!(
|
||||
"Invalid or unsafe table name provided: {}",
|
||||
@ -170,6 +155,8 @@ fn get_table_schema(conn: &Connection, table_name: &str) -> RusqliteResult<Vec<C
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
// get_foreign_key_columns() removed - not needed with hard deletes (no ON CONFLICT logic)
|
||||
|
||||
pub fn drop_triggers_for_table(
|
||||
tx: &Transaction, // Arbeitet direkt auf einer Transaktion
|
||||
table_name: &str,
|
||||
@ -186,8 +173,13 @@ pub fn drop_triggers_for_table(
|
||||
drop_trigger_sql(INSERT_TRIGGER_TPL.replace("{TABLE_NAME}", table_name));
|
||||
let drop_update_trigger_sql =
|
||||
drop_trigger_sql(UPDATE_TRIGGER_TPL.replace("{TABLE_NAME}", table_name));
|
||||
let drop_delete_trigger_sql =
|
||||
drop_trigger_sql(DELETE_TRIGGER_TPL.replace("{TABLE_NAME}", table_name));
|
||||
|
||||
let sql_batch = format!("{}\n{}", drop_insert_trigger_sql, drop_update_trigger_sql);
|
||||
let sql_batch = format!(
|
||||
"{}\n{}\n{}",
|
||||
drop_insert_trigger_sql, drop_update_trigger_sql, drop_delete_trigger_sql
|
||||
);
|
||||
|
||||
tx.execute_batch(&sql_batch)?;
|
||||
Ok(())
|
||||
@ -259,9 +251,11 @@ fn generate_insert_trigger_sql(table_name: &str, pks: &[String], cols: &[String]
|
||||
let column_inserts = if cols.is_empty() {
|
||||
// Nur PKs -> einfacher Insert ins Log
|
||||
format!(
|
||||
"INSERT INTO {log_table} (hlc_timestamp, op_type, table_name, row_pks)
|
||||
VALUES (hlc_new_timestamp(), 'INSERT', '{table}', json_object({pk_payload}));",
|
||||
"INSERT INTO {log_table} (id, haex_timestamp, op_type, table_name, row_pks)
|
||||
VALUES ({uuid_fn}(), NEW.\"{hlc_col}\", 'INSERT', '{table}', json_object({pk_payload}));",
|
||||
log_table = TABLE_CRDT_LOGS,
|
||||
uuid_fn = UUID_FUNCTION_NAME,
|
||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload
|
||||
)
|
||||
@ -269,9 +263,11 @@ fn generate_insert_trigger_sql(table_name: &str, pks: &[String], cols: &[String]
|
||||
cols.iter().fold(String::new(), |mut acc, col| {
|
||||
writeln!(
|
||||
&mut acc,
|
||||
"INSERT INTO {log_table} (hlc_timestamp, op_type, table_name, row_pks, column_name, new_value)
|
||||
VALUES (hlc_new_timestamp(), 'INSERT', '{table}', json_object({pk_payload}), '{column}', json_object('value', NEW.\"{column}\"));",
|
||||
"INSERT INTO {log_table} (id, haex_timestamp, op_type, table_name, row_pks, column_name, new_value)
|
||||
VALUES ({uuid_fn}(), NEW.\"{hlc_col}\", 'INSERT', '{table}', json_object({pk_payload}), '{column}', json_object('value', NEW.\"{column}\"));",
|
||||
log_table = TABLE_CRDT_LOGS,
|
||||
uuid_fn = UUID_FUNCTION_NAME,
|
||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload,
|
||||
column = col
|
||||
@ -312,11 +308,13 @@ fn generate_update_trigger_sql(table_name: &str, pks: &[String], cols: &[String]
|
||||
for col in cols {
|
||||
writeln!(
|
||||
&mut body,
|
||||
"INSERT INTO {log_table} (hlc_timestamp, op_type, table_name, row_pks, column_name, new_value, old_value)
|
||||
SELECT hlc_new_timestamp(), 'UPDATE', '{table}', json_object({pk_payload}), '{column}',
|
||||
"INSERT INTO {log_table} (id, haex_timestamp, op_type, table_name, row_pks, column_name, new_value, old_value)
|
||||
SELECT {uuid_fn}(), NEW.\"{hlc_col}\", 'UPDATE', '{table}', json_object({pk_payload}), '{column}',
|
||||
json_object('value', NEW.\"{column}\"), json_object('value', OLD.\"{column}\")
|
||||
WHERE NEW.\"{column}\" IS NOT OLD.\"{column}\";",
|
||||
log_table = TABLE_CRDT_LOGS,
|
||||
uuid_fn = UUID_FUNCTION_NAME,
|
||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload,
|
||||
column = col
|
||||
@ -324,18 +322,7 @@ fn generate_update_trigger_sql(table_name: &str, pks: &[String], cols: &[String]
|
||||
}
|
||||
}
|
||||
|
||||
// Soft-delete loggen
|
||||
writeln!(
|
||||
&mut body,
|
||||
"INSERT INTO {log_table} (hlc_timestamp, op_type, table_name, row_pks)
|
||||
SELECT hlc_new_timestamp(), 'DELETE', '{table}', json_object({pk_payload})
|
||||
WHERE NEW.\"{tombstone_col}\" = 1 AND OLD.\"{tombstone_col}\" = 0;",
|
||||
log_table = TABLE_CRDT_LOGS,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload,
|
||||
tombstone_col = TOMBSTONE_COLUMN
|
||||
)
|
||||
.unwrap();
|
||||
// Soft-delete Logging entfernt - wir nutzen jetzt Hard Deletes mit eigenem BEFORE DELETE Trigger
|
||||
|
||||
let trigger_name = UPDATE_TRIGGER_TPL.replace("{TABLE_NAME}", table_name);
|
||||
|
||||
@ -348,3 +335,57 @@ fn generate_update_trigger_sql(table_name: &str, pks: &[String], cols: &[String]
|
||||
END;"
|
||||
)
|
||||
}
|
||||
|
||||
/// Generiert das SQL für den BEFORE DELETE-Trigger.
|
||||
/// WICHTIG: BEFORE DELETE damit die Daten noch verfügbar sind!
|
||||
fn generate_delete_trigger_sql(table_name: &str, pks: &[String], cols: &[String]) -> String {
|
||||
let pk_json_payload = pks
|
||||
.iter()
|
||||
.map(|pk| format!("'{}', OLD.\"{}\"", pk, pk))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let mut body = String::new();
|
||||
|
||||
// Alle Spaltenwerte speichern für mögliche Wiederherstellung
|
||||
if !cols.is_empty() {
|
||||
for col in cols {
|
||||
writeln!(
|
||||
&mut body,
|
||||
"INSERT INTO {log_table} (id, haex_timestamp, op_type, table_name, row_pks, column_name, old_value)
|
||||
VALUES ({uuid_fn}(), OLD.\"{hlc_col}\", 'DELETE', '{table}', json_object({pk_payload}), '{column}',
|
||||
json_object('value', OLD.\"{column}\"));",
|
||||
log_table = TABLE_CRDT_LOGS,
|
||||
uuid_fn = UUID_FUNCTION_NAME,
|
||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload,
|
||||
column = col
|
||||
).unwrap();
|
||||
}
|
||||
} else {
|
||||
// Nur PKs -> minimales Delete Log
|
||||
writeln!(
|
||||
&mut body,
|
||||
"INSERT INTO {log_table} (id, haex_timestamp, op_type, table_name, row_pks)
|
||||
VALUES ({uuid_fn}(), OLD.\"{hlc_col}\", 'DELETE', '{table}', json_object({pk_payload}));",
|
||||
log_table = TABLE_CRDT_LOGS,
|
||||
uuid_fn = UUID_FUNCTION_NAME,
|
||||
hlc_col = HLC_TIMESTAMP_COLUMN,
|
||||
table = table_name,
|
||||
pk_payload = pk_json_payload
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let trigger_name = DELETE_TRIGGER_TPL.replace("{TABLE_NAME}", table_name);
|
||||
|
||||
format!(
|
||||
"CREATE TRIGGER IF NOT EXISTS \"{trigger_name}\"
|
||||
BEFORE DELETE ON \"{table_name}\"
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
{body}
|
||||
END;"
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
// src-tauri/src/database/core.rs
|
||||
|
||||
use crate::crdt::trigger::UUID_FUNCTION_NAME;
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::database::DbConnection;
|
||||
use crate::extension::database::executor::SqlExecutor;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use rusqlite::functions::FunctionFlags;
|
||||
use rusqlite::types::Value as SqlValue;
|
||||
use rusqlite::{
|
||||
types::{Value as RusqliteValue, ValueRef},
|
||||
Connection, OpenFlags, ToSql,
|
||||
};
|
||||
use serde_json::Value as JsonValue;
|
||||
use sqlparser::ast::{Query, Select, SetExpr, Statement, TableFactor, TableObject};
|
||||
use sqlparser::ast::{Expr, Query, Select, SetExpr, Statement, TableFactor, TableObject};
|
||||
use sqlparser::dialect::SQLiteDialect;
|
||||
use sqlparser::parser::Parser;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
|
||||
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(),
|
||||
})?;
|
||||
|
||||
// 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
|
||||
.query_row("PRAGMA journal_mode=WAL;", [], |row| row.get(0))
|
||||
.map_err(|e| DatabaseError::PragmaError {
|
||||
@ -73,12 +89,29 @@ pub fn parse_single_statement(sql: &str) -> Result<Statement, DatabaseError> {
|
||||
/// Utility für SQL-Parsing - parst mehrere SQL-Statements
|
||||
pub fn parse_sql_statements(sql: &str) -> Result<Vec<Statement>, DatabaseError> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Prüft ob ein Statement ein RETURNING Clause hat (AST-basiert, sicher)
|
||||
pub fn statement_has_returning(statement: &Statement) -> bool {
|
||||
match statement {
|
||||
Statement::Insert(insert) => insert.returning.is_some(),
|
||||
Statement::Update { returning, .. } => returning.is_some(),
|
||||
Statement::Delete(delete) => delete.returning.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ValueConverter;
|
||||
|
||||
impl ValueConverter {
|
||||
@ -116,11 +149,30 @@ impl ValueConverter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute SQL mit CRDT-Transformation (für Drizzle-Integration)
|
||||
/// Diese Funktion sollte von Drizzle verwendet werden, um CRDT-Support zu erhalten
|
||||
pub fn execute_with_crdt(
|
||||
sql: String,
|
||||
params: Vec<JsonValue>,
|
||||
connection: &DbConnection,
|
||||
hlc_service: &std::sync::MutexGuard<crate::crdt::hlc::HlcService>,
|
||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||
with_connection(connection, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
let _modified_tables = SqlExecutor::execute_internal(&tx, hlc_service, &sql, ¶ms)?;
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
|
||||
// Für Drizzle: gebe leeres Array zurück (wie bei execute ohne RETURNING)
|
||||
Ok(vec![])
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute SQL OHNE CRDT-Transformation (für spezielle Fälle)
|
||||
pub fn execute(
|
||||
sql: String,
|
||||
params: Vec<JsonValue>,
|
||||
connection: &DbConnection,
|
||||
) -> Result<usize, DatabaseError> {
|
||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||
// Konvertiere Parameter
|
||||
let params_converted: Vec<RusqliteValue> = params
|
||||
.iter()
|
||||
@ -129,19 +181,33 @@ pub fn execute(
|
||||
let params_sql: Vec<&dyn ToSql> = params_converted.iter().map(|v| v as &dyn ToSql).collect();
|
||||
|
||||
with_connection(connection, |conn| {
|
||||
let affected_rows = conn.execute(&sql, ¶ms_sql[..]).map_err(|e| {
|
||||
// "Lazy Parsing": Extrahiere den Tabellennamen nur, wenn ein Fehler auftritt,
|
||||
// um den Overhead bei erfolgreichen Operationen zu vermeiden.
|
||||
let table_name = extract_primary_table_name_from_sql(&sql).unwrap_or(None);
|
||||
if sql.to_uppercase().contains("RETURNING") {
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let num_columns = stmt.column_count();
|
||||
let mut rows = stmt.query(¶ms_sql[..])?;
|
||||
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
||||
|
||||
DatabaseError::ExecutionError {
|
||||
sql: sql.clone(),
|
||||
reason: e.to_string(),
|
||||
table: table_name,
|
||||
while let Some(row) = rows.next()? {
|
||||
let mut row_values: Vec<JsonValue> = Vec::with_capacity(num_columns);
|
||||
for i in 0..num_columns {
|
||||
let value_ref = row.get_ref(i)?;
|
||||
let json_val = convert_value_ref_to_json(value_ref)?;
|
||||
row_values.push(json_val);
|
||||
}
|
||||
result_vec.push(row_values);
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(affected_rows)
|
||||
Ok(result_vec)
|
||||
} else {
|
||||
conn.execute(&sql, ¶ms_sql[..]).map_err(|e| {
|
||||
let table_name = extract_primary_table_name_from_sql(&sql).unwrap_or(None);
|
||||
DatabaseError::ExecutionError {
|
||||
sql: sql.clone(),
|
||||
reason: e.to_string(),
|
||||
table: table_name,
|
||||
}
|
||||
})?;
|
||||
Ok(vec![])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -149,7 +215,7 @@ pub fn select(
|
||||
sql: String,
|
||||
params: Vec<JsonValue>,
|
||||
connection: &DbConnection,
|
||||
) -> Result<Vec<HashMap<String, JsonValue>>, DatabaseError> {
|
||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||
// Validiere SQL-Statement
|
||||
let statement = parse_single_statement(&sql)?;
|
||||
|
||||
@ -169,61 +235,36 @@ pub fn select(
|
||||
let params_sql: Vec<&dyn ToSql> = params_converted.iter().map(|v| v as &dyn ToSql).collect();
|
||||
|
||||
with_connection(connection, |conn| {
|
||||
let mut stmt = conn
|
||||
.prepare(&sql)
|
||||
.map_err(|e| DatabaseError::PrepareError {
|
||||
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 mut rows = stmt
|
||||
.query(¶ms_sql[..])
|
||||
.map_err(|e| DatabaseError::QueryError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
let mut result_vec: Vec<HashMap<String, JsonValue>> = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next().map_err(|e| DatabaseError::RowProcessingError {
|
||||
reason: format!("Row iteration error: {}", e),
|
||||
})? {
|
||||
let mut row_map: HashMap<String, JsonValue> = HashMap::with_capacity(num_columns);
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let num_columns = stmt.column_count();
|
||||
let mut rows = stmt.query(¶ms_sql[..])?;
|
||||
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
let mut row_values: Vec<JsonValue> = Vec::with_capacity(num_columns);
|
||||
for i in 0..num_columns {
|
||||
let col_name = &column_names[i];
|
||||
|
||||
/* println!(
|
||||
"--- Processing Column --- Index: {}, Name: '{}'",
|
||||
i, col_name
|
||||
); */
|
||||
|
||||
let value_ref = row
|
||||
.get_ref(i)
|
||||
.map_err(|e| DatabaseError::RowProcessingError {
|
||||
reason: format!("Failed to get column {} ('{}'): {}", i, col_name, e),
|
||||
})?;
|
||||
|
||||
let value_ref = row.get_ref(i)?;
|
||||
let json_val = convert_value_ref_to_json(value_ref)?;
|
||||
|
||||
//println!("Column: {} = {}", column_names[i], json_val);
|
||||
|
||||
row_map.insert(col_name.clone(), json_val);
|
||||
row_values.push(json_val);
|
||||
}
|
||||
result_vec.push(row_map);
|
||||
result_vec.push(row_values);
|
||||
}
|
||||
|
||||
Ok(result_vec)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn select_with_crdt(
|
||||
sql: String,
|
||||
params: Vec<JsonValue>,
|
||||
connection: &DbConnection,
|
||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||
with_connection(&connection, |conn| {
|
||||
SqlExecutor::query_select(conn, &sql, ¶ms)
|
||||
})
|
||||
}
|
||||
|
||||
/// Konvertiert rusqlite ValueRef zu JSON
|
||||
fn convert_value_ref_to_json(value_ref: ValueRef) -> Result<JsonValue, DatabaseError> {
|
||||
pub fn convert_value_ref_to_json(value_ref: ValueRef) -> Result<JsonValue, DatabaseError> {
|
||||
let json_val = match value_ref {
|
||||
ValueRef::Null => JsonValue::Null,
|
||||
ValueRef::Integer(i) => JsonValue::Number(i.into()),
|
||||
@ -327,8 +368,42 @@ fn extract_tables_from_select(select: &Select, tables: &mut Vec<String>) {
|
||||
extract_tables_from_table_factor(&join.relation, tables);
|
||||
}
|
||||
}
|
||||
if let Some(selection) = &select.selection {
|
||||
extract_tables_from_expr_recursive(selection, tables);
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_tables_from_expr_recursive(expr: &Expr, tables: &mut Vec<String>) {
|
||||
match expr {
|
||||
// This is the key: we found a subquery!
|
||||
Expr::Subquery(subquery) => {
|
||||
extract_tables_from_query_recursive(subquery, tables);
|
||||
}
|
||||
// These expressions can contain other expressions
|
||||
Expr::BinaryOp { left, right, .. } => {
|
||||
extract_tables_from_expr_recursive(left, tables);
|
||||
extract_tables_from_expr_recursive(right, tables);
|
||||
}
|
||||
Expr::UnaryOp { expr, .. } => {
|
||||
extract_tables_from_expr_recursive(expr, tables);
|
||||
}
|
||||
Expr::InSubquery { expr, subquery, .. } => {
|
||||
extract_tables_from_expr_recursive(expr, tables);
|
||||
extract_tables_from_query_recursive(subquery, tables);
|
||||
}
|
||||
Expr::Between {
|
||||
expr, low, high, ..
|
||||
} => {
|
||||
extract_tables_from_expr_recursive(expr, tables);
|
||||
extract_tables_from_expr_recursive(low, tables);
|
||||
extract_tables_from_expr_recursive(high, tables);
|
||||
}
|
||||
// ... other expression types can be added here if needed
|
||||
_ => {
|
||||
// Other expressions (like literals, column names, etc.) don't contain tables.
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Extrahiert Tabellennamen aus TableFactor-Strukturen
|
||||
fn extract_tables_from_table_factor(table_factor: &TableFactor, tables: &mut Vec<String>) {
|
||||
match table_factor {
|
||||
|
||||
240
src-tauri/src/database/generated.rs
Normal file
240
src-tauri/src/database/generated.rs
Normal file
@ -0,0 +1,240 @@
|
||||
// Auto-generated from Drizzle schema
|
||||
// DO NOT EDIT MANUALLY
|
||||
// Run 'pnpm generate:rust-types' to regenerate
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HaexSettings {
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub key: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "type")]
|
||||
pub r#type: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub haex_timestamp: Option<String>,
|
||||
}
|
||||
|
||||
impl HaexSettings {
|
||||
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self {
|
||||
id: row.get(0)?,
|
||||
key: row.get(1)?,
|
||||
r#type: row.get(2)?,
|
||||
value: row.get(3)?,
|
||||
haex_timestamp: row.get(4)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HaexExtensions {
|
||||
pub id: String,
|
||||
pub public_key: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub author: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub entry: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub homepage: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<String>,
|
||||
pub signature: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub haex_timestamp: Option<String>,
|
||||
}
|
||||
|
||||
impl HaexExtensions {
|
||||
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self {
|
||||
id: row.get(0)?,
|
||||
public_key: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
version: row.get(3)?,
|
||||
author: row.get(4)?,
|
||||
description: row.get(5)?,
|
||||
entry: row.get(6)?,
|
||||
homepage: row.get(7)?,
|
||||
enabled: row.get(8)?,
|
||||
icon: row.get(9)?,
|
||||
signature: row.get(10)?,
|
||||
haex_timestamp: row.get(11)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HaexExtensionPermissions {
|
||||
pub id: String,
|
||||
pub extension_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resource_type: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub action: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub constraints: Option<String>,
|
||||
pub status: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub haex_timestamp: Option<String>,
|
||||
}
|
||||
|
||||
impl HaexExtensionPermissions {
|
||||
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self {
|
||||
id: row.get(0)?,
|
||||
extension_id: row.get(1)?,
|
||||
resource_type: row.get(2)?,
|
||||
action: row.get(3)?,
|
||||
target: row.get(4)?,
|
||||
constraints: row.get(5)?,
|
||||
status: row.get(6)?,
|
||||
created_at: row.get(7)?,
|
||||
updated_at: row.get(8)?,
|
||||
haex_timestamp: row.get(9)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HaexCrdtLogs {
|
||||
pub id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub haex_timestamp: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub table_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub row_pks: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub op_type: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub column_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub new_value: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub old_value: Option<String>,
|
||||
}
|
||||
|
||||
impl HaexCrdtLogs {
|
||||
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self {
|
||||
id: row.get(0)?,
|
||||
haex_timestamp: row.get(1)?,
|
||||
table_name: row.get(2)?,
|
||||
row_pks: row.get(3)?,
|
||||
op_type: row.get(4)?,
|
||||
column_name: row.get(5)?,
|
||||
new_value: row.get(6)?,
|
||||
old_value: row.get(7)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HaexCrdtSnapshots {
|
||||
pub snapshot_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub epoch_hlc: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub location_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub file_size_bytes: Option<i64>,
|
||||
}
|
||||
|
||||
impl HaexCrdtSnapshots {
|
||||
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self {
|
||||
snapshot_id: row.get(0)?,
|
||||
created: row.get(1)?,
|
||||
epoch_hlc: row.get(2)?,
|
||||
location_url: row.get(3)?,
|
||||
file_size_bytes: row.get(4)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HaexCrdtConfigs {
|
||||
pub key: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
impl HaexCrdtConfigs {
|
||||
pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
|
||||
Ok(Self {
|
||||
key: row.get(0)?,
|
||||
value: row.get(1)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
@ -2,24 +2,28 @@
|
||||
|
||||
pub mod core;
|
||||
pub mod error;
|
||||
pub mod generated;
|
||||
pub mod init;
|
||||
|
||||
use crate::crdt::hlc::HlcService;
|
||||
use crate::database::core::execute_with_crdt;
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::extension::database::executor::SqlExecutor;
|
||||
use crate::table_names::{TABLE_CRDT_CONFIGS, TABLE_SETTINGS};
|
||||
use crate::AppState;
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use std::{fs, sync::Arc};
|
||||
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
|
||||
use tauri_plugin_fs::FsExt;
|
||||
use thiserror::Error;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use trash;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::crdt::hlc::HlcService;
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::table_names::TABLE_CRDT_CONFIGS;
|
||||
use crate::AppState;
|
||||
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
|
||||
|
||||
const VAULT_EXTENSION: &str = ".db";
|
||||
@ -30,7 +34,7 @@ pub fn sql_select(
|
||||
sql: String,
|
||||
params: Vec<JsonValue>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<HashMap<String, JsonValue>>, DatabaseError> {
|
||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||
core::select(sql, params, &state.db)
|
||||
}
|
||||
|
||||
@ -39,10 +43,50 @@ pub fn sql_execute(
|
||||
sql: String,
|
||||
params: Vec<JsonValue>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<usize, DatabaseError> {
|
||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||
core::execute(sql, params, &state.db)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sql_select_with_crdt(
|
||||
sql: String,
|
||||
params: Vec<JsonValue>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||
core::select_with_crdt(sql, params, &state.db)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sql_execute_with_crdt(
|
||||
sql: String,
|
||||
params: Vec<JsonValue>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||
let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
core::execute_with_crdt(sql, params, &state.db, &hlc_service)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sql_query_with_crdt(
|
||||
sql: String,
|
||||
params: Vec<JsonValue>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<Vec<JsonValue>>, DatabaseError> {
|
||||
let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
core::with_connection(&state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
let (_modified_tables, result) =
|
||||
SqlExecutor::query_internal(&tx, &hlc_service, &sql, ¶ms)?;
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolves a database name to the full vault path
|
||||
fn get_vault_path(app_handle: &AppHandle, vault_name: &str) -> Result<String, DatabaseError> {
|
||||
// Sicherstellen, dass der Name eine .db Endung hat
|
||||
@ -91,7 +135,6 @@ pub fn get_vaults_directory(app_handle: &AppHandle) -> Result<String, DatabaseEr
|
||||
Ok(vaults_dir.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
//#[serde(tag = "type", content = "details")]
|
||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -165,15 +208,70 @@ pub fn list_vaults(app_handle: AppHandle) -> Result<Vec<VaultInfo>, DatabaseErro
|
||||
|
||||
/// Checks if a vault with the given name exists
|
||||
#[tauri::command]
|
||||
pub fn vault_exists(app_handle: AppHandle, db_name: String) -> Result<bool, DatabaseError> {
|
||||
let vault_path = get_vault_path(&app_handle, &db_name)?;
|
||||
pub fn vault_exists(app_handle: AppHandle, vault_name: String) -> Result<bool, DatabaseError> {
|
||||
let vault_path = get_vault_path(&app_handle, &vault_name)?;
|
||||
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 delete_vault(app_handle: AppHandle, db_name: String) -> Result<String, DatabaseError> {
|
||||
let vault_path = get_vault_path(&app_handle, &db_name)?;
|
||||
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]
|
||||
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_shm_path = format!("{}-shm", vault_path);
|
||||
let vault_wal_path = format!("{}-wal", vault_path);
|
||||
|
||||
if !Path::new(&vault_path).exists() {
|
||||
return Err(DatabaseError::IoError {
|
||||
@ -182,12 +280,26 @@ pub fn delete_vault(app_handle: AppHandle, db_name: String) -> Result<String, Da
|
||||
});
|
||||
}
|
||||
|
||||
if Path::new(&vault_shm_path).exists() {
|
||||
fs::remove_file(&vault_shm_path).map_err(|e| DatabaseError::IoError {
|
||||
path: vault_shm_path.clone(),
|
||||
reason: format!("Failed to delete vault: {}", e),
|
||||
})?;
|
||||
}
|
||||
|
||||
if Path::new(&vault_wal_path).exists() {
|
||||
fs::remove_file(&vault_wal_path).map_err(|e| DatabaseError::IoError {
|
||||
path: vault_wal_path.clone(),
|
||||
reason: format!("Failed to delete vault: {}", e),
|
||||
})?;
|
||||
}
|
||||
|
||||
fs::remove_file(&vault_path).map_err(|e| DatabaseError::IoError {
|
||||
path: vault_path.clone(),
|
||||
reason: format!("Failed to delete vault: {}", e),
|
||||
})?;
|
||||
|
||||
Ok(format!("Vault '{}' successfully deleted", db_name))
|
||||
Ok(format!("Vault '{}' successfully deleted", vault_name))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -337,9 +449,6 @@ pub fn open_encrypted_database(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, DatabaseError> {
|
||||
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);
|
||||
|
||||
if !Path::new(&vault_path).exists() {
|
||||
@ -362,9 +471,12 @@ fn initialize_session(
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
// 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| {
|
||||
// We convert the HlcError into a DatabaseError
|
||||
DatabaseError::ExecutionError {
|
||||
@ -374,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 {
|
||||
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);
|
||||
drop(db_guard);
|
||||
|
||||
let mut hlc_guard = state.hlc.lock().map_err(|e| DatabaseError::LockError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
*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(())
|
||||
}
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
// src-tauri/src/extension/core/manager.rs
|
||||
|
||||
use crate::database::core::with_connection;
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, ExtensionPreview};
|
||||
use crate::extension::core::types::{copy_directory, Extension, ExtensionSource};
|
||||
use crate::extension::core::ExtensionPermissions;
|
||||
use crate::extension::crypto::ExtensionCrypto;
|
||||
use crate::extension::database::executor::SqlExecutor;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::manager::PermissionManager;
|
||||
use crate::extension::permissions::types::{ExtensionPermission, PermissionStatus};
|
||||
use crate::extension::permissions::types::ExtensionPermission;
|
||||
use crate::table_names::{TABLE_EXTENSIONS, TABLE_EXTENSION_PERMISSIONS};
|
||||
use crate::AppState;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime};
|
||||
@ -22,11 +27,38 @@ pub struct CachedPermission {
|
||||
pub ttl: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MissingExtension {
|
||||
pub id: String,
|
||||
pub public_key: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
struct ExtensionDataFromDb {
|
||||
id: String,
|
||||
manifest: ExtensionManifest,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionManager {
|
||||
pub production_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub dev_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
|
||||
pub missing_extensions: Mutex<Vec<MissingExtension>>,
|
||||
}
|
||||
|
||||
struct ExtractedExtension {
|
||||
temp_dir: PathBuf,
|
||||
manifest: ExtensionManifest,
|
||||
content_hash: String,
|
||||
}
|
||||
|
||||
impl Drop for ExtractedExtension {
|
||||
fn drop(&mut self) {
|
||||
std::fs::remove_dir_all(&self.temp_dir).ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionManager {
|
||||
@ -34,6 +66,195 @@ impl ExtensionManager {
|
||||
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
|
||||
fn extract_and_validate_extension(
|
||||
bytes: Vec<u8>,
|
||||
temp_prefix: &str,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<ExtractedExtension, ExtensionError> {
|
||||
// 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)
|
||||
.map_err(|e| ExtensionError::filesystem_with_path(temp.display().to_string(), 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 {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
}
|
||||
})?;
|
||||
|
||||
archive
|
||||
.extract(&temp)
|
||||
.map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
// Clean up temporary ZIP file
|
||||
let _ = fs::remove_file(&zip_file_path);
|
||||
|
||||
// Read haextension_dir from config if it exists, otherwise use default
|
||||
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()
|
||||
};
|
||||
|
||||
// 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 =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let mut manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
// 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 {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(ExtractedExtension {
|
||||
temp_dir: actual_dir,
|
||||
manifest,
|
||||
content_hash,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_base_extension_dir(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
@ -45,18 +266,26 @@ impl ExtensionManager {
|
||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
|
||||
})?
|
||||
.join("extensions");
|
||||
|
||||
// Sicherstellen, dass das Basisverzeichnis existiert
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(&path)
|
||||
.map_err(|e| ExtensionError::filesystem_with_path(path.display().to_string(), e))?;
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn get_extension_dir(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
extension_id: &str,
|
||||
public_key: &str,
|
||||
extension_name: &str,
|
||||
extension_version: &str,
|
||||
) -> Result<PathBuf, ExtensionError> {
|
||||
let specific_extension_dir = self
|
||||
.get_base_extension_dir(app_handle)?
|
||||
.join(extension_id)
|
||||
.join(public_key)
|
||||
.join(extension_name)
|
||||
.join(extension_version);
|
||||
|
||||
Ok(specific_extension_dir)
|
||||
@ -110,42 +339,176 @@ impl ExtensionManager {
|
||||
prod_extensions.get(extension_id).cloned()
|
||||
}
|
||||
|
||||
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
|
||||
/// Find extension ID by public_key and name (checks dev extensions first, then production)
|
||||
fn find_extension_id_by_public_key_and_name(
|
||||
&self,
|
||||
public_key: &str,
|
||||
name: &str,
|
||||
) -> Result<Option<(String, Extension)>, ExtensionError> {
|
||||
// 1. Check dev extensions first (higher priority)
|
||||
let dev_extensions =
|
||||
self.dev_extensions
|
||||
.lock()
|
||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
for (id, ext) in dev_extensions.iter() {
|
||||
if ext.manifest.public_key == public_key && ext.manifest.name == name {
|
||||
return Ok(Some((id.clone(), ext.clone())));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check production extensions
|
||||
let prod_extensions =
|
||||
self.production_extensions
|
||||
.lock()
|
||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
for (id, ext) in prod_extensions.iter() {
|
||||
if ext.manifest.public_key == public_key && ext.manifest.name == name {
|
||||
return Ok(Some((id.clone(), ext.clone())));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Get extension by public_key and name (used by frontend)
|
||||
pub fn get_extension_by_public_key_and_name(
|
||||
&self,
|
||||
public_key: &str,
|
||||
name: &str,
|
||||
) -> Result<Option<Extension>, ExtensionError> {
|
||||
Ok(self
|
||||
.find_extension_id_by_public_key_and_name(public_key, name)?
|
||||
.map(|(_, ext)| ext))
|
||||
}
|
||||
|
||||
pub fn remove_extension(&self, public_key: &str, name: &str) -> Result<(), ExtensionError> {
|
||||
let (id, _) = self
|
||||
.find_extension_id_by_public_key_and_name(public_key, name)?
|
||||
.ok_or_else(|| ExtensionError::NotFound {
|
||||
public_key: public_key.to_string(),
|
||||
name: name.to_string(),
|
||||
})?;
|
||||
|
||||
// Remove from dev extensions first
|
||||
{
|
||||
let mut dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if dev_extensions.remove(extension_id).is_some() {
|
||||
let mut dev_extensions =
|
||||
self.dev_extensions
|
||||
.lock()
|
||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
if dev_extensions.remove(&id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from production extensions
|
||||
{
|
||||
let mut prod_extensions = self.production_extensions.lock().unwrap();
|
||||
if prod_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut prod_extensions =
|
||||
self.production_extensions
|
||||
.lock()
|
||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
prod_extensions.remove(&id);
|
||||
}
|
||||
|
||||
Err(ExtensionError::NotFound {
|
||||
id: extension_id.to_string(),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_extension_internal(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
extension_id: String,
|
||||
extension_version: String,
|
||||
public_key: &str,
|
||||
extension_name: &str,
|
||||
extension_version: &str,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<(), ExtensionError> {
|
||||
PermissionManager::delete_permissions(state, &extension_id).await?;
|
||||
self.remove_extension(&extension_id)?;
|
||||
// Get the extension from memory to get its ID
|
||||
let extension = self
|
||||
.get_extension_by_public_key_and_name(public_key, extension_name)?
|
||||
.ok_or_else(|| ExtensionError::NotFound {
|
||||
public_key: public_key.to_string(),
|
||||
name: extension_name.to_string(),
|
||||
})?;
|
||||
|
||||
eprintln!("DEBUG: Removing extension with ID: {}", extension.id);
|
||||
eprintln!(
|
||||
"DEBUG: Extension name: {}, version: {}",
|
||||
extension_name, extension_version
|
||||
);
|
||||
|
||||
// Lösche Permissions und Extension-Eintrag in einer Transaktion
|
||||
with_connection(&state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
// Lösche alle Permissions mit extension_id
|
||||
eprintln!(
|
||||
"DEBUG: Deleting permissions for extension_id: {}",
|
||||
extension.id
|
||||
);
|
||||
PermissionManager::delete_permissions_in_transaction(&tx, &hlc_service, &extension.id)?;
|
||||
|
||||
// Lösche Extension-Eintrag mit extension_id
|
||||
let sql = format!("DELETE FROM {} WHERE id = ?", TABLE_EXTENSIONS);
|
||||
eprintln!("DEBUG: Executing SQL: {} with id = {}", sql, extension.id);
|
||||
SqlExecutor::execute_internal_typed(
|
||||
&tx,
|
||||
&hlc_service,
|
||||
&sql,
|
||||
rusqlite::params![&extension.id],
|
||||
)?;
|
||||
|
||||
eprintln!("DEBUG: Committing transaction");
|
||||
tx.commit().map_err(DatabaseError::from)
|
||||
})?;
|
||||
|
||||
eprintln!("DEBUG: Transaction committed successfully");
|
||||
|
||||
// Entferne aus dem In-Memory-Manager
|
||||
self.remove_extension(public_key, extension_name)?;
|
||||
|
||||
// Lösche nur den spezifischen Versions-Ordner: public_key/name/version
|
||||
let extension_dir =
|
||||
self.get_extension_dir(app_handle, &extension_id, &extension_version)?;
|
||||
self.get_extension_dir(app_handle, public_key, extension_name, extension_version)?;
|
||||
|
||||
if extension_dir.exists() {
|
||||
std::fs::remove_dir_all(&extension_dir)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
std::fs::remove_dir_all(&extension_dir).map_err(|e| {
|
||||
ExtensionError::filesystem_with_path(extension_dir.display().to_string(), e)
|
||||
})?;
|
||||
|
||||
// Versuche, leere Parent-Ordner zu löschen
|
||||
// 1. Extension-Name-Ordner (key_hash/name)
|
||||
if let Some(name_dir) = extension_dir.parent() {
|
||||
if name_dir.exists() {
|
||||
if let Ok(entries) = std::fs::read_dir(name_dir) {
|
||||
if entries.count() == 0 {
|
||||
let _ = std::fs::remove_dir(name_dir);
|
||||
|
||||
// 2. Key-Hash-Ordner (key_hash) - nur wenn auch leer
|
||||
if let Some(key_hash_dir) = name_dir.parent() {
|
||||
if key_hash_dir.exists() {
|
||||
if let Ok(entries) = std::fs::read_dir(key_hash_dir) {
|
||||
if entries.count() == 0 {
|
||||
let _ = std::fs::remove_dir(key_hash_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -153,52 +516,23 @@ impl ExtensionManager {
|
||||
|
||||
pub async fn preview_extension_internal(
|
||||
&self,
|
||||
source_path: String,
|
||||
app_handle: &AppHandle,
|
||||
file_bytes: Vec<u8>,
|
||||
) -> Result<ExtensionPreview, ExtensionError> {
|
||||
let source = PathBuf::from(&source_path);
|
||||
|
||||
let temp = std::env::temp_dir().join(format!("haexhub_preview_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let mut archive =
|
||||
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
archive
|
||||
.extract(&temp)
|
||||
.map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest_path = temp.join("manifest.json");
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
let content_hash = ExtensionCrypto::hash_directory(&temp)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_preview", app_handle)?;
|
||||
|
||||
let is_valid_signature = ExtensionCrypto::verify_signature(
|
||||
&manifest.public_key,
|
||||
&content_hash,
|
||||
&manifest.signature,
|
||||
&extracted.manifest.public_key,
|
||||
&extracted.content_hash,
|
||||
&extracted.manifest.signature,
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
let key_hash = manifest.calculate_key_hash()?;
|
||||
let editable_permissions = manifest.to_editable_permissions();
|
||||
|
||||
std::fs::remove_dir_all(&temp).ok();
|
||||
let editable_permissions = extracted.manifest.to_editable_permissions();
|
||||
|
||||
Ok(ExtensionPreview {
|
||||
manifest,
|
||||
manifest: extracted.manifest.clone(),
|
||||
is_valid_signature,
|
||||
key_hash,
|
||||
editable_permissions,
|
||||
})
|
||||
}
|
||||
@ -206,106 +540,338 @@ impl ExtensionManager {
|
||||
pub async fn install_extension_with_permissions_internal(
|
||||
&self,
|
||||
app_handle: AppHandle,
|
||||
source_path: String,
|
||||
file_bytes: Vec<u8>,
|
||||
custom_permissions: EditablePermissions,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<String, ExtensionError> {
|
||||
let source = PathBuf::from(&source_path);
|
||||
let extracted = Self::extract_and_validate_extension(file_bytes, "haexhub_ext", &app_handle)?;
|
||||
|
||||
let temp = std::env::temp_dir().join(format!("haexhub_ext_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
// Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft)
|
||||
ExtensionCrypto::verify_signature(
|
||||
&extracted.manifest.public_key,
|
||||
&extracted.content_hash,
|
||||
&extracted.manifest.signature,
|
||||
)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let mut archive =
|
||||
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
archive
|
||||
.extract(&temp)
|
||||
.map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest_path = temp.join("manifest.json");
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
let content_hash = ExtensionCrypto::hash_directory(&temp)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
let key_hash = manifest.calculate_key_hash()?;
|
||||
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
|
||||
|
||||
let extensions_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
|
||||
})?
|
||||
.join("extensions")
|
||||
.join(&full_extension_id)
|
||||
.join(&manifest.version);
|
||||
|
||||
std::fs::create_dir_all(&extensions_dir)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
copy_directory(
|
||||
temp.to_string_lossy().to_string(),
|
||||
extensions_dir.to_string_lossy().to_string(),
|
||||
let extensions_dir = self.get_extension_dir(
|
||||
&app_handle,
|
||||
&extracted.manifest.public_key,
|
||||
&extracted.manifest.name,
|
||||
&extracted.manifest.version,
|
||||
)?;
|
||||
|
||||
std::fs::remove_dir_all(&temp).ok();
|
||||
// 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)
|
||||
})?;
|
||||
}
|
||||
|
||||
let permissions = custom_permissions.to_internal_permissions(&full_extension_id);
|
||||
std::fs::create_dir_all(&extensions_dir).map_err(|e| {
|
||||
ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e)
|
||||
})?;
|
||||
|
||||
let granted_permissions: Vec<_> = permissions
|
||||
.into_iter()
|
||||
.filter(|p| p.status == PermissionStatus::Granted)
|
||||
.collect();
|
||||
// Copy contents of extracted.temp_dir to extensions_dir
|
||||
// Note: extracted.temp_dir already points to the correct directory with manifest.json
|
||||
for entry in fs::read_dir(&extracted.temp_dir).map_err(|e| {
|
||||
ExtensionError::filesystem_with_path(extracted.temp_dir.display().to_string(), e)
|
||||
})? {
|
||||
let entry = entry.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name();
|
||||
let dest_path = extensions_dir.join(&file_name);
|
||||
|
||||
PermissionManager::save_permissions(state, &full_extension_id, &granted_permissions)
|
||||
.await?;
|
||||
if path.is_dir() {
|
||||
copy_directory(
|
||||
path.to_string_lossy().to_string(),
|
||||
dest_path.to_string_lossy().to_string(),
|
||||
)?;
|
||||
} else {
|
||||
fs::copy(&path, &dest_path).map_err(|e| {
|
||||
ExtensionError::filesystem_with_path(path.display().to_string(), e)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate UUID for extension (Drizzle's $defaultFn only works from JS, not raw SQL)
|
||||
let extension_id = uuid::Uuid::new_v4().to_string();
|
||||
let permissions = custom_permissions.to_internal_permissions(&extension_id);
|
||||
|
||||
// Extension-Eintrag und Permissions in einer Transaktion speichern
|
||||
let actual_extension_id = with_connection(&state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
let hlc_service_guard = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
// Klonen, um den MutexGuard freizugeben, bevor potenziell lange DB-Operationen stattfinden
|
||||
let hlc_service = hlc_service_guard.clone();
|
||||
drop(hlc_service_guard);
|
||||
|
||||
// 1. Extension-Eintrag erstellen mit generierter UUID
|
||||
let insert_ext_sql = format!(
|
||||
"INSERT INTO {} (id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
TABLE_EXTENSIONS
|
||||
);
|
||||
|
||||
SqlExecutor::execute_internal_typed(
|
||||
&tx,
|
||||
&hlc_service,
|
||||
&insert_ext_sql,
|
||||
rusqlite::params![
|
||||
extension_id,
|
||||
extracted.manifest.name,
|
||||
extracted.manifest.version,
|
||||
extracted.manifest.author,
|
||||
extracted.manifest.entry,
|
||||
extracted.manifest.icon,
|
||||
extracted.manifest.public_key,
|
||||
extracted.manifest.signature,
|
||||
extracted.manifest.homepage,
|
||||
extracted.manifest.description,
|
||||
true, // enabled
|
||||
extracted.manifest.single_instance.unwrap_or(false),
|
||||
],
|
||||
)?;
|
||||
|
||||
// 2. Permissions speichern
|
||||
let insert_perm_sql = format!(
|
||||
"INSERT INTO {} (id, extension_id, resource_type, action, target, constraints, status) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
TABLE_EXTENSION_PERMISSIONS
|
||||
);
|
||||
|
||||
for perm in &permissions {
|
||||
use crate::database::generated::HaexExtensionPermissions;
|
||||
let db_perm: HaexExtensionPermissions = perm.into();
|
||||
|
||||
SqlExecutor::execute_internal_typed(
|
||||
&tx,
|
||||
&hlc_service,
|
||||
&insert_perm_sql,
|
||||
rusqlite::params![
|
||||
db_perm.id,
|
||||
db_perm.extension_id,
|
||||
db_perm.resource_type,
|
||||
db_perm.action,
|
||||
db_perm.target,
|
||||
db_perm.constraints,
|
||||
db_perm.status,
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(extension_id.clone())
|
||||
})?;
|
||||
|
||||
let extension = Extension {
|
||||
id: full_extension_id.clone(),
|
||||
name: manifest.name.clone(),
|
||||
id: extension_id.clone(),
|
||||
source: ExtensionSource::Production {
|
||||
path: extensions_dir.clone(),
|
||||
version: manifest.version.clone(),
|
||||
version: extracted.manifest.version.clone(),
|
||||
},
|
||||
manifest: manifest.clone(),
|
||||
manifest: extracted.manifest.clone(),
|
||||
enabled: true,
|
||||
last_accessed: SystemTime::now(),
|
||||
};
|
||||
|
||||
self.add_production_extension(extension)?;
|
||||
|
||||
Ok(full_extension_id)
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
|
||||
}
|
||||
|
||||
impl ExtensionState {
|
||||
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
|
||||
let mut extensions = self.extensions.lock().unwrap();
|
||||
extensions.insert(path, manifest);
|
||||
Ok(actual_extension_id) // Gebe die actual_extension_id an den Caller zurück
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
|
||||
let extensions = self.extensions.lock().unwrap();
|
||||
extensions.values().find(|p| p.name == addon_id).cloned()
|
||||
/// Scannt das Dateisystem beim Start und lädt alle installierten Erweiterungen.
|
||||
pub async fn load_installed_extensions(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<Vec<String>, ExtensionError> {
|
||||
// Clear existing data
|
||||
self.production_extensions
|
||||
.lock()
|
||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
})?
|
||||
.clear();
|
||||
self.permission_cache
|
||||
.lock()
|
||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
})?
|
||||
.clear();
|
||||
self.missing_extensions
|
||||
.lock()
|
||||
.map_err(|e| ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
})?
|
||||
.clear();
|
||||
|
||||
// Lade alle Daten aus der Datenbank
|
||||
let extensions = with_connection(&state.db, |conn| {
|
||||
let sql = format!(
|
||||
"SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled, single_instance FROM {}",
|
||||
TABLE_EXTENSIONS
|
||||
);
|
||||
eprintln!("DEBUG: SQL Query before transformation: {}", sql);
|
||||
|
||||
let results = SqlExecutor::query_select(conn, &sql, &[])?;
|
||||
eprintln!("DEBUG: Query returned {} results", results.len());
|
||||
|
||||
let mut data = Vec::new();
|
||||
for row in results {
|
||||
// Wir erwarten die Werte in der Reihenfolge der SELECT-Anweisung
|
||||
let id = row[0]
|
||||
.as_str()
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing id field".to_string(),
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
let manifest = ExtensionManifest {
|
||||
name: row[1]
|
||||
.as_str()
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing name field".to_string(),
|
||||
})?
|
||||
.to_string(),
|
||||
version: row[2]
|
||||
.as_str()
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing version field".to_string(),
|
||||
})?
|
||||
.to_string(),
|
||||
author: row[3].as_str().map(String::from),
|
||||
entry: row[4].as_str().map(String::from),
|
||||
icon: row[5].as_str().map(String::from),
|
||||
public_key: row[6].as_str().unwrap_or("").to_string(),
|
||||
signature: row[7].as_str().unwrap_or("").to_string(),
|
||||
permissions: ExtensionPermissions::default(),
|
||||
homepage: row[8].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]
|
||||
.as_bool()
|
||||
.or_else(|| row[10].as_i64().map(|v| v != 0))
|
||||
.unwrap_or(false);
|
||||
|
||||
data.push(ExtensionDataFromDb {
|
||||
id,
|
||||
manifest,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
Ok(data)
|
||||
})?;
|
||||
|
||||
// Schritt 2: Die gesammelten Daten verarbeiten (Dateisystem, State-Mutationen).
|
||||
let mut loaded_extension_ids = Vec::new();
|
||||
|
||||
eprintln!("DEBUG: Found {} extensions in database", extensions.len());
|
||||
|
||||
for extension_data in extensions {
|
||||
let extension_id = extension_data.id;
|
||||
eprintln!("DEBUG: Processing extension: {}", extension_id);
|
||||
|
||||
// Use public_key/name/version path structure
|
||||
let extension_path = self.get_extension_dir(
|
||||
app_handle,
|
||||
&extension_data.manifest.public_key,
|
||||
&extension_data.manifest.name,
|
||||
&extension_data.manifest.version,
|
||||
)?;
|
||||
|
||||
// Check if extension directory exists
|
||||
if !extension_path.exists() {
|
||||
eprintln!(
|
||||
"DEBUG: Extension directory missing for: {} at {:?}",
|
||||
extension_id, extension_path
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
let extension = Extension {
|
||||
id: extension_id.clone(),
|
||||
source: ExtensionSource::Production {
|
||||
path: extension_path,
|
||||
version: extension_data.manifest.version.clone(),
|
||||
},
|
||||
manifest: extension_data.manifest,
|
||||
enabled: extension_data.enabled,
|
||||
last_accessed: SystemTime::now(),
|
||||
};
|
||||
|
||||
loaded_extension_ids.push(extension_id.clone());
|
||||
self.add_production_extension(extension)?;
|
||||
}
|
||||
|
||||
Ok(loaded_extension_ids)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,250 +1,214 @@
|
||||
// src-tauri/src/extension/core/manifest.rs
|
||||
|
||||
use crate::extension::crypto::ExtensionCrypto;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::types::{
|
||||
Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints,
|
||||
PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints,
|
||||
Action, DbAction, ExtensionPermission, FsAction, HttpAction, PermissionConstraints,
|
||||
PermissionStatus, ResourceType, ShellAction,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
pub icon: Option<String>,
|
||||
pub public_key: String,
|
||||
pub signature: String,
|
||||
pub permissions: ExtensionManifestPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
pub fn calculate_key_hash(&self) -> Result<String, ExtensionError> {
|
||||
ExtensionCrypto::calculate_key_hash(&self.public_key)
|
||||
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })
|
||||
}
|
||||
|
||||
pub fn full_extension_id(&self) -> Result<String, ExtensionError> {
|
||||
let key_hash = self.calculate_key_hash()?;
|
||||
Ok(format!("{}-{}", key_hash, self.id))
|
||||
}
|
||||
|
||||
pub fn to_editable_permissions(&self) -> EditablePermissions {
|
||||
let mut permissions = Vec::new();
|
||||
|
||||
if let Some(db) = &self.permissions.database {
|
||||
for resource in &db.read {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "db".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: resource.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
for resource in &db.write {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "db".to_string(),
|
||||
action: "write".to_string(),
|
||||
target: resource.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(fs) = &self.permissions.filesystem {
|
||||
for path in &fs.read {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "fs".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: path.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
for path in &fs.write {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "fs".to_string(),
|
||||
action: "write".to_string(),
|
||||
target: path.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(http_list) = &self.permissions.http {
|
||||
for domain in http_list {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "http".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: domain.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(shell_list) = &self.permissions.shell {
|
||||
for command in shell_list {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "shell".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: command.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
EditablePermissions { permissions }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct ExtensionManifestPermissions {
|
||||
#[serde(default)]
|
||||
pub database: Option<DatabaseManifestPermissions>,
|
||||
#[serde(default)]
|
||||
pub filesystem: Option<FilesystemManifestPermissions>,
|
||||
#[serde(default)]
|
||||
pub http: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub shell: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DatabaseManifestPermissions {
|
||||
#[serde(default)]
|
||||
pub read: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub write: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct FilesystemManifestPermissions {
|
||||
#[serde(default)]
|
||||
pub read: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub write: Vec<String>,
|
||||
}
|
||||
|
||||
// Editable Permissions für UI
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EditablePermissions {
|
||||
pub permissions: Vec<EditablePermission>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EditablePermission {
|
||||
pub resource_type: String,
|
||||
pub action: String,
|
||||
/// Repräsentiert einen einzelnen Berechtigungseintrag im Manifest und im UI-Modell.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
|
||||
#[ts(export)]
|
||||
pub struct PermissionEntry {
|
||||
pub target: String,
|
||||
|
||||
/// Die auszuführende Aktion (z.B. "read", "read_write", "GET", "execute").
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub operation: Option<String>,
|
||||
|
||||
/// Optionale, spezifische Einschränkungen für diese Berechtigung.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(type = "Record<string, unknown>")]
|
||||
pub constraints: Option<serde_json::Value>,
|
||||
pub status: String,
|
||||
|
||||
/// Der Status der Berechtigung (wird nur im UI-Modell verwendet).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<PermissionStatus>,
|
||||
}
|
||||
|
||||
impl EditablePermissions {
|
||||
pub fn to_internal_permissions(&self, extension_id: &str) -> Vec<ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.map(|p| ExtensionPermission {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
extension_id: extension_id.to_string(),
|
||||
resource_type: match p.resource_type.as_str() {
|
||||
"fs" => ResourceType::Fs,
|
||||
"http" => ResourceType::Http,
|
||||
"db" => ResourceType::Db,
|
||||
"shell" => ResourceType::Shell,
|
||||
_ => ResourceType::Fs,
|
||||
},
|
||||
action: match p.action.as_str() {
|
||||
"read" => Action::Read,
|
||||
"write" => Action::Write,
|
||||
_ => Action::Read,
|
||||
},
|
||||
target: p.target.clone(),
|
||||
constraints: p
|
||||
.constraints
|
||||
.as_ref()
|
||||
.and_then(|c| Self::parse_constraints(&p.resource_type, c)),
|
||||
status: match p.status.as_str() {
|
||||
"granted" => PermissionStatus::Granted,
|
||||
"denied" => PermissionStatus::Denied,
|
||||
"ask" => PermissionStatus::Ask,
|
||||
_ => PermissionStatus::Denied,
|
||||
},
|
||||
haex_timestamp: None,
|
||||
haex_tombstone: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_constraints(
|
||||
resource_type: &str,
|
||||
json_value: &serde_json::Value,
|
||||
) -> Option<PermissionConstraints> {
|
||||
match resource_type {
|
||||
"db" => serde_json::from_value::<DbConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Database),
|
||||
"fs" => serde_json::from_value::<FsConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Filesystem),
|
||||
"http" => serde_json::from_value::<HttpConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Http),
|
||||
"shell" => serde_json::from_value::<ShellConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Shell),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct ExtensionPreview {
|
||||
pub manifest: ExtensionManifest,
|
||||
pub is_valid_signature: bool,
|
||||
pub key_hash: String,
|
||||
pub editable_permissions: EditablePermissions,
|
||||
}
|
||||
/// Definiert die einheitliche Struktur für alle Berechtigungsarten im Manifest und UI.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
|
||||
#[ts(export)]
|
||||
pub struct ExtensionPermissions {
|
||||
#[serde(default)]
|
||||
pub database: Option<Vec<PermissionEntry>>,
|
||||
#[serde(default)]
|
||||
pub filesystem: Option<Vec<PermissionEntry>>,
|
||||
#[serde(default)]
|
||||
pub http: Option<Vec<PermissionEntry>>,
|
||||
#[serde(default)]
|
||||
pub shell: Option<Vec<PermissionEntry>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionInfoResponse {
|
||||
pub key_hash: String,
|
||||
/// Typ-Alias für bessere Lesbarkeit, wenn die Struktur als UI-Modell verwendet wird.
|
||||
pub type EditablePermissions = ExtensionPermissions;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
|
||||
#[ts(export)]
|
||||
pub struct ExtensionManifest {
|
||||
pub name: String,
|
||||
pub full_id: String,
|
||||
pub version: String,
|
||||
pub display_name: Option<String>,
|
||||
pub namespace: Option<String>,
|
||||
pub allowed_origin: String,
|
||||
pub author: Option<String>,
|
||||
#[serde(default = "default_entry_value")]
|
||||
pub entry: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub public_key: String,
|
||||
pub signature: String,
|
||||
pub permissions: ExtensionPermissions,
|
||||
pub homepage: 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 {
|
||||
/// Konvertiert die Manifest-Berechtigungen in das bearbeitbare UI-Modell,
|
||||
/// indem der Standardstatus `Granted` gesetzt wird.
|
||||
pub fn to_editable_permissions(&self) -> EditablePermissions {
|
||||
let mut editable = self.permissions.clone();
|
||||
|
||||
let set_status_for_list = |list: Option<&mut Vec<PermissionEntry>>| {
|
||||
if let Some(entries) = list {
|
||||
for entry in entries.iter_mut() {
|
||||
entry.status = Some(PermissionStatus::Granted);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
set_status_for_list(editable.database.as_mut());
|
||||
set_status_for_list(editable.filesystem.as_mut());
|
||||
set_status_for_list(editable.http.as_mut());
|
||||
set_status_for_list(editable.shell.as_mut());
|
||||
|
||||
editable
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionPermissions {
|
||||
/// Konvertiert das UI-Modell in die flache Liste von internen `ExtensionPermission`-Objekten.
|
||||
pub fn to_internal_permissions(&self, extension_id: &str) -> Vec<ExtensionPermission> {
|
||||
let mut permissions = Vec::new();
|
||||
|
||||
if let Some(entries) = &self.database {
|
||||
for p in entries {
|
||||
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Db, p) {
|
||||
permissions.push(perm);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(entries) = &self.filesystem {
|
||||
for p in entries {
|
||||
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Fs, p) {
|
||||
permissions.push(perm);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(entries) = &self.http {
|
||||
for p in entries {
|
||||
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Http, p) {
|
||||
permissions.push(perm);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(entries) = &self.shell {
|
||||
for p in entries {
|
||||
if let Some(perm) = Self::create_internal(extension_id, ResourceType::Shell, p) {
|
||||
permissions.push(perm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
permissions
|
||||
}
|
||||
|
||||
/// Parst einen einzelnen `PermissionEntry` und wandelt ihn in die interne, typsichere `ExtensionPermission`-Struktur um.
|
||||
fn create_internal(
|
||||
extension_id: &str,
|
||||
resource_type: ResourceType,
|
||||
p: &PermissionEntry,
|
||||
) -> Option<ExtensionPermission> {
|
||||
let operation_str = p.operation.as_deref().unwrap_or_default();
|
||||
|
||||
let action = match resource_type {
|
||||
ResourceType::Db => DbAction::from_str(operation_str).ok().map(Action::Database),
|
||||
ResourceType::Fs => FsAction::from_str(operation_str)
|
||||
.ok()
|
||||
.map(Action::Filesystem),
|
||||
ResourceType::Http => HttpAction::from_str(operation_str).ok().map(Action::Http),
|
||||
ResourceType::Shell => ShellAction::from_str(operation_str).ok().map(Action::Shell),
|
||||
};
|
||||
|
||||
action.map(|act| ExtensionPermission {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
extension_id: extension_id.to_string(),
|
||||
resource_type: resource_type.clone(),
|
||||
action: act,
|
||||
target: p.target.clone(),
|
||||
constraints: p
|
||||
.constraints
|
||||
.as_ref()
|
||||
.and_then(|c| serde_json::from_value::<PermissionConstraints>(c.clone()).ok()),
|
||||
status: p.status.clone().unwrap_or(PermissionStatus::Ask),
|
||||
haex_timestamp: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExtensionInfoResponse {
|
||||
pub id: String,
|
||||
pub public_key: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub enabled: bool,
|
||||
pub description: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub entry: Option<String>,
|
||||
pub single_instance: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dev_server_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionInfoResponse {
|
||||
pub fn from_extension(
|
||||
extension: &crate::extension::core::types::Extension,
|
||||
) -> Result<Self, ExtensionError> {
|
||||
use crate::extension::core::types::get_tauri_origin;
|
||||
use crate::extension::core::types::ExtensionSource;
|
||||
|
||||
let allowed_origin = get_tauri_origin();
|
||||
let key_hash = extension.manifest.calculate_key_hash()?;
|
||||
let full_id = extension.manifest.full_extension_id()?;
|
||||
let dev_server_url = match &extension.source {
|
||||
ExtensionSource::Development { dev_server_url, .. } => Some(dev_server_url.clone()),
|
||||
ExtensionSource::Production { .. } => None,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
key_hash,
|
||||
id: extension.id.clone(),
|
||||
public_key: extension.manifest.public_key.clone(),
|
||||
name: extension.manifest.name.clone(),
|
||||
full_id,
|
||||
version: extension.manifest.version.clone(),
|
||||
display_name: Some(extension.manifest.name.clone()),
|
||||
namespace: extension.manifest.author.clone(),
|
||||
allowed_origin,
|
||||
author: extension.manifest.author.clone(),
|
||||
enabled: extension.enabled,
|
||||
description: extension.manifest.description.clone(),
|
||||
homepage: extension.manifest.homepage.clone(),
|
||||
icon: extension.manifest.icon.clone(),
|
||||
entry: extension.manifest.entry.clone(),
|
||||
single_instance: extension.manifest.single_instance,
|
||||
dev_server_url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,33 @@
|
||||
// src-tauri/src/extension/core/protocol.rs
|
||||
|
||||
use crate::extension::core::types::get_tauri_origin;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::AppState;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
use mime;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use tauri::http::Uri;
|
||||
use tauri::http::{Request, Response};
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
// Extension protocol name constant
|
||||
pub const EXTENSION_PROTOCOL_NAME: &str = "haex-extension";
|
||||
|
||||
// Cache for extension info (used for asset loading without origin header)
|
||||
lazy_static::lazy_static! {
|
||||
static ref EXTENSION_CACHE: Mutex<Option<ExtensionInfo>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ExtensionInfo {
|
||||
id: String,
|
||||
public_key: String,
|
||||
name: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
@ -21,6 +36,7 @@ enum DataProcessingError {
|
||||
HexDecoding(hex::FromHexError),
|
||||
Utf8Conversion(std::string::FromUtf8Error),
|
||||
JsonParsing(serde_json::Error),
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for DataProcessingError {
|
||||
@ -31,6 +47,7 @@ impl fmt::Display for DataProcessingError {
|
||||
write!(f, "UTF-8-Konvertierungsfehler: {}", e)
|
||||
}
|
||||
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e),
|
||||
DataProcessingError::Custom(msg) => write!(f, "Datenverarbeitungsfehler: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -41,10 +58,17 @@ impl std::error::Error for DataProcessingError {
|
||||
DataProcessingError::HexDecoding(e) => Some(e),
|
||||
DataProcessingError::Utf8Conversion(e) => Some(e),
|
||||
DataProcessingError::JsonParsing(e) => Some(e),
|
||||
DataProcessingError::Custom(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for DataProcessingError {
|
||||
fn from(msg: String) -> Self {
|
||||
DataProcessingError::Custom(msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hex::FromHexError> for DataProcessingError {
|
||||
fn from(err: hex::FromHexError) -> Self {
|
||||
DataProcessingError::HexDecoding(err)
|
||||
@ -65,18 +89,19 @@ impl From<serde_json::Error> for DataProcessingError {
|
||||
|
||||
pub fn resolve_secure_extension_asset_path(
|
||||
app_handle: &AppHandle,
|
||||
state: State<AppState>,
|
||||
extension_id: &str,
|
||||
state: &State<AppState>,
|
||||
public_key: &str,
|
||||
extension_name: &str,
|
||||
extension_version: &str,
|
||||
requested_asset_path: &str,
|
||||
) -> Result<PathBuf, ExtensionError> {
|
||||
if extension_id.is_empty()
|
||||
|| !extension_id
|
||||
if extension_name.is_empty()
|
||||
|| !extension_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-')
|
||||
{
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: format!("Invalid extension ID: {}", extension_id),
|
||||
reason: format!("Invalid extension name: {}", extension_name),
|
||||
});
|
||||
}
|
||||
|
||||
@ -90,10 +115,12 @@ pub fn resolve_secure_extension_asset_path(
|
||||
});
|
||||
}
|
||||
|
||||
let specific_extension_dir =
|
||||
state
|
||||
.extension_manager
|
||||
.get_extension_dir(app_handle, extension_id, extension_version)?;
|
||||
let specific_extension_dir = state.extension_manager.get_extension_dir(
|
||||
app_handle,
|
||||
public_key,
|
||||
extension_name,
|
||||
extension_version,
|
||||
)?;
|
||||
|
||||
let clean_relative_path = requested_asset_path
|
||||
.replace('\\', "/")
|
||||
@ -148,99 +175,307 @@ pub fn extension_protocol_handler(
|
||||
app_handle: &AppHandle,
|
||||
request: &Request<Vec<u8>>,
|
||||
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
let uri_ref = request.uri();
|
||||
println!("Protokoll Handler für: {}", uri_ref);
|
||||
// Get the origin from the request
|
||||
let origin = request
|
||||
.headers()
|
||||
.get("origin")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let host = uri_ref
|
||||
.host()
|
||||
.ok_or("Kein Host (Extension ID) in URI gefunden")?
|
||||
.to_string();
|
||||
// Only allow same-protocol requests or tauri origin
|
||||
// For null/empty origin (initial load), use wildcard
|
||||
let protocol_prefix = format!("{}://", EXTENSION_PROTOCOL_NAME);
|
||||
let allowed_origin = if origin.starts_with(&protocol_prefix) || origin == get_tauri_origin() {
|
||||
origin
|
||||
} else if origin.is_empty() || origin == "null" {
|
||||
"*" // Allow initial load without origin
|
||||
} else {
|
||||
// Reject other origins
|
||||
return Response::builder()
|
||||
.status(403)
|
||||
.body(Vec::from("Origin not allowed"))
|
||||
.map_err(|e| e.into());
|
||||
};
|
||||
|
||||
// Handle OPTIONS requests for CORS preflight
|
||||
if request.method() == "OPTIONS" {
|
||||
return Response::builder()
|
||||
.status(200)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.header("Access-Control-Allow-Credentials", "true")
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
|
||||
let uri_ref = request.uri();
|
||||
let referer = request
|
||||
.headers()
|
||||
.get("referer")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
println!("Protokoll Handler für: {}", uri_ref);
|
||||
println!("Origin: {}", origin);
|
||||
println!("Referer: {}", referer);
|
||||
|
||||
let path_str = uri_ref.path();
|
||||
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
|
||||
let resource_segments: Vec<&str> = segments_iter.collect();
|
||||
let raw_asset_path = resource_segments.join("/");
|
||||
|
||||
// Try to decode base64-encoded extension info from URI
|
||||
// Format:
|
||||
// - Desktop: haex-extension://<base64>/{assetPath}
|
||||
// - Android: http://localhost/{base64}/{assetPath}
|
||||
let host = uri_ref.host().unwrap_or("");
|
||||
println!("URI Host: {}", host);
|
||||
|
||||
let (info, segments_after_version) = if host == "localhost" || host == format!("{}.localhost", EXTENSION_PROTOCOL_NAME).as_str() {
|
||||
// Android format: http://haex-extension.localhost/{base64}/{assetPath}
|
||||
// Extract base64 from first path segment
|
||||
println!("Android format detected: http://{}/...", host);
|
||||
let mut segments_iter = path_str.split('/').filter(|s| !s.is_empty());
|
||||
|
||||
if let Some(first_segment) = segments_iter.next() {
|
||||
println!("First path segment (base64): {}", first_segment);
|
||||
match BASE64_STANDARD.decode(first_segment) {
|
||||
Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) {
|
||||
Ok(json_str) => match serde_json::from_str::<ExtensionInfo>(&json_str) {
|
||||
Ok(info) => {
|
||||
println!("=== Extension Info from path (Android) ===");
|
||||
println!(" PublicKey: {}", info.public_key);
|
||||
println!(" Name: {}", info.name);
|
||||
println!(" Version: {}", info.version);
|
||||
cache_extension_info(&info);
|
||||
|
||||
// Remaining segments after base64 are the asset path
|
||||
let remaining: Vec<String> = segments_iter.map(|s| s.to_string()).collect();
|
||||
(info, remaining)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to parse JSON from base64 path: {}", e);
|
||||
return Response::builder()
|
||||
.status(400)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.body(Vec::from(format!("Invalid extension info in base64 path: {}", e)))
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to decode UTF-8 from base64 path: {}", e);
|
||||
return Response::builder()
|
||||
.status(400)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.body(Vec::from(format!("Invalid UTF-8 in base64 path: {}", e)))
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to decode base64 from path: {}", e);
|
||||
return Response::builder()
|
||||
.status(400)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.body(Vec::from(format!("Invalid base64 in path: {}", e)))
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("No path segment found for Android format");
|
||||
return Response::builder()
|
||||
.status(400)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.body(Vec::from("No base64 segment found in path"))
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
} else if host != "localhost" && !host.is_empty() {
|
||||
// Desktop format: haex-extension://<base64>/{assetPath}
|
||||
println!("Desktop format detected: haex-extension://<base64>/...");
|
||||
match BASE64_STANDARD.decode(host) {
|
||||
Ok(decoded_bytes) => match String::from_utf8(decoded_bytes) {
|
||||
Ok(json_str) => match serde_json::from_str::<ExtensionInfo>(&json_str) {
|
||||
Ok(info) => {
|
||||
println!("=== Extension Info from base64-encoded host ===");
|
||||
println!(" PublicKey: {}", info.public_key);
|
||||
println!(" Name: {}", info.name);
|
||||
println!(" Version: {}", info.version);
|
||||
cache_extension_info(&info);
|
||||
|
||||
// Parse path segments as asset path
|
||||
// Format: haex-extension://<base64>/{asset_path}
|
||||
// All extension info is in the base64-encoded host
|
||||
let segments: Vec<String> = path_str
|
||||
.split('/')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
(info, segments)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to parse JSON from base64 host: {}", e);
|
||||
return Response::builder()
|
||||
.status(400)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.body(Vec::from(format!("Invalid extension info in base64 host: {}", e)))
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to decode UTF-8 from base64 host: {}", e);
|
||||
return Response::builder()
|
||||
.status(400)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.body(Vec::from(format!("Invalid UTF-8 in base64 host: {}", e)))
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to decode base64 host: {}", e);
|
||||
return Response::builder()
|
||||
.status(400)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.body(Vec::from(format!("Invalid base64 in host: {}", e)))
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No base64 host - use path-based parsing (for localhost/Android/Windows)
|
||||
parse_extension_info_from_path(path_str, origin, uri_ref, referer, &allowed_origin)?
|
||||
};
|
||||
|
||||
// Construct asset path from remaining segments
|
||||
let raw_asset_path = segments_after_version.join("/");
|
||||
|
||||
// Simple asset loading: if path is empty, serve index.html, otherwise try to load the asset
|
||||
// This is framework-agnostic and lets the file system determine if it exists
|
||||
let asset_to_load = if raw_asset_path.is_empty() {
|
||||
"index.html"
|
||||
} else {
|
||||
&raw_asset_path
|
||||
};
|
||||
|
||||
match process_hex_encoded_json(&host) {
|
||||
Ok(info) => {
|
||||
println!("Daten erfolgreich verarbeitet:");
|
||||
println!(" ID: {}", info.id);
|
||||
println!(" Version: {}", info.version);
|
||||
let absolute_secure_path = resolve_secure_extension_asset_path(
|
||||
app_handle,
|
||||
state,
|
||||
&info.id,
|
||||
&info.version,
|
||||
&asset_to_load,
|
||||
)?;
|
||||
println!("Path: {}", path_str);
|
||||
println!("Asset to load: {}", asset_to_load);
|
||||
|
||||
println!("absolute_secure_path: {}", absolute_secure_path.display());
|
||||
let absolute_secure_path = resolve_secure_extension_asset_path(
|
||||
app_handle,
|
||||
&state,
|
||||
&info.public_key,
|
||||
&info.name,
|
||||
&info.version,
|
||||
&asset_to_load,
|
||||
)?;
|
||||
|
||||
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
|
||||
match fs::read(&absolute_secure_path) {
|
||||
Ok(content) => {
|
||||
let mime_type = mime_guess::from_path(&absolute_secure_path)
|
||||
.first_or(mime::APPLICATION_OCTET_STREAM)
|
||||
.to_string();
|
||||
let content_length = content.len();
|
||||
println!(
|
||||
"Liefere {} ({}, {} bytes) ",
|
||||
absolute_secure_path.display(),
|
||||
mime_type,
|
||||
content_length
|
||||
);
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", mime_type)
|
||||
.header("Content-Length", content_length.to_string())
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.body(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Fehler beim Lesen der Datei {}: {}",
|
||||
absolute_secure_path.display(),
|
||||
e
|
||||
);
|
||||
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
|
||||
404
|
||||
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
403
|
||||
} else {
|
||||
500
|
||||
};
|
||||
println!("Resolved path: {}", absolute_secure_path.display());
|
||||
println!("File exists: {}", absolute_secure_path.exists());
|
||||
|
||||
Response::builder()
|
||||
.status(status_code)
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"Asset nicht gefunden oder ist kein File: {}",
|
||||
absolute_secure_path.display()
|
||||
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
|
||||
match fs::read(&absolute_secure_path) {
|
||||
Ok(content) => {
|
||||
let mime_type = mime_guess::from_path(&absolute_secure_path)
|
||||
.first_or(mime::APPLICATION_OCTET_STREAM)
|
||||
.to_string();
|
||||
|
||||
// Note: Base tag and polyfills are now injected by the SDK at runtime
|
||||
// No server-side HTML modification needed
|
||||
|
||||
let content_length = content.len();
|
||||
println!(
|
||||
"Liefere {} ({}, {} bytes) ",
|
||||
absolute_secure_path.display(),
|
||||
mime_type,
|
||||
content_length
|
||||
);
|
||||
Response::builder()
|
||||
.status(404)
|
||||
.status(200)
|
||||
.header("Content-Type", &mime_type)
|
||||
.header("Content-Length", content_length.to_string())
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.header("Access-Control-Allow-Credentials", "true")
|
||||
.body(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Fehler beim Lesen der Datei {}: {}",
|
||||
absolute_secure_path.display(),
|
||||
e
|
||||
);
|
||||
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
|
||||
404
|
||||
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
403
|
||||
} else {
|
||||
500
|
||||
};
|
||||
Response::builder()
|
||||
.status(status_code)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
|
||||
} else {
|
||||
// Asset not found - try index.html fallback for SPA routing
|
||||
// This allows client-side routing to work (e.g., /settings -> index.html)
|
||||
if asset_to_load != "index.html" {
|
||||
eprintln!(
|
||||
"Asset nicht gefunden: {}, versuche index.html fallback für SPA routing",
|
||||
absolute_secure_path.display()
|
||||
);
|
||||
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
let index_path = resolve_secure_extension_asset_path(
|
||||
app_handle,
|
||||
&state,
|
||||
&info.public_key,
|
||||
&info.name,
|
||||
&info.version,
|
||||
"index.html",
|
||||
)?;
|
||||
|
||||
if index_path.exists() && index_path.is_file() {
|
||||
match fs::read(&index_path) {
|
||||
Ok(content) => {
|
||||
let mime_type = "text/html";
|
||||
|
||||
// Note: Base tag and polyfills are injected by SDK at runtime
|
||||
|
||||
let content_length = content.len();
|
||||
return Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", mime_type)
|
||||
.header("Content-Length", content_length.to_string())
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.header("Access-Control-Allow-Credentials", "true")
|
||||
.body(content)
|
||||
.map_err(|e| e.into());
|
||||
}
|
||||
Err(_) => {
|
||||
// Fall through to 404
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No fallback available - return 404
|
||||
eprintln!(
|
||||
"Asset nicht gefunden oder ist kein File: {}",
|
||||
absolute_secure_path.display()
|
||||
);
|
||||
Response::builder()
|
||||
.status(404)
|
||||
.header("Access-Control-Allow-Origin", allowed_origin)
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "*")
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,3 +485,195 @@ fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProces
|
||||
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?;
|
||||
Ok(extension_info)
|
||||
}
|
||||
|
||||
fn parse_encoded_info_from_origin_or_uri_or_referer_or_cache(
|
||||
origin: &str,
|
||||
uri_ref: &Uri,
|
||||
referer: &str,
|
||||
) -> Result<ExtensionInfo, DataProcessingError> {
|
||||
// Return direkt ExtensionInfo (dekodiert)
|
||||
// 1-3. Bestehende Fallbacks (wie vorher, aber return decoded Info statt hex)
|
||||
if !origin.is_empty() && origin != "null" {
|
||||
if let Ok(hex) = parse_from_origin(origin) {
|
||||
if let Ok(info) = process_hex_encoded_json(&hex) {
|
||||
cache_extension_info(&info); // Cache setzen
|
||||
println!("Parsed und gecached aus Origin: {}", hex);
|
||||
return Ok(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Fallback zu URI-Parsing");
|
||||
if let Ok(hex) = parse_from_uri_path(uri_ref) {
|
||||
if let Ok(info) = process_hex_encoded_json(&hex) {
|
||||
cache_extension_info(&info); // Cache setzen
|
||||
println!("Parsed und gecached aus URI: {}", hex);
|
||||
return Ok(info);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Fallback zu Referer-Parsing: {}", referer);
|
||||
if !referer.is_empty() && referer != "null" {
|
||||
if let Ok(hex) = parse_from_uri_string(referer) {
|
||||
if let Ok(info) = process_hex_encoded_json(&hex) {
|
||||
cache_extension_info(&info); // Cache setzen
|
||||
println!("Parsed und gecached aus Referer: {}", hex);
|
||||
return Ok(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback: Globaler Cache (für Assets in derselben Session)
|
||||
println!("Fallback zu Cache");
|
||||
if let Some(cached_info) = get_cached_extension_info() {
|
||||
println!(
|
||||
"Gecached Info verwendet: PublicKey={}, Name={}, Version={}",
|
||||
cached_info.public_key, cached_info.name, cached_info.version
|
||||
);
|
||||
return Ok(cached_info);
|
||||
}
|
||||
|
||||
Err(
|
||||
"Kein gültiger Hex in Origin, URI, Referer oder Cache gefunden"
|
||||
.to_string()
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
// NEU: Cache-Helper (Mutex-sicher)
|
||||
fn cache_extension_info(info: &ExtensionInfo) {
|
||||
if let Ok(mut cache) = EXTENSION_CACHE.lock() {
|
||||
*cache = Some(info.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cached_extension_info() -> Option<ExtensionInfo> {
|
||||
if let Ok(cache) = EXTENSION_CACHE.lock() {
|
||||
cache.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hex_from_url_string(url_str: &str) -> Result<String, DataProcessingError> {
|
||||
// Suche nach Scheme-Ende (://)
|
||||
let scheme_end = match url_str.find("://") {
|
||||
Some(pos) => pos + 3, // Nach "://"
|
||||
_none => return Err("Kein Scheme in URL".to_string().into()),
|
||||
};
|
||||
|
||||
let after_scheme = &url_str[scheme_end..];
|
||||
let path_start = match after_scheme.find('/') {
|
||||
Some(pos) => pos,
|
||||
_none => return Err("Kein Path in URL".to_string().into()),
|
||||
};
|
||||
|
||||
let path = &after_scheme[path_start..]; // z.B. "/7b22.../index.html"
|
||||
let mut segments = path.split('/').filter(|s| !s.is_empty());
|
||||
|
||||
let first_segment = match segments.next() {
|
||||
Some(seg) => seg,
|
||||
_none => return Err("Kein Path-Segment in URL".to_string().into()),
|
||||
};
|
||||
|
||||
validate_and_return_hex(first_segment)
|
||||
}
|
||||
|
||||
// Vereinfachte parse_from_origin
|
||||
fn parse_from_origin(origin: &str) -> Result<String, DataProcessingError> {
|
||||
parse_hex_from_url_string(origin)
|
||||
}
|
||||
|
||||
// Vereinfachte parse_from_uri_path
|
||||
fn parse_from_uri_path(uri_ref: &Uri) -> Result<String, DataProcessingError> {
|
||||
let uri_str = uri_ref.to_string();
|
||||
parse_hex_from_url_string(&uri_str)
|
||||
}
|
||||
|
||||
// Vereinfachte parse_from_uri_string (für Referer)
|
||||
fn parse_from_uri_string(uri_str: &str) -> Result<String, DataProcessingError> {
|
||||
parse_hex_from_url_string(uri_str)
|
||||
}
|
||||
|
||||
// validate_and_return_hex bleibt unverändert (aus letztem Vorschlag)
|
||||
fn validate_and_return_hex(segment: &str) -> Result<String, DataProcessingError> {
|
||||
if segment.is_empty() {
|
||||
return Err("Kein Extension-Info (hex) im Path".to_string().into());
|
||||
}
|
||||
if segment.len() % 2 != 0 {
|
||||
return Err("Ungültiger Hex: Ungerade Länge".to_string().into());
|
||||
}
|
||||
if !segment.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return Err("Ungültiger Hex: Ungültige Zeichen".to_string().into());
|
||||
}
|
||||
Ok(segment.to_string())
|
||||
}
|
||||
|
||||
fn encode_hex_for_log(info: &ExtensionInfo) -> String {
|
||||
let json_str = serde_json::to_string(info).unwrap_or_default();
|
||||
hex::encode(json_str.as_bytes())
|
||||
}
|
||||
|
||||
// Helper function to parse extension info from path segments
|
||||
fn parse_extension_info_from_path(
|
||||
path_str: &str,
|
||||
origin: &str,
|
||||
uri_ref: &Uri,
|
||||
referer: &str,
|
||||
allowed_origin: &str,
|
||||
) -> Result<(ExtensionInfo, Vec<String>), Box<dyn std::error::Error>> {
|
||||
let mut segments_iter = path_str.split('/').filter(|s| !s.is_empty());
|
||||
|
||||
match (segments_iter.next(), segments_iter.next(), segments_iter.next()) {
|
||||
(Some(public_key), Some(name), Some(version)) => {
|
||||
println!("=== Extension Protocol Handler (path-based) ===");
|
||||
println!("Full URI: {}", uri_ref);
|
||||
println!("Parsed from path segments:");
|
||||
println!(" PublicKey: {}", public_key);
|
||||
println!(" Name: {}", name);
|
||||
println!(" Version: {}", version);
|
||||
|
||||
let info = ExtensionInfo {
|
||||
public_key: public_key.to_string(),
|
||||
name: name.to_string(),
|
||||
version: version.to_string(),
|
||||
};
|
||||
|
||||
cache_extension_info(&info);
|
||||
|
||||
// Collect remaining segments as asset path (owned strings)
|
||||
let remaining: Vec<String> = segments_iter.map(|s| s.to_string()).collect();
|
||||
|
||||
Ok((info, remaining))
|
||||
}
|
||||
_ => {
|
||||
// Fallback: Try hex-encoded format for backwards compatibility
|
||||
match parse_encoded_info_from_origin_or_uri_or_referer_or_cache(
|
||||
origin, uri_ref, referer,
|
||||
) {
|
||||
Ok(decoded) => {
|
||||
println!("=== Extension Protocol Handler (legacy hex format) ===");
|
||||
println!("Full URI: {}", uri_ref);
|
||||
println!("Decoded info:");
|
||||
println!(" PublicKey: {}", decoded.public_key);
|
||||
println!(" Name: {}", decoded.name);
|
||||
println!(" Version: {}", decoded.version);
|
||||
|
||||
// For legacy format, collect all segments after parsing (owned strings)
|
||||
let segments: Vec<String> = path_str
|
||||
.split('/')
|
||||
.filter(|s| !s.is_empty())
|
||||
.skip(1) // Skip the hex segment
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
Ok((decoded, segments))
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fehler beim Parsen (alle Fallbacks): {}", e);
|
||||
Err(format!("Ungültige Anfrage: {}", e).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,11 +21,15 @@ pub enum ExtensionSource {
|
||||
/// Complete extension data structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Extension {
|
||||
/// UUID from database (primary key)
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// Extension source (production path or dev server)
|
||||
pub source: ExtensionSource,
|
||||
/// Extension manifest containing all metadata (name, version, public_key, etc.)
|
||||
pub manifest: ExtensionManifest,
|
||||
/// Whether the extension is enabled
|
||||
pub enabled: bool,
|
||||
/// Last time the extension was accessed
|
||||
pub last_accessed: SystemTime,
|
||||
}
|
||||
|
||||
@ -47,7 +51,9 @@ pub fn get_tauri_origin() -> String {
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
// On Android, with http://*.localhost URLs, the origin is "null"
|
||||
// This is a browser security feature for local/file protocols
|
||||
"null".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
|
||||
@ -1,973 +0,0 @@
|
||||
/// src-tauri/src/extension/core.rs
|
||||
use crate::extension::crypto::ExtensionCrypto;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::manager::PermissionManager;
|
||||
use crate::extension::permissions::types::{
|
||||
Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints,
|
||||
PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints,
|
||||
};
|
||||
use crate::AppState;
|
||||
use mime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Digest;
|
||||
use sha2::Sha256;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tauri::State;
|
||||
use tauri::{
|
||||
http::{Request, Response},
|
||||
AppHandle, Manager, Runtime, UriSchemeContext,
|
||||
};
|
||||
use zip::ZipArchive;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ExtensionPreview {
|
||||
pub manifest: ExtensionManifest,
|
||||
pub is_valid_signature: bool,
|
||||
pub key_hash: String,
|
||||
pub editable_permissions: EditablePermissions,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EditablePermissions {
|
||||
pub permissions: Vec<EditablePermission>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EditablePermission {
|
||||
pub resource_type: String,
|
||||
pub action: String,
|
||||
pub target: String,
|
||||
pub constraints: Option<serde_json::Value>,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl EditablePermissions {
|
||||
/// Konvertiert EditablePermissions zu internen ExtensionPermissions
|
||||
pub fn to_internal_permissions(&self, extension_id: &str) -> Vec<ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.map(|p| ExtensionPermission {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
extension_id: extension_id.to_string(),
|
||||
resource_type: match p.resource_type.as_str() {
|
||||
"fs" => ResourceType::Fs,
|
||||
"http" => ResourceType::Http,
|
||||
"db" => ResourceType::Db,
|
||||
"shell" => ResourceType::Shell,
|
||||
_ => ResourceType::Fs, // Fallback
|
||||
},
|
||||
action: match p.action.as_str() {
|
||||
"read" => Action::Read,
|
||||
"write" => Action::Write,
|
||||
_ => Action::Read, // Fallback
|
||||
},
|
||||
target: p.target.clone(),
|
||||
constraints: p
|
||||
.constraints
|
||||
.as_ref()
|
||||
.and_then(|c| Self::parse_constraints(&p.resource_type, c)),
|
||||
status: match p.status.as_str() {
|
||||
"granted" => PermissionStatus::Granted,
|
||||
"denied" => PermissionStatus::Denied,
|
||||
"ask" => PermissionStatus::Ask,
|
||||
_ => PermissionStatus::Denied, // Fallback
|
||||
},
|
||||
haex_timestamp: None,
|
||||
haex_tombstone: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_constraints(
|
||||
resource_type: &str,
|
||||
json_value: &serde_json::Value,
|
||||
) -> Option<PermissionConstraints> {
|
||||
match resource_type {
|
||||
"db" => serde_json::from_value::<DbConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Database),
|
||||
"fs" => serde_json::from_value::<FsConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Filesystem),
|
||||
"http" => serde_json::from_value::<HttpConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Http),
|
||||
"shell" => serde_json::from_value::<ShellConstraints>(json_value.clone())
|
||||
.ok()
|
||||
.map(PermissionConstraints::Shell),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Filtert nur granted Permissions
|
||||
pub fn filter_granted(&self) -> Vec<EditablePermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.filter(|p| p.status == "granted")
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
pub icon: Option<String>,
|
||||
pub public_key: String,
|
||||
pub signature: String,
|
||||
pub permissions: ExtensionManifestPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
/// Berechnet den Key Hash für diese Extension
|
||||
pub fn calculate_key_hash(&self) -> Result<String, ExtensionError> {
|
||||
ExtensionCrypto::calculate_key_hash(&self.public_key)
|
||||
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })
|
||||
}
|
||||
|
||||
/// Generiert die vollständige Extension ID mit Key Hash Prefix
|
||||
pub fn full_extension_id(&self) -> Result<String, ExtensionError> {
|
||||
let key_hash = self.calculate_key_hash()?;
|
||||
Ok(format!("{}-{}", key_hash, self.id))
|
||||
}
|
||||
pub fn to_editable_permissions(&self) -> EditablePermissions {
|
||||
let mut database = Vec::new();
|
||||
let mut filesystem = Vec::new();
|
||||
let mut http = Vec::new();
|
||||
|
||||
if let Some(db) = &self.permissions.database {
|
||||
for resource in &db.read {
|
||||
database.push(EditableDatabasePermission {
|
||||
operation: "read".to_string(),
|
||||
resource: resource.clone(),
|
||||
status: PermissionStatus::Granted,
|
||||
});
|
||||
}
|
||||
for resource in &db.write {
|
||||
database.push(EditableDatabasePermission {
|
||||
operation: "write".to_string(),
|
||||
resource: resource.clone(),
|
||||
status: PermissionStatus::Granted,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(fs) = &self.permissions.filesystem {
|
||||
for path in &fs.read {
|
||||
filesystem.push(EditableFilesystemPermission {
|
||||
operation: "read".to_string(),
|
||||
path: path.clone(),
|
||||
status: PermissionStatus::Granted,
|
||||
});
|
||||
}
|
||||
for path in &fs.write {
|
||||
filesystem.push(EditableFilesystemPermission {
|
||||
operation: "write".to_string(),
|
||||
path: path.clone(),
|
||||
status: PermissionStatus::Granted,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(http_list) = &self.permissions.http {
|
||||
for domain in http_list {
|
||||
http.push(EditableHttpPermission {
|
||||
domain: domain.clone(),
|
||||
status: PermissionStatus::Granted,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
EditablePermissions {
|
||||
database,
|
||||
filesystem,
|
||||
http,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
/// Konvertiert Manifest zu EditablePermissions (neue Version)
|
||||
pub fn to_editable_permissions(&self) -> EditablePermissions {
|
||||
let mut permissions = Vec::new();
|
||||
|
||||
// Database Permissions
|
||||
if let Some(db) = &self.permissions.database {
|
||||
for resource in &db.read {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "db".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: resource.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
for resource in &db.write {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "db".to_string(),
|
||||
action: "write".to_string(),
|
||||
target: resource.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Filesystem Permissions
|
||||
if let Some(fs) = &self.permissions.filesystem {
|
||||
for path in &fs.read {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "fs".to_string(),
|
||||
action: "read".to_string(),
|
||||
target: path.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
for path in &fs.write {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "fs".to_string(),
|
||||
action: "write".to_string(),
|
||||
target: path.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP Permissions
|
||||
if let Some(http_list) = &self.permissions.http {
|
||||
for domain in http_list {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "http".to_string(),
|
||||
action: "read".to_string(), // HTTP ist meist read
|
||||
target: domain.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Shell Permissions
|
||||
if let Some(shell_list) = &self.permissions.shell {
|
||||
for command in shell_list {
|
||||
permissions.push(EditablePermission {
|
||||
resource_type: "shell".to_string(),
|
||||
action: "read".to_string(), // Shell hat keine action mehr im Schema
|
||||
target: command.clone(),
|
||||
constraints: None,
|
||||
status: "granted".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
EditablePermissions { permissions }
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionInfoResponse {
|
||||
pub key_hash: String,
|
||||
pub name: String,
|
||||
pub full_id: String,
|
||||
pub version: String,
|
||||
pub display_name: Option<String>,
|
||||
pub namespace: Option<String>,
|
||||
pub allowed_origin: String,
|
||||
}
|
||||
|
||||
impl ExtensionInfoResponse {
|
||||
pub fn from_extension(extension: &Extension) -> Result<Self, ExtensionError> {
|
||||
// Bestimme die allowed_origin basierend auf Tauri-Konfiguration
|
||||
let allowed_origin = get_tauri_origin();
|
||||
let key_hash = extension
|
||||
.manifest
|
||||
.calculate_key_hash()
|
||||
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })?;
|
||||
let full_id = extension
|
||||
.manifest
|
||||
.full_extension_id()
|
||||
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })?;
|
||||
|
||||
Ok(Self {
|
||||
key_hash,
|
||||
name: extension.manifest.name.clone(),
|
||||
full_id,
|
||||
version: extension.manifest.version.clone(),
|
||||
display_name: Some(extension.manifest.name.clone()),
|
||||
namespace: extension.manifest.author.clone(),
|
||||
allowed_origin,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tauri_origin() -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"https://tauri.localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
"tauri://localhost".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension source type (production vs development)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtensionSource {
|
||||
Production {
|
||||
path: PathBuf,
|
||||
version: String,
|
||||
},
|
||||
Development {
|
||||
dev_server_url: String,
|
||||
manifest_path: PathBuf,
|
||||
auto_reload: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Complete extension data structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Extension {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub source: ExtensionSource,
|
||||
pub manifest: ExtensionManifest,
|
||||
pub enabled: bool,
|
||||
pub last_accessed: SystemTime,
|
||||
}
|
||||
|
||||
/// Cached permission data for performance
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedPermission {
|
||||
pub permissions: Vec<DbExtensionPermission>,
|
||||
pub cached_at: SystemTime,
|
||||
pub ttl: Duration,
|
||||
}
|
||||
|
||||
/// Enhanced extension manager
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionManager {
|
||||
pub production_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub dev_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
|
||||
}
|
||||
|
||||
impl ExtensionManager {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn get_base_extension_dir(&self, app_handle: AppHandle) -> Result<PathBuf, ExtensionError> {
|
||||
let path = app_handle
|
||||
.path()
|
||||
.app_local_data_dir() // Korrekt für Ressourcen
|
||||
// Wenn du stattdessen App Local Data willst: .app_local_data_dir()
|
||||
.map_err(|e| ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
|
||||
})?
|
||||
.join("extensions");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn get_extension_dir(
|
||||
&self,
|
||||
app_handle: AppHandle,
|
||||
extension_id: &str,
|
||||
extension_version: &str,
|
||||
) -> Result<PathBuf, ExtensionError> {
|
||||
let specific_extension_dir = self
|
||||
.get_base_extension_dir(app_handle)?
|
||||
.join(extension_id)
|
||||
.join(extension_version);
|
||||
|
||||
Ok(specific_extension_dir)
|
||||
}
|
||||
|
||||
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate filesystem permissions
|
||||
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
|
||||
fs_perms.validate()?;
|
||||
}
|
||||
*/
|
||||
match &extension.source {
|
||||
ExtensionSource::Production { .. } => {
|
||||
let mut extensions = self.production_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Production source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate filesystem permissions
|
||||
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
|
||||
fs_perms.validate()?;
|
||||
} */
|
||||
|
||||
match &extension.source {
|
||||
ExtensionSource::Development { .. } => {
|
||||
let mut extensions = self.dev_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Development source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
|
||||
// Dev extensions take priority
|
||||
let dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if let Some(extension) = dev_extensions.get(extension_id) {
|
||||
return Some(extension.clone());
|
||||
}
|
||||
|
||||
// Then check production
|
||||
let prod_extensions = self.production_extensions.lock().unwrap();
|
||||
prod_extensions.get(extension_id).cloned()
|
||||
}
|
||||
|
||||
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
|
||||
{
|
||||
let mut dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if dev_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut prod_extensions = self.production_extensions.lock().unwrap();
|
||||
if prod_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ExtensionError::NotFound {
|
||||
id: extension_id.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn remove_extension_internal(
|
||||
&self,
|
||||
app_handle: AppHandle,
|
||||
extension_id: String,
|
||||
extension_version: String,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<(), ExtensionError> {
|
||||
// Permissions löschen (verwendet jetzt die neue Methode)
|
||||
PermissionManager::delete_permissions(state, &extension_id).await?;
|
||||
|
||||
// Extension aus Manager entfernen
|
||||
self.remove_extension(&extension_id)?;
|
||||
|
||||
let extension_dir =
|
||||
self.get_extension_dir(app_handle, &extension_id, &extension_version)?;
|
||||
|
||||
// Dateien löschen
|
||||
if extension_dir.exists() {
|
||||
std::fs::remove_dir_all(&extension_dir)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn preview_extension_internal(
|
||||
&self,
|
||||
source_path: String,
|
||||
) -> Result<ExtensionPreview, ExtensionError> {
|
||||
let source = PathBuf::from(&source_path);
|
||||
|
||||
// ZIP in temp entpacken
|
||||
let temp = std::env::temp_dir().join(format!("haexhub_preview_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let mut archive =
|
||||
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
archive
|
||||
.extract(&temp)
|
||||
.map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
// Manifest laden
|
||||
let manifest_path = temp.join("manifest.json");
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
// Signatur verifizieren
|
||||
let content_hash = ExtensionCrypto::hash_directory(&temp)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
let is_valid_signature = ExtensionCrypto::verify_signature(
|
||||
&manifest.public_key,
|
||||
&content_hash,
|
||||
&manifest.signature,
|
||||
)
|
||||
.is_ok();
|
||||
|
||||
let key_hash = manifest.calculate_key_hash()?;
|
||||
|
||||
// Editable permissions erstellen
|
||||
let editable_permissions = manifest.to_editable_permissions();
|
||||
|
||||
// Cleanup
|
||||
std::fs::remove_dir_all(&temp).ok();
|
||||
|
||||
Ok(ExtensionPreview {
|
||||
manifest,
|
||||
is_valid_signature,
|
||||
key_hash,
|
||||
editable_permissions,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn install_extension_with_permissions_internal(
|
||||
&self,
|
||||
app_handle: AppHandle,
|
||||
source_path: String,
|
||||
custom_permissions: EditablePermissions,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<String, ExtensionError> {
|
||||
let source = PathBuf::from(&source_path);
|
||||
|
||||
// 1. ZIP entpacken
|
||||
let temp = std::env::temp_dir().join(format!("haexhub_ext_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
let mut archive =
|
||||
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Invalid ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
archive
|
||||
.extract(&temp)
|
||||
.map_err(|e| ExtensionError::InstallationFailed {
|
||||
reason: format!("Cannot extract ZIP: {}", e),
|
||||
})?;
|
||||
|
||||
// 2. Manifest laden
|
||||
let manifest_path = temp.join("manifest.json");
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Cannot read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
// 3. Signatur verifizieren
|
||||
let content_hash = ExtensionCrypto::hash_directory(&temp)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)
|
||||
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
|
||||
|
||||
// 4. Key Hash berechnen
|
||||
let key_hash = manifest.calculate_key_hash()?;
|
||||
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
|
||||
|
||||
// 5. Zielverzeichnis
|
||||
let extensions_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
|
||||
})?
|
||||
.join("extensions")
|
||||
.join(&full_extension_id)
|
||||
.join(&manifest.version);
|
||||
|
||||
std::fs::create_dir_all(&extensions_dir)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
// 6. Dateien kopieren
|
||||
copy_directory(
|
||||
temp.to_string_lossy().to_string(),
|
||||
extensions_dir.to_string_lossy().to_string(),
|
||||
)?;
|
||||
|
||||
// 7. Temp aufräumen
|
||||
std::fs::remove_dir_all(&temp).ok();
|
||||
|
||||
// 8. Custom Permissions konvertieren und speichern
|
||||
let permissions = custom_permissions.to_internal_permissions(&full_extension_id);
|
||||
let granted_permissions = permissions.filter_granted();
|
||||
PermissionManager::save_permissions(&state.db, &granted_permissions).await?;
|
||||
|
||||
// 9. Extension registrieren
|
||||
let extension = Extension {
|
||||
id: full_extension_id.clone(),
|
||||
name: manifest.name.clone(),
|
||||
source: ExtensionSource::Production {
|
||||
path: extensions_dir.clone(),
|
||||
version: manifest.version.clone(),
|
||||
},
|
||||
manifest: manifest.clone(),
|
||||
enabled: true,
|
||||
last_accessed: SystemTime::now(),
|
||||
};
|
||||
|
||||
state
|
||||
.extension_manager
|
||||
.add_production_extension(extension)?;
|
||||
|
||||
Ok(full_extension_id)
|
||||
}
|
||||
}
|
||||
|
||||
// For backward compatibility
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
|
||||
}
|
||||
|
||||
impl ExtensionState {
|
||||
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
|
||||
let mut extensions = self.extensions.lock().unwrap();
|
||||
extensions.insert(path, manifest);
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
|
||||
let extensions = self.extensions.lock().unwrap();
|
||||
extensions.values().find(|p| p.name == addon_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ExtensionInfo {
|
||||
id: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DataProcessingError {
|
||||
HexDecoding(hex::FromHexError),
|
||||
Utf8Conversion(std::string::FromUtf8Error),
|
||||
JsonParsing(serde_json::Error),
|
||||
}
|
||||
|
||||
// Implementierung von Display für benutzerfreundliche Fehlermeldungen
|
||||
impl fmt::Display for DataProcessingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DataProcessingError::HexDecoding(e) => write!(f, "Hex-Dekodierungsfehler: {}", e),
|
||||
DataProcessingError::Utf8Conversion(e) => {
|
||||
write!(f, "UTF-8-Konvertierungsfehler: {}", e)
|
||||
}
|
||||
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implementierung von std::error::Error (optional, aber gute Praxis für bibliotheksähnlichen Code)
|
||||
impl std::error::Error for DataProcessingError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
DataProcessingError::HexDecoding(e) => Some(e),
|
||||
DataProcessingError::Utf8Conversion(e) => Some(e),
|
||||
DataProcessingError::JsonParsing(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implementierung von From-Traits für einfache Verwendung des '?'-Operators
|
||||
impl From<hex::FromHexError> for DataProcessingError {
|
||||
fn from(err: hex::FromHexError) -> Self {
|
||||
DataProcessingError::HexDecoding(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for DataProcessingError {
|
||||
fn from(err: std::string::FromUtf8Error) -> Self {
|
||||
DataProcessingError::Utf8Conversion(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for DataProcessingError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
DataProcessingError::JsonParsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_directory(source: String, destination: String) -> Result<(), ExtensionError> {
|
||||
println!(
|
||||
"Kopiere Verzeichnis von '{}' nach '{}'",
|
||||
source, destination
|
||||
);
|
||||
|
||||
let source_path = PathBuf::from(&source);
|
||||
let destination_path = PathBuf::from(&destination);
|
||||
|
||||
if !source_path.exists() || !source_path.is_dir() {
|
||||
return Err(ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("Source directory '{}' not found", source),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Optionen für fs_extra::dir::copy
|
||||
let mut options = fs_extra::dir::CopyOptions::new();
|
||||
options.overwrite = true; // Überschreibe Zieldateien, falls sie existieren
|
||||
options.copy_inside = true; // Kopiere den *Inhalt* des Quellordners in den Zielordner
|
||||
// options.content_only = true; // Alternative: nur Inhalt kopieren, Zielordner muss existieren
|
||||
options.buffer_size = 64000; // Standard-Puffergröße, kann angepasst werden
|
||||
|
||||
// Führe die Kopieroperation aus
|
||||
fs_extra::dir::copy(&source_path, &destination_path, &options).map_err(|e| {
|
||||
ExtensionError::Filesystem {
|
||||
source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resolve_secure_extension_asset_path(
|
||||
app_handle: AppHandle,
|
||||
state: State<AppState>,
|
||||
extension_id: &str,
|
||||
extension_version: &str,
|
||||
requested_asset_path: &str,
|
||||
) -> Result<PathBuf, ExtensionError> {
|
||||
// 1. Validiere die Extension ID
|
||||
if extension_id.is_empty()
|
||||
|| !extension_id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-')
|
||||
{
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: format!("Invalid extension ID: {}", extension_id),
|
||||
});
|
||||
}
|
||||
|
||||
if extension_version.is_empty()
|
||||
|| !extension_version
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
|
||||
{
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: format!("Invalid extension version: {}", extension_version),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Verzeichnis für die spezifische Erweiterung
|
||||
let specific_extension_dir =
|
||||
state
|
||||
.extension_manager
|
||||
.get_extension_dir(app_handle, extension_id, extension_version)?;
|
||||
|
||||
// 4. Bereinige den angeforderten Asset-Pfad
|
||||
let clean_relative_path = requested_asset_path
|
||||
.replace('\\', "/")
|
||||
.trim_start_matches('/')
|
||||
.split('/')
|
||||
.filter(|&part| !part.is_empty() && part != "." && part != "..")
|
||||
.collect::<PathBuf>();
|
||||
|
||||
if clean_relative_path.as_os_str().is_empty() && requested_asset_path != "/" {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Empty or invalid asset path".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Setze den finalen Pfad zusammen
|
||||
let final_path = specific_extension_dir.join(clean_relative_path);
|
||||
|
||||
// 6. SICHERHEITSCHECK
|
||||
match final_path.canonicalize() {
|
||||
Ok(canonical_path) => {
|
||||
let canonical_base = specific_extension_dir
|
||||
.canonicalize()
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
if canonical_path.starts_with(&canonical_base) {
|
||||
Ok(canonical_path)
|
||||
} else {
|
||||
eprintln!( /* ... Sicherheitswarnung ... */ );
|
||||
Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Path traversal attempt: {}", requested_asset_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Fehler bei canonicalize (z.B. Pfad existiert nicht)
|
||||
if final_path.starts_with(&specific_extension_dir) {
|
||||
Ok(final_path) // Nicht-kanonisierten Pfad zurückgeben
|
||||
} else {
|
||||
eprintln!( /* ... Sicherheitswarnung ... */ );
|
||||
Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Invalid asset path: {}", requested_asset_path),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extension_protocol_handler<R: Runtime>(
|
||||
state: State<AppState>,
|
||||
app_handle: AppHandle,
|
||||
request: &Request<Vec<u8>>,
|
||||
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
let uri_ref = request.uri();
|
||||
println!("Protokoll Handler für: {}", uri_ref);
|
||||
|
||||
let host = uri_ref
|
||||
.host()
|
||||
.ok_or("Kein Host (Extension ID) in URI gefunden")?
|
||||
.to_string();
|
||||
|
||||
let path_str = uri_ref.path();
|
||||
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
|
||||
let resource_segments: Vec<&str> = segments_iter.collect();
|
||||
let raw_asset_path = resource_segments.join("/");
|
||||
let asset_to_load = if raw_asset_path.is_empty() {
|
||||
"index.html"
|
||||
} else {
|
||||
&raw_asset_path
|
||||
};
|
||||
|
||||
match process_hex_encoded_json(&host) {
|
||||
Ok(info) => {
|
||||
println!("Daten erfolgreich verarbeitet:");
|
||||
println!(" ID: {}", info.id);
|
||||
println!(" Version: {}", info.version);
|
||||
let absolute_secure_path = resolve_secure_extension_asset_path(
|
||||
app_handle,
|
||||
state,
|
||||
&info.id,
|
||||
&info.version,
|
||||
&asset_to_load,
|
||||
)?;
|
||||
|
||||
println!("absolute_secure_path: {}", absolute_secure_path.display());
|
||||
|
||||
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
|
||||
match fs::read(&absolute_secure_path) {
|
||||
Ok(content) => {
|
||||
let mime_type = mime_guess::from_path(&absolute_secure_path)
|
||||
.first_or(mime::APPLICATION_OCTET_STREAM)
|
||||
.to_string();
|
||||
let content_length = content.len();
|
||||
println!(
|
||||
"Liefere {} ({}, {} bytes) ", // Content-Length zum Log hinzugefügt
|
||||
absolute_secure_path.display(),
|
||||
mime_type,
|
||||
content_length
|
||||
);
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", mime_type)
|
||||
.header("Content-Length", content_length.to_string()) // <-- HIER HINZUGEFÜGT
|
||||
// Optional, aber gut für Streaming-Fähigkeit:
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.body(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Fehler beim Lesen der Datei {}: {}",
|
||||
absolute_secure_path.display(),
|
||||
e
|
||||
);
|
||||
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
|
||||
404
|
||||
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
403
|
||||
} else {
|
||||
500
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(status_code)
|
||||
.body(Vec::new()) // Leerer Body für Fehler
|
||||
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Datei nicht gefunden oder es ist keine Datei
|
||||
eprintln!(
|
||||
"Asset nicht gefunden oder ist kein File: {}",
|
||||
absolute_secure_path.display()
|
||||
);
|
||||
Response::builder()
|
||||
.status(404) // HTTP 404 Not Found
|
||||
.body(Vec::new())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
|
||||
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.body(Vec::new()) // Leerer Body für Fehler
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProcessingError> {
|
||||
// Schritt 1: Hex-String zu Bytes dekodieren
|
||||
let bytes = hex::decode(hex_input)?; // Konvertiert hex::FromHexError automatisch
|
||||
|
||||
// Schritt 2: Bytes zu UTF-8-String konvertieren
|
||||
let json_string = String::from_utf8(bytes)?; // Konvertiert FromUtf8Error automatisch
|
||||
|
||||
// Schritt 3: JSON-String zu Struktur parsen
|
||||
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?; // Konvertiert serde_json::Error automatisch
|
||||
|
||||
Ok(extension_info)
|
||||
}
|
||||
@ -1,26 +1,16 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
// src-tauri/src/extension/crypto.rs
|
||||
use crate::extension::error::ExtensionError;
|
||||
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub struct 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
|
||||
pub fn verify_signature(
|
||||
public_key_hex: &str,
|
||||
@ -45,30 +35,110 @@ impl ExtensionCrypto {
|
||||
}
|
||||
|
||||
/// Berechnet Hash eines Verzeichnisses (für Verifikation)
|
||||
pub fn hash_directory(dir: &std::path::Path) -> Result<String, String> {
|
||||
use std::fs;
|
||||
pub fn hash_directory(dir: &Path, manifest_path: &Path) -> Result<String, ExtensionError> {
|
||||
// 1. Alle Dateipfade rekursiv sammeln
|
||||
let mut all_files = Vec::new();
|
||||
Self::collect_files_recursively(dir, &mut all_files)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
let mut entries: Vec<_> = fs::read_dir(dir)
|
||||
.map_err(|e| format!("Cannot read directory: {}", e))?
|
||||
.filter_map(|e| e.ok())
|
||||
// 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();
|
||||
|
||||
// Sortieren für deterministische Hashes
|
||||
entries.sort_by_key(|e| e.path());
|
||||
// 3. Sortiere nach relativen Pfaden
|
||||
relative_files.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let content = fs::read(&path)
|
||||
.map_err(|e| format!("Cannot read file {}: {}", path.display(), e))?;
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
// Canonicalize manifest path for comparison (important on Android where symlinks may differ)
|
||||
// Also ensure the canonical path is still within the allowed directory (security check)
|
||||
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:
|
||||
let content_str = fs::read_to_string(&file_path)
|
||||
.map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
|
||||
// Parse zu einem generischen JSON-Wert
|
||||
let mut manifest: serde_json::Value =
|
||||
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
|
||||
if let Some(obj) = manifest.as_object_mut() {
|
||||
obj.insert(
|
||||
"signature".to_string(),
|
||||
serde_json::Value::String("".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
// Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS)
|
||||
// serde_json sortiert die Keys automatisch alphabetisch
|
||||
let canonical_manifest_content =
|
||||
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 {
|
||||
// FÜR ALLE ANDEREN DATEIEN:
|
||||
let content =
|
||||
fs::read(&file_path).map_err(|e| ExtensionError::Filesystem { source: e })?;
|
||||
hasher.update(&content);
|
||||
} else if path.is_dir() {
|
||||
let subdir_hash = Self::hash_directory(&path)?;
|
||||
hasher.update(hex::decode(&subdir_hash).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(hex::encode(hasher.finalize()))
|
||||
}
|
||||
|
||||
fn collect_files_recursively(dir: &Path, file_list: &mut Vec<PathBuf>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
Self::collect_files_recursively(&path, file_list)?;
|
||||
} else {
|
||||
file_list.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
// src-tauri/src/extension/database/executor.rs (neu)
|
||||
// src-tauri/src/extension/database/executor.rs
|
||||
|
||||
use crate::crdt::hlc::HlcService;
|
||||
use crate::crdt::transformer::CrdtTransformer;
|
||||
use crate::crdt::trigger;
|
||||
use crate::database::core::{parse_sql_statements, ValueConverter};
|
||||
use crate::database::core::{convert_value_ref_to_json, parse_sql_statements, ValueConverter};
|
||||
use crate::database::error::DatabaseError;
|
||||
use rusqlite::{params_from_iter, Transaction};
|
||||
use rusqlite::{params_from_iter, types::Value as SqliteValue, ToSql, Transaction};
|
||||
use serde_json::Value as JsonValue;
|
||||
use sqlparser::ast::Statement;
|
||||
use std::collections::HashSet;
|
||||
@ -14,29 +14,28 @@ use std::collections::HashSet;
|
||||
pub struct SqlExecutor;
|
||||
|
||||
impl SqlExecutor {
|
||||
/// Führt SQL aus (mit CRDT-Transformation) - OHNE Permission-Check
|
||||
pub fn execute_internal(
|
||||
/// Führt ein SQL Statement OHNE RETURNING aus (mit CRDT)
|
||||
/// Returns: modified_schema_tables
|
||||
pub fn execute_internal_typed(
|
||||
tx: &Transaction,
|
||||
hlc_service: &HlcService,
|
||||
sql: &str,
|
||||
params: &[JsonValue],
|
||||
params: &[&dyn ToSql],
|
||||
) -> Result<HashSet<String>, DatabaseError> {
|
||||
// Parameter validation
|
||||
let total_placeholders = sql.matches('?').count();
|
||||
if total_placeholders != params.len() {
|
||||
return Err(DatabaseError::ParameterMismatchError {
|
||||
expected: total_placeholders,
|
||||
provided: params.len(),
|
||||
let mut ast_vec = parse_sql_statements(sql)?;
|
||||
|
||||
if ast_vec.len() != 1 {
|
||||
return Err(DatabaseError::ExecutionError {
|
||||
sql: sql.to_string(),
|
||||
reason: "execute_internal_typed should only receive a single SQL statement"
|
||||
.to_string(),
|
||||
table: None,
|
||||
});
|
||||
}
|
||||
|
||||
// SQL parsing
|
||||
let mut ast_vec = parse_sql_statements(sql)?;
|
||||
let mut statement = ast_vec.pop().unwrap();
|
||||
|
||||
let transformer = CrdtTransformer::new();
|
||||
|
||||
// Generate HLC timestamp
|
||||
let hlc_timestamp =
|
||||
hlc_service
|
||||
.new_timestamp_and_persist(tx)
|
||||
@ -44,110 +43,242 @@ impl SqlExecutor {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
// Transform statements
|
||||
let mut modified_schema_tables = HashSet::new();
|
||||
for statement in &mut ast_vec {
|
||||
if let Some(table_name) =
|
||||
transformer.transform_execute_statement(statement, &hlc_timestamp)?
|
||||
{
|
||||
modified_schema_tables.insert(table_name);
|
||||
}
|
||||
if let Some(table_name) = transformer.transform_execute_statement_with_table_info(
|
||||
&mut statement,
|
||||
&hlc_timestamp,
|
||||
)? {
|
||||
modified_schema_tables.insert(table_name);
|
||||
}
|
||||
|
||||
// Convert parameters
|
||||
let sql_values = ValueConverter::convert_params(params)?;
|
||||
let sql_str = statement.to_string();
|
||||
eprintln!("DEBUG: Transformed execute SQL: {}", sql_str);
|
||||
|
||||
// Execute statements
|
||||
for statement in ast_vec {
|
||||
let sql_str = statement.to_string();
|
||||
// Führe Statement aus
|
||||
tx.execute(&sql_str, params)
|
||||
.map_err(|e| DatabaseError::ExecutionError {
|
||||
sql: sql_str.clone(),
|
||||
table: None,
|
||||
reason: format!("Execute failed: {}", e),
|
||||
})?;
|
||||
|
||||
tx.execute(&sql_str, params_from_iter(sql_values.iter()))
|
||||
.map_err(|e| DatabaseError::ExecutionError {
|
||||
sql: sql_str.clone(),
|
||||
table: None,
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
if let Statement::CreateTable(create_table_details) = statement {
|
||||
let table_name_str = create_table_details.name.to_string();
|
||||
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
||||
}
|
||||
// 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: {}", table_name_str);
|
||||
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
|
||||
}
|
||||
|
||||
Ok(modified_schema_tables)
|
||||
}
|
||||
|
||||
/// Führt SELECT aus (mit CRDT-Transformation) - OHNE Permission-Check
|
||||
pub fn select_internal(
|
||||
conn: &rusqlite::Connection,
|
||||
/// Führt ein SQL Statement MIT RETURNING aus (mit CRDT)
|
||||
/// Returns: (modified_schema_tables, returning_results)
|
||||
pub fn query_internal_typed(
|
||||
tx: &Transaction,
|
||||
hlc_service: &HlcService,
|
||||
sql: &str,
|
||||
params: &[JsonValue],
|
||||
) -> Result<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(),
|
||||
params: &[&dyn ToSql],
|
||||
) -> Result<(HashSet<String>, Vec<Vec<JsonValue>>), DatabaseError> {
|
||||
let mut ast_vec = parse_sql_statements(sql)?;
|
||||
|
||||
if ast_vec.len() != 1 {
|
||||
return Err(DatabaseError::ExecutionError {
|
||||
sql: sql.to_string(),
|
||||
reason: "query_internal_typed should only receive a single SQL statement"
|
||||
.to_string(),
|
||||
table: None,
|
||||
});
|
||||
}
|
||||
|
||||
let mut ast_vec = parse_sql_statements(sql)?;
|
||||
let mut statement = ast_vec.pop().unwrap();
|
||||
|
||||
if ast_vec.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
let transformer = CrdtTransformer::new();
|
||||
|
||||
let last_statement = ast_vec.pop().unwrap();
|
||||
let mut stmt_to_execute = last_statement;
|
||||
|
||||
transformer.transform_select_statement(&mut stmt_to_execute)?;
|
||||
let transformed_sql = stmt_to_execute.to_string();
|
||||
|
||||
let mut prepared_stmt =
|
||||
conn.prepare(&transformed_sql)
|
||||
.map_err(|e| DatabaseError::ExecutionError {
|
||||
sql: transformed_sql.clone(),
|
||||
let hlc_timestamp =
|
||||
hlc_service
|
||||
.new_timestamp_and_persist(tx)
|
||||
.map_err(|e| DatabaseError::HlcError {
|
||||
reason: e.to_string(),
|
||||
table: None,
|
||||
})?;
|
||||
|
||||
let column_names: Vec<String> = prepared_stmt
|
||||
let mut modified_schema_tables = HashSet::new();
|
||||
if let Some(table_name) = transformer.transform_execute_statement_with_table_info(
|
||||
&mut statement,
|
||||
&hlc_timestamp,
|
||||
)? {
|
||||
modified_schema_tables.insert(table_name);
|
||||
}
|
||||
|
||||
let sql_str = statement.to_string();
|
||||
eprintln!("DEBUG: Transformed SQL (with RETURNING): {}", sql_str);
|
||||
|
||||
// Prepare und query ausführen
|
||||
let mut stmt = tx
|
||||
.prepare(&sql_str)
|
||||
.map_err(|e| DatabaseError::ExecutionError {
|
||||
sql: sql_str.clone(),
|
||||
table: None,
|
||||
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 rows = prepared_stmt
|
||||
.query_map(params_from_iter(sql_params.iter()), |row| {
|
||||
crate::extension::database::row_to_json_value(row, &column_names)
|
||||
})
|
||||
.map_err(|e| DatabaseError::QueryError {
|
||||
let mut rows = stmt
|
||||
.query(params_from_iter(params.iter()))
|
||||
.map_err(|e| DatabaseError::ExecutionError {
|
||||
sql: sql_str.clone(),
|
||||
table: None,
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for row_result in rows {
|
||||
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError {
|
||||
reason: e.to_string(),
|
||||
})?);
|
||||
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
||||
|
||||
// Lese alle RETURNING Zeilen
|
||||
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::new();
|
||||
for i in 0..num_columns {
|
||||
let value_ref = row.get_ref(i).map_err(|e| DatabaseError::ExecutionError {
|
||||
sql: sql_str.clone(),
|
||||
table: None,
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
let json_value = convert_value_ref_to_json(value_ref)?;
|
||||
row_values.push(json_value);
|
||||
}
|
||||
result_vec.push(row_values);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
// 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))
|
||||
}
|
||||
|
||||
/// Führt ein einzelnes SQL Statement OHNE Typinformationen aus (JSON params)
|
||||
pub fn execute_internal(
|
||||
tx: &Transaction,
|
||||
hlc_service: &HlcService,
|
||||
sql: &str,
|
||||
params: &[JsonValue],
|
||||
) -> Result<HashSet<String>, DatabaseError> {
|
||||
let sql_params: Vec<SqliteValue> = params
|
||||
.iter()
|
||||
.map(|v| crate::database::core::ValueConverter::json_to_rusqlite_value(v))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let param_refs: Vec<&dyn ToSql> = sql_params.iter().map(|p| p as &dyn ToSql).collect();
|
||||
Self::execute_internal_typed(tx, hlc_service, sql, ¶m_refs)
|
||||
}
|
||||
|
||||
/// Query-Variante (mit RETURNING) OHNE Typinformationen (JSON params)
|
||||
pub fn query_internal(
|
||||
tx: &Transaction,
|
||||
hlc_service: &HlcService,
|
||||
sql: &str,
|
||||
params: &[JsonValue],
|
||||
) -> Result<(HashSet<String>, Vec<Vec<JsonValue>>), DatabaseError> {
|
||||
let sql_params: Vec<SqliteValue> = params
|
||||
.iter()
|
||||
.map(|v| crate::database::core::ValueConverter::json_to_rusqlite_value(v))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let param_refs: Vec<&dyn ToSql> = sql_params.iter().map(|p| p as &dyn ToSql).collect();
|
||||
Self::query_internal_typed(tx, hlc_service, sql, ¶m_refs)
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
|
||||
if ast_vec.len() != 1 {
|
||||
return Err(DatabaseError::ExecutionError {
|
||||
sql: sql.to_string(),
|
||||
reason: "query_select should only receive a single SELECT statement".to_string(),
|
||||
table: None,
|
||||
});
|
||||
}
|
||||
|
||||
// Hard Delete: Keine SELECT-Transformation mehr nötig
|
||||
let stmt_to_execute = ast_vec.pop().unwrap();
|
||||
let transformed_sql = stmt_to_execute.to_string();
|
||||
|
||||
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 num_columns = prepared_stmt.column_count();
|
||||
|
||||
let param_refs: Vec<&dyn ToSql> = sql_params.iter().map(|p| p as &dyn ToSql).collect();
|
||||
|
||||
let mut rows = prepared_stmt.query(params_from_iter(param_refs.iter()))?;
|
||||
|
||||
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 {
|
||||
let value_ref = row.get_ref(i)?;
|
||||
let json_value = convert_value_ref_to_json(value_ref)?;
|
||||
row_values.push(json_value);
|
||||
}
|
||||
result.push(row_values);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
// src-tauri/src/extension/database/mod.rs
|
||||
|
||||
pub mod executor;
|
||||
use crate::crdt::hlc::HlcService;
|
||||
use crate::crdt::transformer::CrdtTransformer;
|
||||
use crate::crdt::trigger;
|
||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::extension::database::executor::SqlExecutor;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::validator::SqlPermissionValidator;
|
||||
use crate::AppState;
|
||||
@ -22,15 +22,11 @@ use tauri::State;
|
||||
/// Führt Statements mit korrekter Parameter-Bindung aus
|
||||
pub struct StatementExecutor<'a> {
|
||||
transaction: &'a Transaction<'a>,
|
||||
hlc_service: &'a HlcService,
|
||||
}
|
||||
|
||||
impl<'a> StatementExecutor<'a> {
|
||||
fn new(transaction: &'a Transaction<'a>, hlc_service: &'a HlcService) -> Self {
|
||||
Self {
|
||||
transaction,
|
||||
hlc_service,
|
||||
}
|
||||
fn new(transaction: &'a Transaction<'a>) -> Self {
|
||||
Self { transaction }
|
||||
}
|
||||
|
||||
/// Führt ein einzelnes Statement mit Parametern aus
|
||||
@ -112,12 +108,21 @@ impl<'a> StatementExecutor<'a> {
|
||||
pub async fn extension_sql_execute(
|
||||
sql: &str,
|
||||
params: Vec<JsonValue>,
|
||||
extension_id: String,
|
||||
public_key: String,
|
||||
name: String,
|
||||
state: State<'_, AppState>,
|
||||
hlc_service: State<'_, HlcService>,
|
||||
) -> Result<Vec<String>, ExtensionError> {
|
||||
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||
// Get extension to retrieve its ID
|
||||
let extension = state
|
||||
.extension_manager
|
||||
.get_extension_by_public_key_and_name(&public_key, &name)?
|
||||
.ok_or_else(|| ExtensionError::NotFound {
|
||||
public_key: public_key.clone(),
|
||||
name: name.clone(),
|
||||
})?;
|
||||
|
||||
// Permission check
|
||||
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
|
||||
SqlPermissionValidator::validate_sql(&state, &extension.id, sql).await?;
|
||||
|
||||
// Parameter validation
|
||||
validate_params(sql, ¶ms)?;
|
||||
@ -125,56 +130,87 @@ pub async fn extension_sql_execute(
|
||||
// SQL parsing
|
||||
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
|
||||
with_connection(&state.db, |conn| {
|
||||
let tx = conn.transaction().map_err(DatabaseError::from)?;
|
||||
|
||||
let transformer = CrdtTransformer::new();
|
||||
let executor = StatementExecutor::new(&tx, &hlc_service);
|
||||
|
||||
// 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
|
||||
let hlc_timestamp =
|
||||
hlc_service
|
||||
.new_timestamp_and_persist(&tx)
|
||||
.map_err(|e| DatabaseError::HlcError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
let hlc_timestamp = hlc_service
|
||||
.new_timestamp_and_persist(&tx)
|
||||
.map_err(|e| DatabaseError::HlcError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
// Transform statements
|
||||
let mut modified_schema_tables = HashSet::new();
|
||||
for statement in &mut ast_vec {
|
||||
if let Some(table_name) =
|
||||
transformer.transform_execute_statement(statement, &hlc_timestamp)?
|
||||
{
|
||||
modified_schema_tables.insert(table_name);
|
||||
}
|
||||
}
|
||||
// Transform statement
|
||||
transformer.transform_execute_statement(&mut statement, &hlc_timestamp)?;
|
||||
|
||||
// Convert parameters
|
||||
// Convert parameters to references
|
||||
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
|
||||
for statement in ast_vec {
|
||||
executor.execute_statement_with_params(&statement, &sql_values)?;
|
||||
let result = if has_returning {
|
||||
// Use query_internal for statements with RETURNING
|
||||
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 {
|
||||
let table_name_str = create_table_details.name.to_string();
|
||||
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
|
||||
);
|
||||
}
|
||||
// Handle CREATE TABLE trigger setup
|
||||
if let Statement::CreateTable(ref create_table_details) = statement {
|
||||
// Extract table name and remove quotes (both " and `)
|
||||
let raw_name = create_table_details.name.to_string();
|
||||
println!("DEBUG: Raw table name from AST: {:?}", raw_name);
|
||||
println!("DEBUG: Raw table name chars: {:?}", raw_name.chars().collect::<Vec<_>>());
|
||||
|
||||
let table_name_str = raw_name
|
||||
.trim_matches('"')
|
||||
.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
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
|
||||
Ok(modified_schema_tables.into_iter().collect())
|
||||
Ok(result)
|
||||
})
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
@ -183,11 +219,21 @@ pub async fn extension_sql_execute(
|
||||
pub async fn extension_sql_select(
|
||||
sql: &str,
|
||||
params: Vec<JsonValue>,
|
||||
extension_id: String,
|
||||
public_key: String,
|
||||
name: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<JsonValue>, ExtensionError> {
|
||||
) -> Result<Vec<Vec<JsonValue>>, ExtensionError> {
|
||||
// Get extension to retrieve its ID
|
||||
let extension = state
|
||||
.extension_manager
|
||||
.get_extension_by_public_key_and_name(&public_key, &name)?
|
||||
.ok_or_else(|| ExtensionError::NotFound {
|
||||
public_key: public_key.clone(),
|
||||
name: name.clone(),
|
||||
})?;
|
||||
|
||||
// Permission check
|
||||
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
|
||||
SqlPermissionValidator::validate_sql(&state, &extension.id, sql).await?;
|
||||
|
||||
// Parameter validation
|
||||
validate_params(sql, ¶ms)?;
|
||||
@ -213,17 +259,10 @@ pub async fn extension_sql_select(
|
||||
}
|
||||
}
|
||||
|
||||
// Database operation
|
||||
// Database operation - return Vec<Vec<JsonValue>> like sql_select_with_crdt
|
||||
with_connection(&state.db, |conn| {
|
||||
let sql_params = ValueConverter::convert_params(¶ms)?;
|
||||
let transformer = CrdtTransformer::new();
|
||||
|
||||
// Use the last statement for result set
|
||||
let last_statement = ast_vec.pop().unwrap();
|
||||
let mut stmt_to_execute = last_statement;
|
||||
|
||||
// Transform the statement
|
||||
transformer.transform_select_statement(&mut stmt_to_execute)?;
|
||||
let stmt_to_execute = ast_vec.pop().unwrap();
|
||||
let transformed_sql = stmt_to_execute.to_string();
|
||||
|
||||
// Prepare and execute query
|
||||
@ -235,51 +274,34 @@ pub async fn extension_sql_select(
|
||||
table: None,
|
||||
})?;
|
||||
|
||||
let column_names: Vec<String> = prepared_stmt
|
||||
.column_names()
|
||||
.into_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)
|
||||
})
|
||||
let num_columns = prepared_stmt.column_count();
|
||||
let mut rows = prepared_stmt
|
||||
.query(params_from_iter(sql_params.iter()))
|
||||
.map_err(|e| DatabaseError::QueryError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for row_result in rows {
|
||||
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError {
|
||||
reason: e.to_string(),
|
||||
})?);
|
||||
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next().map_err(|e| DatabaseError::QueryError {
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
||||
@ -301,15 +323,6 @@ fn count_sql_placeholders(sql: &str) -> usize {
|
||||
sql.matches('?').count()
|
||||
}
|
||||
|
||||
/// Kürzt SQL für Fehlermeldungen
|
||||
fn truncate_sql(sql: &str, max_length: usize) -> String {
|
||||
if sql.len() <= max_length {
|
||||
sql.to_string()
|
||||
} else {
|
||||
format!("{}...", &sql[..max_length])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -327,12 +340,12 @@ mod tests {
|
||||
assert_eq!(count_sql_placeholders("SELECT * FROM users"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/* #[test]
|
||||
fn test_truncate_sql() {
|
||||
let sql = "SELECT * FROM very_long_table_name";
|
||||
assert_eq!(truncate_sql(sql, 10), "SELECT * F...");
|
||||
assert_eq!(truncate_sql(sql, 50), sql);
|
||||
}
|
||||
} */
|
||||
|
||||
#[test]
|
||||
fn test_validate_params() {
|
||||
|
||||
@ -9,14 +9,17 @@ pub enum ExtensionErrorCode {
|
||||
SecurityViolation = 1000,
|
||||
NotFound = 1001,
|
||||
PermissionDenied = 1002,
|
||||
MutexPoisoned = 1003,
|
||||
Database = 2000,
|
||||
Filesystem = 2001,
|
||||
FilesystemWithPath = 2004,
|
||||
Http = 2002,
|
||||
Shell = 2003,
|
||||
Manifest = 3000,
|
||||
Validation = 3001,
|
||||
InvalidPublicKey = 4000,
|
||||
InvalidSignature = 4001,
|
||||
InvalidActionString = 4004,
|
||||
SignatureVerificationFailed = 4002,
|
||||
CalculateHash = 4003,
|
||||
Installation = 5000,
|
||||
@ -36,8 +39,8 @@ pub enum ExtensionError {
|
||||
#[error("Security violation: {reason}")]
|
||||
SecurityViolation { reason: String },
|
||||
|
||||
#[error("Extension not found: {id}")]
|
||||
NotFound { id: String },
|
||||
#[error("Extension not found: {name} (public_key: {public_key})")]
|
||||
NotFound { public_key: String, name: String },
|
||||
|
||||
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
|
||||
PermissionDenied {
|
||||
@ -58,6 +61,12 @@ pub enum ExtensionError {
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("Filesystem operation failed at '{path}': {source}")]
|
||||
FilesystemWithPath {
|
||||
path: String,
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("HTTP request failed: {reason}")]
|
||||
Http { reason: String },
|
||||
|
||||
@ -76,6 +85,12 @@ pub enum ExtensionError {
|
||||
#[error("Invalid Public Key: {reason}")]
|
||||
InvalidPublicKey { reason: String },
|
||||
|
||||
#[error("Invalid Action: {input} for resource {resource_type}")]
|
||||
InvalidActionString {
|
||||
input: String,
|
||||
resource_type: String,
|
||||
},
|
||||
|
||||
#[error("Invalid Signature: {reason}")]
|
||||
InvalidSignature { reason: String },
|
||||
|
||||
@ -87,6 +102,9 @@ pub enum ExtensionError {
|
||||
|
||||
#[error("Extension installation failed: {reason}")]
|
||||
InstallationFailed { reason: String },
|
||||
|
||||
#[error("A mutex was poisoned: {reason}")]
|
||||
MutexPoisoned { reason: String },
|
||||
}
|
||||
|
||||
impl ExtensionError {
|
||||
@ -98,6 +116,7 @@ impl ExtensionError {
|
||||
ExtensionError::PermissionDenied { .. } => ExtensionErrorCode::PermissionDenied,
|
||||
ExtensionError::Database { .. } => ExtensionErrorCode::Database,
|
||||
ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem,
|
||||
ExtensionError::FilesystemWithPath { .. } => ExtensionErrorCode::FilesystemWithPath,
|
||||
ExtensionError::Http { .. } => ExtensionErrorCode::Http,
|
||||
ExtensionError::Shell { .. } => ExtensionErrorCode::Shell,
|
||||
ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest,
|
||||
@ -109,6 +128,8 @@ impl ExtensionError {
|
||||
}
|
||||
ExtensionError::InstallationFailed { .. } => ExtensionErrorCode::Installation,
|
||||
ExtensionError::CalculateHashError { .. } => ExtensionErrorCode::CalculateHash,
|
||||
ExtensionError::MutexPoisoned { .. } => ExtensionErrorCode::MutexPoisoned,
|
||||
ExtensionError::InvalidActionString { .. } => ExtensionErrorCode::InvalidActionString,
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,6 +154,14 @@ impl ExtensionError {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create a filesystem error with path context
|
||||
pub fn filesystem_with_path<P: Into<String>>(path: P, source: std::io::Error) -> Self {
|
||||
Self::FilesystemWithPath {
|
||||
path: path.into(),
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for ExtensionError {
|
||||
|
||||
@ -58,7 +58,7 @@ impl FilesystemPath {
|
||||
/// This would be implemented in your Tauri backend
|
||||
pub fn resolve_system_path(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle,
|
||||
_app_handle: &tauri::AppHandle,
|
||||
) -> Result<String, ExtensionError> {
|
||||
/* let base_dir = match self.path_type {
|
||||
FilesystemPathType::AppData => app_handle.path().app_data_dir(),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/// src-tauri/src/extension/mod.rs
|
||||
use crate::{
|
||||
extension::{
|
||||
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||
core::{manager::ExtensionManager, EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
|
||||
error::ExtensionError,
|
||||
},
|
||||
AppState,
|
||||
@ -16,19 +16,45 @@ pub mod permissions;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_extension_info(
|
||||
extension_id: String,
|
||||
public_key: String,
|
||||
name: String,
|
||||
state: State<AppState>,
|
||||
) -> Result<ExtensionInfoResponse, String> {
|
||||
) -> Result<ExtensionInfoResponse, ExtensionError> {
|
||||
let extension = state
|
||||
.extension_manager
|
||||
.get_extension(&extension_id)
|
||||
.ok_or_else(|| format!("Extension nicht gefunden: {}", extension_id))?;
|
||||
.get_extension_by_public_key_and_name(&public_key, &name)?
|
||||
.ok_or_else(|| ExtensionError::NotFound {
|
||||
public_key: public_key.clone(),
|
||||
name: name.clone(),
|
||||
})?;
|
||||
|
||||
ExtensionInfoResponse::from_extension(&extension).map_err(|e| format!("{:?}", e))
|
||||
ExtensionInfoResponse::from_extension(&extension)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_all_extensions(state: State<AppState>) -> Result<Vec<ExtensionInfoResponse>, String> {
|
||||
pub async fn get_all_extensions(
|
||||
app_handle: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<ExtensionInfoResponse>, String> {
|
||||
// Check if extensions are loaded, if not load them first
|
||||
/* let needs_loading = {
|
||||
let prod_exts = state
|
||||
.extension_manager
|
||||
.production_extensions
|
||||
.lock()
|
||||
.unwrap();
|
||||
let dev_exts = state.extension_manager.dev_extensions.lock().unwrap();
|
||||
prod_exts.is_empty() && dev_exts.is_empty()
|
||||
}; */
|
||||
|
||||
/* if needs_loading { */
|
||||
state
|
||||
.extension_manager
|
||||
.load_installed_extensions(&app_handle, &state)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to load extensions: {:?}", e))?;
|
||||
/* } */
|
||||
|
||||
let mut extensions = Vec::new();
|
||||
|
||||
// Production Extensions
|
||||
@ -56,19 +82,20 @@ pub fn get_all_extensions(state: State<AppState>) -> Result<Vec<ExtensionInfoRes
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn preview_extension(
|
||||
app_handle: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
source_path: String,
|
||||
file_bytes: Vec<u8>,
|
||||
) -> Result<ExtensionPreview, ExtensionError> {
|
||||
state
|
||||
.extension_manager
|
||||
.preview_extension_internal(source_path)
|
||||
.preview_extension_internal(&app_handle, file_bytes)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_extension_with_permissions(
|
||||
app_handle: AppHandle,
|
||||
source_path: String,
|
||||
file_bytes: Vec<u8>,
|
||||
custom_permissions: EditablePermissions,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, ExtensionError> {
|
||||
@ -76,7 +103,7 @@ pub async fn install_extension_with_permissions(
|
||||
.extension_manager
|
||||
.install_extension_with_permissions_internal(
|
||||
app_handle,
|
||||
source_path,
|
||||
file_bytes,
|
||||
custom_permissions,
|
||||
&state,
|
||||
)
|
||||
@ -160,25 +187,246 @@ pub async fn install_extension(
|
||||
#[tauri::command]
|
||||
pub async fn remove_extension(
|
||||
app_handle: AppHandle,
|
||||
extension_id: String,
|
||||
extension_version: String,
|
||||
public_key: String,
|
||||
name: String,
|
||||
version: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ExtensionError> {
|
||||
state
|
||||
.extension_manager
|
||||
.remove_extension_internal(&app_handle, extension_id, extension_version, &state)
|
||||
.remove_extension_internal(&app_handle, &public_key, &name, &version, &state)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn is_extension_installed(
|
||||
extension_id: String,
|
||||
public_key: String,
|
||||
name: String,
|
||||
extension_version: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<bool, String> {
|
||||
if let Some(ext) = state.extension_manager.get_extension(&extension_id) {
|
||||
) -> Result<bool, ExtensionError> {
|
||||
if let Some(ext) = state
|
||||
.extension_manager
|
||||
.get_extension_by_public_key_and_name(&public_key, &name)?
|
||||
{
|
||||
Ok(ext.manifest.version == extension_version)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct HaextensionConfig {
|
||||
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)]
|
||||
struct DevConfig {
|
||||
#[serde(default = "default_port")]
|
||||
port: u16,
|
||||
#[serde(default = "default_host")]
|
||||
host: String,
|
||||
#[serde(default = "default_haextension_dir")]
|
||||
haextension_dir: String,
|
||||
}
|
||||
|
||||
fn default_port() -> u16 {
|
||||
5173
|
||||
}
|
||||
|
||||
fn default_host() -> 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
|
||||
async fn check_dev_server_health(url: &str) -> bool {
|
||||
use std::time::Duration;
|
||||
use tauri_plugin_http::reqwest;
|
||||
|
||||
// Try to connect with a short timeout
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(3))
|
||||
.build();
|
||||
|
||||
if let Ok(client) = client {
|
||||
// Just check if the root responds (most dev servers respond to / with their app)
|
||||
if let Ok(response) = client.get(url).send().await {
|
||||
// Accept any response (200, 404, etc.) - we just want to know the server is running
|
||||
return response.status().as_u16() < 500;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_dev_extension(
|
||||
extension_path: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, ExtensionError> {
|
||||
use crate::extension::core::{
|
||||
manifest::ExtensionManifest,
|
||||
types::{Extension, ExtensionSource},
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
let extension_path_buf = PathBuf::from(&extension_path);
|
||||
|
||||
// 1. Read haextension.config.json to get dev server config and haextension directory
|
||||
let config_path = extension_path_buf.join("haextension.config.json");
|
||||
let (host, port, haextension_dir) = if config_path.exists() {
|
||||
let config_content =
|
||||
std::fs::read_to_string(&config_path).map_err(|e| ExtensionError::ValidationError {
|
||||
reason: format!("Failed to read haextension.config.json: {}", e),
|
||||
})?;
|
||||
|
||||
let config: HaextensionConfig =
|
||||
serde_json::from_str(&config_content).map_err(|e| ExtensionError::ValidationError {
|
||||
reason: format!("Failed to parse haextension.config.json: {}", e),
|
||||
})?;
|
||||
|
||||
(config.dev.host, config.dev.port, config.dev.haextension_dir)
|
||||
} else {
|
||||
// Default values if config doesn't exist
|
||||
(default_host(), default_port(), default_haextension_dir())
|
||||
};
|
||||
|
||||
let dev_server_url = format!("http://{}:{}", host, port);
|
||||
eprintln!("📡 Dev server URL: {}", dev_server_url);
|
||||
eprintln!("📁 Haextension directory: {}", haextension_dir);
|
||||
|
||||
// 1.5. Check if dev server is running
|
||||
if !check_dev_server_health(&dev_server_url).await {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: format!(
|
||||
"Dev server at {} is not reachable. Please start your dev server first (e.g., 'npm run dev')",
|
||||
dev_server_url
|
||||
),
|
||||
});
|
||||
}
|
||||
eprintln!("✅ Dev server is reachable");
|
||||
|
||||
// 2. Validate and build path to manifest: <extension_path>/<haextension_dir>/manifest.json
|
||||
let manifest_relative_path = format!("{}/manifest.json", haextension_dir);
|
||||
let manifest_path = ExtensionManager::validate_path_in_directory(
|
||||
&extension_path_buf,
|
||||
&manifest_relative_path,
|
||||
true,
|
||||
)?
|
||||
.ok_or_else(|| ExtensionError::ManifestError {
|
||||
reason: format!(
|
||||
"Manifest not found at: {}/manifest.json. Make sure you run 'npx @haexhub/sdk init' first.",
|
||||
haextension_dir
|
||||
),
|
||||
})?;
|
||||
|
||||
// 3. Read and parse manifest
|
||||
let manifest_content =
|
||||
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
|
||||
reason: format!("Failed to read manifest: {}", e),
|
||||
})?;
|
||||
|
||||
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
// 4. Generate a unique ID for dev extension: dev_<public_key>_<name>
|
||||
let extension_id = format!("dev_{}_{}", manifest.public_key, manifest.name);
|
||||
|
||||
// 5. Check if dev extension already exists (allow reload)
|
||||
if let Some(existing) = state
|
||||
.extension_manager
|
||||
.get_extension_by_public_key_and_name(&manifest.public_key, &manifest.name)?
|
||||
{
|
||||
// If it's already a dev extension, remove it first (to allow reload)
|
||||
if let ExtensionSource::Development { .. } = &existing.source {
|
||||
state
|
||||
.extension_manager
|
||||
.remove_extension(&manifest.public_key, &manifest.name)?;
|
||||
}
|
||||
// Note: Production extensions can coexist with dev extensions
|
||||
// Dev extensions have priority during lookup
|
||||
}
|
||||
|
||||
// 6. Create dev extension
|
||||
let extension = Extension {
|
||||
id: extension_id.clone(),
|
||||
source: ExtensionSource::Development {
|
||||
dev_server_url: dev_server_url.clone(),
|
||||
manifest_path: manifest_path.clone(),
|
||||
auto_reload: true,
|
||||
},
|
||||
manifest: manifest.clone(),
|
||||
enabled: true,
|
||||
last_accessed: SystemTime::now(),
|
||||
};
|
||||
|
||||
// 7. Add to dev extensions (no database entry for dev extensions)
|
||||
state.extension_manager.add_dev_extension(extension)?;
|
||||
|
||||
eprintln!(
|
||||
"✅ Dev extension loaded: {} v{} ({})",
|
||||
manifest.name, manifest.version, dev_server_url
|
||||
);
|
||||
|
||||
Ok(extension_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_dev_extension(
|
||||
public_key: String,
|
||||
name: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ExtensionError> {
|
||||
// Only remove from dev_extensions, not production_extensions
|
||||
let mut dev_exts = state.extension_manager.dev_extensions.lock().map_err(|e| {
|
||||
ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
// Find and remove by public_key and name
|
||||
let to_remove = dev_exts
|
||||
.iter()
|
||||
.find(|(_, ext)| ext.manifest.public_key == public_key && ext.manifest.name == name)
|
||||
.map(|(id, _)| id.clone());
|
||||
|
||||
if let Some(id) = to_remove {
|
||||
dev_exts.remove(&id);
|
||||
eprintln!("✅ Dev extension removed: {}", name);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ExtensionError::NotFound { public_key, name })
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_all_dev_extensions(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<ExtensionInfoResponse>, ExtensionError> {
|
||||
let dev_exts = state.extension_manager.dev_extensions.lock().map_err(|e| {
|
||||
ExtensionError::MutexPoisoned {
|
||||
reason: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut extensions = Vec::new();
|
||||
for ext in dev_exts.values() {
|
||||
extensions.push(ExtensionInfoResponse::from_extension(ext)?);
|
||||
}
|
||||
|
||||
Ok(extensions)
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
use crate::table_names::TABLE_EXTENSION_PERMISSIONS;
|
||||
use crate::AppState;
|
||||
use crate::database::core::with_connection;
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::extension::database::executor::SqlExecutor;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permissions::types::{Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints, PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints};
|
||||
use serde_json;
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use crate::extension::permissions::types::{Action, ExtensionPermission, PermissionStatus, ResourceType};
|
||||
use tauri::State;
|
||||
use url::Url;
|
||||
use crate::database::generated::HaexExtensionPermissions;
|
||||
use rusqlite::params;
|
||||
|
||||
pub struct PermissionManager;
|
||||
|
||||
@ -16,7 +15,6 @@ impl PermissionManager {
|
||||
/// Speichert alle Permissions einer Extension
|
||||
pub async fn save_permissions(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
permissions: &[ExtensionPermission],
|
||||
) -> Result<(), ExtensionError> {
|
||||
with_connection(&app_state.db, |conn| {
|
||||
@ -29,31 +27,29 @@ impl PermissionManager {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
let sql = format!(
|
||||
"INSERT INTO {} (id, extension_id, resource_type, action, target, constraints, status) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
TABLE_EXTENSION_PERMISSIONS
|
||||
);
|
||||
|
||||
for perm in permissions {
|
||||
let resource_type_str = format!("{:?}", perm.resource_type).to_lowercase();
|
||||
let action_str = format!("{:?}", perm.action).to_lowercase();
|
||||
// 1. Konvertiere App-Struct zu DB-Struct
|
||||
let db_perm: HaexExtensionPermissions = perm.into();
|
||||
|
||||
let constraints_json = perm
|
||||
.constraints
|
||||
.as_ref()
|
||||
.map(|c| serde_json::to_string(c).ok())
|
||||
.flatten();
|
||||
|
||||
let sql = "INSERT INTO haex_extension_permissions
|
||||
(id, extension_id, resource_type, action, target, constraints, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
let params = vec![
|
||||
json!(perm.id),
|
||||
json!(extension_id),
|
||||
json!(resource_type_str),
|
||||
json!(action_str),
|
||||
json!(perm.target),
|
||||
json!(constraints_json),
|
||||
json!(perm.status.as_str()),
|
||||
// 2. Erstelle typsichere Parameter
|
||||
let params = params![
|
||||
db_perm.id,
|
||||
db_perm.extension_id,
|
||||
db_perm.resource_type,
|
||||
db_perm.action,
|
||||
db_perm.target,
|
||||
db_perm.constraints,
|
||||
db_perm.status,
|
||||
];
|
||||
|
||||
SqlExecutor::execute_internal(&tx, &hlc_service, sql, ¶ms)?;
|
||||
// 3. Führe mit dem typsicheren Executor aus
|
||||
// HINWEIS: Du musst eine `execute_internal_typed` Funktion erstellen!
|
||||
SqlExecutor::execute_internal_typed(&tx, &hlc_service, &sql, params)?;
|
||||
}
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
@ -77,32 +73,24 @@ impl PermissionManager {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
let resource_type_str = format!("{:?}", permission.resource_type).to_lowercase();
|
||||
let action_str = format!("{:?}", permission.action).to_lowercase();
|
||||
let db_perm: HaexExtensionPermissions = permission.into();
|
||||
|
||||
let constraints_json = permission
|
||||
.constraints
|
||||
.as_ref()
|
||||
.map(|c| serde_json::to_string(c).ok())
|
||||
.flatten();
|
||||
let sql = format!(
|
||||
"UPDATE {} SET resource_type = ?, action = ?, target = ?, constraints = ?, status = ? WHERE id = ?",
|
||||
TABLE_EXTENSION_PERMISSIONS
|
||||
);
|
||||
|
||||
let sql = "UPDATE haex_extension_permissions
|
||||
SET resource_type = ?, action = ?, target = ?, constraints = ?, status = ?
|
||||
WHERE id = ?";
|
||||
|
||||
let params = vec![
|
||||
json!(resource_type_str),
|
||||
json!(action_str),
|
||||
json!(permission.target),
|
||||
json!(constraints_json),
|
||||
json!(permission.status.as_str()),
|
||||
json!(permission.id),
|
||||
let params = params![
|
||||
db_perm.resource_type,
|
||||
db_perm.action,
|
||||
db_perm.target,
|
||||
db_perm.constraints,
|
||||
db_perm.status,
|
||||
db_perm.id,
|
||||
];
|
||||
|
||||
SqlExecutor::execute_internal(&tx, &hlc_service, sql, ¶ms)?;
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(())
|
||||
SqlExecutor::execute_internal_typed(&tx, &hlc_service, &sql, params)?;
|
||||
tx.commit().map_err(DatabaseError::from)
|
||||
})
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
@ -123,16 +111,10 @@ impl PermissionManager {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
let sql = "UPDATE haex_extension_permissions
|
||||
SET status = ?
|
||||
WHERE id = ?";
|
||||
|
||||
let params = vec![json!(new_status.as_str()), json!(permission_id)];
|
||||
|
||||
SqlExecutor::execute_internal(&tx, &hlc_service, sql, ¶ms)?;
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(())
|
||||
let sql = format!("UPDATE {} SET status = ? WHERE id = ?", TABLE_EXTENSION_PERMISSIONS);
|
||||
let params = params![new_status.as_str(), permission_id];
|
||||
SqlExecutor::execute_internal_typed(&tx, &hlc_service, &sql, params)?;
|
||||
tx.commit().map_err(DatabaseError::from)
|
||||
})
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
@ -151,14 +133,9 @@ impl PermissionManager {
|
||||
})?;
|
||||
|
||||
// Echtes DELETE - wird vom CrdtTransformer zu UPDATE umgewandelt
|
||||
let sql = "DELETE FROM haex_extension_permissions WHERE id = ?";
|
||||
|
||||
let params = vec![json!(permission_id)];
|
||||
|
||||
SqlExecutor::execute_internal(&tx, &hlc_service, sql, ¶ms)?;
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(())
|
||||
let sql = format!("DELETE FROM {} WHERE id = ?", TABLE_EXTENSION_PERMISSIONS);
|
||||
SqlExecutor::execute_internal_typed(&tx, &hlc_service, &sql, params![permission_id])?;
|
||||
tx.commit().map_err(DatabaseError::from)
|
||||
}).map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
@ -175,110 +152,44 @@ impl PermissionManager {
|
||||
reason: "Failed to lock HLC service".to_string(),
|
||||
})?;
|
||||
|
||||
// Echtes DELETE - wird vom CrdtTransformer zu UPDATE umgewandelt
|
||||
let sql = "DELETE FROM haex_extension_permissions WHERE extension_id = ?";
|
||||
|
||||
let params = vec![json!(extension_id)];
|
||||
|
||||
SqlExecutor::execute_internal(&tx, &hlc_service, sql, ¶ms)?;
|
||||
|
||||
tx.commit().map_err(DatabaseError::from)?;
|
||||
Ok(())
|
||||
let sql = format!("DELETE FROM {} WHERE extension_id = ?", TABLE_EXTENSION_PERMISSIONS);
|
||||
SqlExecutor::execute_internal_typed(&tx, &hlc_service, &sql, params![extension_id])?;
|
||||
tx.commit().map_err(DatabaseError::from)
|
||||
}).map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
/// Löscht alle Permissions einer Extension innerhalb einer bestehenden Transaktion
|
||||
pub fn delete_permissions_in_transaction(
|
||||
tx: &rusqlite::Transaction,
|
||||
hlc_service: &crate::crdt::hlc::HlcService,
|
||||
extension_id: &str,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let sql = format!("DELETE FROM {} WHERE extension_id = ?", TABLE_EXTENSION_PERMISSIONS);
|
||||
SqlExecutor::execute_internal_typed(tx, hlc_service, &sql, params![extension_id])?;
|
||||
Ok(())
|
||||
}
|
||||
/// Lädt alle Permissions einer Extension
|
||||
pub async fn get_permissions(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
) -> Result<Vec<ExtensionPermission>, ExtensionError> {
|
||||
with_connection(&app_state.db, |conn| {
|
||||
let sql = "SELECT id, extension_id, resource_type, action, target, constraints, status, haex_timestamp, haex_tombstone
|
||||
FROM haex_extension_permissions
|
||||
WHERE extension_id = ?";
|
||||
let sql = format!("SELECT * FROM {} WHERE extension_id = ?", TABLE_EXTENSION_PERMISSIONS);
|
||||
let mut stmt = conn.prepare(&sql).map_err(DatabaseError::from)?;
|
||||
|
||||
let params = vec![json!(extension_id)];
|
||||
let perms_iter = stmt.query_map(params![extension_id], |row| {
|
||||
HaexExtensionPermissions::from_row(row)
|
||||
})?;
|
||||
|
||||
// SELECT nutzt select_internal
|
||||
let results = SqlExecutor::select_internal(conn, sql, ¶ms)?;
|
||||
|
||||
// Parse JSON results zu ExtensionPermission
|
||||
let permissions = results
|
||||
.into_iter()
|
||||
.map(|row| Self::parse_permission_from_json(row))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let permissions = perms_iter
|
||||
.filter_map(Result::ok)
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
Ok(permissions)
|
||||
}).map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
// Helper für JSON -> ExtensionPermission Konvertierung
|
||||
fn parse_permission_from_json(json: serde_json::Value) -> Result<ExtensionPermission, DatabaseError> {
|
||||
|
||||
let obj = json.as_object().ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Expected JSON object".to_string(),
|
||||
})?;
|
||||
|
||||
let resource_type = Self::parse_resource_type(
|
||||
obj.get("resource_type")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing resource_type".to_string(),
|
||||
})?
|
||||
)?;
|
||||
|
||||
let action = Self::parse_action(
|
||||
obj.get("action")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing action".to_string(),
|
||||
})?
|
||||
)?;
|
||||
|
||||
let status = PermissionStatus::from_str(
|
||||
obj.get("status")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing status".to_string(),
|
||||
})?
|
||||
)?; // Jetzt funktioniert das ?
|
||||
|
||||
let constraints = obj.get("constraints")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|json_str| Self::parse_constraints(&resource_type, json_str))
|
||||
.transpose()?;
|
||||
|
||||
Ok(ExtensionPermission {
|
||||
id: obj.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing id".to_string(),
|
||||
})?
|
||||
.to_string(),
|
||||
extension_id: obj.get("extension_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing extension_id".to_string(),
|
||||
})?
|
||||
.to_string(),
|
||||
resource_type,
|
||||
action,
|
||||
target: obj.get("target")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| DatabaseError::SerializationError {
|
||||
reason: "Missing target".to_string(),
|
||||
})?
|
||||
.to_string(),
|
||||
constraints,
|
||||
status,
|
||||
haex_timestamp: obj.get("haex_timestamp")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
haex_tombstone: obj.get("haex_tombstone")
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|i| i == 1),
|
||||
})
|
||||
}
|
||||
|
||||
/// Prüft Datenbankberechtigungen
|
||||
pub async fn check_database_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
@ -286,6 +197,30 @@ impl PermissionManager {
|
||||
action: Action,
|
||||
table_name: &str,
|
||||
) -> 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 has_permission = permissions
|
||||
@ -294,7 +229,7 @@ impl PermissionManager {
|
||||
.filter(|perm| perm.resource_type == ResourceType::Db)
|
||||
.filter(|perm| perm.action == action) // action ist nicht mehr Option
|
||||
.any(|perm| {
|
||||
if perm.target != "*" && perm.target != table_name {
|
||||
if perm.target != "*" && perm.target != clean_table_name {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
@ -311,7 +246,7 @@ impl PermissionManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft Dateisystem-Berechtigungen
|
||||
/* /// Prüft Dateisystem-Berechtigungen
|
||||
pub async fn check_filesystem_permission(
|
||||
app_state: &State<'_, AppState>,
|
||||
extension_id: &str,
|
||||
@ -471,9 +406,9 @@ impl PermissionManager {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
*/
|
||||
// Helper-Methoden - müssen DatabaseError statt ExtensionError zurückgeben
|
||||
fn parse_resource_type(s: &str) -> Result<ResourceType, DatabaseError> {
|
||||
pub fn parse_resource_type(s: &str) -> Result<ResourceType, DatabaseError> {
|
||||
match s {
|
||||
"fs" => Ok(ResourceType::Fs),
|
||||
"http" => Ok(ResourceType::Http),
|
||||
@ -485,51 +420,7 @@ impl PermissionManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_action(s: &str) -> Result<Action, DatabaseError> {
|
||||
match s {
|
||||
"read" => Ok(Action::Read),
|
||||
"write" => Ok(Action::Write),
|
||||
_ => Err(DatabaseError::SerializationError {
|
||||
reason: format!("Unknown action: {}", s),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_constraints(
|
||||
resource_type: &ResourceType,
|
||||
json: &str,
|
||||
) -> Result<PermissionConstraints, DatabaseError> {
|
||||
match resource_type {
|
||||
ResourceType::Db => {
|
||||
let constraints: DbConstraints = serde_json::from_str(json)
|
||||
.map_err(|e| DatabaseError::SerializationError {
|
||||
reason: format!("Failed to parse DB constraints: {}", e),
|
||||
})?;
|
||||
Ok(PermissionConstraints::Database(constraints))
|
||||
}
|
||||
ResourceType::Fs => {
|
||||
let constraints: FsConstraints = serde_json::from_str(json)
|
||||
.map_err(|e| DatabaseError::SerializationError {
|
||||
reason: format!("Failed to parse FS constraints: {}", e),
|
||||
})?;
|
||||
Ok(PermissionConstraints::Filesystem(constraints))
|
||||
}
|
||||
ResourceType::Http => {
|
||||
let constraints: HttpConstraints = serde_json::from_str(json)
|
||||
.map_err(|e| DatabaseError::SerializationError {
|
||||
reason: format!("Failed to parse HTTP constraints: {}", e),
|
||||
})?;
|
||||
Ok(PermissionConstraints::Http(constraints))
|
||||
}
|
||||
ResourceType::Shell => {
|
||||
let constraints: ShellConstraints = serde_json::from_str(json)
|
||||
.map_err(|e| DatabaseError::SerializationError {
|
||||
reason: format!("Failed to parse Shell constraints: {}", e),
|
||||
})?;
|
||||
Ok(PermissionConstraints::Shell(constraints))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_path_pattern(pattern: &str, path: &str) -> bool {
|
||||
if pattern.ends_with("/*") {
|
||||
@ -557,7 +448,7 @@ impl PermissionManager {
|
||||
}
|
||||
|
||||
// Convenience-Funktionen für die verschiedenen Subsysteme
|
||||
impl PermissionManager {
|
||||
/* impl PermissionManager {
|
||||
// Convenience-Methoden
|
||||
pub async fn can_read_file(
|
||||
app_state: &State<'_, AppState>,
|
||||
@ -647,4 +538,4 @@ impl PermissionManager {
|
||||
.filter(|perm| perm.status == PermissionStatus::Ask)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
@ -1,7 +1,159 @@
|
||||
use crate::extension::error::ExtensionError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::database::error::DatabaseError;
|
||||
// --- Spezifische Aktionen ---
|
||||
|
||||
/// Definiert Aktionen, die auf eine Datenbank angewendet werden können.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub enum DbAction {
|
||||
Read,
|
||||
ReadWrite,
|
||||
Create,
|
||||
Delete,
|
||||
AlterDrop,
|
||||
}
|
||||
|
||||
impl DbAction {
|
||||
/// Prüft, ob diese Aktion Lesezugriff gewährt (implizites Recht).
|
||||
pub fn allows_read(&self) -> bool {
|
||||
matches!(self, DbAction::Read | DbAction::ReadWrite)
|
||||
}
|
||||
|
||||
/// Prüft, ob diese Aktion Schreibzugriff gewährt.
|
||||
pub fn allows_write(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
DbAction::ReadWrite | DbAction::Create | DbAction::Delete
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DbAction {
|
||||
type Err = ExtensionError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"read" => Ok(DbAction::Read),
|
||||
"readwrite" | "read_write" => Ok(DbAction::ReadWrite),
|
||||
"create" => Ok(DbAction::Create),
|
||||
"delete" => Ok(DbAction::Delete),
|
||||
"alterdrop" | "alter_drop" => Ok(DbAction::AlterDrop),
|
||||
_ => Err(ExtensionError::InvalidActionString {
|
||||
input: s.to_string(),
|
||||
resource_type: "database".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Definiert Aktionen, die auf das Dateisystem angewendet werden können.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub enum FsAction {
|
||||
Read,
|
||||
ReadWrite,
|
||||
}
|
||||
|
||||
impl FsAction {
|
||||
/// Prüft, ob diese Aktion Lesezugriff gewährt (implizites Recht).
|
||||
pub fn allows_read(&self) -> bool {
|
||||
matches!(self, FsAction::Read | FsAction::ReadWrite)
|
||||
}
|
||||
|
||||
/// Prüft, ob diese Aktion Schreibzugriff gewährt.
|
||||
pub fn allows_write(&self) -> bool {
|
||||
matches!(self, FsAction::ReadWrite)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FsAction {
|
||||
type Err = ExtensionError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"read" => Ok(FsAction::Read),
|
||||
"readwrite" | "read_write" => Ok(FsAction::ReadWrite),
|
||||
_ => Err(ExtensionError::InvalidActionString {
|
||||
input: s.to_string(),
|
||||
resource_type: "filesystem".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Definiert Aktionen (HTTP-Methoden), die auf HTTP-Anfragen angewendet werden können.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
#[ts(export)]
|
||||
pub enum HttpAction {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Patch,
|
||||
Delete,
|
||||
#[serde(rename = "*")]
|
||||
All,
|
||||
}
|
||||
|
||||
impl FromStr for HttpAction {
|
||||
type Err = ExtensionError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_uppercase().as_str() {
|
||||
"GET" => Ok(HttpAction::Get),
|
||||
"POST" => Ok(HttpAction::Post),
|
||||
"PUT" => Ok(HttpAction::Put),
|
||||
"PATCH" => Ok(HttpAction::Patch),
|
||||
"DELETE" => Ok(HttpAction::Delete),
|
||||
"*" => Ok(HttpAction::All),
|
||||
_ => Err(ExtensionError::InvalidActionString {
|
||||
input: s.to_string(),
|
||||
resource_type: "http".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Definiert Aktionen, die auf Shell-Befehle angewendet werden können.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export)]
|
||||
pub enum ShellAction {
|
||||
Execute,
|
||||
}
|
||||
|
||||
impl FromStr for ShellAction {
|
||||
type Err = ExtensionError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"execute" => Ok(ShellAction::Execute),
|
||||
_ => Err(ExtensionError::InvalidActionString {
|
||||
input: s.to_string(),
|
||||
resource_type: "shell".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Haupt-Typen für Berechtigungen ---
|
||||
|
||||
/// Ein typsicherer Container, der die spezifische Aktion für einen Ressourcentyp enthält.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub enum Action {
|
||||
Database(DbAction),
|
||||
Filesystem(FsAction),
|
||||
Http(HttpAction),
|
||||
Shell(ShellAction),
|
||||
}
|
||||
|
||||
/// Die interne Repräsentation einer einzelnen, gewährten Berechtigung.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionPermission {
|
||||
pub id: String,
|
||||
@ -12,16 +164,13 @@ pub struct ExtensionPermission {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub constraints: Option<PermissionConstraints>,
|
||||
pub status: PermissionStatus,
|
||||
|
||||
// CRDT Felder
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub haex_tombstone: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub haex_timestamp: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export)]
|
||||
pub enum ResourceType {
|
||||
Fs,
|
||||
Http,
|
||||
@ -29,21 +178,142 @@ pub enum ResourceType {
|
||||
Shell,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Action {
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export)]
|
||||
pub enum PermissionStatus {
|
||||
Ask,
|
||||
Granted,
|
||||
Denied,
|
||||
}
|
||||
|
||||
// --- Constraint-Typen (unverändert) ---
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
|
||||
#[serde(untagged)]
|
||||
#[ts(export)]
|
||||
pub enum PermissionConstraints {
|
||||
Database(DbConstraints),
|
||||
Filesystem(FsConstraints),
|
||||
Http(HttpConstraints),
|
||||
Shell(ShellConstraints),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
|
||||
#[ts(export)]
|
||||
pub struct DbConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub where_clause: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub columns: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
|
||||
#[ts(export)]
|
||||
pub struct FsConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_file_size: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_extensions: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub recursive: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
|
||||
#[ts(export)]
|
||||
pub struct HttpConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub methods: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rate_limit: Option<RateLimit>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
|
||||
#[ts(export)]
|
||||
pub struct RateLimit {
|
||||
pub requests: u32,
|
||||
pub per_minutes: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)]
|
||||
#[ts(export)]
|
||||
pub struct ShellConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_subcommands: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_flags: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub forbidden_args: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// --- Konvertierungen zwischen ExtensionPermission und HaexExtensionPermissions ---
|
||||
|
||||
impl ResourceType {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
ResourceType::Fs => "fs",
|
||||
ResourceType::Http => "http",
|
||||
ResourceType::Db => "db",
|
||||
ResourceType::Shell => "shell",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Result<Self, ExtensionError> {
|
||||
match s {
|
||||
"fs" => Ok(ResourceType::Fs),
|
||||
"http" => Ok(ResourceType::Http),
|
||||
"db" => Ok(ResourceType::Db),
|
||||
"shell" => Ok(ResourceType::Shell),
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: format!("Unknown resource type: {}", s),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn as_str(&self) -> String {
|
||||
match self {
|
||||
Action::Database(action) => serde_json::to_string(action)
|
||||
.unwrap_or_default()
|
||||
.trim_matches('"')
|
||||
.to_string(),
|
||||
Action::Filesystem(action) => serde_json::to_string(action)
|
||||
.unwrap_or_default()
|
||||
.trim_matches('"')
|
||||
.to_string(),
|
||||
Action::Http(action) => serde_json::to_string(action)
|
||||
.unwrap_or_default()
|
||||
.trim_matches('"')
|
||||
.to_string(),
|
||||
Action::Shell(action) => serde_json::to_string(action)
|
||||
.unwrap_or_default()
|
||||
.trim_matches('"')
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(resource_type: &ResourceType, s: &str) -> Result<Self, ExtensionError> {
|
||||
match resource_type {
|
||||
ResourceType::Db => Ok(Action::Database(DbAction::from_str(s)?)),
|
||||
ResourceType::Fs => Ok(Action::Filesystem(FsAction::from_str(s)?)),
|
||||
ResourceType::Http => {
|
||||
let action: HttpAction =
|
||||
serde_json::from_str(&format!("\"{}\"", s)).map_err(|_| {
|
||||
ExtensionError::InvalidActionString {
|
||||
input: s.to_string(),
|
||||
resource_type: "http".to_string(),
|
||||
}
|
||||
})?;
|
||||
Ok(Action::Http(action))
|
||||
}
|
||||
ResourceType::Shell => Ok(Action::Shell(ShellAction::from_str(s)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PermissionStatus {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
@ -53,104 +323,69 @@ impl PermissionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Result<Self, DatabaseError> {
|
||||
pub fn from_str(s: &str) -> Result<Self, ExtensionError> {
|
||||
match s {
|
||||
"ask" => Ok(PermissionStatus::Ask),
|
||||
"granted" => Ok(PermissionStatus::Granted),
|
||||
"denied" => Ok(PermissionStatus::Denied),
|
||||
_ => Err(DatabaseError::SerializationError {
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: format!("Unknown permission status: {}", s),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum PermissionConstraints {
|
||||
Database(DbConstraints),
|
||||
Filesystem(FsConstraints),
|
||||
Http(HttpConstraints),
|
||||
Shell(ShellConstraints),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DbConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub where_clause: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub columns: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct FsConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_file_size: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_extensions: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub recursive: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct HttpConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub methods: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rate_limit: Option<RateLimit>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct RateLimit {
|
||||
pub requests: u32,
|
||||
pub per_minutes: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ShellConstraints {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_subcommands: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_flags: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub forbidden_args: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// Wenn du weiterhin gruppierte Permissions brauchst:
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct EditablePermissions {
|
||||
pub permissions: Vec<ExtensionPermission>,
|
||||
}
|
||||
|
||||
// Oder gruppiert nach Typ:
|
||||
impl EditablePermissions {
|
||||
pub fn database_permissions(&self) -> Vec<&ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.filter(|p| p.resource_type == ResourceType::Db)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn filesystem_permissions(&self) -> Vec<&ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.filter(|p| p.resource_type == ResourceType::Fs)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn http_permissions(&self) -> Vec<&ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.filter(|p| p.resource_type == ResourceType::Http)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn shell_permissions(&self) -> Vec<&ExtensionPermission> {
|
||||
self.permissions
|
||||
.iter()
|
||||
.filter(|p| p.resource_type == ResourceType::Shell)
|
||||
.collect()
|
||||
impl From<&ExtensionPermission> for crate::database::generated::HaexExtensionPermissions {
|
||||
fn from(perm: &ExtensionPermission) -> Self {
|
||||
Self {
|
||||
id: perm.id.clone(),
|
||||
extension_id: perm.extension_id.clone(),
|
||||
resource_type: Some(perm.resource_type.as_str().to_string()),
|
||||
action: Some(perm.action.as_str().to_string()),
|
||||
target: Some(perm.target.clone()),
|
||||
constraints: perm
|
||||
.constraints
|
||||
.as_ref()
|
||||
.and_then(|c| serde_json::to_string(c).ok()),
|
||||
status: perm.status.as_str().to_string(),
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
haex_timestamp: perm.haex_timestamp.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::database::generated::HaexExtensionPermissions> for ExtensionPermission {
|
||||
fn from(db_perm: crate::database::generated::HaexExtensionPermissions) -> Self {
|
||||
let resource_type = db_perm
|
||||
.resource_type
|
||||
.as_deref()
|
||||
.and_then(|s| ResourceType::from_str(s).ok())
|
||||
.unwrap_or(ResourceType::Db);
|
||||
|
||||
let action = db_perm
|
||||
.action
|
||||
.as_deref()
|
||||
.and_then(|s| Action::from_str(&resource_type, s).ok())
|
||||
.unwrap_or(Action::Database(DbAction::Read));
|
||||
|
||||
let status =
|
||||
PermissionStatus::from_str(db_perm.status.as_str()).unwrap_or(PermissionStatus::Denied);
|
||||
|
||||
let constraints = db_perm
|
||||
.constraints
|
||||
.as_deref()
|
||||
.and_then(|s| serde_json::from_str(s).ok());
|
||||
|
||||
Self {
|
||||
id: db_perm.id,
|
||||
extension_id: db_perm.extension_id,
|
||||
resource_type,
|
||||
action,
|
||||
target: db_perm.target.unwrap_or_default(),
|
||||
constraints,
|
||||
status,
|
||||
haex_timestamp: db_perm.haex_timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,14 @@ use tauri::State;
|
||||
pub struct SqlPermissionValidator;
|
||||
|
||||
impl SqlPermissionValidator {
|
||||
/// Prüft ob eine Tabelle zur Extension gehört (basierend auf keyHash Präfix)
|
||||
/// Format: {keyHash}_{extensionName}_{tableName}
|
||||
fn is_own_table(extension_id: &str, table_name: &str) -> bool {
|
||||
// Tabellennamen sind im Format: {keyHash}_{extensionName}_{tableName}
|
||||
// extension_id ist der keyHash der Extension
|
||||
table_name.starts_with(&format!("{}_", extension_id))
|
||||
}
|
||||
|
||||
/// Validiert ein SQL-Statement gegen die Permissions einer Extension
|
||||
pub async fn validate_sql(
|
||||
app_state: &State<'_, AppState>,
|
||||
@ -54,7 +62,7 @@ impl SqlPermissionValidator {
|
||||
PermissionManager::check_database_permission(
|
||||
app_state,
|
||||
extension_id,
|
||||
Action::Read,
|
||||
Action::Database(super::types::DbAction::Read),
|
||||
&table_name,
|
||||
)
|
||||
.await?;
|
||||
@ -75,7 +83,7 @@ impl SqlPermissionValidator {
|
||||
PermissionManager::check_database_permission(
|
||||
app_state,
|
||||
extension_id,
|
||||
Action::Write,
|
||||
Action::Database(super::types::DbAction::ReadWrite),
|
||||
&table_name,
|
||||
)
|
||||
.await?;
|
||||
@ -97,7 +105,7 @@ impl SqlPermissionValidator {
|
||||
PermissionManager::check_database_permission(
|
||||
app_state,
|
||||
extension_id,
|
||||
Action::Write,
|
||||
Action::Database(super::types::DbAction::Create),
|
||||
&table_name,
|
||||
)
|
||||
.await?;
|
||||
@ -119,7 +127,7 @@ impl SqlPermissionValidator {
|
||||
PermissionManager::check_database_permission(
|
||||
app_state,
|
||||
extension_id,
|
||||
Action::Write,
|
||||
Action::Database(super::types::DbAction::AlterDrop),
|
||||
&table_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
mod crdt;
|
||||
mod database;
|
||||
mod extension;
|
||||
use crate::{
|
||||
crdt::hlc::HlcService,
|
||||
database::DbConnection,
|
||||
extension::core::{ExtensionManager, ExtensionState},
|
||||
};
|
||||
use crate::{crdt::hlc::HlcService, database::DbConnection, extension::core::ExtensionManager};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::Manager;
|
||||
|
||||
@ -21,10 +17,10 @@ pub struct AppState {
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let protocol_name = "haex-extension";
|
||||
use extension::core::EXTENSION_PROTOCOL_NAME;
|
||||
|
||||
tauri::Builder::default()
|
||||
.register_uri_scheme_protocol(protocol_name, move |context, request| {
|
||||
.register_uri_scheme_protocol(EXTENSION_PROTOCOL_NAME, move |context, request| {
|
||||
// Hole den AppState aus dem Context
|
||||
let app_handle = context.app_handle();
|
||||
let state = app_handle.state::<AppState>();
|
||||
@ -60,7 +56,7 @@ pub fn run() {
|
||||
hlc: Mutex::new(HlcService::new()),
|
||||
extension_manager: ExtensionManager::new(),
|
||||
})
|
||||
.manage(ExtensionState::default())
|
||||
//.manage(ExtensionState::default())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
@ -72,18 +68,25 @@ pub fn run() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
database::create_encrypted_database,
|
||||
database::delete_vault,
|
||||
database::move_vault_to_trash,
|
||||
database::list_vaults,
|
||||
database::open_encrypted_database,
|
||||
database::sql_execute_with_crdt,
|
||||
database::sql_execute,
|
||||
database::sql_query_with_crdt,
|
||||
database::sql_select_with_crdt,
|
||||
database::sql_select,
|
||||
database::vault_exists,
|
||||
extension::database::extension_sql_execute,
|
||||
extension::database::extension_sql_select,
|
||||
extension::get_all_dev_extensions,
|
||||
extension::get_all_extensions,
|
||||
extension::get_extension_info,
|
||||
extension::install_extension_with_permissions,
|
||||
extension::is_extension_installed,
|
||||
extension::load_dev_extension,
|
||||
extension::preview_extension,
|
||||
extension::remove_dev_extension,
|
||||
extension::remove_extension,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
// src-tauri/src/models.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, SystemTime};
|
||||
use thiserror::Error;
|
||||
|
||||
/* #[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
pub icon: Option<String>,
|
||||
pub permissions: ExtensionPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionPermissions {
|
||||
pub database: Option<DatabasePermissions>,
|
||||
pub http: Option<Vec<String>>,
|
||||
pub filesystem: Option<String>,
|
||||
}
|
||||
|
||||
/// Enum to represent the source of an extension
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtensionSource {
|
||||
/// Production extension installed in app data
|
||||
Production { path: PathBuf, version: String },
|
||||
/// Development mode extension with live reloading
|
||||
Development {
|
||||
dev_server_url: String,
|
||||
manifest_path: PathBuf,
|
||||
auto_reload: bool,
|
||||
},
|
||||
} */
|
||||
|
||||
/*
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<std::collections::HashMap<String, ExtensionManifest>>,
|
||||
}
|
||||
|
||||
impl ExtensionState {
|
||||
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
|
||||
let mut extensions = self.extensions.lock().unwrap();
|
||||
extensions.insert(path, manifest);
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
|
||||
let extensions = self.extensions.lock().unwrap();
|
||||
extensions.values().find(|p| p.name == addon_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DbExtensionPermission {
|
||||
pub id: String,
|
||||
pub extension_id: String,
|
||||
pub resource: String,
|
||||
pub operation: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Comprehensive error type for all extension-related operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExtensionError {
|
||||
/// Security violation detected
|
||||
#[error("Security violation: {reason}")]
|
||||
SecurityViolation { reason: String },
|
||||
|
||||
/// Extension not found
|
||||
#[error("Extension not found: {id}")]
|
||||
NotFound { id: String },
|
||||
|
||||
/// Permission denied
|
||||
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
|
||||
PermissionDenied {
|
||||
extension_id: String,
|
||||
operation: String,
|
||||
resource: String,
|
||||
},
|
||||
|
||||
/// IO error during extension operations
|
||||
#[error("IO error: {source}")]
|
||||
Io {
|
||||
#[from]
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
/// Error during extension manifest parsing
|
||||
#[error("Manifest error: {reason}")]
|
||||
ManifestError { reason: String },
|
||||
|
||||
/// Input validation error
|
||||
#[error("Validation error: {reason}")]
|
||||
ValidationError { reason: String },
|
||||
|
||||
/// Development server error
|
||||
#[error("Dev server error: {reason}")]
|
||||
DevServerError { reason: String },
|
||||
}
|
||||
*/
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user