mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(dynamodb): support querying, editing, and exporting
This commit is contained in:
@@ -337,8 +337,8 @@ jobs:
|
||||
RUSTC_WRAPPER: sccache
|
||||
# The fast lane skips only system font discovery while retaining the other default capabilities.
|
||||
RUST_FEATURE_MODE: ${{ github.event_name == 'pull_request' && needs.changes.outputs.rust_full != 'true' && 'fast' || 'full' }}
|
||||
RUST_FAST_FEATURES: dbx/duckdb-sidecar,dbx/mq-admin,dbx/sqlite-sqlcipher,dbx-core/duckdb-sidecar,dbx-core/mq-admin,dbx-core/sqlite-sqlcipher,dbx-web/duckdb-sidecar,dbx-web/mq-admin,dbx-web/sqlite-sqlcipher
|
||||
RUST_FULL_FEATURES: dbx/duckdb-sidecar,dbx/mq-admin,dbx/sqlite-sqlcipher,dbx/system-fonts,dbx-core/duckdb-sidecar,dbx-core/mq-admin,dbx-core/sqlite-sqlcipher,dbx-core/system-fonts,dbx-web/duckdb-sidecar,dbx-web/mq-admin,dbx-web/sqlite-sqlcipher,dbx-web/system-fonts
|
||||
RUST_FAST_FEATURES: dbx/duckdb-sidecar,dbx/dynamodb,dbx/mq-admin,dbx/sqlite-sqlcipher,dbx-core/duckdb-sidecar,dbx-core/dynamodb,dbx-core/mq-admin,dbx-core/sqlite-sqlcipher,dbx-web/duckdb-sidecar,dbx-web/dynamodb,dbx-web/mq-admin,dbx-web/sqlite-sqlcipher
|
||||
RUST_FULL_FEATURES: dbx/duckdb-sidecar,dbx/dynamodb,dbx/mq-admin,dbx/sqlite-sqlcipher,dbx/system-fonts,dbx-core/duckdb-sidecar,dbx-core/dynamodb,dbx-core/mq-admin,dbx-core/sqlite-sqlcipher,dbx-core/system-fonts,dbx-web/duckdb-sidecar,dbx-web/dynamodb,dbx-web/mq-admin,dbx-web/sqlite-sqlcipher,dbx-web/system-fonts
|
||||
# Fork PRs cannot read repository secrets, so retain the GHA backend for them.
|
||||
SCCACHE_GHA_ENABLED: ${{ secrets.SCCACHE_S3_BUCKET == '' && 'true' || 'false' }}
|
||||
steps:
|
||||
@@ -416,8 +416,8 @@ jobs:
|
||||
RUSTC_WRAPPER: sccache
|
||||
# The fast lane skips only system font discovery while retaining the other default capabilities.
|
||||
RUST_FEATURE_MODE: ${{ github.event_name == 'pull_request' && needs.changes.outputs.rust_full != 'true' && 'fast' || 'full' }}
|
||||
RUST_FAST_FEATURES: dbx/duckdb-sidecar,dbx/mq-admin,dbx/sqlite-sqlcipher,dbx-core/duckdb-sidecar,dbx-core/mq-admin,dbx-core/sqlite-sqlcipher,dbx-web/duckdb-sidecar,dbx-web/mq-admin,dbx-web/sqlite-sqlcipher
|
||||
RUST_FULL_FEATURES: dbx/duckdb-sidecar,dbx/mq-admin,dbx/sqlite-sqlcipher,dbx/system-fonts,dbx-core/duckdb-sidecar,dbx-core/mq-admin,dbx-core/sqlite-sqlcipher,dbx-core/system-fonts,dbx-web/duckdb-sidecar,dbx-web/mq-admin,dbx-web/sqlite-sqlcipher,dbx-web/system-fonts
|
||||
RUST_FAST_FEATURES: dbx/duckdb-sidecar,dbx/dynamodb,dbx/mq-admin,dbx/sqlite-sqlcipher,dbx-core/duckdb-sidecar,dbx-core/dynamodb,dbx-core/mq-admin,dbx-core/sqlite-sqlcipher,dbx-web/duckdb-sidecar,dbx-web/dynamodb,dbx-web/mq-admin,dbx-web/sqlite-sqlcipher
|
||||
RUST_FULL_FEATURES: dbx/duckdb-sidecar,dbx/dynamodb,dbx/mq-admin,dbx/sqlite-sqlcipher,dbx/system-fonts,dbx-core/duckdb-sidecar,dbx-core/dynamodb,dbx-core/mq-admin,dbx-core/sqlite-sqlcipher,dbx-core/system-fonts,dbx-web/duckdb-sidecar,dbx-web/dynamodb,dbx-web/mq-admin,dbx-web/sqlite-sqlcipher,dbx-web/system-fonts
|
||||
# Fork PRs cannot read repository secrets, so retain the GHA backend for them.
|
||||
SCCACHE_GHA_ENABLED: ${{ secrets.SCCACHE_S3_BUCKET == '' && 'true' || 'false' }}
|
||||
steps:
|
||||
|
||||
@@ -535,7 +535,7 @@ jobs:
|
||||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
run: |
|
||||
cargo zigbuild --release -p dbx-web --target ${{ matrix.target }} \
|
||||
--no-default-features --features "duckdb-sidecar,mq-admin"
|
||||
--no-default-features --features "duckdb-sidecar,dynamodb,mq-admin"
|
||||
|
||||
- name: Package static browser runtime
|
||||
env:
|
||||
|
||||
Generated
+497
-48
@@ -481,6 +481,18 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "aws-credential-types"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.17.3"
|
||||
@@ -505,6 +517,307 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-runtime"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.64.0",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"bytes-utils",
|
||||
"fastrand",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-dynamodb"
|
||||
version = "1.104.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f04c47115cc8d46dcc94a9a81e7a3384cea859283c1a737729691d4221f11584"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.63.6",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-observability 0.2.6",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"http 0.2.12",
|
||||
"http 1.5.0",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sigv4"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-http 0.64.0",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"hex",
|
||||
"hmac 0.13.0",
|
||||
"http 0.2.12",
|
||||
"http 1.5.0",
|
||||
"percent-encoding",
|
||||
"sha2 0.11.0",
|
||||
"time",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-async"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http"
|
||||
version = "0.63.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"bytes-utils",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http"
|
||||
version = "0.64.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"bytes-utils",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http-client"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c1c8a04cb31ba74d0115af5a890bb8c0d48fba64b52812fa13929a6ef0cc83c"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"h2 0.3.27",
|
||||
"h2 0.4.15",
|
||||
"http 0.2.12",
|
||||
"http 1.5.0",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"hyper 1.11.0",
|
||||
"hyper-rustls 0.24.2",
|
||||
"hyper-rustls 0.27.9",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"rustls 0.21.12",
|
||||
"rustls 0.23.43",
|
||||
"rustls-native-certs 0.8.4",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-json"
|
||||
version = "0.62.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-schema 0.1.0",
|
||||
"aws-smithy-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-observability"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-observability"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "483b858ff67522011c4786310c5cd8fd88d0be7ea3d5f1a48328446300c4269e"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.64.0",
|
||||
"aws-smithy-http-client",
|
||||
"aws-smithy-observability 0.3.0",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-schema 0.2.0",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"http 0.2.12",
|
||||
"http 1.5.0",
|
||||
"http-body 0.4.6",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime-api"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b98f2e1fd67ec06618f9c291e5e495a468e60519e44c9c1979cd0521f3affdb"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api-macros",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"http 1.5.0",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime-api-macros"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-schema"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"http 1.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-schema"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"http 1.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fce83ce9abbb198d25bc7131e468d0f9fe1257125e58c39f3f9fc9f5098c9647"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"bytes",
|
||||
"bytes-utils",
|
||||
"futures-core",
|
||||
"http 0.2.12",
|
||||
"http 1.5.0",
|
||||
"http-body 0.4.6",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
"itoa",
|
||||
"num-integer",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"ryu",
|
||||
"serde",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-types"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-schema 0.2.0",
|
||||
"aws-smithy-types",
|
||||
"rustc_version",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.9"
|
||||
@@ -516,10 +829,10 @@ dependencies = [
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper 1.11.0",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
@@ -550,8 +863,8 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
@@ -585,6 +898,16 @@ version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9"
|
||||
|
||||
[[package]]
|
||||
name = "base64-simd"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
|
||||
dependencies = [
|
||||
"outref",
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
@@ -872,6 +1195,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes-utils"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.6.1"
|
||||
@@ -1725,6 +2058,7 @@ dependencies = [
|
||||
"arc-swap",
|
||||
"argon2",
|
||||
"async-trait",
|
||||
"aws-sdk-dynamodb",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"calamine",
|
||||
@@ -3106,6 +3440,25 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"indexmap 2.14.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.15"
|
||||
@@ -3117,7 +3470,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"indexmap 2.14.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
@@ -3331,6 +3684,17 @@ dependencies = [
|
||||
"markup5ever",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
@@ -3341,6 +3705,17 @@ dependencies = [
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.1.0"
|
||||
@@ -3348,7 +3723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3359,8 +3734,8 @@ checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
@@ -3394,6 +3769,30 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.11.0"
|
||||
@@ -3404,9 +3803,9 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"h2 0.4.15",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
@@ -3416,16 +3815,32 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.32",
|
||||
"log",
|
||||
"rustls 0.21.12",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"http 1.5.0",
|
||||
"hyper 1.11.0",
|
||||
"hyper-util",
|
||||
"rustls 0.23.43",
|
||||
"rustls-native-certs 0.8.4",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tower-service",
|
||||
@@ -3440,7 +3855,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper 1.11.0",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
@@ -3458,14 +3873,14 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"hyper 1.11.0",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"socket2 0.6.5",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -3773,7 +4188,7 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
|
||||
dependencies = [
|
||||
"socket2",
|
||||
"socket2 0.6.5",
|
||||
"widestring",
|
||||
"windows-registry 0.6.1",
|
||||
"windows-result 0.4.1",
|
||||
@@ -4502,7 +4917,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"sha1 0.11.0",
|
||||
"sha2 0.11.0",
|
||||
"socket2",
|
||||
"socket2 0.6.5",
|
||||
"stringprep",
|
||||
"strsim",
|
||||
"take_mut",
|
||||
@@ -4567,7 +4982,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"httparse",
|
||||
"memchr",
|
||||
"mime",
|
||||
@@ -4611,7 +5026,7 @@ dependencies = [
|
||||
"rand 0.10.2",
|
||||
"rustls 0.23.43",
|
||||
"serde",
|
||||
"socket2",
|
||||
"socket2 0.6.5",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
@@ -5234,6 +5649,12 @@ dependencies = [
|
||||
"thiserror 2.0.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.14.0-rc.7"
|
||||
@@ -5559,6 +5980,12 @@ version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.5"
|
||||
@@ -5985,7 +6412,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.43",
|
||||
"socket2",
|
||||
"socket2 0.6.5",
|
||||
"thiserror 2.0.19",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -6023,7 +6450,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"socket2 0.6.5",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -6208,7 +6635,7 @@ dependencies = [
|
||||
"rustls-native-certs 0.8.4",
|
||||
"ryu",
|
||||
"sha1_smol",
|
||||
"socket2",
|
||||
"socket2 0.6.5",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tokio-util",
|
||||
@@ -6278,6 +6705,12 @@ dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-lite"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
@@ -6304,12 +6737,12 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"h2 0.4.15",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper 1.11.0",
|
||||
"hyper-rustls 0.27.9",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
@@ -6350,11 +6783,11 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper 1.11.0",
|
||||
"hyper-rustls 0.27.9",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -6525,7 +6958,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"flume",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"log",
|
||||
"rustls-native-certs 0.7.3",
|
||||
"rustls-pemfile 2.2.0",
|
||||
@@ -7558,6 +7991,16 @@ version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
@@ -7995,7 +8438,7 @@ dependencies = [
|
||||
"glob",
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -8264,7 +8707,7 @@ dependencies = [
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"infer",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
@@ -8311,7 +8754,7 @@ dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"jni 0.21.1",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
@@ -8334,7 +8777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"objc2",
|
||||
@@ -8366,7 +8809,7 @@ dependencies = [
|
||||
"dom_query",
|
||||
"dunce",
|
||||
"glob",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"infer",
|
||||
"json-patch",
|
||||
"log",
|
||||
@@ -8625,7 +9068,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"socket2 0.6.5",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -8670,7 +9113,7 @@ dependencies = [
|
||||
"postgres-protocol",
|
||||
"postgres-types",
|
||||
"rand 0.10.2",
|
||||
"socket2",
|
||||
"socket2 0.6.5",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"whoami",
|
||||
@@ -8908,8 +9351,8 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
@@ -9048,7 +9491,7 @@ dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.7",
|
||||
@@ -9068,7 +9511,7 @@ checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.5",
|
||||
@@ -9358,6 +9801,12 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vsimd"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
||||
|
||||
[[package]]
|
||||
name = "vswhom"
|
||||
version = "0.1.0"
|
||||
@@ -10393,7 +10842,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"gdkx11",
|
||||
"gtk",
|
||||
"http",
|
||||
"http 1.5.0",
|
||||
"javascriptcore-rs",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
|
||||
@@ -75,7 +75,7 @@ dev: node_modules/.modules.yaml check-tauri-dev-port
|
||||
$(PNPM) dev:tauri
|
||||
|
||||
dev-fast: node_modules/.modules.yaml check-tauri-dev-port
|
||||
$(PNPM) tauri dev -- --no-default-features --features duckdb-sidecar
|
||||
$(PNPM) tauri dev -- --no-default-features --features duckdb-sidecar,dynamodb
|
||||
|
||||
dev-web: node_modules/.modules.yaml
|
||||
$(PNPM) dev:web
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a" x1="-15.53" y1="90.53" x2="90.53" y2="-15.53" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.70667 0 0 1.70667 0 0)"><stop offset="0" stop-color="#2e27ad"/><stop offset="1" stop-color="#527fff"/></linearGradient></defs><path fill="url(#a)" d="M0 0h128v128H0z"/><path d="M56.745 21.266c-14.062 0-29.013 4.164-29.013 11.947v13.736a6.93 6.93 0 002.577 5.037 6.946 6.946 0 00-2.577 5.12v13.653a6.93 6.93 0 002.577 5.12A6.946 6.946 0 0027.732 81v13.654a2.133 2.133 0 000 .356c.46 7.493 15.138 11.52 29.013 11.52 13.876 0 28.553-4.027 29.014-11.553a1.28 1.28 0 000-.36V80.963a6.827 6.827 0 00-2.577-5.12 6.93 6.93 0 002.577-5.12v-1.637l-11.28 11.263a27.7 27.7 0 005.836-2.373 4.403 4.403 0 012.03 3.073c0 3.124-6.62 6.521-16.45 7.904a72.073 72.073 0 01-9.15.646H55.04c-13.79-.324-23.894-4.676-23.894-8.533a4.403 4.403 0 012.034-3.07c5.359 2.987 14.043 4.76 23.566 4.76h.19l.954-3.413h-1.144c-9.386 0-18.088-1.862-22.713-4.797-1.843-1.212-2.87-2.523-2.887-3.667v-7.903c5.223 3.994 15.634 6.077 25.6 6.077 1.349 0 2.7 0 4.014-.12l.97-3.414c-1.707.12-3.294.17-4.984.17-14.643 0-25.6-4.488-25.6-8.533a4.403 4.403 0 012.034-3.073c4.71 2.645 11.944 4.3 20.153 4.676l.087-3.413c-8.073-.393-15.36-2.133-19.387-4.71-1.826-1.178-2.852-2.51-2.887-3.67v-7.883c5.223 3.993 15.634 6.076 25.6 6.076h.377l1.81-3.413h-2.187c-14.643 0-25.6-4.489-25.6-8.533 0-4.045 10.957-8.534 25.6-8.534a61.44 61.44 0 0114.644 1.707h9.37c-5.12-3.123-13.961-5.12-24.014-5.12zm12.8 8.517a1.707 1.707 0 00-1.516.936L57.789 51.2a1.707 1.707 0 001.516 2.527h9.66l-9.59 31.93a1.707 1.707 0 00.784 1.963 1.707 1.707 0 00.853.24 1.707 1.707 0 001.317-.58l37.546-39.253a1.707 1.707 0 00-1.23-2.883h-9.573l4.367-13.127a1.707 1.707 0 00-1.707-2.233zm.957 3.43h18.773l-4.37 13.106a1.707 1.707 0 001.707 2.254h7.953l-29.883 31.23 8.21-27.307a1.707 1.707 0 00-.273-1.5 1.707 1.707 0 00-1.367-.683h-9.18zm-34.237 11.62a2.133 2.133 0 00-2.133 2.133 2.133 2.133 0 002.133 2.133 2.133 2.133 0 002.134-2.133 2.133 2.133 0 00-2.134-2.133zm0 23.893a2.133 2.133 0 00-2.133 2.133 2.133 2.133 0 002.133 2.134 2.133 2.133 0 002.134-2.134 2.133 2.133 0 00-2.134-2.133zm-5.12 18.11c5.223 3.96 15.634 6.04 25.6 6.04 9.967 0 20.378-2.08 25.6-6.04v7.85c0 4.028-10.956 8.533-25.6 8.533-14.643 0-25.6-4.454-25.6-8.533zm5.12 5.783a2.133 2.133 0 00-2.133 2.134 2.133 2.133 0 002.133 2.133 2.133 2.133 0 002.134-2.133 2.133 2.133 0 00-2.134-2.134z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -1075,6 +1075,14 @@ const driverProfiles: Record<
|
||||
access: { type: "access", port: 0, user: "", label: "Microsoft Access", icon: "access" },
|
||||
mongodb: { type: "mongodb", port: 27017, user: "", label: "MongoDB", icon: "mongodb" },
|
||||
"mongodb-legacy": { type: "mongodb", port: 27017, user: "", label: "MongoDB (Legacy)", icon: "mongodb" },
|
||||
dynamodb: {
|
||||
type: "dynamodb",
|
||||
port: 443,
|
||||
user: "",
|
||||
label: "Amazon DynamoDB",
|
||||
icon: "dynamodb",
|
||||
host: "dynamodb.us-east-1.amazonaws.com",
|
||||
},
|
||||
clickhouse: {
|
||||
type: "clickhouse",
|
||||
port: 8123,
|
||||
@@ -2363,6 +2371,11 @@ function applyProfile(val: string, preserveConnectionFields = false) {
|
||||
if (profile.type === "sqlite") {
|
||||
form.value.database = undefined;
|
||||
}
|
||||
if (profile.type === "dynamodb") {
|
||||
form.value.database = "us-east-1";
|
||||
form.value.connection_string = undefined;
|
||||
form.value.ssl = true;
|
||||
}
|
||||
if (profile.type === "h2") {
|
||||
h2ConnectionMode.value = "file";
|
||||
form.value.host = "";
|
||||
@@ -2781,6 +2794,7 @@ const iconTypeMap: Record<string, string> = {
|
||||
access: "access",
|
||||
redis: "redis",
|
||||
mongodb: "mongodb",
|
||||
dynamodb: "dynamodb",
|
||||
duckdb: "duckdb",
|
||||
clickhouse: "clickhouse",
|
||||
sqlserver: "sqlserver",
|
||||
@@ -2871,6 +2885,7 @@ const dbOptions: DbOption[] = [
|
||||
{ value: "opentenbase", label: "OpenTenBase" },
|
||||
{ value: "mysql", label: "MySQL" },
|
||||
{ value: "mongodb", label: "MongoDB" },
|
||||
{ value: "dynamodb", label: "Amazon DynamoDB" },
|
||||
{ value: "redis", label: "Redis" },
|
||||
{ value: "oracle", label: "Oracle" },
|
||||
{ value: "sqlite", label: "SQLite" },
|
||||
@@ -2987,7 +3002,7 @@ const dbCategoryDefinitions: Array<{
|
||||
{
|
||||
key: "document",
|
||||
titleKey: "connection.databaseCategoryDocument",
|
||||
optionValues: ["mongodb", "redis", "elasticsearch", "easysearch", "meilisearch", "hbase", "manticoresearch", "cassandra"],
|
||||
optionValues: ["mongodb", "dynamodb", "redis", "elasticsearch", "easysearch", "meilisearch", "hbase", "manticoresearch", "cassandra"],
|
||||
},
|
||||
{
|
||||
key: "graph_ai",
|
||||
@@ -3799,6 +3814,15 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
|
||||
throw new Error(t("connection.kingbaseDatabaseRequired"));
|
||||
}
|
||||
}
|
||||
if (config.db_type === "dynamodb") {
|
||||
config.database = config.database?.trim() || "us-east-1";
|
||||
config.username = config.username.trim();
|
||||
config.password = config.password.trim();
|
||||
config.connection_string = config.connection_string?.trim() || undefined;
|
||||
if (!config.username || !config.password) {
|
||||
throw new Error(t("connection.dynamodbCredentialsRequired"));
|
||||
}
|
||||
}
|
||||
if (config.db_type === "gaussdb") {
|
||||
const serialized = serializeGaussdbHosts(gaussdbHostEntries.value);
|
||||
config.host = serialized.host;
|
||||
@@ -6659,6 +6683,38 @@ function openExternalUrl(url: string) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- DynamoDB: endpoint, region, AWS credentials -->
|
||||
<template v-else-if="form.db_type === 'dynamodb'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.dynamodbEndpoint") }}</Label>
|
||||
<Input v-model="form.host" class="col-span-2" placeholder="dynamodb.us-east-1.amazonaws.com" />
|
||||
<Input v-model.number="form.port" type="number" class="col-span-1" min="1" max="65535" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<span />
|
||||
<label class="col-span-3 flex items-center gap-2 text-sm">
|
||||
<input v-model="form.ssl" type="checkbox" />
|
||||
<span>{{ t("connection.sslEnable") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.dynamodbRegion") }}</Label>
|
||||
<Input v-model="form.database" class="col-span-3" placeholder="us-east-1" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.dynamodbAccessKeyId") }}</Label>
|
||||
<Input v-model="form.username" class="col-span-3" autocomplete="username" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.dynamodbSecretAccessKey") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.dynamodbSessionToken") }}</Label>
|
||||
<PasswordInput v-model="form.connection_string" class="col-span-3" :placeholder="t('connection.dynamodbSessionTokenPlaceholder')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- MongoDB: URL or form -->
|
||||
<template v-else-if="form.db_type === 'mongodb'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
|
||||
@@ -18,6 +18,7 @@ import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import QueryLoadingState from "@/components/common/QueryLoadingState.vue";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { DynamoDbIndexInfo, DynamoDbTableDescription } from "@/lib/backend/api";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { clampSearchSplitWidth } from "@/lib/dataGrid/dataGridSearchSplit";
|
||||
import { documentViewerFontStyle } from "@/lib/document/documentViewerFontStyle";
|
||||
@@ -55,6 +56,7 @@ import {
|
||||
type ElasticsearchQueryType,
|
||||
} from "@/lib/app/documentStoreProvider";
|
||||
import {
|
||||
formatDocumentStoreIdLabel,
|
||||
isDocumentStoreIdentityField,
|
||||
normalizeDocumentStoreRouting,
|
||||
parseDocumentStoreInputValue,
|
||||
@@ -87,6 +89,7 @@ import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
|
||||
import { documentDataGridColumnLayoutScopeKey } from "@/lib/dataGrid/dataGridColumnLayoutStorage";
|
||||
import { documentGridColumnVisibilityScopeKey, migrateDocumentGridColumnVisibilityToLayout } from "@/lib/document/documentGridColumnVisibilityStorage";
|
||||
import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import JsonEditNode from "./JsonEditNode.vue";
|
||||
import type { EditNode } from "@/types/editor";
|
||||
import type { ColumnInfo, DatabaseType, QueryResult, QueryTab } from "@/types/database";
|
||||
@@ -95,6 +98,7 @@ import { Splitpanes, Pane } from "splitpanes";
|
||||
import "splitpanes/dist/splitpanes.css";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const settingsStore = useSettingsStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
@@ -108,6 +112,7 @@ const props = defineProps<{
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type ViewMode = "document" | "table";
|
||||
const DYNAMODB_DEFAULT_EXPORT_ROW_LIMIT = 10_000;
|
||||
|
||||
const documents = ref<JsonRecord[]>([]);
|
||||
const copyDocuments = ref<JsonRecord[]>([]);
|
||||
@@ -224,14 +229,15 @@ watch(
|
||||
|
||||
const pageTotal = computed(() => paginationTotal.value);
|
||||
const documentPageCount = computed(() => (pageTotal.value === undefined ? undefined : Math.max(1, Math.ceil(pageTotal.value / pageSize.value))));
|
||||
const canGoNextPage = computed(() =>
|
||||
canGoNextDocumentPage({
|
||||
const canGoNextPage = computed(() => {
|
||||
if (documentStoreProvider.value.kind === "dynamodb") return dynamodbHasNextCursor.value;
|
||||
return canGoNextDocumentPage({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
rowCount: documents.value.length,
|
||||
paginationTotal: pageTotal.value,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
const documentRequestLimit = computed(() => {
|
||||
if (documentStoreProvider.value.kind !== "elasticsearch" || paginationTotal.value === undefined) return pageSize.value;
|
||||
return documentPageRequestLimit(page.value, pageSize.value, paginationTotal.value);
|
||||
@@ -244,6 +250,8 @@ const tableFindPaneStyle = computed(() => {
|
||||
const documentFontStyle = computed(() => documentViewerFontStyle(settingsStore.editorSettings));
|
||||
const documentStoreLabels = computed(() => ({
|
||||
documentsLabel: documentStoreProvider.value.documentsLabel({ total: total.value ?? 0, totalIsExact: totalIsExact.value, t }),
|
||||
filterInputLabel: documentStoreProvider.value.kind === "dynamodb" ? t("dynamodb.filter") : documentStoreProvider.value.filterInputLabel,
|
||||
sortInputLabel: documentStoreProvider.value.kind === "dynamodb" ? t("dynamodb.sortKey") : documentStoreProvider.value.sortInputLabel,
|
||||
queryPreview: documentQueryPreview.value,
|
||||
}));
|
||||
|
||||
@@ -269,6 +277,39 @@ const documentFilterFieldSearch = ref<Record<string, string>>({});
|
||||
const documentFilterRules = ref<DocumentFilterRule[]>([]);
|
||||
const appliedDocumentFilter = ref<Record<string, unknown> | null>(null);
|
||||
const elasticsearchMappingFields = ref<ColumnInfo[]>([]);
|
||||
const dynamodbTableDescription = ref<DynamoDbTableDescription | null>(null);
|
||||
const dynamodbIndexName = ref("__table__");
|
||||
const dynamodbPageCursors = ref<Array<string | undefined>>([undefined]);
|
||||
const dynamodbHasNextCursor = ref(false);
|
||||
const dynamodbExactTotal = ref<number | undefined>();
|
||||
let dynamodbExactCountKey: string | null = null;
|
||||
|
||||
const dynamodbIndexOptions = computed<Array<{ value: string; label: string; index?: DynamoDbIndexInfo }>>(() => [
|
||||
{ value: "__table__", label: t("dynamodb.baseTable") },
|
||||
...(dynamodbTableDescription.value?.indexes ?? []).map((index) => ({
|
||||
value: index.name,
|
||||
label: `${index.name} (${index.kind === "global" ? "GSI" : "LSI"} · ${index.projectionType})`,
|
||||
index,
|
||||
})),
|
||||
]);
|
||||
|
||||
const dynamodbSelectedIndex = computed(() => {
|
||||
if (dynamodbIndexName.value === "__table__") return undefined;
|
||||
return dynamodbTableDescription.value?.indexes.find((index) => index.name === dynamodbIndexName.value);
|
||||
});
|
||||
|
||||
const dynamodbPartialProjectionReadOnly = computed(() => documentStoreProvider.value.kind === "dynamodb" && !!dynamodbSelectedIndex.value && dynamodbSelectedIndex.value.projectionType !== "ALL");
|
||||
const documentStoreEditable = computed(() => !dynamodbPartialProjectionReadOnly.value);
|
||||
const documentStoreEditDisabledReason = computed(() => (dynamodbPartialProjectionReadOnly.value ? t("dynamodb.partialProjectionReadOnly", { projection: dynamodbSelectedIndex.value?.projectionType ?? "UNKNOWN" }) : undefined));
|
||||
|
||||
const dynamodbSelectedKey = computed(() => {
|
||||
const table = dynamodbTableDescription.value;
|
||||
if (!table) return null;
|
||||
if (dynamodbIndexName.value === "__table__") {
|
||||
return { partitionKey: table.partitionKey, sortKey: table.sortKey };
|
||||
}
|
||||
return dynamodbSelectedIndex.value ?? null;
|
||||
});
|
||||
|
||||
const pendingDelete = ref<PendingDelete | null>(null);
|
||||
const documentFilterComposingEditors = new Set<string>();
|
||||
@@ -281,9 +322,7 @@ const selectedDoc = computed(() => {
|
||||
});
|
||||
const selectedDocumentIdLabel = computed(() => {
|
||||
if (isNew.value) return "New";
|
||||
const id = selectedDoc.value?._id;
|
||||
if (id === undefined || id === null) return "";
|
||||
return typeof id === "object" ? stringifyDocumentStoreValue(id, documentStoreProvider.value.kind) : String(id);
|
||||
return formatDocumentStoreIdLabel(selectedDoc.value?._id, documentStoreProvider.value.kind);
|
||||
});
|
||||
const selectedDocumentIdWidth = computed(() => `${Math.min(Math.max(Array.from(selectedDocumentIdLabel.value).length + 2, 5), 52)}ch`);
|
||||
|
||||
@@ -299,6 +338,12 @@ const deleteDetails = computed(() => {
|
||||
if (!pending) return "";
|
||||
if (pending.kind === "document") {
|
||||
const id = documents.value[pending.index]?._id ?? "";
|
||||
if (documentStoreProvider.value.kind === "dynamodb") {
|
||||
return t("dynamodb.documentDetails", {
|
||||
table: props.collection,
|
||||
id: formatDocumentStoreIdLabel(id, "dynamodb"),
|
||||
});
|
||||
}
|
||||
const displayId = mongoDocumentIdForGrid(id);
|
||||
if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch") {
|
||||
const product = props.databaseType === "easysearch" ? "Easysearch" : props.databaseType === "meilisearch" ? "Meilisearch" : "Elasticsearch";
|
||||
@@ -346,8 +391,9 @@ const gridResult = computed<QueryResult>(() => {
|
||||
return { columns, column_types: columnTypes, rows, mongo_documents: docs, mongo_copy_documents: copyDocuments.value, affected_rows: 0, execution_time_ms: 0, truncated: false };
|
||||
});
|
||||
|
||||
async function exportAllMongoDocuments(onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void): Promise<QueryResult | undefined> {
|
||||
if (documentStoreProvider.value.kind !== "mongodb") return undefined;
|
||||
async function exportAllDocumentStoreDocuments(onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void): Promise<QueryResult | undefined> {
|
||||
const kind = documentStoreProvider.value.kind;
|
||||
if (kind !== "mongodb" && kind !== "dynamodb") return undefined;
|
||||
|
||||
const connectionId = props.connectionId;
|
||||
const database = props.database;
|
||||
@@ -356,21 +402,23 @@ async function exportAllMongoDocuments(onProgress?: (info: { rowsExported: numbe
|
||||
const sort = currentDocumentSortJson(sortInput.value);
|
||||
const exportSettings = settingsStore.editorSettings;
|
||||
const batchSize = Math.max(1, Math.trunc(exportSettings.exportBatchSize));
|
||||
const rowLimit = exportSettings.exportRowLimitEnabled ? Math.max(0, Math.trunc(exportSettings.exportRowLimit)) : Number.POSITIVE_INFINITY;
|
||||
const rowLimit = exportSettings.exportRowLimitEnabled ? Math.max(0, Math.trunc(exportSettings.exportRowLimit)) : kind === "dynamodb" ? DYNAMODB_DEFAULT_EXPORT_ROW_LIMIT : Number.POSITIVE_INFINITY;
|
||||
const exportExecutionId = uuid();
|
||||
const exportStartedAt = performance.now();
|
||||
const exportedDocuments: JsonRecord[] = [];
|
||||
let exportedCopyDocuments: JsonRecord[] | undefined = [];
|
||||
let exportedCopyDocuments: JsonRecord[] | undefined = kind === "mongodb" ? [] : undefined;
|
||||
let totalRows: number | null = null;
|
||||
let cursor: string | undefined;
|
||||
const seenCursors = new Set<string>();
|
||||
|
||||
while (exportedDocuments.length < rowLimit) {
|
||||
const requestLimit = Math.min(batchSize, rowLimit - exportedDocuments.length);
|
||||
const requestLimit = Math.min(batchSize, kind === "dynamodb" ? 1000 : Number.POSITIVE_INFINITY, rowLimit - exportedDocuments.length);
|
||||
if (requestLimit <= 0) break;
|
||||
const result = await api.documentFindDocuments(connectionId, database, collection, exportedDocuments.length, requestLimit, filter, undefined, sort, undefined, exportExecutionId);
|
||||
const result = await api.documentFindDocuments(connectionId, database, collection, kind === "dynamodb" ? 0 : exportedDocuments.length, requestLimit, filter, undefined, sort, undefined, exportExecutionId, cursor);
|
||||
const pageDocuments = result.documents.slice(0, requestLimit).map(asRecord);
|
||||
exportedDocuments.push(...pageDocuments);
|
||||
|
||||
if (exportedCopyDocuments) {
|
||||
if (kind === "mongodb" && exportedCopyDocuments) {
|
||||
if (result.extended_documents?.length === result.documents.length) {
|
||||
exportedCopyDocuments.push(...result.extended_documents.slice(0, pageDocuments.length).map(asRecord));
|
||||
} else {
|
||||
@@ -378,19 +426,36 @@ async function exportAllMongoDocuments(onProgress?: (info: { rowsExported: numbe
|
||||
}
|
||||
}
|
||||
|
||||
if (result.total_is_exact !== false) totalRows = Math.min(result.total, rowLimit);
|
||||
if (kind === "mongodb" && result.total_is_exact !== false) totalRows = Math.min(result.total, rowLimit);
|
||||
onProgress?.({ rowsExported: exportedDocuments.length, totalRows });
|
||||
|
||||
if (kind === "dynamodb") {
|
||||
cursor = result.next_cursor;
|
||||
if (!cursor) break;
|
||||
if (seenCursors.has(cursor)) throw new Error(t("dynamodb.repeatedCursor"));
|
||||
seenCursors.add(cursor);
|
||||
continue;
|
||||
}
|
||||
|
||||
const reachedExactTotal = result.total_is_exact !== false && exportedDocuments.length >= result.total;
|
||||
if (pageDocuments.length === 0 || pageDocuments.length < requestLimit || reachedExactTotal) break;
|
||||
}
|
||||
|
||||
if (kind === "dynamodb") {
|
||||
const truncatedByLimit = !!cursor && exportedDocuments.length >= rowLimit;
|
||||
totalRows = truncatedByLimit ? null : exportedDocuments.length;
|
||||
onProgress?.({ rowsExported: exportedDocuments.length, totalRows });
|
||||
if (truncatedByLimit) {
|
||||
toast(t("dynamodb.exportLimitReached", { count: rowLimit }), 6000);
|
||||
}
|
||||
}
|
||||
|
||||
const result = mongoDocumentsToQueryResult(exportedDocuments, performance.now() - exportStartedAt, totalRows ?? exportedDocuments.length, exportedCopyDocuments, totalRows !== null);
|
||||
if (result.columns.length === 0) result.columns = gridResult.value.columns;
|
||||
result.column_types = mongoDocumentGridColumnTypes(exportedDocuments, result.columns);
|
||||
result.column_types = kind === "mongodb" ? mongoDocumentGridColumnTypes(exportedDocuments, result.columns) : undefined;
|
||||
result.affected_rows = exportedDocuments.length;
|
||||
result.truncated = false;
|
||||
result.has_more = false;
|
||||
result.truncated = kind === "dynamodb" && !!cursor && exportedDocuments.length >= rowLimit;
|
||||
result.has_more = result.truncated;
|
||||
return result;
|
||||
}
|
||||
const expandedDocumentFilterFieldPaths = ref<Set<string>>(new Set());
|
||||
@@ -616,8 +681,38 @@ function resetDocumentFilterBuilder() {
|
||||
documentFilterRules.value = documentFilterFieldOptions.value.length > 0 ? [createDocumentFilterRule()] : [];
|
||||
}
|
||||
|
||||
function dynamodbCountFilterKey(filter: string | undefined): string {
|
||||
return JSON.stringify([props.connectionId, props.collection, filter ?? ""]);
|
||||
}
|
||||
|
||||
function resetDynamoDbExactCount() {
|
||||
dynamodbExactCountKey = null;
|
||||
dynamodbExactTotal.value = undefined;
|
||||
}
|
||||
|
||||
function resetDynamoDbPagination(options: { preserveExactCount?: boolean } = {}) {
|
||||
dynamodbPageCursors.value = [undefined];
|
||||
dynamodbHasNextCursor.value = false;
|
||||
paginationTotal.value = undefined;
|
||||
if (!options.preserveExactCount) resetDynamoDbExactCount();
|
||||
}
|
||||
|
||||
function currentDocumentFilter(): string | undefined {
|
||||
return currentDocumentFilterJson(filterInput.value, appliedDocumentFilter.value, documentStoreProvider.value.kind);
|
||||
const filter = currentDocumentFilterJson(filterInput.value, appliedDocumentFilter.value, documentStoreProvider.value.kind);
|
||||
if (documentStoreProvider.value.kind !== "dynamodb" || dynamodbIndexName.value === "__table__") return filter;
|
||||
const parsed = filter ? JSON.parse(filter) : {};
|
||||
return JSON.stringify({ ...parsed, $index: dynamodbIndexName.value });
|
||||
}
|
||||
|
||||
function selectDynamoDbIndex(value: unknown) {
|
||||
const next = typeof value === "string" && value ? value : "__table__";
|
||||
if (dynamodbIndexName.value === next) return;
|
||||
dynamodbIndexName.value = next;
|
||||
if (dynamodbPartialProjectionReadOnly.value && isEditing.value) cancelEdit();
|
||||
sortInput.value = "";
|
||||
page.value = 0;
|
||||
resetDynamoDbPagination();
|
||||
void load({ page: 0 });
|
||||
}
|
||||
|
||||
function resizeDocumentQueryInput(el: HTMLTextAreaElement | undefined) {
|
||||
@@ -763,7 +858,30 @@ function documentStoreWriteApis(documentType?: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function prepareDynamoDbDocumentIdentity(document: JsonRecord): { document: JsonRecord; id: string } {
|
||||
const table = dynamodbTableDescription.value;
|
||||
if (!table) throw new Error(t("dynamodb.tableMetadataUnavailable"));
|
||||
|
||||
const next = { ...document };
|
||||
const metadataId = next._id && typeof next._id === "object" && !Array.isArray(next._id) ? (next._id as JsonRecord) : undefined;
|
||||
const identity: JsonRecord = {};
|
||||
const keys = [table.partitionKey, table.sortKey].filter((value): value is NonNullable<typeof value> => !!value);
|
||||
for (const key of keys) {
|
||||
const value = next[key.name] ?? metadataId?.[key.name];
|
||||
if (value === undefined || value === null || value === "") {
|
||||
throw new Error(t("dynamodb.keyRequired", { key: key.name }));
|
||||
}
|
||||
next[key.name] = value;
|
||||
identity[key.name] = value;
|
||||
}
|
||||
next._id = identity;
|
||||
return { document: next, id: serializeDocumentStoreId(identity, "dynamodb") };
|
||||
}
|
||||
|
||||
async function gridSave(changes: DocumentGridChanges) {
|
||||
if (!documentStoreEditable.value) {
|
||||
throw new Error(documentStoreEditDisabledReason.value);
|
||||
}
|
||||
const cols = changes.columns;
|
||||
const idColIdx = cols.indexOf("_id");
|
||||
if (idColIdx < 0) throw new Error("No _id column");
|
||||
@@ -818,8 +936,14 @@ async function gridSave(changes: DocumentGridChanges) {
|
||||
if (!doc) continue;
|
||||
const routing = isEs ? documentRoutingFromDocument(doc) : undefined;
|
||||
const updated = buildPathIdentityUpdatedDocument(doc, dirtyCols, cols, kind);
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(updated, { kind, mode: "update" });
|
||||
const documentId = serializeDocumentStoreId(doc._id ?? id, kind);
|
||||
if (kind === "dynamodb") {
|
||||
const normalized = prepareDynamoDbDocumentIdentity(updated);
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(normalized.document, { kind, mode: "update" });
|
||||
await api.documentUpdateDocument(props.connectionId, props.database, props.collection, documentId, stringifyDocumentStoreValue(writeDocument, kind));
|
||||
continue;
|
||||
}
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(updated, { kind, mode: "update" });
|
||||
await api.documentUpdateDocument(props.connectionId, props.database, props.collection, documentId, stringifyDocumentStoreValue(writeDocument, kind), routing);
|
||||
continue;
|
||||
}
|
||||
@@ -848,7 +972,12 @@ async function gridSave(changes: DocumentGridChanges) {
|
||||
const idValue = newRow[idColIdx];
|
||||
const id = idValue === null || idValue === undefined || idValue === "" ? null : serializeDocumentStoreId(parseDocumentStoreInputValue(idValue, kind), kind);
|
||||
const routing = isEs ? documentRoutingFromGridRow(newRow, cols) : undefined;
|
||||
if (id) {
|
||||
if (kind === "dynamodb") {
|
||||
if (idValue !== null && idValue !== undefined && idValue !== "") doc._id = parseDocumentStoreInputValue(idValue, kind);
|
||||
const normalized = prepareDynamoDbDocumentIdentity(doc);
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(normalized.document, { kind, mode: "insert" });
|
||||
await api.documentInsertDocument(props.connectionId, props.database, props.collection, stringifyDocumentStoreValue(writeDocument, kind));
|
||||
} else if (id) {
|
||||
await api.documentUpdateDocument(props.connectionId, props.database, props.collection, id, stringifyDocumentStoreValue(doc, kind), routing);
|
||||
} else {
|
||||
await api.documentInsertDocument(props.connectionId, props.database, props.collection, stringifyDocumentStoreValue(doc, kind), routing);
|
||||
@@ -861,6 +990,10 @@ async function gridSave(changes: DocumentGridChanges) {
|
||||
}
|
||||
|
||||
if (isEs) resetElasticsearchTotals({ preservePaginationTotal: true });
|
||||
if (kind === "dynamodb") {
|
||||
page.value = 0;
|
||||
resetDynamoDbPagination();
|
||||
}
|
||||
await load();
|
||||
}
|
||||
|
||||
@@ -920,6 +1053,14 @@ function buildElasticsearchPartialUpdateDocument(changes: Map<number, MongoInput
|
||||
return document;
|
||||
}
|
||||
|
||||
function formatDynamoDbOperationPreview(action: "insert" | "put" | "delete", id: unknown, document?: Record<string, unknown>): string {
|
||||
const operation = action === "insert" ? "INSERT ITEM" : action === "put" ? "PUT ITEM" : "DELETE ITEM";
|
||||
const lines = [`DBX DYNAMODB ${operation}`, `table: ${JSON.stringify(props.collection)}`];
|
||||
if (id !== undefined) lines.push("key:", stringifyDocumentStoreValue(id, "dynamodb", 2));
|
||||
if (document) lines.push("item:", stringifyDocumentStoreValue(document, "dynamodb", 2));
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function previewDocumentChanges(changes: DocumentGridChanges): Promise<string[]> {
|
||||
const { dirtyRows, deletedRows, newRows, newRowMeta, columns, rows } = changes;
|
||||
const idColIdx = columns.indexOf("_id");
|
||||
@@ -938,6 +1079,14 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
|
||||
const updateDoc = buildElasticsearchPartialUpdateDocument(dirtyCols, columns);
|
||||
const routing = documentRoutingFromGridRow(row, columns);
|
||||
stmts.push(`POST /${coll}/_update/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}\n${stringifyDocumentStoreValue({ doc: updateDoc.$set ?? updateDoc }, "elasticsearch", 2)}`);
|
||||
} else if (kind === "dynamodb") {
|
||||
const sourceDocument = documents.value[rowIdx];
|
||||
if (!sourceDocument) continue;
|
||||
const documentId = sourceDocument._id ?? id;
|
||||
const updated = buildPathIdentityUpdatedDocument(sourceDocument, dirtyCols, columns, "dynamodb");
|
||||
const normalized = prepareDynamoDbDocumentIdentity(updated);
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(normalized.document, { kind: "dynamodb", mode: "update" });
|
||||
stmts.push(formatDynamoDbOperationPreview("put", documentId, writeDocument));
|
||||
} else {
|
||||
const sourceDocument = documents.value[rowIdx];
|
||||
if (!sourceDocument) continue;
|
||||
@@ -957,6 +1106,10 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
|
||||
const id = row?.[idColIdx];
|
||||
if (id == null) continue;
|
||||
if (isPathIdentityStore) {
|
||||
if (kind === "dynamodb") {
|
||||
stmts.push(formatDynamoDbOperationPreview("delete", documents.value[rowIdx]?._id ?? id));
|
||||
continue;
|
||||
}
|
||||
if (!isEs) {
|
||||
stmts.push(formatMeilisearchDocumentOperationPreview({ action: "delete", index: coll, id: documents.value[rowIdx]?._id ?? id }));
|
||||
continue;
|
||||
@@ -971,6 +1124,14 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
|
||||
for (const [newRowIndex, newRow] of newRows.entries()) {
|
||||
const doc = isPathIdentityStore ? buildPathIdentityInsertDocument(newRow, columns, kind) : buildMongoGridInsertDocument(newRow, columns, newRowMeta[newRowIndex]);
|
||||
if (isPathIdentityStore) {
|
||||
if (kind === "dynamodb") {
|
||||
const idValue = idColIdx >= 0 ? newRow[idColIdx] : undefined;
|
||||
if (idValue !== null && idValue !== undefined && idValue !== "") doc._id = parseDocumentStoreInputValue(idValue, "dynamodb");
|
||||
const normalized = prepareDynamoDbDocumentIdentity(doc);
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(normalized.document, { kind: "dynamodb", mode: "insert" });
|
||||
stmts.push(formatDynamoDbOperationPreview("insert", JSON.parse(normalized.id), writeDocument));
|
||||
continue;
|
||||
}
|
||||
if (!isEs) {
|
||||
const idValue = idColIdx >= 0 ? newRow[idColIdx] : null;
|
||||
const id = idValue === null || idValue === undefined || idValue === "" ? undefined : parseDocumentStoreInputValue(idValue, "meilisearch");
|
||||
@@ -1132,13 +1293,24 @@ async function load(options: { page?: number } = {}) {
|
||||
const collection = props.collection;
|
||||
const storeKind = documentStoreProvider.value.kind;
|
||||
const filter = currentDocumentFilter();
|
||||
if (storeKind === "dynamodb") {
|
||||
const countKey = dynamodbCountFilterKey(filter);
|
||||
if (dynamodbExactCountKey !== countKey) {
|
||||
dynamodbExactCountKey = countKey;
|
||||
dynamodbExactTotal.value = undefined;
|
||||
}
|
||||
}
|
||||
const countRequest: LoadedDocumentQueryTotalCountRequest = { connectionId, database, collection, filter, generation: requestGeneration, storeKind };
|
||||
if (storeKind === "elasticsearch" && elasticsearchCountKey !== null && elasticsearchCountKey !== elasticsearchCountFilterKey(filter)) {
|
||||
resetElasticsearchTotals();
|
||||
}
|
||||
const sort = currentDocumentSortJson(sortInput.value);
|
||||
const skip = requestPage * pageSize.value;
|
||||
const result = await api.documentFindDocuments(connectionId, database, collection, skip, documentRequestLimit.value, filter, undefined, sort, undefined, executionId);
|
||||
const cursor = storeKind === "dynamodb" ? dynamodbPageCursors.value[requestPage] : undefined;
|
||||
if (storeKind === "dynamodb" && requestPage > 0 && !cursor) {
|
||||
throw new Error(t("dynamodb.pageCursorUnavailable"));
|
||||
}
|
||||
const skip = storeKind === "dynamodb" ? 0 : requestPage * pageSize.value;
|
||||
const result = await api.documentFindDocuments(connectionId, database, collection, skip, documentRequestLimit.value, filter, undefined, sort, undefined, executionId, cursor);
|
||||
if (documentLoadExecutionId.value !== executionId) return;
|
||||
if (connectionId !== props.connectionId || database !== props.database || collection !== props.collection || storeKind !== documentStoreProvider.value.kind) return;
|
||||
const nextDocuments =
|
||||
@@ -1159,6 +1331,12 @@ async function load(options: { page?: number } = {}) {
|
||||
copyDocuments.value = nextCopyDocuments;
|
||||
mongoCopyDocumentsAvailable.value = hasTypePreservingCopyDocuments;
|
||||
loadedDocumentQueryTotalCountRequest = countRequest;
|
||||
if (storeKind === "dynamodb") {
|
||||
const nextCursors = dynamodbPageCursors.value.slice(0, requestPage + 1);
|
||||
nextCursors[requestPage + 1] = result.next_cursor;
|
||||
dynamodbPageCursors.value = nextCursors;
|
||||
dynamodbHasNextCursor.value = !!result.next_cursor;
|
||||
}
|
||||
if (nextDocuments.length > 0) {
|
||||
const keySet = new Set<string>();
|
||||
keySet.add("_id");
|
||||
@@ -1172,6 +1350,13 @@ async function load(options: { page?: number } = {}) {
|
||||
}
|
||||
if (storeKind === "elasticsearch") {
|
||||
applyElasticsearchSearchTotal(result.total, result.total_is_exact !== false, filter);
|
||||
} else if (storeKind === "dynamodb") {
|
||||
cancelElasticsearchCount();
|
||||
const lowerBound = requestPage * pageSize.value + nextDocuments.length + (result.next_cursor ? 1 : 0);
|
||||
const exactTotal = dynamodbExactTotal.value ?? (!result.next_cursor ? lowerBound : undefined);
|
||||
total.value = exactTotal ?? lowerBound;
|
||||
totalIsExact.value = exactTotal !== undefined;
|
||||
paginationTotal.value = exactTotal;
|
||||
} else {
|
||||
cancelElasticsearchCount();
|
||||
const totals = resolveDocumentQueryTotals(result.total, result.total_is_exact !== false, {
|
||||
@@ -1212,11 +1397,15 @@ async function countExactDocumentTotal(): Promise<number | undefined> {
|
||||
paginationTotal.value = totals.paginationTotal;
|
||||
return exactCount;
|
||||
}
|
||||
const exactCount = await api.mongoCountDocuments(request.connectionId, request.database, request.collection, request.filter, "accurate");
|
||||
const exactCount = request.storeKind === "dynamodb" ? await api.documentCountDocuments(request.connectionId, request.collection, request.filter) : await api.mongoCountDocuments(request.connectionId, request.database, request.collection, request.filter, "accurate");
|
||||
if (!isCurrentDocumentQueryTotalCountRequest(request)) return undefined;
|
||||
if (!Number.isFinite(exactCount) || exactCount < 0) {
|
||||
throw new Error("invalid count");
|
||||
}
|
||||
if (request.storeKind === "dynamodb") {
|
||||
dynamodbExactCountKey = dynamodbCountFilterKey(request.filter);
|
||||
dynamodbExactTotal.value = exactCount;
|
||||
}
|
||||
const totals = resolveDocumentQueryTotals(exactCount, true);
|
||||
total.value = totals.total;
|
||||
totalIsExact.value = totals.totalIsExact;
|
||||
@@ -1226,6 +1415,7 @@ async function countExactDocumentTotal(): Promise<number | undefined> {
|
||||
|
||||
async function refreshDocuments() {
|
||||
if (documentStoreProvider.value.kind === "elasticsearch") resetElasticsearchTotals({ preservePaginationTotal: true });
|
||||
if (documentStoreProvider.value.kind === "dynamodb") resetDynamoDbExactCount();
|
||||
await load();
|
||||
}
|
||||
|
||||
@@ -1248,20 +1438,49 @@ async function cancelDocumentLoad() {
|
||||
function applyFilter() {
|
||||
page.value = 0;
|
||||
if (documentStoreProvider.value.kind === "elasticsearch") resetElasticsearchTotals();
|
||||
if (documentStoreProvider.value.kind === "dynamodb") resetDynamoDbPagination();
|
||||
void load();
|
||||
}
|
||||
|
||||
function paginate(offset: number, limit: number) {
|
||||
async function paginate(offset: number, limit: number) {
|
||||
const normalizedLimit = normalizeResultPageSize(limit, pageSize.value);
|
||||
const pageSizeChanged = normalizedLimit !== pageSize.value;
|
||||
pageSize.value = normalizedLimit;
|
||||
if (documentStoreProvider.value.kind === "dynamodb" && pageSizeChanged) {
|
||||
page.value = 0;
|
||||
resetDynamoDbPagination({ preserveExactCount: true });
|
||||
await load({ page: 0 });
|
||||
return;
|
||||
}
|
||||
const requestedPage = Math.floor(Math.max(0, offset) / normalizedLimit);
|
||||
const nextPage = clampDocumentPage(requestedPage, normalizedLimit, paginationTotal.value);
|
||||
void load({ page: nextPage });
|
||||
if (documentStoreProvider.value.kind !== "dynamodb") {
|
||||
await load({ page: nextPage });
|
||||
return;
|
||||
}
|
||||
for (let cursorPage = 0; cursorPage <= nextPage; cursorPage += 1) {
|
||||
if (cursorPage > 0 && !dynamodbPageCursors.value[cursorPage]) {
|
||||
error.value = t("dynamodb.pageCursorUnavailable");
|
||||
return;
|
||||
}
|
||||
if (cursorPage === nextPage) {
|
||||
await load({ page: cursorPage });
|
||||
return;
|
||||
}
|
||||
if (!dynamodbPageCursors.value[cursorPage + 1]) {
|
||||
await load({ page: cursorPage });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onSort(column: string, _columnIndex: number, direction: "asc" | "desc" | null) {
|
||||
if (documentStoreProvider.value.kind === "dynamodb" && direction && dynamodbSelectedKey.value?.sortKey?.name !== column) {
|
||||
error.value = t("dynamodb.sortKeyOnly", { key: dynamodbSelectedKey.value?.sortKey?.name || t("dynamodb.none") });
|
||||
return;
|
||||
}
|
||||
sortInput.value = documentStoreProvider.value.sortInputForColumn(column, direction);
|
||||
page.value = 0;
|
||||
if (documentStoreProvider.value.kind === "dynamodb") resetDynamoDbPagination({ preserveExactCount: true });
|
||||
void load();
|
||||
}
|
||||
|
||||
@@ -1275,7 +1494,7 @@ function asRecord(value: unknown): JsonRecord {
|
||||
function documentIdentity(doc: JsonRecord | undefined): string | null {
|
||||
const id = doc?._id;
|
||||
if (id === null || id === undefined) return null;
|
||||
return typeof id === "object" ? JSON.stringify(id) : String(id);
|
||||
return serializeDocumentStoreId(id, documentStoreProvider.value.kind);
|
||||
}
|
||||
|
||||
function syncSelectedDocumentAfterLoad(previousSelectedIdx: number | null, previousSelectedId: string | null) {
|
||||
@@ -1385,6 +1604,7 @@ function selectDoc(idx: number) {
|
||||
}
|
||||
|
||||
function startNew() {
|
||||
if (!documentStoreEditable.value) return;
|
||||
selectedIdx.value = null;
|
||||
editJson.value = emptyDocumentJson();
|
||||
editFields.value = [createEditNode("", "", false, false)];
|
||||
@@ -1395,6 +1615,7 @@ function startNew() {
|
||||
}
|
||||
|
||||
function startEdit() {
|
||||
if (!documentStoreEditable.value) return;
|
||||
const doc = selectedDoc.value;
|
||||
if (!doc) return;
|
||||
// Issue #2952: open whole-document JSON editing by default (DBeaver-style), not field tree.
|
||||
@@ -1563,6 +1784,9 @@ function resolveDocumentStorePathId(id: unknown): string | null {
|
||||
|
||||
function resolveWriteIdentityFromEditor(doc: JsonRecord, currentId: unknown, currentRouting: string | undefined): { writeId: string; writeRouting?: string } | null {
|
||||
const kind = documentStoreProvider.value.kind;
|
||||
if (kind === "dynamodb") {
|
||||
return { writeId: prepareDynamoDbDocumentIdentity(doc).id };
|
||||
}
|
||||
const hasPayloadId = Object.prototype.hasOwnProperty.call(doc, "_id");
|
||||
const writeId = hasPayloadId ? resolveDocumentStorePathId(doc._id) : resolveDocumentStorePathId(currentId);
|
||||
if (!writeId) return null;
|
||||
@@ -1571,7 +1795,7 @@ function resolveWriteIdentityFromEditor(doc: JsonRecord, currentId: unknown, cur
|
||||
}
|
||||
|
||||
async function saveDoc() {
|
||||
if (isSavingDocument.value) return;
|
||||
if (isSavingDocument.value || !documentStoreEditable.value) return;
|
||||
error.value = "";
|
||||
isSavingDocument.value = true;
|
||||
try {
|
||||
@@ -1579,13 +1803,14 @@ async function saveDoc() {
|
||||
if (!doc) return;
|
||||
|
||||
const kind = documentStoreProvider.value.kind;
|
||||
const writeDocument = kind === "dynamodb" ? prepareDynamoDbDocumentIdentity(doc).document : doc;
|
||||
|
||||
if (isNew.value) {
|
||||
const apis = documentStoreWriteApis();
|
||||
const explicitId = kind === "mongodb" || doc._id === undefined || doc._id === null || doc._id === "" ? null : resolveDocumentStorePathId(doc._id);
|
||||
const explicitId = kind === "mongodb" || writeDocument._id === undefined || writeDocument._id === null || writeDocument._id === "" ? null : resolveDocumentStorePathId(writeDocument._id);
|
||||
await insertDocumentStoreDocumentCore({
|
||||
kind,
|
||||
document: doc,
|
||||
document: writeDocument,
|
||||
explicitId,
|
||||
routing: normalizeDocumentStoreRouting(doc._routing),
|
||||
apis,
|
||||
@@ -1605,7 +1830,7 @@ async function saveDoc() {
|
||||
}
|
||||
const currentRouting = documentRoutingFromDocument(current);
|
||||
const apis = documentStoreWriteApis(kind === "elasticsearch" ? documentTypeFromDocument(current) : undefined);
|
||||
const write = resolveWriteIdentityFromEditor(doc, currentId, currentRouting);
|
||||
const write = resolveWriteIdentityFromEditor(writeDocument, currentId, currentRouting);
|
||||
if (!write) {
|
||||
error.value = t("mongo.jsonIdRequired");
|
||||
return;
|
||||
@@ -1615,8 +1840,7 @@ async function saveDoc() {
|
||||
write: { id: write.writeId, routing: write.writeRouting },
|
||||
current: { id: deleteId, routing: kind === "elasticsearch" ? currentRouting : undefined },
|
||||
});
|
||||
// Rekey writes first then deletes; write failure leaves the old document intact.
|
||||
await applyDocumentStoreIdentityPlan({ kind, plan, document: doc, apis });
|
||||
await applyDocumentStoreIdentityPlan({ kind, plan, document: writeDocument, apis });
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -1626,6 +1850,10 @@ async function saveDoc() {
|
||||
documentEditMode.value = "fields";
|
||||
editFields.value = [];
|
||||
if (kind === "elasticsearch") resetElasticsearchTotals({ preservePaginationTotal: true });
|
||||
if (kind === "dynamodb") {
|
||||
page.value = 0;
|
||||
resetDynamoDbPagination();
|
||||
}
|
||||
await load();
|
||||
if (selectedIdx.value !== null && documents.value[selectedIdx.value]) {
|
||||
editJson.value = stringifyDocumentStoreValue(documents.value[selectedIdx.value], documentStoreProvider.value.kind, 2);
|
||||
@@ -1649,6 +1877,10 @@ async function applyDeleteDoc(idx: number) {
|
||||
editJson.value = "";
|
||||
}
|
||||
if (documentStoreProvider.value.kind === "elasticsearch") resetElasticsearchTotals({ preservePaginationTotal: true });
|
||||
if (documentStoreProvider.value.kind === "dynamodb") {
|
||||
page.value = 0;
|
||||
resetDynamoDbPagination();
|
||||
}
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
@@ -1656,6 +1888,7 @@ async function applyDeleteDoc(idx: number) {
|
||||
}
|
||||
|
||||
function requestDeleteDoc(idx: number) {
|
||||
if (!documentStoreEditable.value) return;
|
||||
if (!settingsStore.editorSettings.confirmDangerousSqlExecution) {
|
||||
void applyDeleteDoc(idx);
|
||||
return;
|
||||
@@ -1688,7 +1921,7 @@ function nextPage() {
|
||||
}
|
||||
|
||||
function docPreview(doc: JsonRecord): string {
|
||||
const id = doc._id || "";
|
||||
const id = formatDocumentStoreIdLabel(doc._id, documentStoreProvider.value.kind);
|
||||
const keys = Object.keys(doc)
|
||||
.filter((k) => k !== "_id")
|
||||
.slice(0, 3);
|
||||
@@ -1724,6 +1957,15 @@ watch([viewMode, isEditing, selectedIdx], ([mode, editing, index]) => {
|
||||
documentViewerSearchActive.value = false;
|
||||
});
|
||||
|
||||
async function loadDynamoDbTableDescription() {
|
||||
if (documentStoreProvider.value.kind !== "dynamodb") return;
|
||||
try {
|
||||
dynamodbTableDescription.value = await api.dynamodbDescribeTable(props.connectionId, props.collection);
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener("pointerdown", handleDocumentBrowserPointerDown, true);
|
||||
try {
|
||||
@@ -1731,6 +1973,7 @@ onMounted(async () => {
|
||||
} catch (e) {
|
||||
console.warn("[DBX] ensureConnected failed for", props.connectionId, e);
|
||||
}
|
||||
await loadDynamoDbTableDescription();
|
||||
// Mapping metadata enriches the filter builder, but it must not delay the
|
||||
// first page of documents when the mapping endpoint is slow.
|
||||
void loadElasticsearchMappingFields();
|
||||
@@ -1807,7 +2050,26 @@ defineExpose({ focusSearch });
|
||||
|
||||
<span class="shrink-0 ml-1">{{ documentStoreLabels.documentsLabel }}</span>
|
||||
|
||||
<Button v-if="viewMode === 'document'" variant="ghost" size="icon" class="h-5 w-5" @click="startNew"><Plus class="h-3 w-3" /></Button>
|
||||
<div v-if="documentStoreProvider.kind === 'dynamodb' && dynamodbTableDescription" class="ml-1 flex min-w-0 items-center gap-1.5">
|
||||
<Select :model-value="dynamodbIndexName" @update:model-value="selectDynamoDbIndex">
|
||||
<SelectTrigger class="h-6 w-44 min-w-0 text-xs" :title="t('dynamodb.index')">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="option in dynamodbIndexOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span v-if="dynamodbSelectedKey" class="max-w-64 truncate font-mono text-[11px] text-muted-foreground" :title="t('dynamodb.keySummary', { partitionKey: dynamodbSelectedKey.partitionKey.name, sortKey: dynamodbSelectedKey.sortKey?.name || t('dynamodb.none') })">
|
||||
PK: {{ dynamodbSelectedKey.partitionKey.name }}<template v-if="dynamodbSelectedKey.sortKey"> · SK: {{ dynamodbSelectedKey.sortKey.name }}</template>
|
||||
</span>
|
||||
<Badge v-if="dynamodbPartialProjectionReadOnly" variant="outline" class="h-5 rounded border-amber-500/50 px-1.5 text-[10px] text-amber-600 dark:text-amber-400" :title="documentStoreEditDisabledReason">
|
||||
{{ dynamodbSelectedIndex?.projectionType }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Button v-if="viewMode === 'document'" variant="ghost" size="icon" class="h-5 w-5" :disabled="!documentStoreEditable" :title="documentStoreEditDisabledReason" @click="startNew"><Plus class="h-3 w-3" /></Button>
|
||||
<Button v-if="viewMode === 'document'" variant="ghost" size="icon" class="h-5 w-5" @click="refreshDocuments"><RefreshCw class="h-3 w-3" :class="{ 'animate-spin': loading }" /></Button>
|
||||
|
||||
<div v-if="viewMode === 'document'" class="flex items-center gap-1 ml-1">
|
||||
@@ -1988,7 +2250,7 @@ defineExpose({ focusSearch });
|
||||
context="results"
|
||||
:database-type="props.databaseType"
|
||||
:mongo-update-target="mongoUpdateTarget"
|
||||
editable
|
||||
:editable="documentStoreEditable"
|
||||
:custom-save-handler="customSaveHandler"
|
||||
:loading="loading"
|
||||
:sql="documentStoreLabels.queryPreview"
|
||||
@@ -1999,7 +2261,7 @@ defineExpose({ focusSearch });
|
||||
:inexact-total-row-count-mode="documentStoreProvider.kind === 'mongodb' ? 'estimated' : 'at-least'"
|
||||
:pagination-total-row-count="pageTotal"
|
||||
:count-total-rows="countExactDocumentTotal"
|
||||
:full-export-result="documentStoreProvider.kind === 'mongodb' ? exportAllMongoDocuments : undefined"
|
||||
:full-export-result="documentStoreProvider.kind === 'mongodb' || documentStoreProvider.kind === 'dynamodb' ? exportAllDocumentStoreDocuments : undefined"
|
||||
@sort="onSort"
|
||||
@reload="refreshDocuments"
|
||||
@paginate="(offset: number, limit: number) => paginate(offset, limit)"
|
||||
@@ -2210,7 +2472,7 @@ defineExpose({ focusSearch });
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<span class="text-blue-600 dark:text-blue-400 text-xs font-medium select-none shrink-0">{{ documentStoreProvider.filterInputLabel }}</span>
|
||||
<span class="text-blue-600 dark:text-blue-400 text-xs font-medium select-none shrink-0">{{ documentStoreLabels.filterInputLabel }}</span>
|
||||
<textarea
|
||||
ref="filterInputRef"
|
||||
v-model="filterInput"
|
||||
@@ -2249,7 +2511,7 @@ defineExpose({ focusSearch });
|
||||
<span class="h-5 w-px bg-border group-hover:bg-primary/60" />
|
||||
</button>
|
||||
<div class="flex flex-1 items-center gap-1 px-2 py-0.5 min-w-0">
|
||||
<span class="text-orange-600 dark:text-orange-400 text-xs font-medium select-none shrink-0">{{ documentStoreProvider.sortInputLabel }}</span>
|
||||
<span class="text-orange-600 dark:text-orange-400 text-xs font-medium select-none shrink-0">{{ documentStoreLabels.sortInputLabel }}</span>
|
||||
<textarea
|
||||
ref="sortInputRef"
|
||||
v-model="sortInput"
|
||||
@@ -2290,7 +2552,7 @@ defineExpose({ focusSearch });
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div v-for="(doc, idx) in documents" :key="idx" class="px-3 py-1.5 border-b text-xs font-mono cursor-pointer hover:bg-accent/50 flex items-center gap-2 group" :class="{ 'bg-accent': selectedIdx === idx }" @click="selectDoc(idx)">
|
||||
<span class="truncate flex-1">{{ docPreview(doc) }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100 text-destructive shrink-0" @click.stop="requestDeleteDoc(idx)">
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5 opacity-0 group-hover:opacity-100 text-destructive shrink-0" :disabled="!documentStoreEditable" :title="documentStoreEditDisabledReason" @click.stop="requestDeleteDoc(idx)">
|
||||
<Trash2 class="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -2310,7 +2572,7 @@ defineExpose({ focusSearch });
|
||||
<input class="min-w-0 w-full cursor-text select-text appearance-none border-0 bg-transparent p-0 text-inherit outline-none focus:ring-0" :value="selectedDocumentIdLabel" :aria-label="`_id: ${selectedDocumentIdLabel}`" readonly spellcheck="false" />
|
||||
</Badge>
|
||||
<span class="flex-1" />
|
||||
<Button v-if="!isEditing" variant="ghost" size="sm" class="h-6 text-xs" @click="startEdit">{{ t("mongo.edit") }}</Button>
|
||||
<Button v-if="!isEditing" variant="ghost" size="sm" class="h-6 text-xs" :disabled="!documentStoreEditable" :title="documentStoreEditDisabledReason" @click="startEdit">{{ t("mongo.edit") }}</Button>
|
||||
<template v-if="isEditing">
|
||||
<div class="flex items-center border rounded-md overflow-hidden mr-1">
|
||||
<Button variant="ghost" size="sm" class="h-6 rounded-none px-2 text-xs" :class="{ 'bg-accent': documentEditMode === 'json' }" :disabled="isSavingDocument" @click="setDocumentEditMode('json')">{{ t("mongo.editModeJson") }}</Button>
|
||||
|
||||
+358
-6
@@ -6,8 +6,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const backend = vi.hoisted(() => ({
|
||||
getColumns: vi.fn(),
|
||||
documentFindDocuments: vi.fn(),
|
||||
documentCountDocuments: vi.fn(),
|
||||
dynamodbDescribeTable: vi.fn(),
|
||||
cancelQuery: vi.fn(),
|
||||
ensureConnected: vi.fn(),
|
||||
documentInsertDocument: vi.fn(),
|
||||
documentUpdateDocument: vi.fn(),
|
||||
documentDeleteDocument: vi.fn(),
|
||||
documentSaveMeilisearchBatch: vi.fn(),
|
||||
}));
|
||||
@@ -24,6 +28,8 @@ const dataGrid = vi.hoisted(() => ({
|
||||
column_types?: string[];
|
||||
rows: Array<Array<string | number | boolean | null>>;
|
||||
mongo_copy_documents?: unknown[];
|
||||
truncated?: boolean;
|
||||
has_more?: boolean;
|
||||
}
|
||||
| undefined
|
||||
>)
|
||||
@@ -31,8 +37,12 @@ const dataGrid = vi.hoisted(() => ({
|
||||
customSaveHandler: undefined as
|
||||
| {
|
||||
save: (changes: { dirtyRows: Map<number, Map<number, unknown>>; deletedRows: Set<number>; newRows: unknown[][]; newRowMeta: unknown[]; columns: string[]; rows: unknown[][] }) => Promise<void>;
|
||||
preview?: (changes: { dirtyRows: Map<number, Map<number, unknown>>; deletedRows: Set<number>; newRows: unknown[][]; newRowMeta: unknown[]; columns: string[]; rows: unknown[][] }) => Promise<string[]>;
|
||||
}
|
||||
| undefined,
|
||||
countTotalRows: undefined as (() => Promise<number | undefined>) | undefined,
|
||||
paginate: undefined as ((offset: number, limit: number) => Promise<void>) | undefined,
|
||||
editable: true,
|
||||
}));
|
||||
|
||||
const settings = vi.hoisted(() => ({
|
||||
@@ -54,13 +64,19 @@ const settings = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("vue-i18n", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("vue-i18n")>()),
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
useI18n: () => ({
|
||||
t: (key: string, params?: Record<string, unknown>) => (key === "dynamodb.items" ? `${params?.count} Items` : key),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
getColumns: backend.getColumns,
|
||||
documentFindDocuments: backend.documentFindDocuments,
|
||||
documentCountDocuments: backend.documentCountDocuments,
|
||||
dynamodbDescribeTable: backend.dynamodbDescribeTable,
|
||||
cancelQuery: backend.cancelQuery,
|
||||
documentInsertDocument: backend.documentInsertDocument,
|
||||
documentUpdateDocument: backend.documentUpdateDocument,
|
||||
documentDeleteDocument: backend.documentDeleteDocument,
|
||||
documentSaveMeilisearchBatch: backend.documentSaveMeilisearchBatch,
|
||||
}));
|
||||
@@ -90,10 +106,14 @@ vi.mock("@/components/grid/DataGrid.vue", async () => {
|
||||
columnLayoutScopeKey: { type: String, default: "" },
|
||||
fullExportResult: { type: Function, default: undefined },
|
||||
customSaveHandler: { type: Object, default: undefined },
|
||||
countTotalRows: { type: Function, default: undefined },
|
||||
editable: { type: Boolean, default: false },
|
||||
},
|
||||
setup(props, { expose, slots }) {
|
||||
setup(props, { attrs, expose, slots }) {
|
||||
dataGrid.fullExportResult = props.fullExportResult as typeof dataGrid.fullExportResult;
|
||||
dataGrid.customSaveHandler = props.customSaveHandler as typeof dataGrid.customSaveHandler;
|
||||
dataGrid.countTotalRows = props.countTotalRows as typeof dataGrid.countTotalRows;
|
||||
dataGrid.paginate = attrs.onPaginate as typeof dataGrid.paginate;
|
||||
expose({
|
||||
visibleColumnCount: 2,
|
||||
displayableColumnCount: 2,
|
||||
@@ -113,8 +133,9 @@ vi.mock("@/components/grid/DataGrid.vue", async () => {
|
||||
multiRowTranspose: false,
|
||||
setMultiRowTranspose: vi.fn(),
|
||||
});
|
||||
return () =>
|
||||
h(
|
||||
return () => {
|
||||
dataGrid.editable = props.editable;
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
"data-testid": "data-grid",
|
||||
@@ -124,6 +145,7 @@ vi.mock("@/components/grid/DataGrid.vue", async () => {
|
||||
"data-result-hidden-column-keys": JSON.stringify((props.result as { local_hidden_column_keys?: string[] }).local_hidden_column_keys ?? []),
|
||||
"data-result-column-types": JSON.stringify((props.result as { column_types?: string[] }).column_types ?? []),
|
||||
"data-result-rows": JSON.stringify((props.result as { rows?: unknown[] }).rows ?? []),
|
||||
"data-editable": String(props.editable),
|
||||
},
|
||||
[
|
||||
slots["search-bar"]?.({
|
||||
@@ -134,6 +156,7 @@ vi.mock("@/components/grid/DataGrid.vue", async () => {
|
||||
}),
|
||||
],
|
||||
);
|
||||
};
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -215,8 +238,9 @@ vi.mock("@/components/ui/select", async () => {
|
||||
props: {
|
||||
modelValue: { type: String, default: "" },
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
return () => h("div", { "data-testid": "select", "data-model-value": props.modelValue }, slots.default?.());
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, { emit, slots }) {
|
||||
return () => h("div", { "data-testid": "select", "data-model-value": props.modelValue, onClick: () => props.modelValue === "__table__" && emit("update:modelValue", "by_status") }, slots.default?.());
|
||||
},
|
||||
});
|
||||
const passthrough = (name: string) =>
|
||||
@@ -284,14 +308,23 @@ beforeEach(async () => {
|
||||
});
|
||||
backend.getColumns.mockReset();
|
||||
backend.documentFindDocuments.mockReset();
|
||||
backend.documentCountDocuments.mockReset();
|
||||
backend.dynamodbDescribeTable.mockReset();
|
||||
backend.cancelQuery.mockReset();
|
||||
backend.ensureConnected.mockReset();
|
||||
backend.documentInsertDocument.mockReset();
|
||||
backend.documentUpdateDocument.mockReset();
|
||||
backend.documentDeleteDocument.mockReset();
|
||||
backend.documentSaveMeilisearchBatch.mockReset();
|
||||
dataGrid.fullExportResult = undefined;
|
||||
dataGrid.customSaveHandler = undefined;
|
||||
dataGrid.countTotalRows = undefined;
|
||||
dataGrid.paginate = undefined;
|
||||
dataGrid.editable = true;
|
||||
documentJsonEditor.openSearch.mockClear();
|
||||
backend.documentDeleteDocument.mockResolvedValue(undefined);
|
||||
backend.documentInsertDocument.mockResolvedValue("created");
|
||||
backend.documentUpdateDocument.mockResolvedValue(1);
|
||||
backend.documentSaveMeilisearchBatch.mockResolvedValue(0);
|
||||
settings.editorSettings.mongoViewMode = "table";
|
||||
settings.editorSettings.columnWidthDensity = "standard";
|
||||
@@ -306,6 +339,16 @@ beforeEach(async () => {
|
||||
settings.updateEditorSettings.mockReset();
|
||||
settings.updateEditorSettings.mockImplementation((partial: Partial<typeof settings.editorSettings>) => Object.assign(settings.editorSettings, partial));
|
||||
backend.ensureConnected.mockResolvedValue(undefined);
|
||||
backend.documentCountDocuments.mockResolvedValue(0);
|
||||
backend.dynamodbDescribeTable.mockResolvedValue({
|
||||
name: "orders",
|
||||
status: "ACTIVE",
|
||||
itemCount: 0,
|
||||
sizeBytes: 0,
|
||||
partitionKey: { name: "tenant_id", attributeType: "S" },
|
||||
sortKey: { name: "order_id", attributeType: "S" },
|
||||
indexes: [],
|
||||
});
|
||||
backend.getColumns.mockResolvedValue([
|
||||
{ name: "buyers", data_type: "nested" },
|
||||
{ name: "buyers.email", data_type: "text" },
|
||||
@@ -443,6 +486,315 @@ describe("DocumentBrowser Elasticsearch field search", () => {
|
||||
});
|
||||
|
||||
describe("DocumentBrowser MongoDB filter value types", () => {
|
||||
it("loads DynamoDB table metadata before requesting the first page", async () => {
|
||||
app?.unmount();
|
||||
let resolveDescription!: (value: { name: string; status: string; itemCount: number; sizeBytes: number; partitionKey: { name: string; attributeType: string }; sortKey: { name: string; attributeType: string }; indexes: never[] }) => void;
|
||||
backend.dynamodbDescribeTable.mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveDescription = resolve;
|
||||
}),
|
||||
);
|
||||
backend.documentFindDocuments.mockClear();
|
||||
app = createApp(DocumentBrowser, {
|
||||
connectionId: "dynamodb-1",
|
||||
database: "us-east-1",
|
||||
collection: "orders",
|
||||
databaseType: "dynamodb",
|
||||
});
|
||||
app.mount(root!);
|
||||
await flushUi();
|
||||
|
||||
expect(backend.dynamodbDescribeTable).toHaveBeenCalledOnce();
|
||||
expect(backend.documentFindDocuments).not.toHaveBeenCalled();
|
||||
|
||||
resolveDescription({
|
||||
name: "orders",
|
||||
status: "ACTIVE",
|
||||
itemCount: 0,
|
||||
sizeBytes: 0,
|
||||
partitionKey: { name: "tenant_id", attributeType: "S" },
|
||||
sortKey: { name: "order_id", attributeType: "S" },
|
||||
indexes: [],
|
||||
});
|
||||
await flushUi();
|
||||
|
||||
expect(backend.documentFindDocuments).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("labels new DynamoDB rows as conditional inserts in the executable preview", async () => {
|
||||
app?.unmount();
|
||||
backend.documentFindDocuments.mockResolvedValue({
|
||||
documents: [{ _id: { tenant_id: "tenant-04", order_id: "ORD-000994" }, tenant_id: "tenant-04", order_id: "ORD-000994", note: "existing" }],
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
});
|
||||
app = createApp(DocumentBrowser, {
|
||||
connectionId: "dynamodb-1",
|
||||
database: "us-east-1",
|
||||
collection: "orders",
|
||||
databaseType: "dynamodb",
|
||||
});
|
||||
app.mount(root!);
|
||||
await flushUi();
|
||||
|
||||
const preview = await dataGrid.customSaveHandler!.preview!({
|
||||
dirtyRows: new Map(),
|
||||
deletedRows: new Set(),
|
||||
newRows: [['{"tenant_id":"tenant-04","order_id":"ORD-new"}', "tenant-04", "ORD-new", "new item"]],
|
||||
newRowMeta: [{}],
|
||||
columns: ["_id", "tenant_id", "order_id", "note"],
|
||||
rows: [['{"tenant_id":"tenant-04","order_id":"ORD-000994"}', "tenant-04", "ORD-000994", "existing"]],
|
||||
});
|
||||
|
||||
expect(preview).toHaveLength(1);
|
||||
expect(preview[0]).toContain('DBX DYNAMODB INSERT ITEM\ntable: "orders"');
|
||||
expect(preview[0]).toContain('"order_id": "ORD-new"');
|
||||
});
|
||||
|
||||
it("uses one backend update for a DynamoDB key migration and previews the old source key", async () => {
|
||||
app?.unmount();
|
||||
backend.documentFindDocuments.mockResolvedValue({
|
||||
documents: [{ _id: { tenant_id: "tenant-04", order_id: "ORD-old" }, tenant_id: "tenant-04", order_id: "ORD-old", note: "existing" }],
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
});
|
||||
app = createApp(DocumentBrowser, {
|
||||
connectionId: "dynamodb-1",
|
||||
database: "us-east-1",
|
||||
collection: "orders",
|
||||
databaseType: "dynamodb",
|
||||
});
|
||||
app.mount(root!);
|
||||
await flushUi();
|
||||
|
||||
const changes = {
|
||||
dirtyRows: new Map([[0, new Map([[2, "ORD-new"]])]]),
|
||||
deletedRows: new Set<number>(),
|
||||
newRows: [],
|
||||
newRowMeta: [],
|
||||
columns: ["_id", "tenant_id", "order_id", "note"],
|
||||
rows: [['{"tenant_id":"tenant-04","order_id":"ORD-old"}', "tenant-04", "ORD-old", "existing"]],
|
||||
};
|
||||
const preview = await dataGrid.customSaveHandler!.preview!(changes);
|
||||
|
||||
expect(preview).toHaveLength(1);
|
||||
expect(preview[0]).toContain('key:\n{\n "tenant_id": "tenant-04",\n "order_id": "ORD-old"\n}');
|
||||
expect(preview[0]).toContain('"order_id": "ORD-new"');
|
||||
|
||||
await dataGrid.customSaveHandler!.save(changes);
|
||||
expect(backend.documentUpdateDocument).toHaveBeenCalledTimes(1);
|
||||
expect(backend.documentUpdateDocument.mock.calls[0]?.slice(0, 4)).toEqual(["dynamodb-1", "us-east-1", "orders", '{"tenant_id":"tenant-04","order_id":"ORD-old"}']);
|
||||
expect(JSON.parse(backend.documentUpdateDocument.mock.calls[0]?.[4])).toMatchObject({ tenant_id: "tenant-04", order_id: "ORD-new", note: "existing" });
|
||||
expect(backend.documentInsertDocument).not.toHaveBeenCalled();
|
||||
expect(backend.documentDeleteDocument).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("makes partial-projection DynamoDB index results read-only", async () => {
|
||||
app?.unmount();
|
||||
backend.dynamodbDescribeTable.mockResolvedValue({
|
||||
name: "orders",
|
||||
status: "ACTIVE",
|
||||
itemCount: 1,
|
||||
sizeBytes: 100,
|
||||
partitionKey: { name: "tenant_id", attributeType: "S" },
|
||||
sortKey: { name: "order_id", attributeType: "S" },
|
||||
indexes: [
|
||||
{
|
||||
name: "by_status",
|
||||
kind: "global",
|
||||
partitionKey: { name: "status", attributeType: "S" },
|
||||
sortKey: { name: "created_at", attributeType: "N" },
|
||||
projectionType: "KEYS_ONLY",
|
||||
nonKeyAttributes: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
app = createApp(DocumentBrowser, {
|
||||
connectionId: "dynamodb-1",
|
||||
database: "us-east-1",
|
||||
collection: "orders",
|
||||
databaseType: "dynamodb",
|
||||
});
|
||||
app.mount(root!);
|
||||
await flushUi();
|
||||
|
||||
expect(dataGrid.editable).toBe(true);
|
||||
root!.querySelector<HTMLElement>('[data-testid="select"][data-model-value="__table__"]')!.click();
|
||||
await flushUi();
|
||||
|
||||
expect(dataGrid.editable).toBe(false);
|
||||
expect(root!.textContent).toContain("KEYS_ONLY");
|
||||
await expect(
|
||||
dataGrid.customSaveHandler!.save({
|
||||
dirtyRows: new Map(),
|
||||
deletedRows: new Set(),
|
||||
newRows: [],
|
||||
newRowMeta: [],
|
||||
columns: ["_id"],
|
||||
rows: [],
|
||||
}),
|
||||
).rejects.toThrow("dynamodb.partialProjectionReadOnly");
|
||||
});
|
||||
|
||||
it("keeps ALL-projection DynamoDB index results editable", async () => {
|
||||
app?.unmount();
|
||||
backend.dynamodbDescribeTable.mockResolvedValue({
|
||||
name: "orders",
|
||||
status: "ACTIVE",
|
||||
itemCount: 1,
|
||||
sizeBytes: 100,
|
||||
partitionKey: { name: "tenant_id", attributeType: "S" },
|
||||
sortKey: { name: "order_id", attributeType: "S" },
|
||||
indexes: [
|
||||
{
|
||||
name: "by_status",
|
||||
kind: "global",
|
||||
partitionKey: { name: "status", attributeType: "S" },
|
||||
sortKey: { name: "created_at", attributeType: "N" },
|
||||
projectionType: "ALL",
|
||||
nonKeyAttributes: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
app = createApp(DocumentBrowser, {
|
||||
connectionId: "dynamodb-1",
|
||||
database: "us-east-1",
|
||||
collection: "orders",
|
||||
databaseType: "dynamodb",
|
||||
});
|
||||
app.mount(root!);
|
||||
await flushUi();
|
||||
|
||||
root!.querySelector<HTMLElement>('[data-testid="select"][data-model-value="__table__"]')!.click();
|
||||
await flushUi();
|
||||
expect(dataGrid.editable).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps an exact DynamoDB count while moving between cursor pages", async () => {
|
||||
app?.unmount();
|
||||
backend.documentCountDocuments.mockResolvedValue(250);
|
||||
backend.documentFindDocuments.mockReset();
|
||||
backend.documentFindDocuments
|
||||
.mockResolvedValueOnce({
|
||||
documents: Array.from({ length: 100 }, (_, index) => ({ _id: { tenant_id: "tenant-a", order_id: String(index) } })),
|
||||
total: 101,
|
||||
total_is_exact: false,
|
||||
next_cursor: "cursor-1",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
documents: Array.from({ length: 100 }, (_, index) => ({ _id: { tenant_id: "tenant-a", order_id: String(index + 100) } })),
|
||||
total: 101,
|
||||
total_is_exact: false,
|
||||
next_cursor: "cursor-2",
|
||||
});
|
||||
app = createApp(DocumentBrowser, {
|
||||
connectionId: "dynamodb-1",
|
||||
database: "us-east-1",
|
||||
collection: "orders",
|
||||
databaseType: "dynamodb",
|
||||
});
|
||||
app.mount(root!);
|
||||
await flushUi();
|
||||
|
||||
expect(await dataGrid.countTotalRows!()).toBe(250);
|
||||
await flushUi();
|
||||
expect(root!.textContent).toContain("250 Items");
|
||||
|
||||
await dataGrid.paginate!(100, 100);
|
||||
await flushUi();
|
||||
expect(backend.documentFindDocuments.mock.calls.at(-1)?.[10]).toBe("cursor-1");
|
||||
expect(root!.textContent).toContain("250 Items");
|
||||
});
|
||||
|
||||
it("exports all DynamoDB pages with opaque cursors instead of offsets", async () => {
|
||||
app?.unmount();
|
||||
settings.editorSettings.exportBatchSize = 2;
|
||||
backend.documentFindDocuments.mockReset();
|
||||
backend.documentFindDocuments
|
||||
.mockResolvedValueOnce({
|
||||
documents: [{ _id: { tenant_id: "tenant-a", order_id: "visible" }, tenant_id: "tenant-a", order_id: "visible" }],
|
||||
total: 2,
|
||||
total_is_exact: false,
|
||||
next_cursor: "visible-cursor",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
documents: [
|
||||
{ _id: { tenant_id: "tenant-a", order_id: "001" }, tenant_id: "tenant-a", order_id: "001", amount: { $dbxDynamoDb: { version: 1, type: "number", value: "9007199254740993" } } },
|
||||
{ _id: { tenant_id: "tenant-a", order_id: "002" }, tenant_id: "tenant-a", order_id: "002", tags: { $dbxDynamoDb: { version: 1, type: "stringSet", value: ["new", "paid"] } } },
|
||||
],
|
||||
total: 3,
|
||||
total_is_exact: false,
|
||||
next_cursor: "cursor-1",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
documents: [{ _id: { tenant_id: "tenant-b", order_id: "003" }, tenant_id: "tenant-b", order_id: "003" }],
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
});
|
||||
app = createApp(DocumentBrowser, {
|
||||
connectionId: "dynamodb-1",
|
||||
database: "us-east-1",
|
||||
collection: "orders",
|
||||
databaseType: "dynamodb",
|
||||
});
|
||||
app.mount(root!);
|
||||
await flushUi();
|
||||
|
||||
expect(dataGrid.fullExportResult).toBeTypeOf("function");
|
||||
const progress = vi.fn();
|
||||
const result = await dataGrid.fullExportResult!(progress);
|
||||
|
||||
expect(backend.documentFindDocuments.mock.calls.slice(1).map((call) => [call[3], call[4], call[10]])).toEqual([
|
||||
[0, 2, undefined],
|
||||
[0, 2, "cursor-1"],
|
||||
]);
|
||||
expect(result?.rows).toHaveLength(3);
|
||||
expect(result?.rows[0]).toContain('{"$dbxDynamoDb":{"version":1,"type":"number","value":"9007199254740993"}}');
|
||||
expect(result?.rows[1]).toContain('{"$dbxDynamoDb":{"version":1,"type":"stringSet","value":["new","paid"]}}');
|
||||
expect(progress).toHaveBeenLastCalledWith({ rowsExported: 3, totalRows: 3 });
|
||||
});
|
||||
|
||||
it("caps an otherwise unlimited DynamoDB export at 10,000 items", async () => {
|
||||
app?.unmount();
|
||||
settings.editorSettings.exportBatchSize = 1000;
|
||||
backend.documentFindDocuments.mockReset();
|
||||
backend.documentFindDocuments.mockResolvedValueOnce({
|
||||
documents: [{ _id: { tenant_id: "visible", order_id: "visible" } }],
|
||||
total: 2,
|
||||
total_is_exact: false,
|
||||
next_cursor: "visible-cursor",
|
||||
});
|
||||
for (let pageIndex = 0; pageIndex < 10; pageIndex += 1) {
|
||||
backend.documentFindDocuments.mockResolvedValueOnce({
|
||||
documents: Array.from({ length: 1000 }, (_, rowIndex) => ({
|
||||
_id: { tenant_id: `tenant-${pageIndex}`, order_id: String(rowIndex) },
|
||||
tenant_id: `tenant-${pageIndex}`,
|
||||
order_id: String(rowIndex),
|
||||
})),
|
||||
total: 1001,
|
||||
total_is_exact: false,
|
||||
next_cursor: `cursor-${pageIndex + 1}`,
|
||||
});
|
||||
}
|
||||
app = createApp(DocumentBrowser, {
|
||||
connectionId: "dynamodb-1",
|
||||
database: "us-east-1",
|
||||
collection: "orders",
|
||||
databaseType: "dynamodb",
|
||||
});
|
||||
app.mount(root!);
|
||||
await flushUi();
|
||||
|
||||
const progress = vi.fn();
|
||||
const result = await dataGrid.fullExportResult!(progress);
|
||||
|
||||
expect(result?.rows).toHaveLength(10_000);
|
||||
expect(result?.truncated).toBe(true);
|
||||
expect(result?.has_more).toBe(true);
|
||||
expect(backend.documentFindDocuments).toHaveBeenCalledTimes(11);
|
||||
expect(progress).toHaveBeenLastCalledWith({ rowsExported: 10_000, totalRows: null });
|
||||
});
|
||||
|
||||
it("exports all matching MongoDB documents without changing the visible page", async () => {
|
||||
app?.unmount();
|
||||
settings.editorSettings.exportBatchSize = 2;
|
||||
|
||||
@@ -22,6 +22,7 @@ const assetIcons: Record<string, string> = {
|
||||
redis: "redis",
|
||||
mongodb: "mongodb",
|
||||
mongodb_legacy: "mongodb",
|
||||
dynamodb: "dynamodb",
|
||||
clickhouse: "clickhouse",
|
||||
duckdb: "duckdb",
|
||||
mariadb: "mariadb",
|
||||
|
||||
@@ -397,7 +397,7 @@ const SEARCH_SCOPE_TO_NODE_TYPES: Record<SearchScope, TreeNodeType[]> = {
|
||||
connection: ["connection"],
|
||||
database: ["database", "redis-db", "mq-tenant", "nacos-namespace", "consul-root", "mongo-db"],
|
||||
schema: ["schema"],
|
||||
table: ["table", "mongo-collection", "mongo-bucket", "vector-collection", "elasticsearch-index"],
|
||||
table: ["table", "mongo-collection", "mongo-bucket", "dynamodb-table", "vector-collection", "elasticsearch-index"],
|
||||
view: ["view"],
|
||||
};
|
||||
|
||||
@@ -679,7 +679,7 @@ const sidebarCommentLabelWidths = shallowRef(new Map<string, number>());
|
||||
let sidebarCommentMeasureFrame = 0;
|
||||
const sidebarTreeContentWidth = ref(0);
|
||||
let sidebarTreeContentMeasureFrame = 0;
|
||||
const sidebarTableNameDisplayTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "mongo-collection", "vector-collection", "elasticsearch-index"]);
|
||||
const sidebarTableNameDisplayTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "mongo-collection", "dynamodb-table", "vector-collection", "elasticsearch-index"]);
|
||||
const sidebarStorageDisplayTypes = new Set<TreeNodeType>(["database", "table", "materialized_view"]);
|
||||
|
||||
function sidebarCommentLabel(node: TreeNode): string {
|
||||
@@ -1365,6 +1365,8 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
|
||||
await store.loadRedisDatabases(connId);
|
||||
} else if (config.db_type === "mongodb") {
|
||||
await store.loadMongoDatabases(connId);
|
||||
} else if (config.db_type === "dynamodb") {
|
||||
await store.loadDynamoDbTables(connId);
|
||||
} else if (config.db_type === "elasticsearch" || config.db_type === "easysearch" || config.db_type === "meilisearch") {
|
||||
await store.loadElasticsearchIndices(connId);
|
||||
} else if (config.db_type === "qdrant" || config.db_type === "milvus" || config.db_type === "weaviate" || config.db_type === "chromadb") {
|
||||
|
||||
@@ -752,6 +752,8 @@ async function toggle(requestId = beginNavigationRequest()) {
|
||||
await connectionStore.loadConsulRoot(node.connectionId);
|
||||
} else if (config?.db_type === "mongodb") {
|
||||
await connectionStore.loadMongoDatabases(node.connectionId);
|
||||
} else if (config?.db_type === "dynamodb") {
|
||||
await connectionStore.loadDynamoDbTables(node.connectionId);
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
||||
// Expand: list indices (like other db types list databases).
|
||||
await connectionStore.loadElasticsearchIndices(node.connectionId);
|
||||
@@ -1317,6 +1319,18 @@ function openMongoTreeData(node: TreeNode) {
|
||||
queryStore.openMongoBucket(node.connectionId, node.database, node.label);
|
||||
return;
|
||||
}
|
||||
if (node.type === "dynamodb-table") {
|
||||
const tab = queryStore.createTab(node.connectionId, node.database, `${node.database}.${node.label}`, "mongo");
|
||||
queryStore.updateSql(tab, node.label);
|
||||
queryStore.setTableMeta(tab, {
|
||||
database: node.database,
|
||||
tableName: node.label,
|
||||
tableType: "DYNAMODB TABLE",
|
||||
columns: [],
|
||||
primaryKeys: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (node.type !== "mongo-collection") return;
|
||||
const tab = queryStore.createTab(node.connectionId, node.database, tabTitle, "mongo");
|
||||
queryStore.updateSql(tab, node.label);
|
||||
@@ -3765,7 +3779,7 @@ const canOpenSqlFileExecution = computed(() => {
|
||||
const canExportAllDatabases = computed(() => {
|
||||
if (activeNode.value.type !== "connection" || !activeNode.value.connectionId) return false;
|
||||
const dbType = connectionStore.getConfig(activeNode.value.connectionId)?.db_type;
|
||||
return !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(dbType || "");
|
||||
return !["redis", "mongodb", "dynamodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(dbType || "");
|
||||
});
|
||||
|
||||
const canOpenDiagram = computed(() => {
|
||||
@@ -4889,6 +4903,13 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.type === "dynamodb-table") {
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.viewData"), action: toggle, icon: TableProperties });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.type === "mongo-collection") {
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
items.push({ label: "", separator: true });
|
||||
|
||||
@@ -304,6 +304,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
||||
return { icon: Archive, colorClass: "text-cyan-400" };
|
||||
case "mongo-collection":
|
||||
return { icon: Table, colorClass: "text-green-400" };
|
||||
case "dynamodb-table":
|
||||
return { icon: Table, colorClass: "text-amber-500" };
|
||||
case "vector-collection":
|
||||
return { icon: TableProperties, colorClass: "text-cyan-400" };
|
||||
case "elasticsearch-index":
|
||||
@@ -385,7 +387,7 @@ function treeNodeSecondaryValue(node: TreeNode): string | undefined {
|
||||
|
||||
function visibleLabel(node: TreeNode): string {
|
||||
const withValidity = (label: string) => (node.valid === false ? `${label} · INVALID` : label);
|
||||
if (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "vector-collection" || node.type === "elasticsearch-index") {
|
||||
if (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "dynamodb-table" || node.type === "vector-collection" || node.type === "elasticsearch-index") {
|
||||
return withValidity(sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes));
|
||||
}
|
||||
return withValidity(displayLabel(node));
|
||||
|
||||
@@ -44,7 +44,7 @@ async function copy() {
|
||||
<!-- card: 卡片类报错信息面板 -->
|
||||
<div v-if="variant === 'card'" class="mx-3 my-2 rounded-lg border border-destructive/30 bg-destructive/10 p-3 shadow-sm shrink-0 select-text flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between gap-2 border-b border-destructive/20 pb-2">
|
||||
<div class="flex items-center gap-2 font-semibold text-xs text-destructive">
|
||||
<div data-native-clipboard class="flex items-center gap-2 font-semibold text-xs text-destructive">
|
||||
<TriangleAlert class="h-4 w-4 text-destructive shrink-0" aria-hidden="true" />
|
||||
<span>{{ displayTitle }}</span>
|
||||
</div>
|
||||
@@ -58,14 +58,14 @@ async function copy() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-h-40 min-h-[56px] overflow-y-auto rounded bg-background/80 dark:bg-background/50 border border-destructive/20 p-2.5 text-xs font-mono leading-relaxed text-destructive break-words whitespace-pre-wrap select-text cursor-text" @mousedown.stop @click.stop>
|
||||
<div data-native-clipboard class="max-h-40 min-h-[56px] overflow-y-auto rounded bg-background/80 dark:bg-background/50 border border-destructive/20 p-2.5 text-xs font-mono leading-relaxed text-destructive break-words whitespace-pre-wrap select-text cursor-text" @mousedown.stop @click.stop>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- banner: 紧凑内联横幅 -->
|
||||
<div v-else-if="variant === 'banner'" class="flex items-center gap-2 px-3 py-1.5 border-t bg-destructive/10 text-destructive text-xs shrink-0">
|
||||
<span class="flex-1 min-w-0 break-all">{{ message }}</span>
|
||||
<span data-native-clipboard class="flex-1 min-w-0 break-all">{{ message }}</span>
|
||||
<button v-if="copyMode === 'label'" type="button" class="shrink-0 hover:underline" :aria-label="t('grid.copy')" @click.stop="copy">
|
||||
{{ t("grid.copy") }}
|
||||
</button>
|
||||
@@ -78,7 +78,7 @@ async function copy() {
|
||||
<!-- centered: 居中占满 -->
|
||||
<div v-else class="flex-1 min-h-0 flex flex-col items-center justify-center gap-2 px-6 py-4 text-center">
|
||||
<TriangleAlert class="h-8 w-8 text-destructive/50" aria-hidden="true" />
|
||||
<div class="min-h-0 max-h-48 max-w-lg overflow-auto space-y-1 select-text text-destructive" @mousedown.stop @click.stop>
|
||||
<div data-native-clipboard class="min-h-0 max-h-48 max-w-lg overflow-auto space-y-1 select-text text-destructive" @mousedown.stop @click.stop>
|
||||
<div class="text-sm font-medium">{{ displayTitle }}</div>
|
||||
<div class="text-xs break-all cursor-text text-destructive/80 select-text">{{ message }}</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, nextTick } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import ErrorBanner from "@/components/ui/ErrorBanner.vue";
|
||||
import { eventTargetAllowsNativeClipboard } from "@/lib/common/clipboard";
|
||||
|
||||
const mountedContainers: HTMLDivElement[] = [];
|
||||
|
||||
function findNativeClipboardRegion(container: HTMLElement, text: string): HTMLElement {
|
||||
const region = Array.from(container.querySelectorAll<HTMLElement>("[data-native-clipboard]")).find((element) => element.textContent?.trim() === text);
|
||||
if (!region) throw new Error(`Native clipboard region not found: ${text}`);
|
||||
return region;
|
||||
}
|
||||
|
||||
function selectRegion(region: HTMLElement) {
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(region);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
}
|
||||
|
||||
async function mountCardBanner() {
|
||||
const container = document.createElement("div");
|
||||
mountedContainers.push(container);
|
||||
document.body.append(container);
|
||||
|
||||
const app = createApp(ErrorBanner, {
|
||||
variant: "card",
|
||||
title: "Save failed",
|
||||
message: "Conditional request failed",
|
||||
});
|
||||
app.use(
|
||||
createI18n({
|
||||
legacy: false,
|
||||
locale: "en",
|
||||
messages: { en: { grid: { copy: "Copy", dismiss: "Dismiss" } } },
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
}),
|
||||
);
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
return container;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
window.getSelection()?.removeAllRanges();
|
||||
for (const container of mountedContainers.splice(0)) container.remove();
|
||||
});
|
||||
|
||||
describe("ErrorBanner native clipboard selection", () => {
|
||||
it.each(["Save failed", "Conditional request failed"])("preserves native copy for selected %s text", async (text) => {
|
||||
const container = await mountCardBanner();
|
||||
selectRegion(findNativeClipboardRegion(container, text));
|
||||
|
||||
expect(
|
||||
eventTargetAllowsNativeClipboard({
|
||||
key: "c",
|
||||
metaKey: true,
|
||||
target: container,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves grid copy shortcuts active without a text selection", async () => {
|
||||
const container = await mountCardBanner();
|
||||
|
||||
expect(
|
||||
eventTargetAllowsNativeClipboard({
|
||||
key: "c",
|
||||
metaKey: true,
|
||||
target: container,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -443,6 +443,13 @@ export default {
|
||||
sqliteExtensionBrowse: "Select SQLite extension",
|
||||
user: "User",
|
||||
password: "Password",
|
||||
dynamodbEndpoint: "Endpoint",
|
||||
dynamodbRegion: "Region",
|
||||
dynamodbAccessKeyId: "Access Key ID",
|
||||
dynamodbSecretAccessKey: "Secret Access Key",
|
||||
dynamodbSessionToken: "Session Token",
|
||||
dynamodbSessionTokenPlaceholder: "Optional",
|
||||
dynamodbCredentialsRequired: "DynamoDB requires an Access Key ID and Secret Access Key",
|
||||
savePassword: "Save password",
|
||||
savePasswordHint: "The password is saved, so you won't need to enter it again.",
|
||||
savePasswordSessionHint: "The password is kept only for this session and cleared when you quit the app.",
|
||||
@@ -4510,6 +4517,23 @@ export default {
|
||||
filterPlaceholder: "Filter...",
|
||||
sortPlaceholder: "Sort...",
|
||||
},
|
||||
dynamodb: {
|
||||
tableMetadataUnavailable: "DynamoDB table metadata is not loaded. Refresh and try again.",
|
||||
keyRequired: "DynamoDB items require the key attribute {key}",
|
||||
baseTable: "Base table",
|
||||
filter: "filter",
|
||||
sortKey: "sort key",
|
||||
items: "{count} Items",
|
||||
index: "DynamoDB index",
|
||||
keySummary: "Partition key: {partitionKey}; sort key: {sortKey}",
|
||||
none: "none",
|
||||
sortKeyOnly: "DynamoDB Query can sort only by the current index sort key: {key}",
|
||||
pageCursorUnavailable: "The DynamoDB page cursor is unavailable. Reload from the first page.",
|
||||
repeatedCursor: "DynamoDB returned a repeated page cursor. Export was stopped.",
|
||||
partialProjectionReadOnly: "This {projection} index does not contain the complete item. Editing is disabled to prevent unprojected attributes from being overwritten.",
|
||||
exportLimitReached: "DynamoDB export stopped at {count} items to limit memory and read usage. Set an export row limit to choose a different boundary.",
|
||||
documentDetails: "DynamoDB table: {table}\nItem key: {id}",
|
||||
},
|
||||
vector: {
|
||||
collectionFallback: "collection",
|
||||
productCollection: "{product} collection",
|
||||
|
||||
@@ -445,6 +445,13 @@ export default withEnglishFallback({
|
||||
sqliteExtensionBrowse: "Seleccionar extensión de SQLite",
|
||||
user: "Usuario",
|
||||
password: "Contraseña",
|
||||
dynamodbEndpoint: "Endpoint",
|
||||
dynamodbRegion: "Región",
|
||||
dynamodbAccessKeyId: "ID de clave de acceso",
|
||||
dynamodbSecretAccessKey: "Clave de acceso secreta",
|
||||
dynamodbSessionToken: "Token de sesión",
|
||||
dynamodbSessionTokenPlaceholder: "Opcional",
|
||||
dynamodbCredentialsRequired: "DynamoDB requiere un ID de clave de acceso y una clave de acceso secreta",
|
||||
savePassword: "Guardar contraseña",
|
||||
savePasswordHint: "La contraseña se guardará para que no tengas que volver a introducirla.",
|
||||
savePasswordSessionHint: "La contraseña se conserva solo durante esta sesión y se borra al salir de la aplicación.",
|
||||
@@ -4352,6 +4359,23 @@ export default withEnglishFallback({
|
||||
indexOtherOptions: "Otras opciones",
|
||||
indexPropertiesUnavailable: "El controlador Legacy de MongoDB no puede informar disperso, expiración, background o tamaño de bucket. Conéctese con el controlador nativo para verlos.",
|
||||
},
|
||||
dynamodb: {
|
||||
tableMetadataUnavailable: "Los metadatos de la tabla DynamoDB no se han cargado. Actualice e inténtelo de nuevo.",
|
||||
keyRequired: "Los elementos de DynamoDB requieren el atributo de clave {key}",
|
||||
baseTable: "Tabla base",
|
||||
filter: "filtro",
|
||||
sortKey: "clave de ordenación",
|
||||
items: "{count} elementos",
|
||||
index: "Índice de DynamoDB",
|
||||
keySummary: "Clave de partición: {partitionKey}; clave de ordenación: {sortKey}",
|
||||
none: "ninguna",
|
||||
sortKeyOnly: "DynamoDB Query solo puede ordenar por la clave de ordenación del índice actual: {key}",
|
||||
pageCursorUnavailable: "El cursor de página de DynamoDB no está disponible. Recargue desde la primera página.",
|
||||
repeatedCursor: "DynamoDB devolvió un cursor de página repetido. Se detuvo la exportación.",
|
||||
partialProjectionReadOnly: "Este índice {projection} no contiene el elemento completo. La edición está deshabilitada para evitar sobrescribir atributos no proyectados.",
|
||||
exportLimitReached: "La exportación de DynamoDB se detuvo en {count} elementos para limitar el uso de memoria y lecturas. Configure un límite de filas de exportación para elegir otro alcance.",
|
||||
documentDetails: "Tabla de DynamoDB: {table}\nClave del elemento: {id}",
|
||||
},
|
||||
vector: {
|
||||
collectionFallback: "colección",
|
||||
productCollection: "Colección de {product}",
|
||||
|
||||
@@ -444,6 +444,13 @@ export default withEnglishFallback({
|
||||
sqliteExtensionBrowse: "Seleziona estensione SQLite",
|
||||
user: "Utente",
|
||||
password: "Password",
|
||||
dynamodbEndpoint: "Endpoint",
|
||||
dynamodbRegion: "Regione",
|
||||
dynamodbAccessKeyId: "ID chiave di accesso",
|
||||
dynamodbSecretAccessKey: "Chiave di accesso segreta",
|
||||
dynamodbSessionToken: "Token di sessione",
|
||||
dynamodbSessionTokenPlaceholder: "Opzionale",
|
||||
dynamodbCredentialsRequired: "DynamoDB richiede un ID chiave di accesso e una chiave di accesso segreta",
|
||||
savePassword: "Salva password",
|
||||
savePasswordHint: "La password verrà salvata, quindi non dovrai inserirla di nuovo.",
|
||||
savePasswordSessionHint: "La password viene conservata solo per questa sessione e cancellata alla chiusura dell'app.",
|
||||
@@ -4350,6 +4357,23 @@ export default withEnglishFallback({
|
||||
indexOtherOptions: "Altre opzioni",
|
||||
indexPropertiesUnavailable: "Il driver Legacy di MongoDB non può riportare sparsità, scadenza, background o dimensione bucket. Connettersi con il driver nativo per vederli.",
|
||||
},
|
||||
dynamodb: {
|
||||
tableMetadataUnavailable: "I metadati della tabella DynamoDB non sono caricati. Aggiorna e riprova.",
|
||||
keyRequired: "Gli elementi DynamoDB richiedono l'attributo chiave {key}",
|
||||
baseTable: "Tabella base",
|
||||
filter: "filtro",
|
||||
sortKey: "chiave di ordinamento",
|
||||
items: "{count} elementi",
|
||||
index: "Indice DynamoDB",
|
||||
keySummary: "Chiave di partizione: {partitionKey}; chiave di ordinamento: {sortKey}",
|
||||
none: "nessuna",
|
||||
sortKeyOnly: "DynamoDB Query può ordinare solo per la chiave di ordinamento dell'indice corrente: {key}",
|
||||
pageCursorUnavailable: "Il cursore di pagina DynamoDB non è disponibile. Ricarica dalla prima pagina.",
|
||||
repeatedCursor: "DynamoDB ha restituito un cursore di pagina ripetuto. L'esportazione è stata interrotta.",
|
||||
partialProjectionReadOnly: "Questo indice {projection} non contiene l'elemento completo. La modifica è disabilitata per evitare di sovrascrivere attributi non proiettati.",
|
||||
exportLimitReached: "L'esportazione DynamoDB si è fermata a {count} elementi per limitare memoria e letture. Imposta un limite di righe per scegliere un intervallo diverso.",
|
||||
documentDetails: "Tabella DynamoDB: {table}\nChiave elemento: {id}",
|
||||
},
|
||||
vector: {
|
||||
collectionFallback: "collezione",
|
||||
productCollection: "Collezione {product}",
|
||||
|
||||
@@ -444,6 +444,13 @@ export default withEnglishFallback({
|
||||
sqliteExtensionBrowse: "SQLite拡張を選択",
|
||||
user: "ユーザー",
|
||||
password: "パスワード",
|
||||
dynamodbEndpoint: "エンドポイント",
|
||||
dynamodbRegion: "リージョン",
|
||||
dynamodbAccessKeyId: "アクセスキー ID",
|
||||
dynamodbSecretAccessKey: "シークレットアクセスキー",
|
||||
dynamodbSessionToken: "セッショントークン",
|
||||
dynamodbSessionTokenPlaceholder: "任意",
|
||||
dynamodbCredentialsRequired: "DynamoDB にはアクセスキー ID とシークレットアクセスキーが必要です",
|
||||
savePassword: "パスワードを保存",
|
||||
savePasswordHint: "パスワードを保存し、次回以降の接続では再入力を不要にします。",
|
||||
savePasswordSessionHint: "パスワードは今回の実行中のみ保持され、アプリ終了時に消去されます。",
|
||||
@@ -4380,6 +4387,23 @@ export default withEnglishFallback({
|
||||
indexOtherOptions: "その他のオプション",
|
||||
indexPropertiesUnavailable: "MongoDB Legacy ドライバーはスパース、有効期限、バックグラウンド、バケットサイズを報告できません。ネイティブドライバーで接続して確認してください。",
|
||||
},
|
||||
dynamodb: {
|
||||
tableMetadataUnavailable: "DynamoDB テーブルのメタデータが読み込まれていません。更新して再試行してください。",
|
||||
keyRequired: "DynamoDB 項目にはキー属性 {key} が必要です",
|
||||
baseTable: "ベーステーブル",
|
||||
filter: "フィルター",
|
||||
sortKey: "ソートキー",
|
||||
items: "{count} 件",
|
||||
index: "DynamoDB インデックス",
|
||||
keySummary: "パーティションキー: {partitionKey}; ソートキー: {sortKey}",
|
||||
none: "なし",
|
||||
sortKeyOnly: "DynamoDB Query は現在のインデックスのソートキー {key} でのみ並べ替えできます",
|
||||
pageCursorUnavailable: "DynamoDB のページカーソルを利用できません。最初のページから再読み込みしてください。",
|
||||
repeatedCursor: "DynamoDB が同じページカーソルを返したため、エクスポートを停止しました。",
|
||||
partialProjectionReadOnly: "この {projection} インデックスには項目全体が含まれないため、未投影属性の上書きを防ぐ目的で編集を無効にしました。",
|
||||
exportLimitReached: "メモリと読み取り使用量を制限するため、DynamoDB のエクスポートは {count} 件で停止しました。別の範囲にするにはエクスポート行数上限を設定してください。",
|
||||
documentDetails: "DynamoDB テーブル: {table}\n項目キー: {id}",
|
||||
},
|
||||
vector: {
|
||||
collectionFallback: "コレクション",
|
||||
productCollection: "{product} コレクション",
|
||||
|
||||
@@ -437,6 +437,13 @@ export default withEnglishFallback({
|
||||
sqliteExtensionBrowse: "SQLite 확장 선택",
|
||||
user: "사용자",
|
||||
password: "비밀번호",
|
||||
dynamodbEndpoint: "엔드포인트",
|
||||
dynamodbRegion: "리전",
|
||||
dynamodbAccessKeyId: "액세스 키 ID",
|
||||
dynamodbSecretAccessKey: "비밀 액세스 키",
|
||||
dynamodbSessionToken: "세션 토큰",
|
||||
dynamodbSessionTokenPlaceholder: "선택",
|
||||
dynamodbCredentialsRequired: "DynamoDB에는 액세스 키 ID와 비밀 액세스 키가 필요합니다",
|
||||
savePassword: "비밀번호 저장",
|
||||
savePasswordHint: "비밀번호를 저장하여 다음 연결부터 다시 입력하지 않아도 됩니다.",
|
||||
savePasswordSessionHint: "비밀번호는 이번 실행 동안만 유지되며 앱을 종료하면 삭제됩니다.",
|
||||
@@ -3995,6 +4002,23 @@ export default withEnglishFallback({
|
||||
indexOtherOptions: "기타 옵션",
|
||||
indexPropertiesUnavailable: "MongoDB Legacy 드라이버는 희소, 만료, 백그라운드 또는 버킷 크기를 보고할 수 없습니다. 네이티브 드라이버로 연결하여 확인하세요.",
|
||||
},
|
||||
dynamodb: {
|
||||
tableMetadataUnavailable: "DynamoDB 테이블 메타데이터가 로드되지 않았습니다. 새로 고친 후 다시 시도하세요.",
|
||||
keyRequired: "DynamoDB 항목에는 키 속성 {key}이(가) 필요합니다",
|
||||
baseTable: "기본 테이블",
|
||||
filter: "필터",
|
||||
sortKey: "정렬 키",
|
||||
items: "항목 {count}개",
|
||||
index: "DynamoDB 인덱스",
|
||||
keySummary: "파티션 키: {partitionKey}; 정렬 키: {sortKey}",
|
||||
none: "없음",
|
||||
sortKeyOnly: "DynamoDB Query는 현재 인덱스의 정렬 키 {key}로만 정렬할 수 있습니다",
|
||||
pageCursorUnavailable: "DynamoDB 페이지 커서를 사용할 수 없습니다. 첫 페이지부터 다시 불러오세요.",
|
||||
repeatedCursor: "DynamoDB가 중복 페이지 커서를 반환하여 내보내기를 중지했습니다.",
|
||||
partialProjectionReadOnly: "이 {projection} 인덱스에는 전체 항목이 포함되지 않으므로 투영되지 않은 속성이 덮어쓰이지 않도록 편집을 비활성화했습니다.",
|
||||
exportLimitReached: "메모리와 읽기 사용량을 제한하기 위해 DynamoDB 내보내기가 {count}개 항목에서 중지되었습니다. 다른 범위를 사용하려면 내보내기 행 제한을 설정하세요.",
|
||||
documentDetails: "DynamoDB 테이블: {table}\n항목 키: {id}",
|
||||
},
|
||||
vector: {
|
||||
collectionFallback: "컬렉션",
|
||||
productCollection: "{product} 컬렉션",
|
||||
|
||||
@@ -445,6 +445,13 @@ export default withEnglishFallback({
|
||||
sqliteExtensionBrowse: "Selecionar extensão SQLite",
|
||||
user: "Usuário",
|
||||
password: "Senha",
|
||||
dynamodbEndpoint: "Endpoint",
|
||||
dynamodbRegion: "Região",
|
||||
dynamodbAccessKeyId: "ID da chave de acesso",
|
||||
dynamodbSecretAccessKey: "Chave de acesso secreta",
|
||||
dynamodbSessionToken: "Token de sessão",
|
||||
dynamodbSessionTokenPlaceholder: "Opcional",
|
||||
dynamodbCredentialsRequired: "O DynamoDB requer um ID de chave de acesso e uma chave de acesso secreta",
|
||||
savePassword: "Salvar senha",
|
||||
savePasswordHint: "A senha será salva para que você não precise digitá-la novamente.",
|
||||
savePasswordSessionHint: "A senha é mantida apenas nesta sessão e apagada ao sair do aplicativo.",
|
||||
@@ -4352,6 +4359,23 @@ export default withEnglishFallback({
|
||||
indexOtherOptions: "Outras opções",
|
||||
indexPropertiesUnavailable: "O driver Legacy do MongoDB não pode informar esparso, expiração, background ou tamanho do bucket. Conecte-se com o driver nativo para vê-los.",
|
||||
},
|
||||
dynamodb: {
|
||||
tableMetadataUnavailable: "Os metadados da tabela DynamoDB não foram carregados. Atualize e tente novamente.",
|
||||
keyRequired: "Os itens do DynamoDB exigem o atributo de chave {key}",
|
||||
baseTable: "Tabela base",
|
||||
filter: "filtro",
|
||||
sortKey: "chave de ordenação",
|
||||
items: "{count} itens",
|
||||
index: "Índice do DynamoDB",
|
||||
keySummary: "Chave de partição: {partitionKey}; chave de ordenação: {sortKey}",
|
||||
none: "nenhuma",
|
||||
sortKeyOnly: "DynamoDB Query só pode ordenar pela chave de ordenação do índice atual: {key}",
|
||||
pageCursorUnavailable: "O cursor de página do DynamoDB não está disponível. Recarregue a partir da primeira página.",
|
||||
repeatedCursor: "O DynamoDB retornou um cursor de página repetido. A exportação foi interrompida.",
|
||||
partialProjectionReadOnly: "Este índice {projection} não contém o item completo. A edição foi desativada para evitar a substituição de atributos não projetados.",
|
||||
exportLimitReached: "A exportação do DynamoDB parou em {count} itens para limitar o uso de memória e leituras. Defina um limite de linhas para escolher outro intervalo.",
|
||||
documentDetails: "Tabela do DynamoDB: {table}\nChave do item: {id}",
|
||||
},
|
||||
vector: {
|
||||
collectionFallback: "coleção",
|
||||
productCollection: "coleção {product}",
|
||||
|
||||
@@ -368,6 +368,13 @@ export default withEnglishFallback({
|
||||
sqliteExtensionBrowse: "选择 SQLite 扩展库",
|
||||
user: "用户名",
|
||||
password: "密码",
|
||||
dynamodbEndpoint: "Endpoint",
|
||||
dynamodbRegion: "区域",
|
||||
dynamodbAccessKeyId: "Access Key ID",
|
||||
dynamodbSecretAccessKey: "Secret Access Key",
|
||||
dynamodbSessionToken: "Session Token",
|
||||
dynamodbSessionTokenPlaceholder: "可选",
|
||||
dynamodbCredentialsRequired: "DynamoDB 需要填写 Access Key ID 和 Secret Access Key",
|
||||
savePassword: "保存密码",
|
||||
savePasswordHint: "密码将被保存,后续连接无需再次输入。",
|
||||
savePasswordSessionHint: "密码仅在本次运行期间保留,退出应用后清除。",
|
||||
@@ -4498,6 +4505,23 @@ export default withEnglishFallback({
|
||||
filterPlaceholder: "过滤条件...",
|
||||
sortPlaceholder: "排序条件...",
|
||||
},
|
||||
dynamodb: {
|
||||
tableMetadataUnavailable: "尚未加载 DynamoDB 表结构,请刷新后重试",
|
||||
keyRequired: "DynamoDB 项目必须填写键字段 {key}",
|
||||
baseTable: "主表",
|
||||
filter: "筛选",
|
||||
sortKey: "排序键",
|
||||
items: "{count} 个项目",
|
||||
index: "DynamoDB 索引",
|
||||
keySummary: "分区键:{partitionKey};排序键:{sortKey}",
|
||||
none: "无",
|
||||
sortKeyOnly: "DynamoDB Query 只能按当前索引的排序键 {key} 排序",
|
||||
pageCursorUnavailable: "当前 DynamoDB 页游标不可用,请从第一页重新加载",
|
||||
repeatedCursor: "DynamoDB 返回了重复的分页游标,已停止导出",
|
||||
partialProjectionReadOnly: "当前 {projection} 索引不包含完整项目,为避免覆盖未投影属性,已禁用编辑",
|
||||
exportLimitReached: "为限制内存和读取用量,DynamoDB 导出已在 {count} 条停止;可设置导出行数上限来调整范围",
|
||||
documentDetails: "DynamoDB 表:{table}\n项目键:{id}",
|
||||
},
|
||||
vector: {
|
||||
collectionFallback: "集合",
|
||||
productCollection: "{product} 集合",
|
||||
|
||||
@@ -445,6 +445,13 @@ export default withEnglishFallback({
|
||||
sqliteExtensionBrowse: "選擇 SQLite 擴充庫",
|
||||
user: "使用者名稱",
|
||||
password: "密碼",
|
||||
dynamodbEndpoint: "Endpoint",
|
||||
dynamodbRegion: "區域",
|
||||
dynamodbAccessKeyId: "Access Key ID",
|
||||
dynamodbSecretAccessKey: "Secret Access Key",
|
||||
dynamodbSessionToken: "Session Token",
|
||||
dynamodbSessionTokenPlaceholder: "選填",
|
||||
dynamodbCredentialsRequired: "DynamoDB 需要填寫 Access Key ID 和 Secret Access Key",
|
||||
savePassword: "儲存密碼",
|
||||
savePasswordHint: "密碼將被儲存,後續連線無需再次輸入。",
|
||||
savePasswordSessionHint: "密碼僅在本次執行期間保留,退出應用程式後清除。",
|
||||
@@ -3694,6 +3701,23 @@ export default withEnglishFallback({
|
||||
invalidNativeJson: "原生 JSON 格式無效:{error}",
|
||||
nativeDocumentRequired: "原生 JSON 的最上層必須是文件物件。",
|
||||
},
|
||||
dynamodb: {
|
||||
tableMetadataUnavailable: "尚未載入 DynamoDB 表結構,請重新整理後再試",
|
||||
keyRequired: "DynamoDB 項目必須填寫鍵欄位 {key}",
|
||||
baseTable: "主表",
|
||||
filter: "篩選",
|
||||
sortKey: "排序鍵",
|
||||
items: "{count} 個項目",
|
||||
index: "DynamoDB 索引",
|
||||
keySummary: "分區鍵:{partitionKey};排序鍵:{sortKey}",
|
||||
none: "無",
|
||||
sortKeyOnly: "DynamoDB Query 只能依目前索引的排序鍵 {key} 排序",
|
||||
pageCursorUnavailable: "目前 DynamoDB 頁面游標不可用,請從第一頁重新載入",
|
||||
repeatedCursor: "DynamoDB 傳回重複的分頁游標,已停止匯出",
|
||||
partialProjectionReadOnly: "目前 {projection} 索引不包含完整項目,為避免覆寫未投影屬性,已停用編輯",
|
||||
exportLimitReached: "為限制記憶體和讀取用量,DynamoDB 匯出已在 {count} 筆停止;可設定匯出列數上限來調整範圍",
|
||||
documentDetails: "DynamoDB 表:{table}\n項目鍵:{id}",
|
||||
},
|
||||
vector: {
|
||||
collectionFallback: "集合",
|
||||
productCollection: "{product} 集合",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { formatDocumentStoreIdLabel, serializeDocumentStoreId } from "@/lib/app/documentJsonValues";
|
||||
import { buildDocumentFilterCondition, currentDocumentFilterJson, documentStoreProviderFor } from "@/lib/app/documentStoreProvider";
|
||||
import { applyDocumentStoreIdentityPlan, insertDocumentStoreDocument } from "@/lib/app/documentStoreSave";
|
||||
|
||||
describe("DynamoDB document store provider", () => {
|
||||
it("uses a dedicated provider and describes a cursor-backed read", () => {
|
||||
const provider = documentStoreProviderFor("dynamodb");
|
||||
|
||||
expect(provider.kind).toBe("dynamodb");
|
||||
expect(
|
||||
provider.queryPreview({
|
||||
collection: "orders",
|
||||
filterJson: '{"tenant_id":"tenant-a","$index":"by_status"}',
|
||||
sortJson: '{"created_at":-1}',
|
||||
skip: 100,
|
||||
limit: 50,
|
||||
}),
|
||||
).toContain('DBX DYNAMODB QUERY / SCAN\ntable: "orders"\nlimit: 50');
|
||||
});
|
||||
|
||||
it("keeps composite key identity and large-number wrappers intact", () => {
|
||||
const id = {
|
||||
tenant_id: "tenant-a",
|
||||
sequence: { $dbxDynamoDb: { version: 1, type: "number", value: "9007199254740993" } },
|
||||
};
|
||||
|
||||
expect(serializeDocumentStoreId(id, "dynamodb")).toBe('{"tenant_id":"tenant-a","sequence":{"$dbxDynamoDb":{"version":1,"type":"number","value":"9007199254740993"}}}');
|
||||
expect(formatDocumentStoreIdLabel(id, "dynamodb")).toBe('{"tenant_id":"tenant-a","sequence":{"$dbxDynamoDb":{"version":1,"type":"number","value":"9007199254740993"}}}');
|
||||
});
|
||||
|
||||
it("builds DynamoDB-compatible structured filters", () => {
|
||||
expect(currentDocumentFilterJson("", { amount: { $gte: 50 } }, "dynamodb")).toBe('{"amount":{"$gte":50}}');
|
||||
expect(buildDocumentFilterCondition({ id: "contains", fieldName: "customer", mode: "like", rawValue: "Acme", conjunction: "AND" }, { kind: "dynamodb" })).toEqual({ customer: { $contains: "Acme" } });
|
||||
expect(buildDocumentFilterCondition({ id: "not-contains", fieldName: "customer", mode: "not-like", rawValue: "Test", conjunction: "AND" }, { kind: "dynamodb" })).toEqual({ customer: { $notContains: "Test" } });
|
||||
});
|
||||
|
||||
it("uses conditional insert semantics even when a new item has an explicit identity", async () => {
|
||||
const insert = vi.fn(async () => "created");
|
||||
const update = vi.fn(async () => 1);
|
||||
|
||||
await insertDocumentStoreDocument({
|
||||
kind: "dynamodb",
|
||||
explicitId: '{"tenant_id":"tenant-a","order_id":"order-1001"}',
|
||||
document: { tenant_id: "tenant-a", order_id: "order-1001", amount: 42 },
|
||||
apis: { insert, update },
|
||||
});
|
||||
|
||||
expect(insert).toHaveBeenCalledWith('{"tenant_id":"tenant-a","order_id":"order-1001","amount":42}', undefined);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delegates a DynamoDB rekey to one atomic backend update", async () => {
|
||||
const calls: string[] = [];
|
||||
const insert = vi.fn(async () => {
|
||||
calls.push("insert");
|
||||
return "created";
|
||||
});
|
||||
const update = vi.fn(async () => {
|
||||
calls.push("update");
|
||||
return 1;
|
||||
});
|
||||
const remove = vi.fn(async () => {
|
||||
calls.push("delete");
|
||||
return 1;
|
||||
});
|
||||
|
||||
await applyDocumentStoreIdentityPlan({
|
||||
kind: "dynamodb",
|
||||
plan: {
|
||||
action: "rekey",
|
||||
writeId: '{"tenant_id":"tenant-a","order_id":"order-1002"}',
|
||||
deleteId: '{"tenant_id":"tenant-a","order_id":"order-1001"}',
|
||||
},
|
||||
document: { tenant_id: "tenant-a", order_id: "order-1002", amount: 42 },
|
||||
apis: { insert, update, delete: remove },
|
||||
});
|
||||
|
||||
expect(calls).toEqual(["update"]);
|
||||
expect(update).toHaveBeenCalledWith('{"tenant_id":"tenant-a","order_id":"order-1001"}', '{"tenant_id":"tenant-a","order_id":"order-1002","amount":42}', undefined);
|
||||
expect(insert).not.toHaveBeenCalled();
|
||||
expect(remove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { connectionDisplayUrlScheme, connectionUrlPlaceholder } from "@/lib/connection/connectionPresentation";
|
||||
import { applyParsedConnectionUrl, parseConnectionUrl } from "@/lib/connection/connectionUrl";
|
||||
import { connectionCanChooseVisibleDatabases } from "@/lib/connection/connectionVisibleDatabases";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
describe("DynamoDB connection presentation", () => {
|
||||
it("uses a DynamoDB endpoint URL as the connection placeholder", () => {
|
||||
expect(connectionUrlPlaceholder("dynamodb")).toBe("https://dynamodb.us-east-1.amazonaws.com");
|
||||
expect(connectionDisplayUrlScheme({ db_type: "dynamodb", ssl: true })).toBe("https");
|
||||
expect(connectionDisplayUrlScheme({ db_type: "dynamodb", ssl: false })).toBe("http");
|
||||
});
|
||||
|
||||
it("parses an HTTPS endpoint while DynamoDB is selected", () => {
|
||||
expect(parseConnectionUrl("https://dynamodb.eu-west-1.amazonaws.com", "dynamodb")).toMatchObject({
|
||||
dbType: "dynamodb",
|
||||
driverProfile: "dynamodb",
|
||||
host: "dynamodb.eu-west-1.amazonaws.com",
|
||||
port: 443,
|
||||
database: "eu-west-1",
|
||||
ssl: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the selected region for a custom endpoint URL", () => {
|
||||
const parsed = parseConnectionUrl("http://127.0.0.1:8000", "dynamodb");
|
||||
const config = applyParsedConnectionUrl(
|
||||
{
|
||||
db_type: "dynamodb",
|
||||
driver_profile: "dynamodb",
|
||||
name: "DynamoDB Local",
|
||||
host: "127.0.0.1",
|
||||
port: 8000,
|
||||
username: "local",
|
||||
password: "local",
|
||||
database: "ap-southeast-1",
|
||||
} as Omit<ConnectionConfig, "id">,
|
||||
parsed,
|
||||
);
|
||||
|
||||
expect(config.database).toBe("ap-southeast-1");
|
||||
expect(config.username).toBe("local");
|
||||
expect(config.password).toBe("local");
|
||||
});
|
||||
|
||||
it("does not offer database visibility selection", () => {
|
||||
expect(connectionCanChooseVisibleDatabases({ db_type: "dynamodb" })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,10 @@ describe("databaseDriverManifest", () => {
|
||||
it("uses agent cursor only for agent or external runtimes", () => {
|
||||
expect(databaseRuntimeMode("gaussdb")).toBe("native");
|
||||
expect(databaseRuntimeMode("meilisearch")).toBe("native");
|
||||
expect(databaseRuntimeMode("dynamodb")).toBe("native");
|
||||
expect(usesAgentCursorForQuery("gaussdb")).toBe(false);
|
||||
expect(usesAgentCursorForQuery("meilisearch")).toBe(false);
|
||||
expect(usesAgentCursorForQuery("dynamodb")).toBe(false);
|
||||
expect(usesAgentCursorForQuery("mysql")).toBe(false);
|
||||
expect(usesAgentCursorForQuery("jdbc")).toBe(true);
|
||||
expect(usesAgentCursorForQuery("prestosql")).toBe(true);
|
||||
|
||||
@@ -28,6 +28,7 @@ describe("sidebar tree item layout", () => {
|
||||
});
|
||||
|
||||
it("renders navigation leaf actions without expanders", () => {
|
||||
expect(canTreeNodeShowExpander({ type: "dynamodb-table", childCount: 0 })).toBe(false);
|
||||
expect(canTreeNodeShowExpander({ type: "nacos-namespace", childCount: 0 })).toBe(false);
|
||||
expect(canTreeNodeShowExpander({ type: "nacos-access-control", childCount: 0 })).toBe(false);
|
||||
expect(canTreeNodeShowExpander({ type: "etcd-root", childCount: 0 })).toBe(false);
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
} from "@/lib/sidebar/treeNodeClick";
|
||||
|
||||
describe("treeNodeClick", () => {
|
||||
it("opens DynamoDB tables in the document browser on single or double activation", () => {
|
||||
expect(treeNodeRowAction("dynamodb-table", false, "single", "dynamodb")).toBe("toggle");
|
||||
expect(treeNodeRowDoubleClickAction("dynamodb-table", false, "double", false, "dynamodb")).toBe("toggle");
|
||||
});
|
||||
|
||||
it("opens synonym nodes as synonym source", () => {
|
||||
expect(objectSourceKindForTreeNode("synonym")).toBe("SYNONYM");
|
||||
expect(treeNodeRowAction("synonym", false)).toBe("open-source");
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { classifySqlRisk } from "@/lib/sql/sqlRisk";
|
||||
|
||||
describe("DynamoDB statement risk", () => {
|
||||
it("treats generated scan statements as reads", () => {
|
||||
expect(classifySqlRisk('DBX DYNAMODB SCAN\ntable: "orders"\nlimit: 1000', { dialect: "dynamodb" }).risk).toBe("read");
|
||||
expect(classifySqlRisk('DBX DYNAMODB QUERY / SCAN\ntable: "orders"\nfilter:\n{"status":"SHIPPED"}', { dialect: "dynamodb" }).risk).toBe("read");
|
||||
});
|
||||
|
||||
it("treats generated item changes as writes", () => {
|
||||
for (const operation of ["INSERT ITEM", "PUT ITEM", "DELETE ITEM"]) {
|
||||
expect(classifySqlRisk(`DBX DYNAMODB ${operation}\ntable: "orders"`, { dialect: "dynamodb" }).risk).toBe("write");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps unknown DynamoDB operations unsafe", () => {
|
||||
expect(classifySqlRisk('DBX DYNAMODB UNKNOWN\ntable: "orders"', { dialect: "dynamodb" }).risk).toBe("unknown");
|
||||
});
|
||||
});
|
||||
@@ -42,9 +42,16 @@ export function serializeDocumentStoreId(value: unknown, kind: DocumentStoreKind
|
||||
if (kind !== "mongodb" && isLosslessJsonNumber(value)) return value.raw;
|
||||
if (kind === "elasticsearch") return String(value);
|
||||
if (kind === "meilisearch") return typeof value === "string" ? `__dbx_meilisearch_string_id__${JSON.stringify(value)}` : String(value);
|
||||
if (kind === "dynamodb") return stringifyJsonPreservingLargeNumbers(value);
|
||||
return serializeMongoDocumentId(value);
|
||||
}
|
||||
|
||||
export function formatDocumentStoreIdLabel(value: unknown, kind: DocumentStoreKind): string {
|
||||
if (value === null || value === undefined) return "";
|
||||
if (typeof value === "object") return stringifyDocumentStoreValue(value, kind);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a whole document JSON payload for MongoDB / Elasticsearch editors.
|
||||
* Accepts standard JSON and Extended JSON objects; rejects non-object roots.
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isElasticsearchCompatibleDatabaseType, isMeilisearchDatabaseType, type
|
||||
import { quoteUnquotedObjectKeys } from "@/lib/mongo/mongoShellCommand";
|
||||
import { formatMongoShellLiteral } from "@/lib/mongo/mongoDocumentValues";
|
||||
|
||||
export type DocumentStoreKind = "mongodb" | "elasticsearch" | "meilisearch";
|
||||
export type DocumentStoreKind = "mongodb" | "dynamodb" | "elasticsearch" | "meilisearch";
|
||||
export type DocumentFilterMode = "equals" | "not-equals" | "like" | "not-like" | "greater-than" | "greater-than-or-equal" | "less-than" | "less-than-or-equal" | "is-null" | "is-not-null";
|
||||
export type DocumentFilterValueType = "auto" | "string" | "number" | "boolean" | "object-id" | "date" | "int32" | "int64" | "decimal128" | "json";
|
||||
export type ElasticsearchBoolClause = "filter" | "must" | "should" | "must_not";
|
||||
@@ -150,6 +150,23 @@ const meilisearchDocumentProvider: DocumentStoreProvider = {
|
||||
sortInputForColumn: mongoDocumentProvider.sortInputForColumn,
|
||||
};
|
||||
|
||||
const dynamodbDocumentProvider: DocumentStoreProvider = {
|
||||
kind: "dynamodb",
|
||||
filterInputLabel: "filter",
|
||||
sortInputLabel: "sort key",
|
||||
documentsLabel: ({ total, totalIsExact, t }) => `${totalIsExact ? "" : "≥"}${t("dynamodb.items", { count: total })}`,
|
||||
queryPreview: ({ collection, filterJson, sortJson, limit }) => {
|
||||
const filter = documentStorePreviewJson(filterJson);
|
||||
const sort = documentStorePreviewJson(sortJson);
|
||||
const operation = filterJson?.includes('"$index"') || (filterJson && filterJson !== "{}") ? "QUERY / SCAN" : "SCAN";
|
||||
const lines = [`DBX DYNAMODB ${operation}`, `table: ${JSON.stringify(collection)}`, `limit: ${limit}`];
|
||||
if (filter) lines.push("filter:", filter);
|
||||
if (sort) lines.push("sort:", sort);
|
||||
return lines.join("\n");
|
||||
},
|
||||
sortInputForColumn: mongoDocumentProvider.sortInputForColumn,
|
||||
};
|
||||
|
||||
function documentStorePreviewJson(value?: string): string | null {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed || trimmed === "{}") return null;
|
||||
@@ -161,6 +178,7 @@ function documentStorePreviewJson(value?: string): string | null {
|
||||
}
|
||||
|
||||
export function documentStoreProviderFor(databaseType?: DatabaseType): DocumentStoreProvider {
|
||||
if (databaseType === "dynamodb") return dynamodbDocumentProvider;
|
||||
if (isElasticsearchCompatibleDatabaseType(databaseType)) return elasticsearchDocumentProvider;
|
||||
if (isMeilisearchDatabaseType(databaseType)) return meilisearchDocumentProvider;
|
||||
return mongoDocumentProvider;
|
||||
@@ -482,8 +500,10 @@ export function buildDocumentFilterCondition(rule: DocumentFilterRule, options:
|
||||
case "not-equals":
|
||||
return { [rule.fieldName]: { $ne: value } };
|
||||
case "like":
|
||||
if (options.kind === "dynamodb") return { [rule.fieldName]: { $contains: value } };
|
||||
return { [rule.fieldName]: { $regex: escapeRegexLiteral(textValue), $options: "i" } };
|
||||
case "not-like":
|
||||
if (options.kind === "dynamodb") return { [rule.fieldName]: { $notContains: value } };
|
||||
return { [rule.fieldName]: { $not: { $regex: escapeRegexLiteral(textValue), $options: "i" } } };
|
||||
case "greater-than":
|
||||
return { [rule.fieldName]: { $gt: value } };
|
||||
@@ -670,6 +690,7 @@ type DocumentQueryInputNormalizer = (input: string) => string | null;
|
||||
|
||||
const documentQueryInputNormalizers: Record<DocumentStoreKind, DocumentQueryInputNormalizer> = {
|
||||
mongodb: normalizeJsonArgument,
|
||||
dynamodb: quoteUnquotedObjectKeys,
|
||||
elasticsearch: quoteUnquotedObjectKeys,
|
||||
meilisearch: quoteUnquotedObjectKeys,
|
||||
};
|
||||
|
||||
@@ -37,12 +37,26 @@ export async function writeDocumentStoreDocument(options: { kind: DocumentStoreK
|
||||
|
||||
/**
|
||||
* Apply an identity plan for an existing document save.
|
||||
* Rekey always writes first, then deletes the old identity — a failed write never deletes.
|
||||
* DynamoDB rekeys atomically in the backend. Other stores write first, then
|
||||
* delete the old identity so a failed write never deletes the source.
|
||||
* Plan coordinates are assumed distinct for rekey (same identity is always `replace`).
|
||||
*/
|
||||
export async function applyDocumentStoreIdentityPlan(options: { kind: DocumentStoreKind; plan: DocumentStoreIdentityPlan; document: Record<string, unknown>; apis: DocumentStoreWriteApis }): Promise<void> {
|
||||
const { kind, plan, document, apis } = options;
|
||||
|
||||
// DynamoDB handles both same-key replacement and key migration in the
|
||||
// backend. Rekey uses one TransactWriteItems request, keyed by the old id.
|
||||
if (kind === "dynamodb") {
|
||||
await writeDocumentStoreDocument({
|
||||
kind,
|
||||
op: "put",
|
||||
id: plan.action === "rekey" ? plan.deleteId : plan.writeId,
|
||||
document,
|
||||
apis,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (plan.action === "replace") {
|
||||
await writeDocumentStoreDocument({
|
||||
kind,
|
||||
@@ -55,7 +69,6 @@ export async function applyDocumentStoreIdentityPlan(options: { kind: DocumentSt
|
||||
return;
|
||||
}
|
||||
|
||||
// Path-identity stores write under the new id first; Mongo inserts a new document.
|
||||
if (kind !== "mongodb") {
|
||||
await writeDocumentStoreDocument({
|
||||
kind,
|
||||
@@ -81,7 +94,7 @@ export async function applyDocumentStoreIdentityPlan(options: { kind: DocumentSt
|
||||
/** Insert a new document (optional explicit path identity uses put). */
|
||||
export async function insertDocumentStoreDocument(options: { kind: DocumentStoreKind; document: Record<string, unknown>; explicitId?: string | null; routing?: string; apis: Pick<DocumentStoreWriteApis, "insert" | "update"> }): Promise<void> {
|
||||
const { kind, document, explicitId, routing, apis } = options;
|
||||
if (kind !== "mongodb" && explicitId) {
|
||||
if (kind !== "mongodb" && kind !== "dynamodb" && explicitId) {
|
||||
await writeDocumentStoreDocument({
|
||||
kind,
|
||||
op: "put",
|
||||
|
||||
@@ -698,6 +698,8 @@ export const mongoDropCollection = forward("mongoDropCollection");
|
||||
export const mongoRenameCollection = forward("mongoRenameCollection");
|
||||
export const mongoCloneCollection = forward("mongoCloneCollection");
|
||||
export const documentFindDocuments = forward("documentFindDocuments");
|
||||
export const documentCountDocuments = forward("documentCountDocuments");
|
||||
export const dynamodbDescribeTable = forward("dynamodbDescribeTable");
|
||||
export const elasticsearchCountDocuments = forward("elasticsearchCountDocuments");
|
||||
export const mongoFindDocuments = forward("mongoFindDocuments");
|
||||
export const mongoParseShellCommand = forward("mongoParseShellCommand");
|
||||
@@ -856,6 +858,9 @@ export type {
|
||||
EtcdPreflightResponse,
|
||||
EtcdDangerousApproval,
|
||||
DocumentQueryResult,
|
||||
DynamoDbKeyInfo,
|
||||
DynamoDbIndexInfo,
|
||||
DynamoDbTableDescription,
|
||||
MongoDocumentResult,
|
||||
HistoryEntry,
|
||||
HistoryConnectionFilter,
|
||||
|
||||
@@ -98,6 +98,7 @@ import type {
|
||||
EtcdWatchPollResponse,
|
||||
EtcdLeaseListResponse,
|
||||
DocumentQueryResult,
|
||||
DynamoDbTableDescription,
|
||||
MongoDocumentResult,
|
||||
MongoCollectionStatsResult,
|
||||
MongoCloneCollectionResult,
|
||||
@@ -3561,7 +3562,7 @@ export async function mongoFindOne(connectionId: string, database: string, colle
|
||||
});
|
||||
}
|
||||
|
||||
export async function documentFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, projection?: string, sort?: string, collation?: string, executionId?: string): Promise<DocumentQueryResult> {
|
||||
export async function documentFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, projection?: string, sort?: string, collation?: string, executionId?: string, cursor?: string): Promise<DocumentQueryResult> {
|
||||
return post("/api/document-store/find-documents", {
|
||||
connectionId,
|
||||
database,
|
||||
@@ -3572,10 +3573,24 @@ export async function documentFindDocuments(connectionId: string, database: stri
|
||||
projection,
|
||||
sort,
|
||||
collation,
|
||||
cursor,
|
||||
executionId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function documentCountDocuments(connectionId: string, collection: string, filter?: string, executionId?: string): Promise<number> {
|
||||
return post("/api/document-store/count-documents", {
|
||||
connectionId,
|
||||
collection,
|
||||
filter,
|
||||
executionId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function dynamodbDescribeTable(connectionId: string, table: string): Promise<DynamoDbTableDescription> {
|
||||
return post("/api/document-store/dynamodb-describe-table", { connectionId, table });
|
||||
}
|
||||
|
||||
export async function elasticsearchCountDocuments(connectionId: string, index: string, filter?: string, executionId?: string): Promise<number> {
|
||||
return post("/api/document-store/elasticsearch-count-documents", {
|
||||
connectionId,
|
||||
|
||||
@@ -3286,6 +3286,31 @@ export interface DocumentQueryResult {
|
||||
extended_documents?: any[];
|
||||
total: number;
|
||||
total_is_exact?: boolean;
|
||||
next_cursor?: string;
|
||||
}
|
||||
|
||||
export interface DynamoDbKeyInfo {
|
||||
name: string;
|
||||
attributeType: "S" | "N" | "B" | string;
|
||||
}
|
||||
|
||||
export interface DynamoDbIndexInfo {
|
||||
name: string;
|
||||
kind: "global" | "local" | string;
|
||||
partitionKey: DynamoDbKeyInfo;
|
||||
sortKey?: DynamoDbKeyInfo;
|
||||
projectionType: "ALL" | "KEYS_ONLY" | "INCLUDE" | string;
|
||||
nonKeyAttributes: string[];
|
||||
}
|
||||
|
||||
export interface DynamoDbTableDescription {
|
||||
name: string;
|
||||
status: string;
|
||||
itemCount: number;
|
||||
sizeBytes: number;
|
||||
partitionKey: DynamoDbKeyInfo;
|
||||
sortKey?: DynamoDbKeyInfo;
|
||||
indexes: DynamoDbIndexInfo[];
|
||||
}
|
||||
|
||||
// Kept for callers that are specifically using MongoDB APIs.
|
||||
@@ -3448,7 +3473,7 @@ export async function mongoParseShellCommand(source: string): Promise<MongoComma
|
||||
return normalizeRustMongoCommand(raw);
|
||||
}
|
||||
|
||||
export async function documentFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, projection?: string, sort?: string, collation?: string, executionId?: string): Promise<DocumentQueryResult> {
|
||||
export async function documentFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, projection?: string, sort?: string, collation?: string, executionId?: string, cursor?: string): Promise<DocumentQueryResult> {
|
||||
return invoke("document_find_documents", {
|
||||
connectionId,
|
||||
database,
|
||||
@@ -3459,10 +3484,24 @@ export async function documentFindDocuments(connectionId: string, database: stri
|
||||
projection,
|
||||
sort,
|
||||
collation,
|
||||
cursor,
|
||||
executionId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function documentCountDocuments(connectionId: string, collection: string, filter?: string, executionId?: string): Promise<number> {
|
||||
return invoke("document_count_documents", {
|
||||
connectionId,
|
||||
collection,
|
||||
filter,
|
||||
executionId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function dynamodbDescribeTable(connectionId: string, table: string): Promise<DynamoDbTableDescription> {
|
||||
return invoke("dynamodb_describe_table", { connectionId, table });
|
||||
}
|
||||
|
||||
export async function elasticsearchCountDocuments(connectionId: string, index: string, filter?: string, executionId?: string): Promise<number> {
|
||||
return invoke("elasticsearch_count_documents", {
|
||||
connectionId,
|
||||
|
||||
@@ -136,6 +136,7 @@ export function connectionDisplayUrlScheme(connection: Pick<ConnectionConfig, "d
|
||||
case "chromadb":
|
||||
case "rqlite":
|
||||
case "turso":
|
||||
case "dynamodb":
|
||||
case "mq":
|
||||
case "consul":
|
||||
return connection.ssl ? "https" : "http";
|
||||
@@ -197,6 +198,9 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string {
|
||||
case "mongodb":
|
||||
return "mongodb://user:password@host:port/database";
|
||||
|
||||
case "dynamodb":
|
||||
return "https://dynamodb.us-east-1.amazonaws.com";
|
||||
|
||||
case "clickhouse":
|
||||
return "clickhouse://user:password@host:port/database";
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
|
||||
zookeeper: { type: "zookeeper", profile: "zookeeper", label: "Apache ZooKeeper", defaultPort: 2181 },
|
||||
mongodb: { type: "mongodb", profile: "mongodb", label: "MongoDB", defaultPort: 27017 },
|
||||
"mongodb+srv": { type: "mongodb", profile: "mongodb", label: "MongoDB", defaultPort: 27017 },
|
||||
dynamodb: { type: "dynamodb", profile: "dynamodb", label: "Amazon DynamoDB", defaultPort: 443 },
|
||||
clickhouse: { type: "clickhouse", profile: "clickhouse", label: "ClickHouse", defaultPort: 8123 },
|
||||
sqlserver: { type: "sqlserver", profile: "sqlserver", label: "SQL Server", defaultPort: 1433 },
|
||||
mssql: { type: "sqlserver", profile: "sqlserver", label: "SQL Server", defaultPort: 1433 },
|
||||
@@ -83,6 +84,7 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
|
||||
|
||||
const HTTP_SELECTED_PROFILES: Record<string, ConnectionProfile> = {
|
||||
clickhouse: SCHEME_PROFILES.clickhouse,
|
||||
dynamodb: SCHEME_PROFILES.dynamodb,
|
||||
elasticsearch: SCHEME_PROFILES.elasticsearch,
|
||||
easysearch: SCHEME_PROFILES.easysearch,
|
||||
meilisearch: SCHEME_PROFILES.meilisearch,
|
||||
@@ -190,6 +192,10 @@ function databaseFromPath(pathname: string): string | undefined {
|
||||
return decodeUrlPart(value.split("/")[0]);
|
||||
}
|
||||
|
||||
function dynamodbRegionFromHost(hostname: string): string | undefined {
|
||||
return hostname.toLowerCase().match(/^dynamodb(?:-fips)?\.([a-z0-9-]+)\.(?:amazonaws\.com(?:\.cn)?|api\.aws)$/)?.[1];
|
||||
}
|
||||
|
||||
function parseZooKeeperUrl(source: string): ParsedConnectionUrl | null {
|
||||
const match = source.match(/^zookeeper:\/\/([^/?#]+)(\/[^?#]*)?(\?[^#]*)?$/i);
|
||||
if (!match) return null;
|
||||
@@ -665,7 +671,7 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
|
||||
...(profile.type === "sqlserver" && parsed.port ? { portExplicit: true } : {}),
|
||||
username: mysqlCredentials?.username ?? decodeUrlPart(parsed.username),
|
||||
password: mysqlCredentials?.password ?? decodeUrlPart(parsed.password),
|
||||
database: profile.type === "victoriametrics" ? "metrics" : isMeilisearch ? undefined : databaseFromPath(parsed.pathname),
|
||||
database: profile.type === "victoriametrics" ? "metrics" : profile.type === "dynamodb" ? dynamodbRegionFromHost(parsed.hostname) : isMeilisearch ? undefined : databaseFromPath(parsed.pathname),
|
||||
urlParams: effectiveUrlParams,
|
||||
ssl: scheme === "rediss" || scheme === "https" || urlParamsRequireTls(profile.type, effectiveUrlParams) || (profile.type === "mysql" && isTidbCloudHost(parsed.hostname)),
|
||||
...(profile.type === "victoriametrics" ? { apiPath: parsed.pathname.replace(/\/+$/, "") } : {}),
|
||||
@@ -759,7 +765,7 @@ export function applyParsedConnectionUrl(config: Omit<ConnectionConfig, "id">, p
|
||||
name: parsed.name?.trim() || config.name,
|
||||
username: applyParsedUsername(config, parsed),
|
||||
password: applyParsedPassword(config, parsed),
|
||||
database: parsed.database,
|
||||
database: parsed.dbType === "dynamodb" ? parsed.database || config.database || "us-east-1" : parsed.database,
|
||||
url_params: parsed.urlParams,
|
||||
ssl: parsed.ssl,
|
||||
connection_string: parsed.connectionString,
|
||||
|
||||
@@ -5,7 +5,7 @@ const DRAFT_VISIBLE_DATABASES_PREFIX = "__visible_draft_";
|
||||
|
||||
// Turso and Cloudflare D1 target one fixed SQLite-compatible `main` namespace;
|
||||
// non-database services expose their own root objects rather than database namespaces.
|
||||
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos", "consul"]);
|
||||
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "dynamodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos", "consul"]);
|
||||
|
||||
type VisibleDatabaseConnectionFields = Pick<
|
||||
ConnectionConfig,
|
||||
|
||||
@@ -27,6 +27,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = {
|
||||
clickhouse: { connection: "database" },
|
||||
sqlserver: { connection: "database", database: "schema" },
|
||||
mongodb: { connection: "special" },
|
||||
dynamodb: { deferred: "table creation requires dedicated DynamoDB key and capacity options" },
|
||||
oracle: { deferred: "Oracle schemas are users; database creation is not a normal connected DDL action" },
|
||||
elasticsearch: { deferred: "index creation is not modeled as database creation" },
|
||||
easysearch: { deferred: "index creation is not modeled as database creation" },
|
||||
|
||||
@@ -27,6 +27,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = {
|
||||
clickhouse: { deferred: "database property editing not verified for first pass" },
|
||||
sqlserver: { deferred: "database options are broad administrative settings and need a dedicated editor" },
|
||||
mongodb: { deferred: "database options are not modeled as SQL database properties" },
|
||||
dynamodb: { deferred: "table settings require a dedicated DynamoDB workflow" },
|
||||
oracle: { deferred: "Oracle database properties are instance/user/tablespace administration" },
|
||||
elasticsearch: { deferred: "index settings are not database properties" },
|
||||
easysearch: { deferred: "index settings are not database properties" },
|
||||
|
||||
@@ -27,6 +27,7 @@ const leafTypes: Set<TreeNodeType> = new Set([
|
||||
"consul-overview",
|
||||
"mongo-gridfs",
|
||||
"mongo-bucket",
|
||||
"dynamodb-table",
|
||||
"vector-collection",
|
||||
"elasticsearch-index",
|
||||
"user-admin",
|
||||
@@ -38,7 +39,7 @@ const leafTypes: Set<TreeNodeType> = new Set([
|
||||
"extension",
|
||||
]);
|
||||
|
||||
const fullWidthLabelTypes: Set<TreeNodeType> = new Set(["table", "view", "materialized_view", "mongo-collection", "mongo-bucket", "vector-collection", "elasticsearch-index"]);
|
||||
const fullWidthLabelTypes: Set<TreeNodeType> = new Set(["table", "view", "materialized_view", "mongo-collection", "mongo-bucket", "dynamodb-table", "vector-collection", "elasticsearch-index"]);
|
||||
|
||||
const emptyContainerTypes: Set<TreeNodeType> = new Set(["saved-sql-root", "saved-sql-folder", "type"]);
|
||||
|
||||
@@ -58,12 +59,13 @@ const pinnableTypes: Set<TreeNodeType> = new Set([
|
||||
"mongo-gridfs",
|
||||
"mongo-bucket",
|
||||
"mongo-collection",
|
||||
"dynamodb-table",
|
||||
"vector-collection",
|
||||
"elasticsearch-index",
|
||||
"nacos-namespace",
|
||||
]);
|
||||
|
||||
const commentTypes: Set<TreeNodeType> = new Set(["connection", "schema", "table", "view", "materialized_view", "column", "mongo-collection", "vector-collection", "elasticsearch-index"]);
|
||||
const commentTypes: Set<TreeNodeType> = new Set(["connection", "schema", "table", "view", "materialized_view", "column", "mongo-collection", "dynamodb-table", "vector-collection", "elasticsearch-index"]);
|
||||
|
||||
export const SIDEBAR_INDENT_DEFAULT_PX = 16;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export type SidebarSelectionCopyAction = "copy-name" | "none";
|
||||
export type SidebarActivation = "single" | "double";
|
||||
|
||||
const dataNodeTypes = new Set<TreeNodeType>(["table", "view", "materialized_view"]);
|
||||
const documentBrowserNodeTypes = new Set<TreeNodeType>(["mongo-collection", "mongo-bucket"]);
|
||||
const documentBrowserNodeTypes = new Set<TreeNodeType>(["mongo-collection", "mongo-bucket", "dynamodb-table"]);
|
||||
const toggleLeafNodeTypes = new Set<TreeNodeType>([
|
||||
"redis-db",
|
||||
"mq-tenant",
|
||||
@@ -24,6 +24,7 @@ const toggleLeafNodeTypes = new Set<TreeNodeType>([
|
||||
"mongo-gridfs",
|
||||
"mongo-collection",
|
||||
"mongo-bucket",
|
||||
"dynamodb-table",
|
||||
"vector-collection",
|
||||
"elasticsearch-index",
|
||||
"user-admin",
|
||||
|
||||
@@ -50,10 +50,29 @@ export function classifySqlRisk(sql: string, options: SqlRiskOptions = {}): SqlR
|
||||
return { ...highest, statements };
|
||||
}
|
||||
|
||||
export function classifySqlStatementRisk(sql: string, _options: SqlRiskOptions = {}): SqlRiskStatementAssessment {
|
||||
export function classifySqlStatementRisk(sql: string, options: SqlRiskOptions = {}): SqlRiskStatementAssessment {
|
||||
const dynamodbRisk = classifyDynamoDbStatementRisk(sql, options.dialect);
|
||||
if (dynamodbRisk) return dynamodbRisk;
|
||||
return classifyTokens(tokenizeSqlForRisk(sql));
|
||||
}
|
||||
|
||||
function classifyDynamoDbStatementRisk(sql: string, dialect?: DatabaseType | string): SqlRiskStatementAssessment | null {
|
||||
if (dialect !== "dynamodb") return null;
|
||||
const header = sql
|
||||
.split(/\r?\n/)
|
||||
.find((line) => line.trim())
|
||||
?.trim()
|
||||
.toUpperCase();
|
||||
if (header === "DBX DYNAMODB SCAN" || header === "DBX DYNAMODB QUERY / SCAN") {
|
||||
return { risk: "read", firstKeyword: "dbx" };
|
||||
}
|
||||
if (header === "DBX DYNAMODB INSERT ITEM" || header === "DBX DYNAMODB PUT ITEM" || header === "DBX DYNAMODB DELETE ITEM") {
|
||||
return { risk: "write", firstKeyword: "dbx" };
|
||||
}
|
||||
if (header?.startsWith("DBX DYNAMODB")) return { risk: "unknown", firstKeyword: "dbx" };
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isSqlRiskMutation(risk: SqlRiskLevel): boolean {
|
||||
return risk !== "read";
|
||||
}
|
||||
|
||||
@@ -1732,7 +1732,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
function invalidateMetadataCachesForNode(node: TreeNode) {
|
||||
if (!node.connectionId) return;
|
||||
const tableName = node.tableName || (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" ? node.label : undefined);
|
||||
const tableName = node.tableName || (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "dynamodb-table" ? node.label : undefined);
|
||||
const match = {
|
||||
connectionId: node.connectionId,
|
||||
database: node.database || undefined,
|
||||
@@ -3135,6 +3135,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await loadConsulRoot(connectionId);
|
||||
} else if (config.db_type === "mongodb") {
|
||||
await loadMongoDatabases(connectionId);
|
||||
} else if (config.db_type === "dynamodb") {
|
||||
await loadDynamoDbTables(connectionId);
|
||||
} else if (config.db_type === "elasticsearch" || config.db_type === "easysearch" || config.db_type === "meilisearch") {
|
||||
// Reload: list indices.
|
||||
await loadElasticsearchIndices(connectionId);
|
||||
@@ -3795,7 +3797,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
async function loadConnectedConnectionRootForSidebarSearch(connectionId: string) {
|
||||
if (!connectedIds.value.has(connectionId)) return;
|
||||
const config = getConfig(connectionId);
|
||||
if (!config || ["redis", "etcd", "zookeeper", "consul", "mongodb", "elasticsearch", "easysearch", "meilisearch", "milvus", "qdrant", "weaviate", "chromadb", "mq", "nacos"].includes(config.db_type)) return;
|
||||
if (!config || ["redis", "etcd", "zookeeper", "consul", "mongodb", "dynamodb", "elasticsearch", "easysearch", "meilisearch", "milvus", "qdrant", "weaviate", "chromadb", "mq", "nacos"].includes(config.db_type)) return;
|
||||
const node = findConnectionNode(connectionId);
|
||||
if (!node || node.type !== "connection" || hasConnectionMetadataChildren(node.children)) return;
|
||||
const scope = { kind: "connection-databases" as const, connectionId, driverProfile: metadataDriverProfile(config) };
|
||||
@@ -4215,6 +4217,42 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDynamoDbTables(connectionId: string) {
|
||||
const node = findConnectionNode(connectionId);
|
||||
if (!node) return;
|
||||
|
||||
let load = beginTreeNodeLoad(node);
|
||||
try {
|
||||
await ensureConnected(connectionId);
|
||||
load = reclaimTreeNodeLoad(load, node);
|
||||
const config = getConfig(connectionId);
|
||||
const region = config?.database?.trim() || "us-east-1";
|
||||
const tables = await withMetadataLoadTimeout(connectionId, api.documentListCollections(connectionId, region), "DynamoDB tables");
|
||||
const targetNode = treeNodeLoadTarget(load);
|
||||
if (!targetNode) return;
|
||||
setChildren(
|
||||
targetNode,
|
||||
[...tables]
|
||||
.sort((left, right) => compareSidebarNames(left.name, right.name))
|
||||
.map((table) => ({
|
||||
id: `${connectionId}:__dynamodb_table:${table.id}`,
|
||||
label: table.name,
|
||||
type: "dynamodb-table" as const,
|
||||
connectionId,
|
||||
database: region,
|
||||
tableName: table.name,
|
||||
isExpanded: false,
|
||||
})),
|
||||
);
|
||||
targetNode.isExpanded = true;
|
||||
} catch (e) {
|
||||
recordMetadataLoadError(connectionId, e, load);
|
||||
throw e;
|
||||
} finally {
|
||||
finishTreeNodeLoad(load);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect an Elasticsearch root without expanding or listing indices.
|
||||
* Used when first opening a connection (test/connect) — connectivity uses
|
||||
@@ -5888,6 +5926,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await loadConsulRoot(node.connectionId);
|
||||
} else if (config?.db_type === "mongodb") {
|
||||
await loadMongoDatabases(node.connectionId);
|
||||
} else if (config?.db_type === "dynamodb") {
|
||||
await loadDynamoDbTables(node.connectionId);
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
||||
await loadElasticsearchIndices(node.connectionId);
|
||||
} else if (config?.db_type === "milvus") {
|
||||
@@ -8006,6 +8046,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
loadNacosNamespaces,
|
||||
updateRedisDbKeyStats,
|
||||
loadMongoDatabases,
|
||||
loadDynamoDbTables,
|
||||
loadMilvusDatabases,
|
||||
openElasticsearchConnectionTree,
|
||||
loadElasticsearchIndices,
|
||||
|
||||
@@ -14,6 +14,7 @@ export type DatabaseType =
|
||||
| "clickhouse"
|
||||
| "sqlserver"
|
||||
| "mongodb"
|
||||
| "dynamodb"
|
||||
| "oracle"
|
||||
| "elasticsearch"
|
||||
| "easysearch"
|
||||
@@ -960,6 +961,7 @@ export type TreeNodeType =
|
||||
| "mongo-buckets"
|
||||
| "mongo-bucket"
|
||||
| "mongo-collection"
|
||||
| "dynamodb-table"
|
||||
| "vector-database"
|
||||
| "vector-collection"
|
||||
| "elasticsearch-index"
|
||||
|
||||
@@ -5,8 +5,9 @@ edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["duckdb-sidecar", "mq-admin", "sqlite-sqlcipher", "system-fonts"]
|
||||
default = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-sqlcipher", "system-fonts"]
|
||||
duckdb-sidecar = []
|
||||
dynamodb = ["dep:aws-sdk-dynamodb"]
|
||||
mq-admin = ["dep:rumqttc"]
|
||||
sqlite-sqlcipher = ["rusqlite/bundled-sqlcipher-vendored-openssl"]
|
||||
openapi = ["dep:utoipa"]
|
||||
@@ -50,6 +51,7 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "str
|
||||
prometheus-parse = "=0.2.5"
|
||||
futures = "0.3"
|
||||
mongodb = { version = "3.2.5", features = ["socks5-proxy"] }
|
||||
aws-sdk-dynamodb = { version = "=1.104.0", optional = true }
|
||||
rumqttc = { version = "0.24", features = ["websocket"], optional = true }
|
||||
russh = "0.60"
|
||||
pageant = "0.2"
|
||||
|
||||
@@ -296,6 +296,35 @@
|
||||
"driverManagement": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"dbType": "dynamodb",
|
||||
"label": "Amazon DynamoDB",
|
||||
"runtimeMode": "native",
|
||||
"mcpMode": "bridge",
|
||||
"singleConnectionPool": true,
|
||||
"metadataConnectionScoped": false,
|
||||
"skipTcpProbe": false,
|
||||
"defaultPort": 8000,
|
||||
"supportLevel": "operate",
|
||||
"capabilities": {
|
||||
"queryExecution": false,
|
||||
"metadataBrowse": true,
|
||||
"objectBrowser": false,
|
||||
"objectSource": false,
|
||||
"schemaSearch": false,
|
||||
"diagram": false,
|
||||
"tableDataEdit": true,
|
||||
"tableStructureEdit": false,
|
||||
"tableImport": false,
|
||||
"dataTransfer": false,
|
||||
"sqlFileExecution": false,
|
||||
"databaseCreate": false,
|
||||
"fieldLineage": false,
|
||||
"sqlExplain": false,
|
||||
"userAdmin": false,
|
||||
"driverManagement": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"dbType": "oracle",
|
||||
"label": "Oracle",
|
||||
|
||||
@@ -87,6 +87,7 @@ pub enum PoolKind {
|
||||
Redis(db::redis_driver::RedisConnection),
|
||||
DuckDbWorker(DuckDbWorkerHandle),
|
||||
MongoDb(mongodb::Client),
|
||||
DynamoDb(db::dynamodb_driver::DynamoDbClient),
|
||||
ClickHouse(db::clickhouse_driver::ChClient),
|
||||
SqlServer(Arc<tokio::sync::Mutex<db::sqlserver::SqlServerClient>>),
|
||||
Elasticsearch(db::elasticsearch_driver::EsClient),
|
||||
@@ -2022,6 +2023,11 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
}
|
||||
DatabaseType::DynamoDb => {
|
||||
let client = db::dynamodb_driver::connect(&db_config, &host, port)?;
|
||||
db::dynamodb_driver::test_connection(&client, connect_timeout).await?;
|
||||
PoolKind::DynamoDb(client)
|
||||
}
|
||||
DatabaseType::ClickHouse => {
|
||||
let username = if db_config.username.is_empty() { None } else { Some(db_config.username.clone()) };
|
||||
let password = if db_config.password.is_empty() { None } else { Some(db_config.password.clone()) };
|
||||
@@ -3100,6 +3106,18 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
}
|
||||
PoolKind::DynamoDb(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
let timeout = crate::db::connection_timeout();
|
||||
match db::dynamodb_driver::test_connection(&client, timeout).await {
|
||||
Ok(()) => false,
|
||||
Err(err) => {
|
||||
log::warn!("DynamoDB connection pool '{pool_key}' is stale: {err}");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
PoolKind::ClickHouse(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
@@ -4051,6 +4069,13 @@ impl AppState {
|
||||
false
|
||||
}
|
||||
},
|
||||
PoolKind::DynamoDb(client) => match db::dynamodb_driver::test_connection(client, timeout).await {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
log::warn!("DynamoDB connection pool '{key}' is unhealthy: {e}");
|
||||
false
|
||||
}
|
||||
},
|
||||
PoolKind::ClickHouse(client) => match db::clickhouse_driver::test_connection(client, timeout).await {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
@@ -4844,6 +4869,7 @@ fn clone_pool_kind(pool: &PoolKind) -> PoolKind {
|
||||
#[cfg(not(feature = "duckdb-sidecar"))]
|
||||
PoolKind::DuckDbWorker(_) => PoolKind::DuckDbWorker(()),
|
||||
PoolKind::MongoDb(client) => PoolKind::MongoDb(client.clone()),
|
||||
PoolKind::DynamoDb(client) => PoolKind::DynamoDb(client.clone()),
|
||||
PoolKind::ClickHouse(client) => PoolKind::ClickHouse(client.clone()),
|
||||
PoolKind::SqlServer(client) => PoolKind::SqlServer(client.clone()),
|
||||
PoolKind::Elasticsearch(client) => PoolKind::Elasticsearch(client.clone()),
|
||||
@@ -4888,6 +4914,9 @@ async fn close_pool_kind(pool: PoolKind) -> Result<(), String> {
|
||||
PoolKind::MongoDb(client) => {
|
||||
drop(client);
|
||||
}
|
||||
PoolKind::DynamoDb(client) => {
|
||||
drop(client);
|
||||
}
|
||||
PoolKind::ClickHouse(client) => {
|
||||
drop(client);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ pub fn is_single_connection_pool(db_type: &DatabaseType) -> bool {
|
||||
| DatabaseType::Turso
|
||||
| DatabaseType::CloudflareD1
|
||||
| DatabaseType::MongoDb
|
||||
| DatabaseType::DynamoDb
|
||||
| DatabaseType::Hbase
|
||||
| DatabaseType::Oracle
|
||||
| DatabaseType::Dameng
|
||||
|
||||
@@ -13,6 +13,10 @@ pub struct DocumentQueryResult {
|
||||
// unchanged and only send the flag when Elasticsearch reports a lower bound.
|
||||
#[serde(default = "default_total_is_exact", skip_serializing_if = "is_true")]
|
||||
pub total_is_exact: bool,
|
||||
/// Opaque document-store cursor for APIs such as DynamoDB that do not
|
||||
/// support offset pagination.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
fn default_total_is_exact() -> bool {
|
||||
@@ -35,6 +39,7 @@ mod tests {
|
||||
extended_documents: None,
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(result).unwrap();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::db::document_result::DocumentQueryResult;
|
||||
use crate::models::connection::ConnectionConfig;
|
||||
use crate::types::QueryResult;
|
||||
|
||||
const FEATURE_DISABLED: &str = "DynamoDB support is not included in this DBX build";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DynamoDbClient {
|
||||
pub region: String,
|
||||
pub endpoint_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamoDbKeyInfo {
|
||||
pub name: String,
|
||||
pub attribute_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamoDbIndexInfo {
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub partition_key: DynamoDbKeyInfo,
|
||||
pub sort_key: Option<DynamoDbKeyInfo>,
|
||||
pub projection_type: String,
|
||||
pub non_key_attributes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamoDbTableDescription {
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub item_count: u64,
|
||||
pub size_bytes: u64,
|
||||
pub partition_key: DynamoDbKeyInfo,
|
||||
pub sort_key: Option<DynamoDbKeyInfo>,
|
||||
pub indexes: Vec<DynamoDbIndexInfo>,
|
||||
}
|
||||
|
||||
pub fn connect(_config: &ConnectionConfig, _host: &str, _port: u16) -> Result<DynamoDbClient, String> {
|
||||
Err(FEATURE_DISABLED.to_string())
|
||||
}
|
||||
|
||||
pub async fn test_connection(_client: &DynamoDbClient, _timeout: Duration) -> Result<(), String> {
|
||||
Err(FEATURE_DISABLED.to_string())
|
||||
}
|
||||
|
||||
pub async fn execute_statement(
|
||||
_client: &DynamoDbClient,
|
||||
_source: &str,
|
||||
_max_rows: usize,
|
||||
) -> Result<QueryResult, String> {
|
||||
Err(FEATURE_DISABLED.to_string())
|
||||
}
|
||||
|
||||
pub async fn list_tables(_client: &DynamoDbClient) -> Result<Vec<String>, String> {
|
||||
Err(FEATURE_DISABLED.to_string())
|
||||
}
|
||||
|
||||
pub async fn describe_table(_client: &DynamoDbClient, _table_name: &str) -> Result<DynamoDbTableDescription, String> {
|
||||
Err(FEATURE_DISABLED.to_string())
|
||||
}
|
||||
|
||||
pub async fn find_items(
|
||||
_client: &DynamoDbClient,
|
||||
_table_name: &str,
|
||||
_limit: i64,
|
||||
_filter_json: Option<&str>,
|
||||
_sort_json: Option<&str>,
|
||||
_cursor: Option<&str>,
|
||||
) -> Result<DocumentQueryResult, String> {
|
||||
Err(FEATURE_DISABLED.to_string())
|
||||
}
|
||||
|
||||
pub async fn count_items(
|
||||
_client: &DynamoDbClient,
|
||||
_table_name: &str,
|
||||
_filter_json: Option<&str>,
|
||||
) -> Result<u64, String> {
|
||||
Err(FEATURE_DISABLED.to_string())
|
||||
}
|
||||
|
||||
pub async fn insert_item(_client: &DynamoDbClient, _table_name: &str, _doc_json: &str) -> Result<String, String> {
|
||||
Err(FEATURE_DISABLED.to_string())
|
||||
}
|
||||
|
||||
pub async fn update_item(
|
||||
_client: &DynamoDbClient,
|
||||
_table_name: &str,
|
||||
_id: &str,
|
||||
_doc_json: &str,
|
||||
) -> Result<u64, String> {
|
||||
Err(FEATURE_DISABLED.to_string())
|
||||
}
|
||||
|
||||
pub async fn delete_item(_client: &DynamoDbClient, _table_name: &str, _id: &str) -> Result<u64, String> {
|
||||
Err(FEATURE_DISABLED.to_string())
|
||||
}
|
||||
@@ -829,6 +829,7 @@ fn search_response_to_document_result(result: SearchResponse) -> Result<Document
|
||||
extended_documents: None,
|
||||
total,
|
||||
total_is_exact,
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -274,6 +274,7 @@ pub async fn find_documents(
|
||||
extended_documents: None,
|
||||
total: result.total,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ pub mod duckdb_sql;
|
||||
pub mod duckdb_worker_process;
|
||||
#[cfg(feature = "duckdb-sidecar")]
|
||||
pub mod duckdb_worker_protocol;
|
||||
#[cfg(feature = "dynamodb")]
|
||||
#[path = "dynamodb_driver.rs"]
|
||||
pub mod dynamodb_driver;
|
||||
#[cfg(not(feature = "dynamodb"))]
|
||||
#[path = "dynamodb_driver_disabled.rs"]
|
||||
pub mod dynamodb_driver;
|
||||
pub mod easysearch_driver;
|
||||
pub mod elasticsearch_driver;
|
||||
pub mod elasticsearch_sql;
|
||||
|
||||
@@ -189,6 +189,7 @@ pub async fn run_command(client: &Client, database: &str, command_json: &str) ->
|
||||
extended_documents: Some(vec![extended_document]),
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1198,6 +1199,7 @@ async fn find_documents_with_total(
|
||||
extended_documents: Some(extended_documents),
|
||||
total,
|
||||
total_is_exact,
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1354,6 +1356,7 @@ pub async fn find_documents_extended_json(
|
||||
raw_documents: None,
|
||||
total,
|
||||
total_is_exact,
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1422,6 +1425,7 @@ pub async fn aggregate_documents(
|
||||
extended_documents: Some(vec![extended]),
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1479,6 +1483,7 @@ async fn drain_document_cursor(
|
||||
extended_documents: Some(extended_documents),
|
||||
total,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1539,6 +1544,7 @@ pub async fn distinct(
|
||||
extended_documents: Some(extended_documents),
|
||||
total,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2107,6 +2113,7 @@ fn single_document_result(document: Option<Document>) -> MongoDocumentResult {
|
||||
extended_documents: Some(vec![Bson::Document(document).into_canonical_extjson()]),
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
},
|
||||
None => MongoDocumentResult {
|
||||
documents: Vec::new(),
|
||||
@@ -2114,6 +2121,7 @@ fn single_document_result(document: Option<Document>) -> MongoDocumentResult {
|
||||
extended_documents: Some(Vec::new()),
|
||||
total: 0,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,6 +621,7 @@ pub async fn find_documents(
|
||||
extended_documents: None,
|
||||
total: result.affected_rows,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -669,6 +670,7 @@ pub async fn find_documents(
|
||||
extended_documents: None,
|
||||
total: result.affected_rows,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::connection::{AppState, PoolKind};
|
||||
use crate::db::agent_driver::mongo_document_id_params;
|
||||
use crate::db::document_result::DocumentQueryResult;
|
||||
use crate::db::{easysearch_driver, elasticsearch_driver, mongo_driver, vector_driver};
|
||||
use crate::db::{dynamodb_driver, easysearch_driver, elasticsearch_driver, mongo_driver, vector_driver};
|
||||
|
||||
pub use crate::db::vector_driver::CollectionInfo;
|
||||
|
||||
@@ -54,6 +54,7 @@ pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Resul
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
},
|
||||
PoolKind::DynamoDb(client) => Ok(vec![client.region.clone()]),
|
||||
PoolKind::Elasticsearch(_) => Ok(vec!["default".to_string()]),
|
||||
PoolKind::Easysearch(_) => Ok(vec!["default".to_string()]),
|
||||
PoolKind::Meilisearch(_) => Ok(vec!["default".to_string()]),
|
||||
@@ -246,6 +247,20 @@ pub async fn list_collections_core(
|
||||
infos.extend(specs.into_iter().map(|spec| mongo_collection_info(spec.name, spec.kind)));
|
||||
Ok(infos)
|
||||
}
|
||||
PoolKind::DynamoDb(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
let names = dynamodb_driver::list_tables(&client).await?;
|
||||
Ok(names
|
||||
.into_iter()
|
||||
.map(|name| CollectionInfo {
|
||||
id: name.clone(),
|
||||
name,
|
||||
kind: Some("table".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
PoolKind::Elasticsearch(client) => {
|
||||
let names = sort_names(elasticsearch_driver::list_indices(client).await?);
|
||||
Ok(names.into_iter().map(|n| CollectionInfo { name: n.clone(), id: n, ..Default::default() }).collect())
|
||||
@@ -408,6 +423,7 @@ pub async fn find_documents_core(
|
||||
projection: Option<&str>,
|
||||
sort: Option<&str>,
|
||||
collation: Option<&str>,
|
||||
cursor: Option<&str>,
|
||||
) -> Result<DocumentQueryResult, String> {
|
||||
ensure_document_pool(state, connection_id).await?;
|
||||
let connections = state.connections.read().await;
|
||||
@@ -420,6 +436,12 @@ pub async fn find_documents_core(
|
||||
)
|
||||
.await
|
||||
}
|
||||
PoolKind::DynamoDb(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
let _ = (database, skip, projection, collation);
|
||||
dynamodb_driver::find_items(&client, collection, limit, filter, sort, cursor).await
|
||||
}
|
||||
PoolKind::Elasticsearch(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
@@ -469,6 +491,51 @@ pub async fn find_documents_core(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn count_document_store_documents_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
collection: &str,
|
||||
filter: Option<&str>,
|
||||
) -> Result<u64, String> {
|
||||
ensure_document_pool(state, connection_id).await?;
|
||||
let connections = state.connections.read().await;
|
||||
match connections.get(connection_id).ok_or("Not found")? {
|
||||
PoolKind::DynamoDb(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
dynamodb_driver::count_items(&client, collection, filter).await
|
||||
}
|
||||
PoolKind::Elasticsearch(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
elasticsearch_driver::count_documents(&client, collection, filter).await
|
||||
}
|
||||
PoolKind::Easysearch(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
easysearch_driver::count_documents(&client, collection, filter).await
|
||||
}
|
||||
_ => Err("Document count is not supported for this connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn describe_dynamodb_table_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
table: &str,
|
||||
) -> Result<dynamodb_driver::DynamoDbTableDescription, String> {
|
||||
ensure_document_pool(state, connection_id).await?;
|
||||
let connections = state.connections.read().await;
|
||||
match connections.get(connection_id).ok_or("Not found")? {
|
||||
PoolKind::DynamoDb(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
dynamodb_driver::describe_table(&client, table).await
|
||||
}
|
||||
_ => Err("Not a DynamoDB connection".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn count_elasticsearch_documents_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
@@ -509,6 +576,11 @@ pub async fn insert_document_core(
|
||||
let connections = state.connections.read().await;
|
||||
match connections.get(connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => mongo_driver::insert_document(client, database, collection, doc_json).await,
|
||||
PoolKind::DynamoDb(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
dynamodb_driver::insert_item(&client, collection, doc_json).await
|
||||
}
|
||||
PoolKind::Elasticsearch(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
@@ -573,6 +645,11 @@ pub async fn update_document_core(
|
||||
let connections = state.connections.read().await;
|
||||
match connections.get(connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => mongo_driver::update_document(client, database, collection, id, doc_json).await,
|
||||
PoolKind::DynamoDb(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
dynamodb_driver::update_item(&client, collection, id, doc_json).await
|
||||
}
|
||||
PoolKind::Elasticsearch(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
@@ -630,6 +707,11 @@ pub async fn delete_document_core_with_type(
|
||||
let connections = state.connections.read().await;
|
||||
match connections.get(connection_id).ok_or("Not found")? {
|
||||
PoolKind::MongoDb(client) => mongo_driver::delete_document(client, database, collection, id).await,
|
||||
PoolKind::DynamoDb(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
dynamodb_driver::delete_item(&client, collection, id).await
|
||||
}
|
||||
PoolKind::Elasticsearch(client) => {
|
||||
let client = client.clone();
|
||||
drop(connections);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
pub mod agent_catalog;
|
||||
pub mod agent_connection;
|
||||
pub mod agent_events;
|
||||
|
||||
@@ -511,6 +511,8 @@ pub enum DatabaseType {
|
||||
SqlServer,
|
||||
#[serde(rename = "mongodb")]
|
||||
MongoDb,
|
||||
#[serde(rename = "dynamodb")]
|
||||
DynamoDb,
|
||||
#[serde(rename = "oracle")]
|
||||
Oracle,
|
||||
#[serde(rename = "elasticsearch")]
|
||||
@@ -1093,6 +1095,10 @@ impl ConnectionConfig {
|
||||
let db_part = mongo_uri_db_part_for_suffix(&db_part, &suffix);
|
||||
format!("mongodb://{host}:{port}{db_part}{suffix}")
|
||||
}
|
||||
DatabaseType::DynamoDb => {
|
||||
let scheme = if self.ssl { "https" } else { "http" };
|
||||
format!("{scheme}://{host}:{port}")
|
||||
}
|
||||
DatabaseType::Oracle => format!("oracle://{host}:{port}{db_part}"),
|
||||
DatabaseType::Elasticsearch
|
||||
| DatabaseType::Easysearch
|
||||
@@ -1258,6 +1264,10 @@ impl ConnectionConfig {
|
||||
format!("mongodb://{username}:{password}@{host}:{port}{db_part}{suffix}")
|
||||
}
|
||||
}
|
||||
DatabaseType::DynamoDb => {
|
||||
let scheme = if self.ssl { "https" } else { "http" };
|
||||
format!("{scheme}://{host}:{port}")
|
||||
}
|
||||
DatabaseType::Oracle => {
|
||||
format!("oracle://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
|
||||
@@ -224,6 +224,7 @@ pub async fn mongo_find_documents_core(
|
||||
projection,
|
||||
sort,
|
||||
collation,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1299,6 +1300,7 @@ mod tests {
|
||||
extended_documents: Some(vec![serde_json::json!(1), serde_json::json!(2), serde_json::json!(3)]),
|
||||
total: 3,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
};
|
||||
|
||||
let limited = limit_mongo_documents(result, 2);
|
||||
@@ -1323,6 +1325,7 @@ mod tests {
|
||||
extended_documents: None,
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
};
|
||||
|
||||
let query_result = mongo_show_databases_query_result(result.documents, 2).unwrap();
|
||||
@@ -1348,6 +1351,7 @@ mod tests {
|
||||
extended_documents: None,
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
};
|
||||
let query_result = mongo_show_databases_query_result(empty.documents, 100).unwrap();
|
||||
assert_eq!(query_result.columns, ["name", "sizeOnDisk", "empty"]);
|
||||
@@ -1360,6 +1364,7 @@ mod tests {
|
||||
extended_documents: None,
|
||||
total: 1,
|
||||
total_is_exact: true,
|
||||
next_cursor: None,
|
||||
};
|
||||
assert!(mongo_show_databases_query_result(malformed.documents, 100).unwrap_err().contains("databases array"));
|
||||
}
|
||||
|
||||
@@ -2067,6 +2067,15 @@ async fn do_execute_typed(
|
||||
.map(|result| truncate_result_with_max_rows(result, max_rows))
|
||||
}
|
||||
PoolKind::HBase(_) => Err("SQL execution is not supported for HBase connections".to_string()),
|
||||
PoolKind::DynamoDb(client) => {
|
||||
let client = client.clone();
|
||||
let sql = sql.to_string();
|
||||
let max_rows = options.max_rows.unwrap_or(MAX_ROWS);
|
||||
drop(connections);
|
||||
// Keep the AWS SDK cold-path future off this already-large query dispatcher stack.
|
||||
let execution = Box::pin(db::dynamodb_driver::execute_statement(&client, &sql, max_rows));
|
||||
wait_for_query_opt(cancel_token, query_timeout, execution).await
|
||||
}
|
||||
PoolKind::Consul(_) => Err("SQL execution is not supported for Consul connections".to_string()),
|
||||
};
|
||||
result
|
||||
@@ -3498,6 +3507,7 @@ fn pool_kind_has_transactional_path(pool: &PoolKind) -> bool {
|
||||
| PoolKind::DuckDbWorker(_)
|
||||
| PoolKind::Redis(_)
|
||||
| PoolKind::MongoDb(_)
|
||||
| PoolKind::DynamoDb(_)
|
||||
| PoolKind::Elasticsearch(_)
|
||||
| PoolKind::Easysearch(_)
|
||||
| PoolKind::Meilisearch(_)
|
||||
@@ -3766,6 +3776,7 @@ pub async fn execute_statements_in_transaction_on_pool_typed(
|
||||
PoolKind::DuckDbWorker(_)
|
||||
| PoolKind::Redis(_)
|
||||
| PoolKind::MongoDb(_)
|
||||
| PoolKind::DynamoDb(_)
|
||||
| PoolKind::Elasticsearch(_)
|
||||
| PoolKind::Easysearch(_)
|
||||
| PoolKind::Meilisearch(_)
|
||||
@@ -5231,6 +5242,55 @@ for line in sys.stdin:
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dynamodb")]
|
||||
#[tokio::test]
|
||||
#[ignore = "requires DBX_DYNAMODB_ENDPOINT and an orders table"]
|
||||
async fn live_dynamodb_editor_scan_serializes_one_thousand_rows() {
|
||||
let endpoint = std::env::var("DBX_DYNAMODB_ENDPOINT").expect("DBX_DYNAMODB_ENDPOINT is required");
|
||||
let (ssl, address) = endpoint
|
||||
.strip_prefix("https://")
|
||||
.map(|address| (true, address))
|
||||
.or_else(|| endpoint.strip_prefix("http://").map(|address| (false, address)))
|
||||
.expect("DynamoDB endpoint must start with http:// or https://");
|
||||
let (host, port) = address.rsplit_once(':').expect("DynamoDB endpoint must include a port");
|
||||
let dir = std::env::temp_dir().join(format!("dbx-query-dynamodb-{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let storage = Storage::open(&dir.join("storage.db")).await.unwrap();
|
||||
let state = AppState::new(storage);
|
||||
let mut config = test_connection_config(DatabaseType::DynamoDb);
|
||||
config.host = host.to_string();
|
||||
config.port = port.parse().expect("valid DynamoDB port");
|
||||
config.username = "dummy".to_string();
|
||||
config.password = "dummy".to_string();
|
||||
config.database = Some("us-east-1".to_string());
|
||||
config.ssl = ssl;
|
||||
let client = db::dynamodb_driver::connect(&config, host, config.port).unwrap();
|
||||
db::dynamodb_driver::test_connection(&client, Duration::from_secs(5)).await.unwrap();
|
||||
state.configs.write().await.insert(config.id.clone(), config.clone());
|
||||
state.connections.write().await.insert(config.id.clone(), PoolKind::DynamoDb(client));
|
||||
|
||||
let results = execute_multi_core_with_options_for_client_and_progress_typed(
|
||||
&state,
|
||||
&config.id,
|
||||
"us-east-1",
|
||||
"DBX DYNAMODB SCAN\ntable: \"orders\"\nlimit: 1000",
|
||||
None,
|
||||
None,
|
||||
QueryExecutionOptions { max_rows: Some(1000), ..Default::default() },
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].result.rows.len(), 1000);
|
||||
let serialized = serde_json::to_string(&results).unwrap();
|
||||
assert!(!serialized.is_empty());
|
||||
|
||||
drop(state);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
async fn agent_error_state(
|
||||
disposition: &str,
|
||||
) -> (AppState, std::path::PathBuf, std::sync::Arc<crate::db::agent_driver::AgentRuntimeClient>) {
|
||||
|
||||
@@ -247,12 +247,27 @@ pub fn is_write_sql_for_database(sql: &str, database_type: DatabaseType) -> bool
|
||||
if database_type == DatabaseType::VictoriaMetrics {
|
||||
return false;
|
||||
}
|
||||
if database_type == DatabaseType::DynamoDb {
|
||||
if let Some(is_write) = classify_dynamodb_statement_write(sql) {
|
||||
return is_write;
|
||||
}
|
||||
}
|
||||
if let Some(risk) = classify_search_engine_query_risk(sql, database_type) {
|
||||
return risk != SearchEngineQueryRisk::ReadOnly;
|
||||
}
|
||||
is_write_sql_with_database_type(sql, Some(database_type))
|
||||
}
|
||||
|
||||
fn classify_dynamodb_statement_write(source: &str) -> Option<bool> {
|
||||
let header = source.lines().find(|line| !line.trim().is_empty())?.trim().to_ascii_uppercase();
|
||||
match header.as_str() {
|
||||
"DBX DYNAMODB SCAN" | "DBX DYNAMODB QUERY / SCAN" => Some(false),
|
||||
"DBX DYNAMODB INSERT ITEM" | "DBX DYNAMODB PUT ITEM" | "DBX DYNAMODB DELETE ITEM" => Some(true),
|
||||
_ if header.starts_with("DBX DYNAMODB") => Some(true),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum SearchEngineQueryRisk {
|
||||
ReadOnly,
|
||||
@@ -1598,6 +1613,24 @@ mod tests {
|
||||
assert_eq!(strip_sql_comments("SELECT 'hello /* not a comment */'"), "SELECT 'hello '");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_dynamodb_generated_statements_for_read_only_protection() {
|
||||
assert!(!is_write_sql_for_database(
|
||||
"DBX DYNAMODB SCAN\ntable: \"orders\"\nlimit: 1000",
|
||||
DatabaseType::DynamoDb
|
||||
));
|
||||
assert!(!is_write_sql_for_database(
|
||||
"DBX DYNAMODB QUERY / SCAN\ntable: \"orders\"\nfilter:\n{\"status\":\"SHIPPED\"}",
|
||||
DatabaseType::DynamoDb
|
||||
));
|
||||
for operation in ["INSERT ITEM", "PUT ITEM", "DELETE ITEM", "UNKNOWN"] {
|
||||
assert!(is_write_sql_for_database(
|
||||
&format!("DBX DYNAMODB {operation}\ntable: \"orders\""),
|
||||
DatabaseType::DynamoDb
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_search_engine_rest_writes() {
|
||||
assert!(!is_write_sql_for_database("GET /_cluster/health", DatabaseType::Easysearch));
|
||||
|
||||
@@ -675,10 +675,9 @@ pub fn profile_for(db_type: DatabaseType) -> DdlDialectProfile {
|
||||
}
|
||||
|
||||
// Non-tabular / not applicable for relational CREATE TABLE
|
||||
Redis | MongoDb | Elasticsearch | Easysearch | Meilisearch | Qdrant | Milvus | Weaviate | ChromaDb | Neo4j
|
||||
| Cassandra | Etcd | ZooKeeper | Nacos | Consul | InfluxDb | VictoriaMetrics | MessageQueue | Mqtt | Hbase => {
|
||||
conservative_ansi(db_type)
|
||||
}
|
||||
Redis | MongoDb | DynamoDb | Elasticsearch | Easysearch | Meilisearch | Qdrant | Milvus | Weaviate
|
||||
| ChromaDb | Neo4j | Cassandra | Etcd | ZooKeeper | Nacos | Consul | InfluxDb | VictoriaMetrics
|
||||
| MessageQueue | Mqtt | Hbase => conservative_ansi(db_type),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@ name = "dbx-web"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["duckdb-sidecar", "mq-admin", "sqlite-sqlcipher", "system-fonts"]
|
||||
default = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-sqlcipher", "system-fonts"]
|
||||
duckdb-sidecar = ["dbx-core/duckdb-sidecar"]
|
||||
dynamodb = ["dbx-core/dynamodb"]
|
||||
mq-admin = ["dbx-core/mq-admin"]
|
||||
sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"]
|
||||
system-fonts = ["dbx-core/system-fonts"]
|
||||
|
||||
@@ -777,6 +777,8 @@ async fn main() {
|
||||
.route("/document-store/list-databases", post(routes::document_store::list_databases))
|
||||
.route("/document-store/list-collections", post(routes::document_store::list_collections))
|
||||
.route("/document-store/find-documents", post(routes::document_store::find_documents))
|
||||
.route("/document-store/count-documents", post(routes::document_store::count_documents))
|
||||
.route("/document-store/dynamodb-describe-table", post(routes::document_store::describe_dynamodb_table))
|
||||
.route(
|
||||
"/document-store/elasticsearch-count-documents",
|
||||
post(routes::document_store::elasticsearch_count_documents),
|
||||
|
||||
@@ -67,9 +67,26 @@ pub struct DocumentFindRequest {
|
||||
pub projection: Option<String>,
|
||||
pub sort: Option<String>,
|
||||
pub collation: Option<String>,
|
||||
pub cursor: Option<String>,
|
||||
pub execution_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DocumentCountRequest {
|
||||
pub connection_id: String,
|
||||
pub collection: String,
|
||||
pub filter: Option<String>,
|
||||
pub execution_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamoDbDescribeTableRequest {
|
||||
pub connection_id: String,
|
||||
pub table: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ElasticsearchCountDocumentsRequest {
|
||||
@@ -204,12 +221,41 @@ pub async fn find_documents(
|
||||
req.projection.as_deref(),
|
||||
req.sort.as_deref(),
|
||||
req.collation.as_deref(),
|
||||
req.cursor.as_deref(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn count_documents(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<DocumentCountRequest>,
|
||||
) -> Result<Json<u64>, AppError> {
|
||||
let result = run_cancellable(
|
||||
&state,
|
||||
req.execution_id,
|
||||
dbx_core::document_ops::count_document_store_documents_core(
|
||||
&state.app,
|
||||
&req.connection_id,
|
||||
&req.collection,
|
||||
req.filter.as_deref(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn describe_dynamodb_table(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<DynamoDbDescribeTableRequest>,
|
||||
) -> Result<Json<dbx_core::db::dynamodb_driver::DynamoDbTableDescription>, AppError> {
|
||||
let result = dbx_core::document_ops::describe_dynamodb_table_core(&state.app, &req.connection_id, &req.table)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn elasticsearch_count_documents(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(req): Json<ElasticsearchCountDocumentsRequest>,
|
||||
|
||||
@@ -466,6 +466,7 @@ pub async fn find_documents(
|
||||
req.projection.as_deref(),
|
||||
req.sort.as_deref(),
|
||||
req.collation.as_deref(),
|
||||
None,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ test("document table wires on-demand exact total counting for estimated mongo to
|
||||
test("document pagination commits page index with fetched rows", () => {
|
||||
const source = documentBrowserSource();
|
||||
assert.match(source, /async function load\(options: \{ page\?: number \} = \{\}\)/);
|
||||
assert.match(source, /void load\(\{ page: nextPage \}\)/);
|
||||
assert.match(source, /(?:void|await) load\(\{ page: nextPage \}\)/);
|
||||
assert.match(source, /if \(options\.page !== undefined\) page\.value = options\.page;/);
|
||||
});
|
||||
|
||||
|
||||
@@ -13,9 +13,10 @@ name = "dbx_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["duckdb-sidecar", "mq-admin", "sqlite-sqlcipher", "system-fonts"]
|
||||
default = ["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-sqlcipher", "system-fonts"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
duckdb-sidecar = ["dbx-core/duckdb-sidecar"]
|
||||
dynamodb = ["dbx-core/dynamodb"]
|
||||
mq-admin = ["dbx-core/mq-admin"]
|
||||
sqlite-sqlcipher = ["dbx-core/sqlite-sqlcipher"]
|
||||
system-fonts = ["dbx-core/system-fonts"]
|
||||
|
||||
@@ -1105,6 +1105,12 @@ async fn test_connection_with_info_inner(
|
||||
Err(native_err)
|
||||
}
|
||||
}
|
||||
DatabaseType::DynamoDb => {
|
||||
let client = db::dynamodb_driver::connect(&config, &host, port)?;
|
||||
db::dynamodb_driver::test_connection(&client, connect_timeout)
|
||||
.await
|
||||
.map(|_| "Connection successful".to_string())
|
||||
}
|
||||
DatabaseType::ClickHouse => {
|
||||
let username = if config.username.is_empty() { None } else { Some(config.username.clone()) };
|
||||
let password = if config.password.is_empty() { None } else { Some(config.password.clone()) };
|
||||
@@ -1518,6 +1524,11 @@ pub async fn connect_db(
|
||||
}
|
||||
}
|
||||
}
|
||||
DatabaseType::DynamoDb => {
|
||||
let client = db::dynamodb_driver::connect(&db_config, &host, port)?;
|
||||
db::dynamodb_driver::test_connection(&client, connect_timeout).await?;
|
||||
PoolKind::DynamoDb(client)
|
||||
}
|
||||
DatabaseType::ClickHouse => {
|
||||
let username = if db_config.username.is_empty() { None } else { Some(db_config.username.clone()) };
|
||||
let password = if db_config.password.is_empty() { None } else { Some(db_config.password.clone()) };
|
||||
|
||||
@@ -58,6 +58,7 @@ pub async fn document_find_documents(
|
||||
projection: Option<String>,
|
||||
sort: Option<String>,
|
||||
collation: Option<String>,
|
||||
cursor: Option<String>,
|
||||
execution_id: Option<String>,
|
||||
) -> Result<DocumentQueryResult, String> {
|
||||
let app = state.inner().clone();
|
||||
@@ -75,11 +76,43 @@ pub async fn document_find_documents(
|
||||
projection.as_deref(),
|
||||
sort.as_deref(),
|
||||
collation.as_deref(),
|
||||
cursor.as_deref(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn document_count_documents(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
collection: String,
|
||||
filter: Option<String>,
|
||||
execution_id: Option<String>,
|
||||
) -> Result<u64, String> {
|
||||
let app = state.inner().clone();
|
||||
run_cancellable(
|
||||
&app,
|
||||
execution_id,
|
||||
dbx_core::document_ops::count_document_store_documents_core(
|
||||
&app,
|
||||
&connection_id,
|
||||
&collection,
|
||||
filter.as_deref(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn dynamodb_describe_table(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
table: String,
|
||||
) -> Result<dbx_core::db::dynamodb_driver::DynamoDbTableDescription, String> {
|
||||
dbx_core::document_ops::describe_dynamodb_table_core(&state, &connection_id, &table).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn elasticsearch_count_documents(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
|
||||
@@ -147,6 +147,7 @@ pub async fn mongo_find_documents(
|
||||
projection,
|
||||
sort,
|
||||
collation,
|
||||
None,
|
||||
execution_id,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -2003,6 +2003,8 @@ pub fn run() {
|
||||
commands::document_cmd::document_list_databases,
|
||||
commands::document_cmd::document_list_collections,
|
||||
commands::document_cmd::document_find_documents,
|
||||
commands::document_cmd::document_count_documents,
|
||||
commands::document_cmd::dynamodb_describe_table,
|
||||
commands::document_cmd::elasticsearch_count_documents,
|
||||
commands::document_cmd::document_list_gridfs_buckets,
|
||||
commands::document_cmd::document_create_gridfs_bucket,
|
||||
|
||||
Reference in New Issue
Block a user