mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-16 14:50:15 +08:00
Compare commits
86
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b021ceec4b | ||
|
|
d318579745 | ||
|
|
8d17ed0ecd | ||
|
|
3199a99b29 | ||
|
|
39008f7170 | ||
|
|
8bf33d539f | ||
|
|
42f6720acd | ||
|
|
0a3cb12c0b | ||
|
|
202823e731 | ||
|
|
ed088fe923 | ||
|
|
df21c3396a | ||
|
|
90ccdc6432 | ||
|
|
dba371a320 | ||
|
|
d96cd8f0d2 | ||
|
|
bd6e7d48f3 | ||
|
|
8b41e4530f | ||
|
|
a555f1e759 | ||
|
|
fdaa40e431 | ||
|
|
4c2f9d92e5 | ||
|
|
a92011eee6 | ||
|
|
61a7757a0e | ||
|
|
a53ca3592f | ||
|
|
9e682617e6 | ||
|
|
16dda810d7 | ||
|
|
6042f16342 | ||
|
|
0afdd06c80 | ||
|
|
3158aebca4 | ||
|
|
99866fdc12 | ||
|
|
a58eb59a32 | ||
|
|
eda1f8720f | ||
|
|
f54f377344 | ||
|
|
81a9be17b8 | ||
|
|
af42d7f50a | ||
|
|
d083ac1330 | ||
|
|
cdb450dfde | ||
|
|
d0d9ee19ab | ||
|
|
9327ce0ac2 | ||
|
|
1fb9dd7b5a | ||
|
|
e82cac479b | ||
|
|
bf6afb0340 | ||
|
|
4779c3b405 | ||
|
|
31ee01b6fa | ||
|
|
025944a867 | ||
|
|
caee9914b8 | ||
|
|
6e2b1b7a17 | ||
|
|
a379c2d7ba | ||
|
|
8b1054a148 | ||
|
|
7fdc8f7e61 | ||
|
|
32bebd4bad | ||
|
|
4221ea7fe0 | ||
|
|
af11aaa8fe | ||
|
|
5c58343a10 | ||
|
|
a565beb777 | ||
|
|
9df99fb62e | ||
|
|
b3127b9aa7 | ||
|
|
f8677a0689 | ||
|
|
f22a936507 | ||
|
|
b5858637d6 | ||
|
|
475c8129fc | ||
|
|
50d846d087 | ||
|
|
16bd3ca575 | ||
|
|
927184e009 | ||
|
|
d4d9081936 | ||
|
|
a0bcfba73d | ||
|
|
09f4828006 | ||
|
|
cde64e255b | ||
|
|
7b134cef81 | ||
|
|
fde7502df8 | ||
|
|
955b28c520 | ||
|
|
5cfc37e26f | ||
|
|
42cb6b3a9d | ||
|
|
ad245e9f4c | ||
|
|
08260e6a55 | ||
|
|
aea12e9fed | ||
|
|
caef729a8d | ||
|
|
12030822ea | ||
|
|
97cf62d333 | ||
|
|
8240c92247 | ||
|
|
62a194c1b0 | ||
|
|
36e954c440 | ||
|
|
a674c183d5 | ||
|
|
4bab1025d7 | ||
|
|
7d21186158 | ||
|
|
eb5f89dae3 | ||
|
|
70292cb9c0 | ||
|
|
950f45833a |
@@ -30,10 +30,13 @@ jobs:
|
||||
uses: ./.github/workflows/frontend-build.yml
|
||||
secrets: inherit
|
||||
|
||||
# lint-server:
|
||||
# name: Server
|
||||
# uses: dbeaver/dbeaver-common/.github/workflows/java-checkstyle.yml@devel
|
||||
# secrets: inherit
|
||||
lint-server:
|
||||
name: Server
|
||||
uses: dbeaver/dbeaver-common/.github/workflows/java-checkstyle.yml@devel
|
||||
secrets: inherit
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
lint-frontend:
|
||||
name: Frontend
|
||||
|
||||
Vendored
+1
-1
@@ -48,6 +48,7 @@
|
||||
"-Declipse.pde.launch=true",
|
||||
"-XstartOnFirstThread",
|
||||
"-Dfile.encoding=UTF-8",
|
||||
"--enable-native-access=ALL-UNNAMED",
|
||||
"--add-modules=ALL-DEFAULT",
|
||||
"--add-opens=java.base/java.io=ALL-UNNAMED",
|
||||
"--add-opens=java.base/java.lang=ALL-UNNAMED",
|
||||
@@ -64,7 +65,6 @@
|
||||
"--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED",
|
||||
"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED",
|
||||
"--add-opens=java.base/sun.security.ssl=ALL-UNNAMED",
|
||||
"--add-opens=java.base/sun.security.action=ALL-UNNAMED",
|
||||
"--add-opens=java.base/sun.security.util=ALL-UNNAMED",
|
||||
"--add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED",
|
||||
"--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED",
|
||||
|
||||
@@ -28,6 +28,52 @@ You can see a live demo of CloudBeaver server here: https://demo.cloudbeaver.io
|
||||
|
||||
## Changelog
|
||||
|
||||
### 26.1.3 2026-07-20
|
||||
|
||||
- Data Editor:
|
||||
- Fixed copying of large JSON values in the Data Editor - now the full value can be copied and transferred.
|
||||
- Updated the Reference panel to show only relevant rows when a foreign key is null.
|
||||
- General:
|
||||
- Added connection types to visually distinguish database environments using background colors across the Database Navigator and application UI. Users with edit permissions can assign built-in environment types color-coded configurations.
|
||||
- Data Transfer:
|
||||
- Improved memory usage when importing large CSV files.
|
||||
- Fixed CSV export with multi-character quotes and improved quote/delimiter validation
|
||||
- Accessibility:
|
||||
- Enabled the use of the Enter or space keys to toggle checkboxes in the administration tabs and the metadata viewer.
|
||||
- Added the ability to expand and collapse code block elements, including JSON, using the keyboard.
|
||||
- Fixed shortcuts behavior for AZERTY keyboards. Now shortcuts like Ctrl/Cmd + Z work correctly.
|
||||
- Security:
|
||||
- Resolved an authorization vulnerability when testing connections by enforcing strict permission checks. This mitigation prevented unauthenticated users from exploiting JDBC requests to read arbitrary server files.
|
||||
- Resolved an LDAP injection vulnerability by properly escaping user input in the authentication username field, preventing attackers from manipulating search filters, bypassing access restrictions, and enumerating directory users.
|
||||
- Fixed the high vulnerability (GHSA-gv7w-rqvm-qjhr) in the esbuild library. The library was updated to version 0.28.1.
|
||||
- Fixed the high vulnerability (CVE-2026-12143) in the form-data library. The library was updated to version 4.0.6.
|
||||
- Databases and drivers:
|
||||
- The Databend driver was updated to version 0.4.8.
|
||||
- Fixed an issue related to the display of metadata in the navigation tree for DB2, Firebird, and MySQL.
|
||||
|
||||
### 26.1.2 2026-07-06
|
||||
|
||||
- Data Editor:
|
||||
- Added the References panel showing related records from other tables connected by foreign keys. It helps to explore table relationships directly in the Data editor.
|
||||
- Resolved an issue where the Data Editor ignored the text preview's maximum length quota. Increasing this quota now enables the ability to edit large values.
|
||||
- General:
|
||||
- Added the Last Login time to the user form and the user table in the administration part.
|
||||
- Fixed the bug with the inability to select entered text in the Find and Replace panel in editors.
|
||||
- Accessibility:
|
||||
- Added the ability to use the Enter or Space key to turn checkboxes on/off in the connection properties tab.
|
||||
- Security:
|
||||
- Fixed CVE-2026-55449. Resolved a local file disclosure vulnerability by enforcing strict validation of file identifiers and path containment during data updates.
|
||||
- Updated session handling to prevent session fixation vulnerabilities. The application generates new session identifiers upon login.
|
||||
- Fixed the high vulnerability (CVE-2026-12151) in the undici library. The library was updated to version 6.27.0.
|
||||
|
||||
### 26.1.1 2026-06-22
|
||||
|
||||
- General: Added a "Project Info" tab containing the project description to the project context menu.
|
||||
- Security:
|
||||
- Fixed a cross-site scripting vulnerability caused by unsanitized user input in the web interface.
|
||||
- Resolved a security flaw related to user-controlled file paths by adding strict validation to prevent unauthorized resource access.
|
||||
- Fixed the critical vulnerability (CVE-2026-9277) in the shell-quote library (updated to version 1.8.4).
|
||||
|
||||
### 26.1.0 2026-06-01
|
||||
|
||||
### Changes since 26.0.0:
|
||||
|
||||
@@ -8,7 +8,15 @@ mkdir -p ./cloudbeaver/web
|
||||
cd ../../cloudbeaver/webapp
|
||||
|
||||
yarn install --immutable
|
||||
cd ./packages/product-default
|
||||
yarn clear
|
||||
|
||||
cd common-react
|
||||
yarn clear
|
||||
|
||||
cd ../common-typescript
|
||||
yarn clear
|
||||
|
||||
cd ../packages/product-default
|
||||
yarn run bundle
|
||||
|
||||
if [[ "$?" -ne 0 ]] ; then
|
||||
|
||||
@@ -9,8 +9,6 @@ RUN set -eux; \
|
||||
apt-get update && \
|
||||
apt-get dist-upgrade -y --no-install-recommends && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# curl required for historical reasons, see https://github.com/adoptium/containers/issues/255
|
||||
curl \
|
||||
# java.lang.UnsatisfiedLinkError: libfontmanager.so: libfreetype.so.6: cannot open shared object file: No such file or directory
|
||||
# java.lang.NoClassDefFoundError: Could not initialize class sun.awt.X11FontManager
|
||||
# https://github.com/docker-library/openjdk/pull/235#issuecomment-424466077
|
||||
|
||||
@@ -17,6 +17,7 @@ If Not Defined JAVA_OPTS (
|
||||
|
||||
java %JAVA_OPTS% ^
|
||||
-Dfile.encoding=UTF-8 ^
|
||||
--enable-native-access=ALL-UNNAMED ^
|
||||
--add-modules=ALL-DEFAULT ^
|
||||
--add-opens=java.base/java.io=ALL-UNNAMED ^
|
||||
--add-opens=java.base/java.lang=ALL-UNNAMED ^
|
||||
@@ -33,7 +34,6 @@ java %JAVA_OPTS% ^
|
||||
--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED ^
|
||||
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED ^
|
||||
--add-opens=java.base/sun.security.ssl=ALL-UNNAMED ^
|
||||
--add-opens=java.base/sun.security.action=ALL-UNNAMED ^
|
||||
--add-opens=java.base/sun.security.util=ALL-UNNAMED ^
|
||||
--add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED ^
|
||||
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED ^
|
||||
|
||||
@@ -11,6 +11,7 @@ echo "Starting Cloudbeaver Server"
|
||||
|
||||
exec java ${JAVA_OPTS} \
|
||||
-Dfile.encoding=UTF-8 \
|
||||
--enable-native-access=ALL-UNNAMED \
|
||||
--add-modules=ALL-DEFAULT \
|
||||
--add-opens=java.base/java.io=ALL-UNNAMED \
|
||||
--add-opens=java.base/java.lang=ALL-UNNAMED \
|
||||
@@ -27,7 +28,6 @@ exec java ${JAVA_OPTS} \
|
||||
--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED \
|
||||
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED \
|
||||
--add-opens=java.base/sun.security.ssl=ALL-UNNAMED \
|
||||
--add-opens=java.base/sun.security.action=ALL-UNNAMED \
|
||||
--add-opens=java.base/sun.security.util=ALL-UNNAMED \
|
||||
--add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED \
|
||||
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Model
|
||||
Bundle-SymbolicName: io.cloudbeaver.model.cli;singleton:=true
|
||||
Bundle-Version: 1.0.13.qualifier
|
||||
Bundle-Release-Date: 20260706
|
||||
Bundle-Version: 1.0.15.qualifier
|
||||
Bundle-Release-Date: 20260803
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.model.cli;visibility:=reexport
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.model.cli</artifactId>
|
||||
<version>1.0.13-SNAPSHOT</version>
|
||||
<version>1.0.15-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Model
|
||||
Bundle-SymbolicName: io.cloudbeaver.model;singleton:=true
|
||||
Bundle-Version: 1.0.104.qualifier
|
||||
Bundle-Release-Date: 20260706
|
||||
Bundle-Version: 1.0.106.qualifier
|
||||
Bundle-Release-Date: 20260803
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport,
|
||||
|
||||
@@ -13,7 +13,7 @@ meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.smDbProductName.
|
||||
meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.smDbProductVersion.name=Security manager database product version
|
||||
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.readOnly.name = Read-only connection
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = Auto-commit
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommitMode.name = Auto-commit mode
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = Keep-alive interval (seconds)
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultSchema.name = Default schema
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultCatalog.name = Default catalog
|
||||
@@ -1,5 +1,5 @@
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.readOnly.name = Только для чтения
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = Авто-коммит
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommitMode.name = Авто-коммит
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = Интервал Keep-alive (в секундах)
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultSchema.name = Схема по умолчанию
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultCatalog.name = Каталог по умолчанию
|
||||
@@ -1,4 +1,4 @@
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.readOnly.name = Kết nối chỉ đọc
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = Tự động commit
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommitMode.name = Tự động commit
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = Giữ kết nối (tính bằng giây)
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.description = Không tự động ngắt kết nối
|
||||
@@ -1,3 +1,3 @@
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = 自动提交
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommitMode.name = 自动提交
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = 保持连接 (秒)
|
||||
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.description = 无自动断开连接
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.model</artifactId>
|
||||
<version>1.0.104-SNAPSHOT</version>
|
||||
<version>1.0.106-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+1
-3
@@ -77,9 +77,7 @@ public class WebConnectionConfigInputHandler<T extends WebConnectionConfig, C ex
|
||||
}
|
||||
|
||||
dataSource.setFolder(input.getFolder() != null ? registry.getFolder(input.getFolder()) : null);
|
||||
if (input.isDefaultAutoCommit() != null) {
|
||||
dataSource.getConnectionConfiguration().getBootstrap().setDefaultAutoCommit(input.isDefaultAutoCommit());
|
||||
}
|
||||
dataSource.getConnectionConfiguration().getBootstrap().setDefaultAutoCommit(input.isDefaultAutoCommit());
|
||||
WebDataSourceUtils.setConnectionConfiguration(
|
||||
dataSource.getDriver(),
|
||||
dataSource.getConnectionConfiguration(),
|
||||
|
||||
+2
@@ -367,6 +367,7 @@ public class WebGlobalProjectRegistryProxy implements DBPDataSourceRegistry, Dat
|
||||
dataSourceRegistry.initializeDataSources();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DataSourceParseResults loadDataSources(
|
||||
@NotNull List<DBPDataSourceConfigurationStorage> storages,
|
||||
@@ -382,6 +383,7 @@ public class WebGlobalProjectRegistryProxy implements DBPDataSourceRegistry, Dat
|
||||
dataSourceRegistry.saveDataSources();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DataSourceConfigurationManager getConfigurationManager() {
|
||||
return dataSourceRegistry.getConfigurationManager();
|
||||
|
||||
@@ -357,6 +357,13 @@ public class WebSessionProjectImpl extends WebProjectImpl implements DBPAdaptabl
|
||||
return getInputConfigHandler(configInput).createDataSourceContainer();
|
||||
}
|
||||
|
||||
public void updateDataSourceContainerFromInput(
|
||||
@NotNull WebConnectionConfig configInput,
|
||||
@NotNull DataSourceDescriptor dataSource
|
||||
) throws DBWebException {
|
||||
getInputConfigHandler(configInput).updateDataSource(dataSource);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public WebConnectionConfig getConnectionConfigInput(@Nullable Map<String, Object> configMap) {
|
||||
return new WebConnectionConfig(configMap == null ? Map.of() : configMap);
|
||||
|
||||
+7
@@ -69,6 +69,13 @@ public interface SMAuthProviderExternal<AUTH_SESSION extends SMSession> extends
|
||||
@NotNull WebUser user,
|
||||
boolean selfIdentity) throws DBException;
|
||||
|
||||
/**
|
||||
* Whether a user that is not found should be provisioned (auto-created) on external login.
|
||||
*/
|
||||
default boolean isAutoUserProvisioningEnabled(@Nullable SMAuthProviderCustomConfiguration providerConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make some post authentication actions
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.model;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
public enum AutoCommitMode {
|
||||
DEFAULT("Default", null),
|
||||
AUTO_COMMIT("Auto commit", true),
|
||||
MANUAL_COMMIT("Manual commit", false);
|
||||
|
||||
private final String title;
|
||||
private final Boolean value;
|
||||
|
||||
AutoCommitMode(@NotNull String title, @Nullable Boolean value) {
|
||||
this.title = title;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return title;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static AutoCommitMode fromValue(@Nullable Boolean value) {
|
||||
if (value == null) {
|
||||
return DEFAULT;
|
||||
}
|
||||
return value ? AUTO_COMMIT : MANUAL_COMMIT;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Boolean from(@Nullable String mode) {
|
||||
if (CommonUtils.isEmpty(mode)) {
|
||||
return null;
|
||||
}
|
||||
return valueOf(mode).getValue();
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -65,6 +65,7 @@ public class WebConnectionConfig {
|
||||
private Boolean defaultAutoCommit;
|
||||
private String defaultCatalogName;
|
||||
private String defaultSchemaName;
|
||||
private String connectionType;
|
||||
@NotNull
|
||||
private Map<String, String> defaultUserPreferences = new LinkedHashMap<>();
|
||||
|
||||
@@ -102,8 +103,14 @@ public class WebConnectionConfig {
|
||||
expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_KEEP_ALIVE_INTERVAL, -1);
|
||||
readOnly = JSONUtils.getBoolean(
|
||||
expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_READ_ONLY);
|
||||
defaultAutoCommit = JSONUtils.getBoolean(
|
||||
expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_AUTO_COMMIT, true);
|
||||
|
||||
if (expertSettingsValues != null) {
|
||||
defaultAutoCommit = AutoCommitMode.from(
|
||||
JSONUtils.getString(expertSettingsValues, WebExpertSettingsProperties.PROP_AUTO_COMMIT_MODE));
|
||||
} else {
|
||||
defaultAutoCommit = JSONUtils.getBoolean(params, WebExpertSettingsProperties.PROP_AUTO_COMMIT, true);
|
||||
}
|
||||
|
||||
defaultCatalogName = JSONUtils.getString(
|
||||
expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_DEFAULT_CATALOG);
|
||||
defaultSchemaName = JSONUtils.getString(
|
||||
@@ -120,6 +127,7 @@ public class WebConnectionConfig {
|
||||
for (Map<String, Object> nhc : JSONUtils.getObjectList(params, "networkHandlersConfig")) {
|
||||
networkHandlersConfig.add(new WebNetworkHandlerConfigInput(nhc));
|
||||
}
|
||||
connectionType = JSONUtils.getString(params, "connectionType");
|
||||
}
|
||||
|
||||
@Property
|
||||
@@ -381,4 +389,13 @@ public class WebConnectionConfig {
|
||||
public void setDefaultUserPreferences(@NotNull Map<String, String> defaultUserPreferences) {
|
||||
this.defaultUserPreferences = defaultUserPreferences;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getConnectionType() {
|
||||
return connectionType;
|
||||
}
|
||||
|
||||
public void setConnectionType(@Nullable String connectionType) {
|
||||
this.connectionType = connectionType;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-7
@@ -467,7 +467,7 @@ public class WebConnectionInfo {
|
||||
@Property
|
||||
public Map<String, Object> getExpertSettingsValues() {
|
||||
Map<String, Object> expertSettings = new LinkedHashMap<>();
|
||||
expertSettings.put(WebExpertSettingsProperties.PROP_AUTO_COMMIT, isAutocommit());
|
||||
expertSettings.put(WebExpertSettingsProperties.PROP_AUTO_COMMIT_MODE, AutoCommitMode.fromValue(isAutocommit()));
|
||||
expertSettings.put(WebExpertSettingsProperties.PROP_KEEP_ALIVE_INTERVAL, getKeepAliveInterval());
|
||||
expertSettings.put(WebExpertSettingsProperties.PROP_READ_ONLY, isReadOnly());
|
||||
expertSettings.put(WebExpertSettingsProperties.PROP_DEFAULT_CATALOG, getDefaultCatalogName());
|
||||
@@ -541,12 +541,9 @@ public class WebConnectionInfo {
|
||||
}
|
||||
|
||||
@Property
|
||||
public boolean isAutocommit() {
|
||||
Boolean isAutoCommit = dataSourceContainer.getConnectionConfiguration().getBootstrap().getDefaultAutoCommit();
|
||||
if (isAutoCommit == null) {
|
||||
return true;
|
||||
}
|
||||
return isAutoCommit;
|
||||
@Nullable
|
||||
public Boolean isAutocommit() {
|
||||
return dataSourceContainer.getConnectionConfiguration().getBootstrap().getDefaultAutoCommit();
|
||||
}
|
||||
|
||||
@Property
|
||||
@@ -586,6 +583,11 @@ public class WebConnectionInfo {
|
||||
return tools;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getConnectionType() {
|
||||
return dataSourceContainer.getConnectionConfiguration().getConnectionType().getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates param that checks whether credentials were saved only in session.
|
||||
*/
|
||||
@@ -593,4 +595,13 @@ public class WebConnectionInfo {
|
||||
this.credentialsSavedInSession = credentialsSavedInSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database-dependent driver configuration used to drive data import
|
||||
*/
|
||||
@Property
|
||||
@NotNull
|
||||
public WebDriverConfiguration getDriverConfiguration() {
|
||||
return new WebDriverConfiguration(dataSourceContainer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.model;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.model.meta.Property;
|
||||
|
||||
/**
|
||||
* Generic id/name/description dictionary value.
|
||||
*/
|
||||
public class WebDictionaryValueInfo {
|
||||
|
||||
@NotNull
|
||||
private final String id;
|
||||
@NotNull
|
||||
private final String name;
|
||||
@Nullable
|
||||
private final String description;
|
||||
|
||||
public WebDictionaryValueInfo(@NotNull String id, @NotNull String name, @Nullable String description) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Property
|
||||
@NotNull
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Property
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Property
|
||||
@Nullable
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.model;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.model.DBPDataSource;
|
||||
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
|
||||
import org.jkiss.dbeaver.model.DBUtils;
|
||||
import org.jkiss.dbeaver.model.meta.Property;
|
||||
import org.jkiss.dbeaver.model.sql.SQLDialectInsertReplaceMethod;
|
||||
import org.jkiss.dbeaver.model.sql.SQLDialectMetadata;
|
||||
import org.jkiss.dbeaver.model.struct.DBSDataBulkLoader;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WebDriverConfiguration {
|
||||
|
||||
@NotNull
|
||||
private final DBPDataSourceContainer dataSourceContainer;
|
||||
|
||||
public WebDriverConfiguration(@NotNull DBPDataSourceContainer dataSourceContainer) {
|
||||
this.dataSourceContainer = dataSourceContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'On duplicate key' replace methods supported by this connection's SQL dialect
|
||||
*/
|
||||
@Property
|
||||
@NotNull
|
||||
public List<WebDictionaryValueInfo> getSupportedInsertReplaceMethods() {
|
||||
SQLDialectMetadata dialect = dataSourceContainer.getDriver().getScriptDialect();
|
||||
List<SQLDialectInsertReplaceMethod> replaceMethods = dialect.getSupportedInsertReplaceMethodsDescriptors();
|
||||
if (CommonUtils.isEmpty(replaceMethods)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return replaceMethods.stream()
|
||||
.map(method -> new WebDictionaryValueInfo(method.getId(), method.getLabel(), method.getDescription()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the connection's database supports bulk load during data import
|
||||
*/
|
||||
@Property
|
||||
public boolean isSupportsBulkLoad() {
|
||||
DBPDataSource dataSource = dataSourceContainer.getDataSource();
|
||||
return dataSource != null && DBUtils.getAdapter(DBSDataBulkLoader.class, dataSource) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the connection's database supports transaction
|
||||
*/
|
||||
@Property
|
||||
public boolean isSupportsTransactions() {
|
||||
DBPDataSource dataSource = dataSourceContainer.getDataSource();
|
||||
return dataSource != null && dataSource.getInfo().supportsTransactions();
|
||||
}
|
||||
}
|
||||
+43
-3
@@ -1,3 +1,19 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.model;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
@@ -5,6 +21,7 @@ import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.model.DBPObject;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriverConstants;
|
||||
import org.jkiss.dbeaver.model.meta.IPropertyValueListProvider;
|
||||
import org.jkiss.dbeaver.model.meta.IPropertyValueValidator;
|
||||
import org.jkiss.dbeaver.model.meta.Property;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
@@ -15,6 +32,7 @@ import org.jkiss.utils.CommonUtils;
|
||||
public class WebExpertSettingsProperties implements DBPObject {
|
||||
public static final String PROP_READ_ONLY = "readOnly";
|
||||
public static final String PROP_AUTO_COMMIT = "autocommit";
|
||||
public static final String PROP_AUTO_COMMIT_MODE = "autoCommitMode";
|
||||
public static final String PROP_KEEP_ALIVE_INTERVAL = "keepAliveInterval";
|
||||
public static final String PROP_DEFAULT_CATALOG = "defaultCatalogName";
|
||||
public static final String PROP_DEFAULT_SCHEMA = "defaultSchemaName";
|
||||
@@ -30,9 +48,15 @@ public class WebExpertSettingsProperties implements DBPObject {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Property(order = 2, id = PROP_AUTO_COMMIT, visibleIf = AutoCommitFieldValidator.class)
|
||||
public boolean isAutoCommit() {
|
||||
return true;
|
||||
@NotNull
|
||||
@Property(
|
||||
order = 2,
|
||||
id = PROP_AUTO_COMMIT_MODE,
|
||||
visibleIf = AutoCommitFieldValidator.class,
|
||||
listProvider = AutoCommitListProvider.class
|
||||
)
|
||||
public AutoCommitMode isAutoCommitMode() {
|
||||
return AutoCommitMode.DEFAULT;
|
||||
}
|
||||
|
||||
@Property(order = 3, id = PROP_READ_ONLY, visibleIf = ReadOnlyFieldValidator.class)
|
||||
@@ -40,11 +64,13 @@ public class WebExpertSettingsProperties implements DBPObject {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Property(order = 4, id = PROP_DEFAULT_CATALOG, visibleIf = DefaultCatalogFieldVisibleValidator.class)
|
||||
public String getDefaultCatalog() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Property(order = 5, id = PROP_DEFAULT_SCHEMA, visibleIf = DefaultSchemaFieldVisibleValidator.class)
|
||||
public String getDefaultSchema() {
|
||||
return null;
|
||||
@@ -85,4 +111,18 @@ public class WebExpertSettingsProperties implements DBPObject {
|
||||
return CommonUtils.toBoolean(object.driver.getDriverParameter(DBPDriverConstants.PARAM_SUPPORTS_SCHEMA_SELECTION), true);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AutoCommitListProvider implements IPropertyValueListProvider<WebExpertSettingsProperties> {
|
||||
|
||||
@Override
|
||||
public boolean allowCustomValue() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object[] getPossibleValues(@Nullable WebExpertSettingsProperties object) {
|
||||
return AutoCommitMode.values();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-27
@@ -37,7 +37,6 @@ import org.jkiss.dbeaver.model.impl.app.BaseApplicationImpl;
|
||||
import org.jkiss.dbeaver.model.impl.app.BaseWorkspaceImpl;
|
||||
import org.jkiss.dbeaver.model.rm.RMController;
|
||||
import org.jkiss.dbeaver.model.secret.DBSSecretController;
|
||||
import org.jkiss.dbeaver.model.websocket.event.WSEventController;
|
||||
import org.jkiss.dbeaver.runtime.IVariableResolver;
|
||||
import org.jkiss.dbeaver.utils.GeneralUtils;
|
||||
import org.jkiss.dbeaver.utils.RuntimeUtils;
|
||||
@@ -89,12 +88,6 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
|
||||
return new SharedFileLockManager(getApplicationInstanceId());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Path getDefaultWorkingFolder() {
|
||||
return getServerConfigurationController().getWorkspacePath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHeadlessMode() {
|
||||
return true;
|
||||
@@ -192,20 +185,20 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void patchConfigurationWithProperties(
|
||||
Map<String, Object> configProps, IVariableResolver varResolver
|
||||
@NotNull Map<String, Object> configProps,
|
||||
@NotNull IVariableResolver varResolver
|
||||
) {
|
||||
for (Map.Entry<String, Object> entry : configProps.entrySet()) {
|
||||
Object propValue = entry.getValue();
|
||||
if (propValue instanceof String) {
|
||||
entry.setValue(GeneralUtils.replaceVariables((String) propValue, varResolver));
|
||||
if (propValue instanceof String strValue) {
|
||||
entry.setValue(GeneralUtils.replaceVariables(strValue, varResolver));
|
||||
} else if (propValue instanceof Map) {
|
||||
patchConfigurationWithProperties((Map<String, Object>) propValue, varResolver);
|
||||
} else if (propValue instanceof List) {
|
||||
List value = (List) propValue;
|
||||
} else if (propValue instanceof List value) {
|
||||
for (int i = 0; i < value.size(); i++) {
|
||||
Object colItem = value.get(i);
|
||||
if (colItem instanceof String) {
|
||||
value.set(i, GeneralUtils.replaceVariables((String) colItem, varResolver));
|
||||
if (colItem instanceof String strItem) {
|
||||
value.set(i, GeneralUtils.replaceVariables(strItem, varResolver));
|
||||
} else if (colItem instanceof Map) {
|
||||
patchConfigurationWithProperties((Map<String, Object>) colItem, varResolver);
|
||||
}
|
||||
@@ -218,6 +211,8 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
|
||||
@Override
|
||||
public Object start(IApplicationContext context) {
|
||||
initializeApplicationServices();
|
||||
setWorkspacePath(getServerConfigurationController().getWorkspacePath());
|
||||
|
||||
try {
|
||||
startServer();
|
||||
} catch (Exception e) {
|
||||
@@ -255,27 +250,17 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Path getWorkspaceDirectory() {
|
||||
return getServerConfigurationController().getWorkspacePath();
|
||||
}
|
||||
|
||||
|
||||
public String getApplicationId() {
|
||||
try {
|
||||
return getApplicationInstanceId();
|
||||
} catch (DBException e) {
|
||||
return null;
|
||||
// Fallback to hash code
|
||||
return getClass().getSimpleName() + "#" + hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public WSEventController getEventController() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public abstract ServletServerConfigurationController getServerConfigurationController();
|
||||
public abstract ServletServerConfigurationController<?> getServerConfigurationController();
|
||||
|
||||
@Override
|
||||
public boolean isEnvironmentVariablesAccessible() {
|
||||
|
||||
+1
-4
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -53,9 +53,6 @@ public interface ServletApplication extends DBPApplication {
|
||||
@NotNull
|
||||
Path getDataDirectory(boolean create);
|
||||
|
||||
@NotNull
|
||||
Path getWorkspaceDirectory();
|
||||
|
||||
@NotNull
|
||||
Path getHomeDirectory();
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,6 +23,7 @@ import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
|
||||
public interface ServletAuthApplication extends ServletApplication {
|
||||
@NotNull
|
||||
ServletAuthConfiguration getAuthConfiguration();
|
||||
|
||||
@Nullable
|
||||
|
||||
+3
-5
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -67,8 +67,7 @@ public abstract class BaseLocalResourceController implements RMController {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RMProject getProject(@NotNull String projectId, boolean readResources, boolean readProperties)
|
||||
throws DBException {
|
||||
public RMProject getProject(@NotNull String projectId, boolean readResources, boolean readProperties) throws DBException {
|
||||
RMProject project = makeProjectFromId(projectId, true);
|
||||
if (project == null) {
|
||||
return null;
|
||||
@@ -110,8 +109,7 @@ public abstract class BaseLocalResourceController implements RMController {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds)
|
||||
throws DBException {
|
||||
public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds) throws DBException {
|
||||
DBPProject projectMetadata = getWebProject(projectId, false);
|
||||
return doFileReadOperation(
|
||||
projectId,
|
||||
|
||||
+44
-4
@@ -40,6 +40,7 @@ import org.jkiss.dbeaver.model.fs.lock.LockTarget;
|
||||
import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl;
|
||||
import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNLocalFolder;
|
||||
import org.jkiss.dbeaver.model.net.DBWNetworkProfile;
|
||||
import org.jkiss.dbeaver.model.rm.*;
|
||||
import org.jkiss.dbeaver.model.security.SMAdminController;
|
||||
import org.jkiss.dbeaver.model.sql.DBQuotaException;
|
||||
@@ -48,6 +49,7 @@ import org.jkiss.dbeaver.model.websocket.event.WSSessionLogUpdatedEvent;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSNetworkProfileEvent;
|
||||
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
|
||||
import org.jkiss.dbeaver.registry.DataSourceParseResults;
|
||||
import org.jkiss.dbeaver.registry.ResourceTypeDescriptor;
|
||||
@@ -407,8 +409,13 @@ public class LocalResourceController extends BaseLocalResourceController {
|
||||
registry::createDataSource
|
||||
)
|
||||
);
|
||||
Map<String, DBWNetworkProfile> oldNetworkProfiles = registry.getNetworkProfiles().getProfiles().stream()
|
||||
.collect(Collectors.toMap(
|
||||
DBWNetworkProfile::getProfileId,
|
||||
p -> p
|
||||
));
|
||||
DataSourceParseResults parseResults = super.updateProjectDataSourcesConfig(projectId, configuration, dataSourceIds);
|
||||
sendDataSourcesConfigUpdatedEvent(registry, oldDataSources, parseResults);
|
||||
sendConfigUpdatedEvent(registry, oldDataSources, oldNetworkProfiles, parseResults);
|
||||
return parseResults != null;
|
||||
}
|
||||
|
||||
@@ -501,14 +508,48 @@ public class LocalResourceController extends BaseLocalResourceController {
|
||||
return DBNLocalFolder.makeLocalFolderItemPath(projectId, folderPath);
|
||||
}
|
||||
|
||||
private void sendDataSourcesConfigUpdatedEvent(
|
||||
private void sendConfigUpdatedEvent(
|
||||
@NotNull DBPDataSourceRegistry registry,
|
||||
@NotNull Map<String, DataSourceDescriptor> oldDataSources,
|
||||
@NotNull Map<String, DBWNetworkProfile> oldNetworkProfiles,
|
||||
@Nullable DataSourceParseResults parseResults
|
||||
) {
|
||||
if (parseResults == null || credentialsProvider.getActiveUserCredentials() == null || oldDataSources.isEmpty()) {
|
||||
if (parseResults == null || credentialsProvider.getActiveUserCredentials() == null) {
|
||||
return;
|
||||
}
|
||||
boolean profilesChanged = !parseResults.removedProfiles.isEmpty();
|
||||
|
||||
// We don't need to check for updated profiles if there are already added or removed profiles
|
||||
if (!profilesChanged) {
|
||||
for (DBWNetworkProfile updatedProfile : parseResults.updatedProfiles) {
|
||||
if (oldNetworkProfiles.containsKey(updatedProfile.getProfileId())) {
|
||||
DBWNetworkProfile oldProfile = oldNetworkProfiles.get(updatedProfile.getProfileId());
|
||||
if (!oldProfile.equalConfigurations(updatedProfile)) {
|
||||
profilesChanged = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// profile was added
|
||||
profilesChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (profilesChanged) {
|
||||
ServletAppUtils.getServletApplication().getEventController().addEvent(
|
||||
WSNetworkProfileEvent.update(
|
||||
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
|
||||
credentialsProvider.getActiveUserCredentials().getUserId(),
|
||||
registry.getProject().getId()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (oldDataSources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> updatedConfigurationDataSourceIds = new ArrayList<>();
|
||||
List<String> updatedNameDataSourceIds = new ArrayList<>();
|
||||
List<String> updatedInternalConfigurationDataSourceIds = new ArrayList<>();
|
||||
@@ -612,7 +653,6 @@ public class LocalResourceController extends BaseLocalResourceController {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
+10
@@ -123,6 +123,16 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
}
|
||||
}
|
||||
|
||||
public void migrateEventHandlersTo(@NotNull BaseWebSession target) {
|
||||
synchronized (sessionEventHandlers) {
|
||||
for (CBWebSessionEventHandler handler : sessionEventHandlers) {
|
||||
handler.migrateToSession(target);
|
||||
target.addEventHandler(handler);
|
||||
}
|
||||
sessionEventHandlers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBException {
|
||||
return userContext.refresh(smAuthInfo);
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -68,6 +68,13 @@ public class WebSessionEventsFilter {
|
||||
}
|
||||
}
|
||||
|
||||
public void migrateTo(@NotNull WebSessionEventsFilter target) {
|
||||
synchronized (target.subscribedProjectIds) {
|
||||
target.subscribedEventTopics.addAll(this.subscribedEventTopics);
|
||||
target.subscribedProjectIds.addAll(this.subscribedProjectIds);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEventAllowed(WSEvent event) {
|
||||
if (event.isForceProcessed()) {
|
||||
return true;
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ public class ServerGlobalWorkspace extends BaseWorkspaceImpl {
|
||||
@NotNull DBPPlatform platform,
|
||||
@NotNull ServletApplication application
|
||||
) {
|
||||
super(platform, application.getWorkspaceDirectory());
|
||||
super(platform, application.getWorkspacePath());
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,7 +30,7 @@ public interface DBWServiceBindingServlet<APPLICATION extends ServletApplication
|
||||
return true;
|
||||
}
|
||||
|
||||
void addServlets(APPLICATION application, DBWServletContext servletContext) throws DBException;
|
||||
void addServlets(@NotNull APPLICATION application, @NotNull DBWServletContext servletContext) throws DBException;
|
||||
|
||||
@NotNull
|
||||
default List<String> getExcludedServletPaths(@NotNull APPLICATION application) {
|
||||
|
||||
@@ -192,13 +192,16 @@ public class ServletAppUtils {
|
||||
@NotNull String serviceId,
|
||||
@NotNull String origin
|
||||
) throws DBException {
|
||||
String finalOrigin = webAuthApplication.modifyOrigin(origin);
|
||||
String finalOrigin = ServletAppUtils.substringBeforeRootURI(webAuthApplication.modifyOrigin(origin));
|
||||
String serviceUriSegment = removeSideSlashes(webAuthApplication.getAuthServiceUriSegment());
|
||||
String rootUri = removeSideSlashes(getServletApplication().getRootURI());
|
||||
if (finalOrigin.endsWith("/" + rootUri) && serviceUriSegment.startsWith(rootUri + "/")) {
|
||||
finalOrigin = finalOrigin.substring(0, finalOrigin.length() - rootUri.length());
|
||||
}
|
||||
StringBuilder authUriBuilder = new StringBuilder(removeSideSlashes(finalOrigin));
|
||||
if (CommonUtils.isNotEmpty(rootUri)) {
|
||||
authUriBuilder.append("/").append(rootUri);
|
||||
}
|
||||
if (CommonUtils.isNotEmpty(serviceUriSegment)) {
|
||||
authUriBuilder.append("/").append(serviceUriSegment);
|
||||
}
|
||||
@@ -297,12 +300,21 @@ public class ServletAppUtils {
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String getHostOriginFromRequest(@NotNull HttpServletRequest request) {
|
||||
return substringBeforeRootURI(getOriginFromRequest(request));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String getOriginFromRequest(@NotNull HttpServletRequest request) {
|
||||
String origin = request.getHeader(HEADER_ORIGIN);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Origin header: " + origin);
|
||||
}
|
||||
if ("null".equals(origin)) {
|
||||
//strange identity provider bug
|
||||
origin = null;
|
||||
}
|
||||
if (CommonUtils.isEmpty(origin)) {
|
||||
origin = request.getHeader(HEADER_X_ORIGIN);
|
||||
if (log.isTraceEnabled()) {
|
||||
|
||||
+20
-6
@@ -33,6 +33,7 @@ import org.jkiss.dbeaver.model.access.DBAAuthCredentials;
|
||||
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionType;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNativeCredentials;
|
||||
@@ -40,6 +41,7 @@ import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration;
|
||||
import org.jkiss.dbeaver.model.net.ssh.SSHConstants;
|
||||
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceDisconnectEvent;
|
||||
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
|
||||
import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor;
|
||||
import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
@@ -55,9 +57,12 @@ public class WebDataSourceUtils {
|
||||
private WebDataSourceUtils() {
|
||||
}
|
||||
|
||||
public static void saveCredentialsInDataSource(WebConnectionInfo webConnectionInfo, DBPDataSourceContainer dataSourceContainer, DBPConnectionConfiguration configuration) {
|
||||
public static void saveCredentialsInDataSource(
|
||||
@NotNull WebConnectionInfo webConnectionInfo,
|
||||
@NotNull DBPDataSourceContainer dataSourceContainer,
|
||||
@NotNull DBPConnectionConfiguration configuration
|
||||
) {
|
||||
// Properties passed from web
|
||||
// webConnectionInfo may be null in some cases (e.g. connection test when no actual connection exist yet)
|
||||
Map<String, Object> authProperties = webConnectionInfo.getSavedAuthProperties();
|
||||
if (authProperties != null) {
|
||||
authProperties.forEach((s, o) -> configuration.setAuthProperty(s, CommonUtils.toString(o)));
|
||||
@@ -305,9 +310,7 @@ public class WebDataSourceUtils {
|
||||
if (config.getKeepAliveInterval() >= 0) {
|
||||
dsConfig.setKeepAliveInterval(config.getKeepAliveInterval());
|
||||
}
|
||||
if (config.isDefaultAutoCommit() != null) {
|
||||
dsConfig.getBootstrap().setDefaultAutoCommit(config.isDefaultAutoCommit());
|
||||
}
|
||||
dsConfig.getBootstrap().setDefaultAutoCommit(config.isDefaultAutoCommit());
|
||||
dsConfig.getBootstrap().setDefaultCatalogName(config.getDefaultCatalogName());
|
||||
dsConfig.getBootstrap().setDefaultSchemaName(config.getDefaultSchemaName());
|
||||
// Save provider props
|
||||
@@ -320,8 +323,19 @@ public class WebDataSourceUtils {
|
||||
if (config.getConfigurationType() != null) {
|
||||
dsConfig.setConfigurationType(config.getConfigurationType());
|
||||
}
|
||||
if (config.getConnectionType() != null) {
|
||||
DBPConnectionType connectionType = DataSourceProviderRegistry.getInstance().getConnectionType(config.getConnectionType(), null);
|
||||
if (connectionType != null) {
|
||||
dsConfig.setConnectionType(connectionType);
|
||||
}
|
||||
}
|
||||
|
||||
if (CommonUtils.isEmpty(config.getUrl())) {
|
||||
dsConfig.setUrl(driver.getConnectionURL(dsConfig));
|
||||
try {
|
||||
dsConfig.setUrl(driver.getConnectionURL(dsConfig));
|
||||
} catch (DBException e) {
|
||||
log.error("Error preparing connection URL", e);
|
||||
}
|
||||
}
|
||||
// Save network handlers
|
||||
if (config.getNetworkHandlersConfig() != null) {
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,8 @@
|
||||
*/
|
||||
package io.cloudbeaver.websocket;
|
||||
|
||||
import io.cloudbeaver.model.session.BaseWebSession;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
|
||||
|
||||
@@ -24,4 +26,6 @@ public interface CBWebSessionEventHandler {
|
||||
|
||||
void close();
|
||||
|
||||
void migrateToSession(@NotNull BaseWebSession newSession);
|
||||
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Community Product
|
||||
Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true
|
||||
Bundle-Version: 26.1.2.qualifier
|
||||
Bundle-Release-Date: 20260706
|
||||
Bundle-Version: 26.1.4.qualifier
|
||||
Bundle-Release-Date: 20260803
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server.ce
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
</product>
|
||||
</extension>
|
||||
|
||||
<extension point="io.cloudbeaver.feature">
|
||||
<feature id="ai" label="AI Integration" description="Enables AI Chat in the application."/>
|
||||
</extension>
|
||||
|
||||
<extension point="org.jkiss.dbeaver.application">
|
||||
<application
|
||||
id="cloudbeaver-ce"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.product.ce</artifactId>
|
||||
<version>26.1.2-SNAPSHOT</version>
|
||||
<version>26.1.4-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -2,8 +2,8 @@ Manifest-Version: 1.0
|
||||
Bundle-ManifestVersion: 2
|
||||
Bundle-Name: Cloudbeaver Base JDBC drivers
|
||||
Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true
|
||||
Bundle-Version: 1.0.149.qualifier
|
||||
Bundle-Release-Date: 20260706
|
||||
Bundle-Version: 1.0.151.qualifier
|
||||
Bundle-Release-Date: 20260803
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Automatic-Module-Name: io.cloudbeaver.resources.drivers.base
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.resources.drivers.base</artifactId>
|
||||
<version>1.0.149-SNAPSHOT</version>
|
||||
<version>1.0.151-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
</project>
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: CloudBeaver Community
|
||||
Bundle-SymbolicName: io.cloudbeaver.server.ce;singleton:=true
|
||||
Bundle-Version: 26.1.2.qualifier
|
||||
Bundle-Release-Date: 20260706
|
||||
Bundle-Version: 26.1.4.qualifier
|
||||
Bundle-Release-Date: 20260803
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.server.ce</artifactId>
|
||||
<version>26.1.2-SNAPSHOT</version>
|
||||
<version>26.1.4-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+3
-3
@@ -14,11 +14,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.service.admin;
|
||||
package io.cloudbeaver.model.config;
|
||||
|
||||
import io.cloudbeaver.model.config.CBAppConfig;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.server.CBConstants;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.data.json.JSONUtils;
|
||||
@@ -57,7 +57,7 @@ public class AdminServerConfig {
|
||||
@Nullable
|
||||
private String bindSessionToIp;
|
||||
|
||||
public AdminServerConfig(Map<String, Object> params) {
|
||||
public AdminServerConfig(@NotNull Map<String, Object> params) {
|
||||
this.serverName = JSONUtils.getString(params, "serverName");
|
||||
this.adminName = JSONUtils.getString(params, "adminName");
|
||||
this.adminPassword = JSONUtils.getString(params, "adminPassword");
|
||||
+5
-3
@@ -181,6 +181,7 @@ public abstract class CBApplication<T extends CBServerConfig>
|
||||
return getServerConfigurationController().getServerConfiguration();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ServletAuthConfiguration getAuthConfiguration() {
|
||||
return getAppConfiguration();
|
||||
@@ -228,11 +229,11 @@ public abstract class CBApplication<T extends CBServerConfig>
|
||||
Location instanceLoc = Platform.getInstanceLocation();
|
||||
try {
|
||||
if (!instanceLoc.isSet()) { // always false?
|
||||
URL wsLocationURL = getWorkspaceDirectory().toUri().toURL();
|
||||
URL wsLocationURL = getWorkspacePath().toUri().toURL();
|
||||
instanceLoc.set(wsLocationURL, true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error setting workspace location to " + getWorkspaceDirectory().toAbsolutePath(), e);
|
||||
log.error("Error setting workspace location to " + getWorkspacePath().toAbsolutePath(), e);
|
||||
return;
|
||||
}
|
||||
this.systemInformationCollector = createSystemInformationCollector();
|
||||
@@ -443,7 +444,7 @@ public abstract class CBApplication<T extends CBServerConfig>
|
||||
|
||||
@NotNull
|
||||
public Path getDataDirectory(boolean create) {
|
||||
Path dataDir = getWorkspaceDirectory().resolve(CBConstants.RUNTIME_DATA_DIR_NAME);
|
||||
Path dataDir = getWorkspacePath().resolve(CBConstants.RUNTIME_DATA_DIR_NAME);
|
||||
if (create && !Files.exists(dataDir)) {
|
||||
try {
|
||||
Files.createDirectories(dataDir);
|
||||
@@ -751,6 +752,7 @@ public abstract class CBApplication<T extends CBServerConfig>
|
||||
eventController.addEvent(new WSServerConfigurationChangedEvent(sessionId, null));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public abstract CBServerConfigurationController<T> getServerConfigurationController();
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ public class CBApplicationCE extends CBApplication<CBServerConfig> {
|
||||
return new LocalFileController(DBWorkbench.getPlatform().getWorkspace().getAbsolutePath().resolve(DBFileController.DATA_FOLDER));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public CBServerConfigurationControllerEmbedded<CBServerConfig> getServerConfigurationController() {
|
||||
return serverConfigController;
|
||||
|
||||
+7
-6
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,10 +20,7 @@ import com.google.gson.*;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import io.cloudbeaver.model.app.BaseServerConfigurationController;
|
||||
import io.cloudbeaver.model.app.BaseServletApplication;
|
||||
import io.cloudbeaver.model.config.CBAppConfig;
|
||||
import io.cloudbeaver.model.config.CBServerConfig;
|
||||
import io.cloudbeaver.model.config.PasswordPolicyConfiguration;
|
||||
import io.cloudbeaver.model.config.SMControllerConfiguration;
|
||||
import io.cloudbeaver.model.config.*;
|
||||
import io.cloudbeaver.utils.ServletAppUtils;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
@@ -374,6 +371,7 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
|
||||
try (Writer out = new OutputStreamWriter(Files.newOutputStream(runtimeConfigPath), StandardCharsets.UTF_8)) {
|
||||
Gson gson = new GsonBuilder()
|
||||
.setStrictness(Strictness.LENIENT)
|
||||
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
gson.toJson(configurationProperties, out);
|
||||
@@ -508,7 +506,7 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
|
||||
@NotNull
|
||||
protected Map<String, Object> collectServerConfigProperties(
|
||||
@NotNull CBServerConfig serverConfig,
|
||||
Map<String, Object> originServerConfig
|
||||
@NotNull Map<String, Object> originServerConfig
|
||||
) {
|
||||
var serverConfigProperties = new LinkedHashMap<String, Object>();
|
||||
if (!CommonUtils.isEmpty(serverConfig.getServerName())) {
|
||||
@@ -633,6 +631,9 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
|
||||
return serverConfiguration;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public abstract CBServerConfigurationMapper<T, ? extends AdminServerConfig> createServerConfigurationInputMapper();
|
||||
|
||||
public CBAppConfig getAppConfiguration() {
|
||||
return appConfiguration;
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,7 @@ package io.cloudbeaver.server;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.InstanceCreator;
|
||||
import io.cloudbeaver.model.config.AdminServerConfig;
|
||||
import io.cloudbeaver.model.config.CBServerConfig;
|
||||
import io.cloudbeaver.model.config.WebDatabaseConfig;
|
||||
import org.jkiss.code.NotNull;
|
||||
@@ -41,6 +42,11 @@ public class CBServerConfigurationControllerEmbedded<T extends CBServerConfig> e
|
||||
super(serverConfig, homeDirectory);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CBServerConfigurationMapper<T, ? extends AdminServerConfig> createServerConfigurationInputMapper() {
|
||||
return new CBServerConfigurationMapper<>();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected Map<String, Object> collectServerConfigProperties(
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.server;
|
||||
|
||||
import io.cloudbeaver.DBWFeatureSet;
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.model.config.AdminServerConfig;
|
||||
import io.cloudbeaver.model.config.CBAppConfig;
|
||||
import io.cloudbeaver.model.config.CBServerConfig;
|
||||
import io.cloudbeaver.utils.ServletAppUtils;
|
||||
import io.cloudbeaver.utils.WebDataSourceUtils;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class CBServerConfigurationMapper<C extends CBServerConfig, I extends AdminServerConfig> {
|
||||
private static final Log log = Log.getLog(CBServerConfigurationMapper.class);
|
||||
|
||||
@NotNull
|
||||
public CBServerConfigurations loadServerConfigurationsFromParams(@NotNull Map<String, Object> params) {
|
||||
CBAppConfig appConfig = new CBAppConfig(CBApplication.getInstance().getAppConfiguration());
|
||||
C serverConfig = createServerConfiguration();
|
||||
serverConfig.setServerName(CBApplication.getInstance().getServerName());
|
||||
serverConfig.setMaxSessionIdleTime(CBApplication.getInstance().getMaxSessionIdleTime());
|
||||
String adminName = null;
|
||||
String adminPassword = null;
|
||||
|
||||
if (!params.isEmpty()) { // FE can send an empty configuration
|
||||
I config = createInputConfiguration(params);
|
||||
adminName = config.getAdminName();
|
||||
adminPassword = config.getAdminPassword();
|
||||
|
||||
populateConfigurations(config, serverConfig, appConfig);
|
||||
}
|
||||
|
||||
return new CBServerConfigurations(
|
||||
serverConfig,
|
||||
appConfig,
|
||||
adminName,
|
||||
adminPassword
|
||||
);
|
||||
}
|
||||
|
||||
protected void populateConfigurations(@NotNull I input, @NotNull C serverConfig, @NotNull CBAppConfig appConfig) {
|
||||
appConfig.setAnonymousAccessEnabled(input.isAnonymousAccessEnabled());
|
||||
appConfig.setSupportsCustomConnections(input.isCustomConnectionsEnabled());
|
||||
appConfig.setPublicCredentialsSaveEnabled(input.isPublicCredentialsSaveEnabled());
|
||||
appConfig.setAdminCredentialsSaveEnabled(input.isAdminCredentialsSaveEnabled());
|
||||
updateDisabledFeaturesConfig(appConfig, input.getEnabledFeatures());
|
||||
// custom logic for enabling embedded drivers
|
||||
updateDisabledDriversConfig(appConfig, input.getDisabledDrivers());
|
||||
appConfig.setResourceManagerEnabled(input.isResourceManagerEnabled());
|
||||
appConfig.setSecretManagerEnabled(input.isSecretManagerEnabled());
|
||||
|
||||
if (CommonUtils.isEmpty(input.getEnabledAuthProviders())) {
|
||||
// All of them
|
||||
appConfig.setEnabledAuthProviders(new String[0]);
|
||||
} else {
|
||||
appConfig.setEnabledAuthProviders(input.getEnabledAuthProviders().toArray(new String[0]));
|
||||
}
|
||||
|
||||
appConfig.setDefaultNavigatorSettings(
|
||||
CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings());
|
||||
|
||||
serverConfig.setServerName(input.getServerName());
|
||||
serverConfig.setMaxSessionIdleTime(input.getSessionExpireTime());
|
||||
if (input.getForceHttps() != null) {
|
||||
serverConfig.setForceHttps(input.getForceHttps());
|
||||
}
|
||||
if (input.getSupportedHosts() != null) {
|
||||
serverConfig.setSupportedHosts(input.getSupportedHosts());
|
||||
}
|
||||
if (input.getBindSessionToIp() != null) {
|
||||
serverConfig.setBindSessionToIp(input.getBindSessionToIp());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisabledFeaturesConfig(@NotNull CBAppConfig appConfig, @NotNull List<String> enabledFeatures) {
|
||||
Set<String> enabledIds = new LinkedHashSet<>(enabledFeatures);
|
||||
appConfig.setEnabledFeatures(enabledFeatures.toArray(new String[0]));
|
||||
String[] disabledFeatures = ServletAppUtils.getServletApplication().getFeatureRegistry().getWebFeatures().stream()
|
||||
.map(DBWFeatureSet::getId)
|
||||
.filter(id -> !enabledIds.contains(id))
|
||||
.toArray(String[]::new);
|
||||
appConfig.setDisabledFeatures(disabledFeatures);
|
||||
}
|
||||
|
||||
// we disable embedded drivers by default and enable it in enabled drivers list
|
||||
// that's why we need so complicated logic for disabling drivers
|
||||
|
||||
private void updateDisabledDriversConfig(@NotNull CBAppConfig appConfig, @NotNull String[] disabledDriversConfig) {
|
||||
Set<String> disabledIds = new LinkedHashSet<>(Arrays.asList(disabledDriversConfig));
|
||||
Set<String> enabledIds = new LinkedHashSet<>(Arrays.asList(appConfig.getEnabledDrivers()));
|
||||
|
||||
// remove all disabled embedded drivers from enabled drivers list
|
||||
enabledIds.removeAll(disabledIds);
|
||||
|
||||
// enable embedded driver if it is not in disabled drivers list
|
||||
for (String driverId : appConfig.getDisabledDrivers()) {
|
||||
if (disabledIds.contains(driverId)) {
|
||||
// driver is also disabled
|
||||
continue;
|
||||
}
|
||||
// driver is removed from disabled list
|
||||
// we need to enable if it is embedded
|
||||
try {
|
||||
DBPDriver driver = WebDataSourceUtils.getDriverById(driverId);
|
||||
if (driver.isEmbedded()) {
|
||||
enabledIds.add(driverId);
|
||||
}
|
||||
} catch (DBWebException e) {
|
||||
log.error("Failed to find driver by id", e);
|
||||
}
|
||||
}
|
||||
appConfig.setDisabledDrivers(disabledDriversConfig);
|
||||
appConfig.setEnabledDrivers(enabledIds.toArray(String[]::new));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected C createServerConfiguration() {
|
||||
return (C) new CBServerConfig();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected I createInputConfiguration(@NotNull Map<String, Object> params) {
|
||||
return (I) new AdminServerConfig(params);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.server;
|
||||
|
||||
import io.cloudbeaver.model.config.CBAppConfig;
|
||||
import io.cloudbeaver.model.config.CBServerConfig;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
|
||||
public record CBServerConfigurations(
|
||||
@NotNull CBServerConfig serverConfig,
|
||||
@NotNull CBAppConfig appConfig,
|
||||
@Nullable String adminName,
|
||||
@Nullable String adminPassword
|
||||
) {
|
||||
}
|
||||
+40
-4
@@ -202,6 +202,43 @@ public class CBSessionManager implements WebAppSessionManager {
|
||||
return httpSession.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the current HTTP session, creates a new one, and binds a new {@link WebSession} to it.
|
||||
*/
|
||||
@NotNull
|
||||
public WebSession rotateSession(
|
||||
@NotNull HttpServletRequest request,
|
||||
@NotNull WebSession oldWebSession
|
||||
) throws DBWebException {
|
||||
HttpSession oldHttpSession = request.getSession(false);
|
||||
if (oldHttpSession != null) {
|
||||
oldHttpSession.invalidate();
|
||||
}
|
||||
String newSessionId = request.getSession(true).getId();
|
||||
|
||||
String locale = oldWebSession.getLocale();
|
||||
String remoteAddr = oldWebSession.getLastRemoteAddr();
|
||||
String remoteUserAgent = oldWebSession.getLastRemoteUserAgent();
|
||||
var requestInfo = new WebHttpRequestInfo(newSessionId, locale, remoteAddr, remoteUserAgent);
|
||||
WebSession newWebSession;
|
||||
try {
|
||||
newWebSession = createWebSessionImpl(requestInfo);
|
||||
} catch (DBException e) {
|
||||
throw new DBWebException(e);
|
||||
}
|
||||
oldWebSession.migrateEventHandlersTo(newWebSession);
|
||||
oldWebSession.getEventsFilter().migrateTo(newWebSession.getEventsFilter());
|
||||
String oldSessionId = oldWebSession.getSessionId();
|
||||
synchronized (sessionMap) {
|
||||
sessionMap.remove(oldSessionId);
|
||||
sessionMap.put(newSessionId, newWebSession);
|
||||
}
|
||||
oldWebSession.close(false, false);
|
||||
|
||||
log.debug("Session rotated '" + oldSessionId + "' -> '" + newSessionId + "'");
|
||||
return newWebSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns not expired session from cache, or restore it.
|
||||
*
|
||||
@@ -288,13 +325,12 @@ public class CBSessionManager implements WebAppSessionManager {
|
||||
@Nullable
|
||||
public WebSession findWebSession(@NotNull HttpServletRequest request) {
|
||||
String sessionId = getSessionId(request);
|
||||
WebSession webSession;
|
||||
synchronized (sessionMap) {
|
||||
var session = sessionMap.get(sessionId);
|
||||
if (session instanceof WebSession) {
|
||||
return (WebSession) session;
|
||||
}
|
||||
return null;
|
||||
webSession = (session instanceof WebSession) ? (WebSession) session : null;
|
||||
}
|
||||
return webSession;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Server
|
||||
Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true
|
||||
Bundle-Version: 26.1.2.qualifier
|
||||
Bundle-Release-Date: 20260706
|
||||
Bundle-Version: 26.1.4.qualifier
|
||||
Bundle-Release-Date: 20260803
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: org.jkiss.dbeaver.model.sql;visibility:=reexport,
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
<eventHandler class="io.cloudbeaver.server.events.WSDataSourceUpdatedEventHandlerImpl">
|
||||
<topic id="cb_datasource"/>
|
||||
</eventHandler>
|
||||
<eventHandler class="io.cloudbeaver.server.events.WSNetworkProfileEventHandler">
|
||||
<topic id="cb_profiles"/>
|
||||
</eventHandler>
|
||||
<eventHandler class="io.cloudbeaver.server.events.WSFolderUpdatedEventHandlerImpl">
|
||||
<topic id="cb_datasource_folder"/>
|
||||
</eventHandler>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.server</artifactId>
|
||||
<version>26.1.2-SNAPSHOT</version>
|
||||
<version>26.1.4-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -570,6 +570,31 @@ type ConnectionInfo {
|
||||
|
||||
"List of tools that can be used with this connection. Returns empty list if no tools are available"
|
||||
tools: [String!]! @since(version: "24.1.3")
|
||||
|
||||
connectionType: ID! @since(version: "26.1.2")
|
||||
|
||||
"Driver configuration"
|
||||
driverConfiguration: DriverConfiguration! @since(version: "26.1.2")
|
||||
}
|
||||
|
||||
"Database-dependent metadata describing features supported by the driver"
|
||||
type DriverConfiguration {
|
||||
"Supported 'on duplicate key' replace methods for this connection's SQL dialect"
|
||||
supportedInsertReplaceMethods: [DictionaryValueInfo!]!
|
||||
"Whether the connection's database supports bulk load during data import"
|
||||
supportsBulkLoad: Boolean!
|
||||
"Whether the connection's database supports transactions"
|
||||
supportsTransactions: Boolean!
|
||||
}
|
||||
|
||||
"Generic dictionary value: an identifier with a human readable name and an optional description"
|
||||
type DictionaryValueInfo {
|
||||
"Value ID"
|
||||
id: ID!
|
||||
"Human readable value name"
|
||||
name: String!
|
||||
"Value description"
|
||||
description: String
|
||||
}
|
||||
|
||||
type ConnectionFolderInfo {
|
||||
@@ -633,6 +658,39 @@ type LogEntry {
|
||||
stackTrace: String
|
||||
}
|
||||
|
||||
type ConnectionType @since(version: "26.1.2") {
|
||||
"Unique identifier of the connection type"
|
||||
id: ID!
|
||||
"Human-readable name of the connection type"
|
||||
name: String!
|
||||
"Description of the connection type"
|
||||
description: String
|
||||
"Color of the connection type (for light theme)"
|
||||
colorLight: String!
|
||||
"Alternative color of the connection type (for dark theme)"
|
||||
colorDark: String
|
||||
"Is autocommit enabled for this connection type"
|
||||
autocommit: Boolean!
|
||||
"Confirm execution of queries for this connection type"
|
||||
confirmExecute: Boolean!
|
||||
"Confirm data change operations (INSERT, UPDATE, DELETE) for this connection type"
|
||||
confirmDataChange: Boolean!
|
||||
"Smart commit mode is enabled for this connection type"
|
||||
smartCommit: Boolean!
|
||||
"Recover smart commit mode is enabled for this connection type"
|
||||
smartCommitRecover: Boolean!
|
||||
"Auto-close transactions is enabled for this connection type"
|
||||
autoCloseTransactions: Boolean!
|
||||
"Auto-close idle transactions period in seconds for this connection type"
|
||||
closeIdleTransactionPeriod: Int!
|
||||
"Auto-close connections is enabled for this connection type"
|
||||
autoCloseConnections: Boolean!
|
||||
"Auto-close idle connections period in seconds for this connection type"
|
||||
closeIdleConnectionPeriod: Int!
|
||||
"Specifies whether this connection type is system-defined or user-defined. System-defined connection types cannot be deleted."
|
||||
predefined: Boolean!
|
||||
}
|
||||
|
||||
####################################################
|
||||
# Input types
|
||||
####################################################
|
||||
@@ -745,6 +803,8 @@ input ConnectionConfig {
|
||||
defaultSchemaName: String @since(version: "25.0.5") @deprecated(reason: "25.2.1 use expertPropertyValues instead")
|
||||
"Default user settings for connection"
|
||||
defaultUserPreferences: Object @since(version: "26.0.2")
|
||||
|
||||
connectionType: ID @since(version: "26.1.2")
|
||||
}
|
||||
|
||||
####################################################
|
||||
@@ -790,6 +850,8 @@ extend type Query {
|
||||
|
||||
"Returns connection driver properties based on configuration"
|
||||
listConnectionDriverProperties( projectId: ID!, config: ConnectionConfig! ): [ObjectPropertyInfo!]!
|
||||
|
||||
listConnectionTypes: [ConnectionType!]! @since(version: "26.1.2")
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
|
||||
@@ -31,6 +31,10 @@ enum CBServerEventId {
|
||||
|
||||
"Open URL request in client"
|
||||
cb_open_url @since(version: "25.3.1")
|
||||
"User-specific object settings updated"
|
||||
cb_object_settings_updated @since(version: "26.1.4")
|
||||
"User-specific object settings deleted"
|
||||
cb_object_settings_deleted @since(version: "26.1.4")
|
||||
}
|
||||
|
||||
"Events sent by client"
|
||||
@@ -57,6 +61,7 @@ enum CBEventTopic {
|
||||
cb_workspace_configuration @since(version: "25.1.1")
|
||||
cb_notification @since(version: "25.2.1")
|
||||
cb_session_action @since(version: "25.3.1")
|
||||
cb_object_settings @since(version: "26.1.4")
|
||||
}
|
||||
|
||||
"Base server event interface"
|
||||
@@ -178,6 +183,26 @@ type WSOpenUrlEvent implements CBServerEvent @since(version: "25.3.1") {
|
||||
actionId: String!
|
||||
}
|
||||
|
||||
"Type of the object that owns user-specific settings"
|
||||
enum WSObjectSettingsType @since(version: "26.1.4") {
|
||||
datasource
|
||||
project
|
||||
}
|
||||
|
||||
"User-specific object settings change event (e.g. navigator view)"
|
||||
type WSObjectSettingsEvent implements CBServerEvent @since(version: "26.1.4") {
|
||||
id: CBServerEventId!
|
||||
topicId: CBEventTopic!
|
||||
"Type of the object the settings belong to"
|
||||
smObjectType: WSObjectSettingsType!
|
||||
"Project of the object"
|
||||
projectId: String
|
||||
"Id of the object"
|
||||
objectId: String!
|
||||
"Ids of the settings that were changed"
|
||||
settingIds: [String!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
emptyEvent: Boolean
|
||||
}
|
||||
|
||||
@@ -68,19 +68,39 @@ input SQLDataFilter {
|
||||
limit: Int
|
||||
|
||||
constraints: [ SQLDataFilterConstraint ]
|
||||
"When true constraints are combined with OR instead of AND"
|
||||
anyConstraint: Boolean
|
||||
where: String
|
||||
orderBy: String
|
||||
}
|
||||
|
||||
type SQLResultReference @since(version: "26.1.1") {
|
||||
"Pair (source-row column index, target-entity column name) used to build a navigation filter"
|
||||
type SQLReferenceColumnMapping @since(version: "26.1.2") {
|
||||
"Position in the result-set bindings"
|
||||
sourceColumnIndex: Int!
|
||||
"Column name in the source result set"
|
||||
sourceColumnName: String!
|
||||
"Ordinal position of the column in the target entity definition"
|
||||
targetColumnIndex: Int!
|
||||
"Column name in the target entity"
|
||||
targetColumnName: String!
|
||||
}
|
||||
|
||||
type SQLResultAssociation @since(version: "26.1.2") {
|
||||
"True for reverse references (incoming FKs)"
|
||||
reference: Boolean!
|
||||
"Name of the association (foreign key)"
|
||||
associationName: String!
|
||||
"Fully qualified name of the entity"
|
||||
"Name of the catalog the target entity belongs to"
|
||||
targetCatalogName: String
|
||||
"Name of the schema the target entity belongs to"
|
||||
targetSchemaName: String
|
||||
"Plain name of the target entity"
|
||||
targetEntityName: String
|
||||
"Navigator node URI of the target entity, suitable for opening it directly in the UI. Null if the entity is not present in the navigator tree"
|
||||
nodePath: String
|
||||
"Indices of result-set columns that participate as the source-side attributes of this reference"
|
||||
columnIndexList: [Int!]!
|
||||
targetNodePath: String
|
||||
"List of column mappings between the source result set and the target entity"
|
||||
columnMapping: [SQLReferenceColumnMapping!]!
|
||||
}
|
||||
|
||||
type SQLResultColumn {
|
||||
@@ -133,11 +153,6 @@ type SQLResultSet {
|
||||
"Returns list of rows in the result set. Each row contains data and metadata"
|
||||
rowsWithMetaData: [SQLResultRowMetaData] @since(version: "23.3.5")
|
||||
|
||||
"Forward foreign keys navigable from this result set"
|
||||
associations: [SQLResultReference!]! @since(version: "26.1.1")
|
||||
"Reverse references (incoming FKs from other entities) navigable from this result set"
|
||||
references: [SQLResultReference!]! @since(version: "26.1.1")
|
||||
|
||||
# True means that resultset was generated by single entity query
|
||||
# New rows can be added, old rows can be deleted
|
||||
singleEntity: Boolean!
|
||||
@@ -281,6 +296,8 @@ type SQLQueryGenerator {
|
||||
input SQLQueryGeneratorOptions {
|
||||
useFullyQualifiedNames: Boolean!
|
||||
compactSql: Boolean!
|
||||
"Include nested objects, comments and permissions in generated DDL (since 26.0.4)"
|
||||
showFullDdl: Boolean
|
||||
}
|
||||
|
||||
####################################################
|
||||
@@ -328,6 +345,15 @@ type TransactionLogInfos {
|
||||
|
||||
extend type Query {
|
||||
|
||||
"Foreign-key navigation metadata for the given result set. Set isReference to true for reverse references, false for forward associations, or omit it to get both"
|
||||
sqlResultAssociations(
|
||||
projectId: ID,
|
||||
connectionId: ID!,
|
||||
contextId: ID!,
|
||||
resultsId: ID!,
|
||||
isReference: Boolean
|
||||
): [SQLResultAssociation!]! @since(version: "26.1.2")
|
||||
|
||||
"Returns SQL dialect info for the specified connection"
|
||||
sqlDialectInfo( projectId: ID, connectionId: ID! ): SQLDialectInfo
|
||||
|
||||
@@ -373,7 +399,7 @@ extend type Query {
|
||||
options: Object @deprecated(reason: "use generatorOptions (26.0.3)")
|
||||
nodePathList: [String!]!,
|
||||
generatorOptions: SQLQueryGeneratorOptions
|
||||
): String!
|
||||
): String! @deprecated(reason: "use asyncSqlGenerateEntityQuery")
|
||||
|
||||
"""
|
||||
Returns an auto-generated SQL query based on provided rows data.
|
||||
@@ -467,19 +493,6 @@ extend type Mutation {
|
||||
dataFormat: ResultDataFormat
|
||||
): AsyncTaskInfo!
|
||||
|
||||
"Creates async task for navigating a foreign-key reference exposed in SQLResultSet.references. Pass associationName, isReference and any one of the chosen reference's columnIndices (used to locate the source entity)."
|
||||
asyncSqlNavigateForeignKey(
|
||||
projectId: ID,
|
||||
connectionId: ID!,
|
||||
contextId: ID!,
|
||||
resultsId: ID!,
|
||||
row: SQLResultRow!,
|
||||
columnIndex: Int!,
|
||||
associationName: String!,
|
||||
isReference: Boolean!,
|
||||
dataFormat: ResultDataFormat
|
||||
): AsyncTaskInfo! @since(version: "26.1.1")
|
||||
|
||||
"Returns transaction log info for the specified project, connection and context"
|
||||
getTransactionLogInfo(
|
||||
projectId: ID!,
|
||||
@@ -582,6 +595,13 @@ extend type Mutation {
|
||||
"Returns row count for async SQL row data count task"
|
||||
asyncSqlRowDataCountResult(taskId: ID!): Int!
|
||||
|
||||
"Creates async task to generate entity SQL. Generated SQL is returned in the taskResult field"
|
||||
asyncSqlGenerateEntityQuery(
|
||||
generatorId: String!,
|
||||
nodePathList: [String!]!,
|
||||
generatorOptions: SQLQueryGeneratorOptions
|
||||
): AsyncTaskInfo!
|
||||
|
||||
"Reads dynamic trace from provided database results"
|
||||
sqlGetDynamicTrace(
|
||||
projectId: ID,
|
||||
|
||||
+22
-7
@@ -21,8 +21,10 @@ import io.cloudbeaver.WebServiceUtils;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.server.CBConstants;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.DBConstants;
|
||||
import org.jkiss.dbeaver.model.DatabaseURL;
|
||||
import org.jkiss.dbeaver.model.connection.*;
|
||||
import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNative;
|
||||
import org.jkiss.dbeaver.model.meta.Property;
|
||||
@@ -180,13 +182,26 @@ public class WebDatabaseDriverInfo {
|
||||
@NotNull
|
||||
@Property
|
||||
public WebPropertyInfo[] getDriverProperties() throws DBWebException {
|
||||
DBPConnectionConfiguration cfg = new DBPConnectionConfiguration();
|
||||
cfg.setUrl(CommonUtils.notEmpty(driver.getSampleURL()));
|
||||
cfg.setHostName(CBConstants.HOST_LOCALHOST);
|
||||
cfg.setHostPort(driver.getDefaultPort());
|
||||
cfg.setDatabaseName(driver.getDefaultDatabase());
|
||||
cfg.setUrl(driver.getConnectionURL(cfg));
|
||||
return WebServiceUtils.getDriverProperties(webSession, driver, null, cfg);
|
||||
try {
|
||||
DBPConnectionConfiguration cfg = new DBPConnectionConfiguration();
|
||||
cfg.setUrl(CommonUtils.notEmpty(driver.getSampleURL()));
|
||||
DatabaseURL.Pattern dbUrlPattern = DatabaseURL.getUrlPattern(CommonUtils.notEmpty(driver.getSampleURL()));
|
||||
for (String propName : dbUrlPattern.getMandatoryPropertyNames()) {
|
||||
switch (propName) {
|
||||
case DBConstants.PROP_HOST -> cfg.setHostName(CBConstants.HOST_LOCALHOST);
|
||||
case DBConstants.PROP_PORT -> cfg.setHostPort(CommonUtils.notNull(driver.getDefaultPort(), "0"));
|
||||
case DBConstants.PROP_SERVER -> cfg.setServerName(CommonUtils.notNull(driver.getDefaultServer(), propName));
|
||||
case DBConstants.PROP_USER -> cfg.setUserName(CommonUtils.notNull(driver.getDefaultUser(), propName));
|
||||
case DBConstants.PROP_DATABASE, DBConstants.PROP_FOLDER, DBConstants.PROP_FILE ->
|
||||
cfg.setDatabaseName(CommonUtils.notNull(driver.getDefaultDatabase(), propName));
|
||||
default -> log.debug("Unexpected mandatory URL property " + propName);
|
||||
}
|
||||
}
|
||||
cfg.setUrl(driver.getConnectionURL(cfg));
|
||||
return WebServiceUtils.getDriverProperties(webSession, driver, null, cfg);
|
||||
} catch (DBException e) {
|
||||
throw new DBWebException("Error obtaining driver properties", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Property
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,7 @@ package io.cloudbeaver.server.events;
|
||||
import io.cloudbeaver.server.BaseWebPlatform;
|
||||
import io.cloudbeaver.server.WebAppUtils;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor;
|
||||
import org.jkiss.dbeaver.model.websocket.WSEventHandler;
|
||||
@@ -33,7 +34,10 @@ public class WSDeleteTempFileHandler implements WSEventHandler<WSEventDeleteTemp
|
||||
|
||||
private static final Log log = Log.getLog(WSDeleteTempFileHandler.class);
|
||||
|
||||
public void resetTempFolder(String sessionId) {
|
||||
public void resetTempFolder(@Nullable String sessionId) {
|
||||
if (sessionId == null) {
|
||||
return;
|
||||
}
|
||||
Path path = WebAppUtils.getWebPlatform()
|
||||
.getTempFolder(new VoidProgressMonitor(), BaseWebPlatform.TEMP_FILE_FOLDER)
|
||||
.resolve(sessionId);
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.server.events;
|
||||
|
||||
import io.cloudbeaver.WebSessionProjectImpl;
|
||||
import io.cloudbeaver.model.session.BaseWebSession;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.websocket.event.datasource.WSNetworkProfileEvent;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
|
||||
public class WSNetworkProfileEventHandler extends WSDefaultEventHandler<WSNetworkProfileEvent> {
|
||||
private static final Log log = Log.getLog(WSNetworkProfileEventHandler.class);
|
||||
|
||||
@Override
|
||||
protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSNetworkProfileEvent event) {
|
||||
if (event.getProjectId() != null) {
|
||||
if (activeUserSession instanceof WebSession webSession) {
|
||||
WebSessionProjectImpl project = webSession.getProjectById(event.getProjectId());
|
||||
if (project == null) {
|
||||
log.debug("Project " + event.getProjectId() + " is not found in session " + webSession.getSessionId());
|
||||
return;
|
||||
}
|
||||
project.getDataSourceRegistry().getNetworkProfiles().reloadProfiles();
|
||||
} else {
|
||||
super.updateSessionData(activeUserSession, event);
|
||||
}
|
||||
} else {
|
||||
DBWorkbench.getPlatform().getNetworkProfiles().reloadProfiles();
|
||||
super.updateSessionData(activeUserSession, event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isAcceptableInSession(@NotNull BaseWebSession activeUserSession, @NotNull WSNetworkProfileEvent event) {
|
||||
if (event.getProjectId() != null && !activeUserSession.isProjectAccessible(event.getProjectId())) {
|
||||
return false;
|
||||
}
|
||||
return super.isAcceptableInSession(activeUserSession, event);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -33,12 +33,16 @@ public class CBClientEventProcessor {
|
||||
|
||||
private static final Log log = Log.getLog(CBClientEventProcessor.class);
|
||||
|
||||
final BaseWebSession webSession;
|
||||
volatile BaseWebSession webSession;
|
||||
|
||||
public CBClientEventProcessor(@NotNull BaseWebSession webSession) {
|
||||
this.webSession = webSession;
|
||||
}
|
||||
|
||||
void setWebSession(@NotNull BaseWebSession webSession) {
|
||||
this.webSession = webSession;
|
||||
}
|
||||
|
||||
public void process(@Nullable String message) {
|
||||
if (CommonUtils.isEmpty(message)) {
|
||||
return;
|
||||
|
||||
+17
-3
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -40,16 +40,21 @@ public class CBEventsLongPolling implements CBWebSessionEventHandler {
|
||||
|
||||
private static final int QUEUE_CAPACITY = 1000;
|
||||
|
||||
private final BaseWebSession webSession;
|
||||
private volatile BaseWebSession webSession;
|
||||
private final BlockingQueue<WSEvent> queue = new LinkedBlockingQueue<>(QUEUE_CAPACITY);
|
||||
private final CBClientEventProcessor processor;
|
||||
private volatile long lastPoll;
|
||||
private final CBEventsLongPollingServlet servlet;
|
||||
|
||||
public CBEventsLongPolling(@NotNull BaseWebSession webSession) {
|
||||
public CBEventsLongPolling(
|
||||
@NotNull BaseWebSession webSession,
|
||||
@NotNull CBEventsLongPollingServlet owner
|
||||
) {
|
||||
this.webSession = webSession;
|
||||
this.lastPoll = System.currentTimeMillis();
|
||||
this.webSession.addEventHandler(this);
|
||||
this.processor = new CBClientEventProcessor(this.webSession);
|
||||
this.servlet = owner;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -88,6 +93,14 @@ public class CBEventsLongPolling implements CBWebSessionEventHandler {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrateToSession(@NotNull BaseWebSession newSession) {
|
||||
String oldSid = this.webSession.getSessionId();
|
||||
this.webSession = newSession;
|
||||
this.processor.setWebSession(newSession);
|
||||
servlet.rekeySession(oldSid, newSession.getSessionId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleWebSessionEvent(@NotNull WSEvent event) {
|
||||
if (!queue.offer(event)) {
|
||||
@@ -106,6 +119,7 @@ public class CBEventsLongPolling implements CBWebSessionEventHandler {
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CBEventsLongPolling{" +
|
||||
|
||||
+16
-18
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -83,7 +83,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
|
||||
return;
|
||||
}
|
||||
|
||||
CBEventsLongPolling ps = getOrCreatePollSession(ws);
|
||||
CBEventsLongPolling ps = getOrCreatePollSession(ws, resp);
|
||||
ps.onPoll();
|
||||
|
||||
List<WSEvent> events = ps.pollEvents(POLL_TIMEOUT_SEC);
|
||||
@@ -111,7 +111,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
|
||||
return;
|
||||
}
|
||||
|
||||
CBEventsLongPolling ps = getOrCreatePollSession(ws);
|
||||
CBEventsLongPolling ps = getOrCreatePollSession(ws, resp);
|
||||
ps.onUserActivity();
|
||||
|
||||
String json = new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
@@ -163,18 +163,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
|
||||
) {
|
||||
|
||||
try {
|
||||
BaseWebSession ws = resolveSession(req);
|
||||
|
||||
resp.setHeader(WSConstants.WS_SESSION_HEADER, ws.getSessionId());
|
||||
ServletAppUtils.addResponseCookie(
|
||||
req,
|
||||
resp,
|
||||
CBConstants.CB_SESSION_COOKIE_NAME,
|
||||
ws.getSessionId(),
|
||||
-1
|
||||
);
|
||||
|
||||
return ws;
|
||||
return resolveSession(req);
|
||||
} catch (SMException e) {
|
||||
log.debug("LP request rejected: access token expired");
|
||||
sendError(resp, HttpConstants.CODE_TOKEN_EXPIRED, e.getMessage());
|
||||
@@ -214,7 +203,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private CBEventsLongPolling getOrCreatePollSession(@NotNull BaseWebSession ws) {
|
||||
private CBEventsLongPolling getOrCreatePollSession(@NotNull BaseWebSession ws, @NotNull HttpServletResponse resp) {
|
||||
final String sid = ws.getSessionId();
|
||||
|
||||
return sessions.compute(sid, (key, existing) -> {
|
||||
@@ -222,9 +211,10 @@ public class CBEventsLongPollingServlet extends HttpServlet {
|
||||
return existing;
|
||||
}
|
||||
|
||||
CBEventsLongPolling ps = new CBEventsLongPolling(ws);
|
||||
CBEventsLongPolling ps = new CBEventsLongPolling(ws, this);
|
||||
|
||||
ps.handleWebSessionEvent(new WSSocketConnectedEvent(ws.getApplication().getApplicationRunId()));
|
||||
resp.setHeader(WSConstants.WS_SESSION_HEADER, sid);
|
||||
log.debug("HTTP Long-Poll channel opened for session " + sid);
|
||||
|
||||
return ps;
|
||||
@@ -253,7 +243,8 @@ public class CBEventsLongPollingServlet extends HttpServlet {
|
||||
|
||||
@NotNull
|
||||
protected static WebHttpRequestInfo createRequestInfo(
|
||||
@Nullable String sessionId, @NotNull HttpServletRequest req
|
||||
@Nullable String sessionId,
|
||||
@NotNull HttpServletRequest req
|
||||
) {
|
||||
return new WebHttpRequestInfo(
|
||||
sessionId,
|
||||
@@ -263,6 +254,13 @@ public class CBEventsLongPollingServlet extends HttpServlet {
|
||||
);
|
||||
}
|
||||
|
||||
void rekeySession(@NotNull String oldSid, @NotNull String newSid) {
|
||||
CBEventsLongPolling moved = sessions.get(oldSid);
|
||||
if (moved != null) {
|
||||
sessions.put(newSid, moved);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a {@link BaseWebSession} from an incoming HTTP request.
|
||||
* <p>
|
||||
|
||||
+25
-3
@@ -32,7 +32,11 @@ public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessi
|
||||
private static final Log log = Log.getLog(CBEventsWebSocket.class);
|
||||
|
||||
@Nullable
|
||||
private BaseWebSession webSession;
|
||||
private volatile BaseWebSession webSession;
|
||||
@Nullable
|
||||
private FromUserEventHandler eventProcessor;
|
||||
@Nullable
|
||||
private WebSocketPingPongCallback pingPongCallback;
|
||||
|
||||
@Override
|
||||
public void onOpen(Session session, EndpointConfig config) {
|
||||
@@ -48,13 +52,31 @@ public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessi
|
||||
log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session");
|
||||
|
||||
session.setMaxIdleTimeout(Duration.ofMinutes(5).toMillis());
|
||||
session.addMessageHandler(String.class, new FromUserEventHandler(webSession));
|
||||
session.addMessageHandler(PongMessage.class, new WebSocketPingPongCallback(webSession));
|
||||
this.eventProcessor = new FromUserEventHandler(webSession);
|
||||
session.addMessageHandler(String.class, eventProcessor);
|
||||
this.pingPongCallback = new WebSocketPingPongCallback(webSession);
|
||||
session.addMessageHandler(PongMessage.class, pingPongCallback);
|
||||
|
||||
CBJettyWebSocketManager.registerWebSocket(webSession.getSessionId(), this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrateToSession(@NotNull BaseWebSession newSession) {
|
||||
BaseWebSession oldSession = this.webSession;
|
||||
this.webSession = newSession;
|
||||
if (eventProcessor != null) {
|
||||
eventProcessor.setWebSession(newSession);
|
||||
}
|
||||
if (pingPongCallback != null) {
|
||||
pingPongCallback.setWebSession(newSession);
|
||||
}
|
||||
if (oldSession != null) {
|
||||
CBJettyWebSocketManager.migrateWebSocket(oldSession.getSessionId(), newSession.getSessionId(), this);
|
||||
}
|
||||
log.debug("EventWebSocket migrated to the " + newSession.getSessionId() + " session");
|
||||
}
|
||||
|
||||
private static class FromUserEventHandler extends CBClientEventProcessor implements MessageHandler.Whole<String> {
|
||||
|
||||
private FromUserEventHandler(@NotNull BaseWebSession webSession) {
|
||||
|
||||
+20
-1
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -39,6 +39,25 @@ public class CBJettyWebSocketManager {
|
||||
socketBySessionId.computeIfAbsent(webSessionId, key -> new CopyOnWriteArrayList<>()).add(webSocket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-keys a live web socket from the old session id to the new one after a session rotation,
|
||||
* so that keep-alive pings keep being delivered to it.
|
||||
*/
|
||||
public static void migrateWebSocket(
|
||||
@NotNull String oldSessionId,
|
||||
@NotNull String newSessionId,
|
||||
@NotNull CBEventsWebSocket webSocket
|
||||
) {
|
||||
List<CBEventsWebSocket> oldSockets = socketBySessionId.get(oldSessionId);
|
||||
if (oldSockets != null) {
|
||||
oldSockets.remove(webSocket);
|
||||
if (oldSockets.isEmpty()) {
|
||||
socketBySessionId.remove(oldSessionId);
|
||||
}
|
||||
}
|
||||
registerWebSocket(newSessionId, webSocket);
|
||||
}
|
||||
|
||||
public static void sendPing() {
|
||||
//remove expired sessions
|
||||
WebAppSessionManager webSessionManager = WebAppUtils.getWebApplication().getSessionManager();
|
||||
|
||||
+5
-1
@@ -24,12 +24,16 @@ import org.jkiss.code.NotNull;
|
||||
|
||||
public class WebSocketPingPongCallback implements MessageHandler.Whole<PongMessage> {
|
||||
@NotNull
|
||||
private final BaseWebSession webSession;
|
||||
private volatile BaseWebSession webSession;
|
||||
|
||||
public WebSocketPingPongCallback(@NotNull BaseWebSession webSession) {
|
||||
this.webSession = webSession;
|
||||
}
|
||||
|
||||
void setWebSession(@NotNull BaseWebSession webSession) {
|
||||
this.webSession = webSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(PongMessage message) {
|
||||
if (webSession instanceof WebHeadlessSession) {
|
||||
|
||||
+6
@@ -275,6 +275,12 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
|
||||
if (project == null) {
|
||||
throw new DBException("Project not found:" + projectId);
|
||||
}
|
||||
var customConnectionsEnabled =
|
||||
ServletAppUtils.getServletApplication().getAppConfiguration().isSupportsCustomConnections();
|
||||
if (!customConnectionsEnabled && project.isPrivateProject()) {
|
||||
throw new DBWebExceptionAccessDenied("Access to private project is denied");
|
||||
}
|
||||
|
||||
RMProject rmProject = project.getRMProject();
|
||||
|
||||
for (String reqProjectPermission : requireProjectPermissions) {
|
||||
|
||||
+13
-2
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -71,7 +71,7 @@ public abstract class WebServiceServletBase extends HttpServlet {
|
||||
} catch (Exception e) {
|
||||
log.error(e);
|
||||
errorMessage = e.getMessage();
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + errorMessage);
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + flattenForResponse(errorMessage));
|
||||
} finally {
|
||||
if (WebAppUtils.getWebApplication() instanceof ApiCallInterceptor apiCallInterceptor) {
|
||||
apiCallInterceptor.onApiCallEvent(
|
||||
@@ -83,6 +83,17 @@ public abstract class WebServiceServletBase extends HttpServlet {
|
||||
|
||||
protected abstract void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException;
|
||||
|
||||
/**
|
||||
* Replaces control characters (line breaks, tabs, etc.) with spaces so the message can be safely
|
||||
* embedded into the JSON error body produced by the servlet container's error handler.
|
||||
*/
|
||||
private String flattenForResponse(String message) {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
return message.replaceAll("\\p{Cntrl}+", " ").trim();
|
||||
}
|
||||
|
||||
protected Map<String, Object> getVariables(HttpServletRequest request) {
|
||||
return gson.fromJson(request.getParameter(REQUEST_PARAM_VARIABLES), MAP_STRING_OBJECT_TYPE);
|
||||
}
|
||||
|
||||
+8
-4
@@ -25,6 +25,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionType;
|
||||
import org.jkiss.dbeaver.model.rm.RMConstants;
|
||||
import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings;
|
||||
import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor;
|
||||
@@ -115,6 +116,9 @@ public interface DBWServiceCore extends DBWService {
|
||||
@NotNull Map<String, Object> connectionConfig
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
List<DBPConnectionType> getConnectionTypes(@NotNull WebSession webSession, @Nullable String id);
|
||||
|
||||
@WebAction
|
||||
WebConnectionInfo getConnectionState(WebSession webSession, @Nullable String projectId, String connectionId) throws DBWebException;
|
||||
|
||||
@@ -159,17 +163,17 @@ public interface DBWServiceCore extends DBWService {
|
||||
@NotNull Map<String, Object> connectionConfig
|
||||
) throws DBWebException;
|
||||
|
||||
@WebAction
|
||||
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
|
||||
WebConnectionInfo testConnection(
|
||||
@NotNull WebSession webSession,
|
||||
@Nullable String projectId,
|
||||
@Nullable @WebObjectId String projectId,
|
||||
@NotNull Map<String, Object> connectionConfig
|
||||
) throws DBWebException;
|
||||
|
||||
@WebAction
|
||||
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
|
||||
WebNetworkEndpointInfo testNetworkHandler(
|
||||
@NotNull WebSession webSession,
|
||||
@Nullable String projectId,
|
||||
@Nullable @WebObjectId String projectId,
|
||||
@Nullable String connectionId,
|
||||
@NotNull WebNetworkHandlerConfigInput nhConfig
|
||||
) throws DBWebException;
|
||||
|
||||
+5
@@ -87,6 +87,11 @@ public class WebServiceBindingCore extends WebServiceBindingBase<DBWServiceCore>
|
||||
getArgumentVal(env, "projectId"),
|
||||
getArgumentVal(env, "config")
|
||||
)
|
||||
).dataFetcher(
|
||||
"listConnectionTypes", env -> getService(env).getConnectionTypes(
|
||||
getWebSession(env),
|
||||
getArgument(env, "id")
|
||||
)
|
||||
)
|
||||
;
|
||||
|
||||
|
||||
+30
-14
@@ -45,6 +45,7 @@ import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.auth.SMObjectType;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
|
||||
import org.jkiss.dbeaver.model.connection.DBPConnectionType;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.dbeaver.model.exec.DBCConnectException;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNDataSource;
|
||||
@@ -310,17 +311,32 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
|
||||
DataSourceDescriptor dataSource = (DataSourceDescriptor) WebDataSourceUtils.getLocalOrGlobalDataSource(
|
||||
webSession, projectId, configInput.getConnectionId());
|
||||
DataSourceDescriptor testDataSource = getDataSourceDescriptor(webSession, dataSource, configInput, project);
|
||||
DBPConnectionConfiguration connectionConfiguration = new DBPConnectionConfiguration(testDataSource.getConnectionConfiguration());
|
||||
return WebServiceUtils.getDriverProperties(
|
||||
webSession,
|
||||
testDataSource.getDriver(),
|
||||
testDataSource,
|
||||
connectionConfiguration
|
||||
);
|
||||
try {
|
||||
DataSourceDescriptor testDataSource = getDataSourceDescriptor(webSession, dataSource, configInput, project);
|
||||
DBPConnectionConfiguration connectionConfiguration
|
||||
= new DBPConnectionConfiguration(testDataSource.getConnectionConfiguration());
|
||||
return WebServiceUtils.getDriverProperties(
|
||||
webSession,
|
||||
testDataSource.getDriver(),
|
||||
testDataSource,
|
||||
connectionConfiguration
|
||||
);
|
||||
} catch (DBWebException e) {
|
||||
log.error("Error getting driver properties", e);
|
||||
return new WebPropertyInfo[0];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<DBPConnectionType> getConnectionTypes(@NotNull WebSession webSession, @Nullable String id) {
|
||||
return DataSourceProviderRegistry.getInstance().getConnectionTypes().stream()
|
||||
.map(ct -> id == null || id.equals(ct.getId()) ? ct : null)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public WebConnectionInfo initConnection(
|
||||
@@ -541,6 +557,9 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
DataSourceDescriptor dataSource = (DataSourceDescriptor) WebDataSourceUtils.getLocalOrGlobalDataSource(
|
||||
webSession, projectId, configInput.getConnectionId());
|
||||
DataSourceDescriptor testDataSource = getDataSourceDescriptor(webSession, dataSource, configInput, project);
|
||||
testDataSource.setTemporary(true);
|
||||
WebConnectionInfo connectionInfo = project.addConnection(testDataSource);
|
||||
connectionInfo.setSavedCredentials(configInput.getCredentials(), configInput.getNetworkHandlersConfig());
|
||||
try {
|
||||
ConnectionTestJob ct = new ConnectionTestJob(
|
||||
testDataSource, param -> {
|
||||
@@ -557,7 +576,6 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
}
|
||||
throw new DBWebException("Connection failed", ct.getConnectError());
|
||||
}
|
||||
WebConnectionInfo connectionInfo = project.createConnectionInfo(testDataSource);
|
||||
connectionInfo.setConnectError(ct.getConnectError());
|
||||
connectionInfo.setServerVersion(ct.getServerVersion());
|
||||
connectionInfo.setClientVersion(ct.getClientVersion());
|
||||
@@ -565,6 +583,8 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
return connectionInfo;
|
||||
} catch (DBException e) {
|
||||
throw new DBWebException("Error connecting to database", e);
|
||||
} finally {
|
||||
project.removeConnection(testDataSource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,11 +605,7 @@ public class WebServiceCore implements DBWServiceCore {
|
||||
}
|
||||
|
||||
testDataSource = (DataSourceDescriptor) dataSource.createCopy(dataSource.getRegistry());
|
||||
WebDataSourceUtils.setConnectionConfiguration(
|
||||
testDataSource.getDriver(),
|
||||
testDataSource.getConnectionConfiguration(),
|
||||
configInput
|
||||
);
|
||||
project.updateDataSourceContainerFromInput(configInput, testDataSource);
|
||||
if (configInput.getSelectedSecretId() != null) {
|
||||
try {
|
||||
dataSource.listSharedCredentials()
|
||||
|
||||
+24
-7
@@ -33,7 +33,6 @@ import org.jkiss.dbeaver.model.struct.rdb.DBSSchema;
|
||||
import org.jkiss.dbeaver.model.struct.rdb.DBSTable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -58,6 +57,7 @@ public class WebDatabaseObjectInfo {
|
||||
public static final String OBJECT_FEATURE_SCHEMA = "schema";
|
||||
public static final String OBJECT_FEATURE_SCRIPT = "script";
|
||||
public static final String OBJECT_FEATURE_SCRIPT_EXTENDED = "scriptExtended";
|
||||
public static final String OBJECT_FEATURE_SUPPORTS_FULL_DDL = "supportsFullDdl";
|
||||
|
||||
private final WebSession session;
|
||||
private final DBSObject object;
|
||||
@@ -168,10 +168,21 @@ public class WebDatabaseObjectInfo {
|
||||
return features.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private void getObjectFeatures(DBSObject object, List<String> features) {
|
||||
private void getObjectFeatures(@NotNull DBSObject object, @NotNull List<String> features) {
|
||||
if (object instanceof DBPScriptObject) {
|
||||
features.add(OBJECT_FEATURE_SCRIPT);
|
||||
}
|
||||
if (object instanceof DBPScriptObjectExt) {
|
||||
features.add(OBJECT_FEATURE_SCRIPT_EXTENDED);
|
||||
}
|
||||
if (object instanceof DBPScriptObjectExt2 scriptObjectExt2 && (
|
||||
scriptObjectExt2.supportsObjectDefinitionOption(DBPScriptObject.OPTION_INCLUDE_NESTED_OBJECTS)
|
||||
|| scriptObjectExt2.supportsObjectDefinitionOption(DBPScriptObject.OPTION_INCLUDE_COMMENTS)
|
||||
|| scriptObjectExt2.supportsObjectDefinitionOption(DBPScriptObject.OPTION_INCLUDE_PERMISSIONS))
|
||||
) {
|
||||
features.add(OBJECT_FEATURE_SUPPORTS_FULL_DDL);
|
||||
}
|
||||
boolean isDiagramSupported = true;
|
||||
if (object instanceof DBPScriptObject) features.add(OBJECT_FEATURE_SCRIPT);
|
||||
if (object instanceof DBPScriptObjectExt) features.add(OBJECT_FEATURE_SCRIPT_EXTENDED);
|
||||
if (object instanceof DBSDataContainer) {
|
||||
features.add(OBJECT_FEATURE_DATA_CONTAINER);
|
||||
if (((DBSDataContainer) object).isFeatureSupported(DBSDataContainer.FEATURE_DATA_FILTER)) {
|
||||
@@ -181,7 +192,9 @@ public class WebDatabaseObjectInfo {
|
||||
isDiagramSupported = false;
|
||||
}
|
||||
}
|
||||
if (object instanceof DBSDataManipulator) features.add(OBJECT_FEATURE_DATA_MANIPULATOR);
|
||||
if (object instanceof DBSDataManipulator) {
|
||||
features.add(OBJECT_FEATURE_DATA_MANIPULATOR);
|
||||
}
|
||||
if (object instanceof DBSEntity) {
|
||||
features.add(OBJECT_FEATURE_ENTITY);
|
||||
if (object instanceof DBSDataType
|
||||
@@ -192,8 +205,12 @@ public class WebDatabaseObjectInfo {
|
||||
features.add(OBJECT_FEATURE_RELATIONAL_ENTITY);
|
||||
}
|
||||
}
|
||||
if (object instanceof DBSSchema) features.add(OBJECT_FEATURE_SCHEMA);
|
||||
if (object instanceof DBSCatalog) features.add(OBJECT_FEATURE_CATALOG);
|
||||
if (object instanceof DBSSchema) {
|
||||
features.add(OBJECT_FEATURE_SCHEMA);
|
||||
}
|
||||
if (object instanceof DBSCatalog) {
|
||||
features.add(OBJECT_FEATURE_CATALOG);
|
||||
}
|
||||
if (object instanceof DBSObjectContainer objectContainer) {
|
||||
features.add(OBJECT_FEATURE_OBJECT_CONTAINER);
|
||||
try {
|
||||
|
||||
+3
-3
@@ -279,7 +279,7 @@ public class WebServiceNavigator implements DBWServiceNavigator {
|
||||
// in that way all related scripts will use the same context and overwrite the user's define context
|
||||
// So why we need restore the default execution context after refreshing datasource
|
||||
DBCExecutionContext defaultContext = DBUtils.getDefaultContext(refreshableObject, false);
|
||||
DBCExecutionContextDefaults contextDefaults = defaultContext.getContextDefaults();
|
||||
DBCExecutionContextDefaults<?, ?> contextDefaults = defaultContext.getContextDefaults();
|
||||
refreshableObject.refreshObject(monitor);
|
||||
if (contextDefaults != null && contextDefaults.getDefaultSchema() != null
|
||||
&& contextDefaults.getDefaultCatalog() != null) {
|
||||
@@ -603,8 +603,8 @@ public class WebServiceNavigator implements DBWServiceNavigator {
|
||||
}
|
||||
|
||||
private void checkProjectEditAccess(@NotNull DBNNode node, @NotNull WebSession session) throws DBException {
|
||||
BaseWebProjectImpl project = (BaseWebProjectImpl) node.getOwnerProject();
|
||||
if (project == null || !hasNodeEditPermission(session, node, project.getRMProject())) {
|
||||
var project = node.getOwnerProject();
|
||||
if (!(project instanceof BaseWebProjectImpl bwp) || !hasNodeEditPermission(session, node, bwp.getRMProject())) {
|
||||
throw new DBException("Access denied");
|
||||
}
|
||||
}
|
||||
|
||||
+12
-6
@@ -81,6 +81,15 @@ public interface DBWServiceSQL extends DBWService {
|
||||
@NotNull WebSQLGeneratorOptions options
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
WebAsyncTaskInfo asyncGenerateEntityQuery(
|
||||
@NotNull WebSession session,
|
||||
@NotNull String generatorId,
|
||||
@NotNull List<String> nodePathList,
|
||||
@NotNull WebSQLGeneratorOptions options
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
String sqlGenerateResultSetQuery(
|
||||
@@ -127,15 +136,12 @@ public interface DBWServiceSQL extends DBWService {
|
||||
@Nullable WebDataFormat dataFormat) throws DBWebException;
|
||||
|
||||
@WebAction
|
||||
WebAsyncTaskInfo asyncNavigateForeignKey(
|
||||
List<WebSQLQueryResultAssociation> getSqlResultAssociations(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull WebSQLContextInfo contextInfo,
|
||||
@NotNull String resultsId,
|
||||
@NotNull WebSQLResultsRow row,
|
||||
int columnIndex,
|
||||
@NotNull String associationName,
|
||||
boolean isReference,
|
||||
@Nullable WebDataFormat dataFormat) throws DBException;
|
||||
@Nullable Boolean isReference
|
||||
) throws DBException;
|
||||
|
||||
/**
|
||||
* Reads dynamic trace from provided database results.
|
||||
|
||||
+49
-6
@@ -41,6 +41,7 @@ public class WebSQLDataFilter {
|
||||
private int offset;
|
||||
private int limit;
|
||||
private String where;
|
||||
private boolean anyConstraint;
|
||||
private final List<WebSQLDataFilterConstraint> constraints = new ArrayList<>();
|
||||
|
||||
public WebSQLDataFilter() {
|
||||
@@ -57,9 +58,10 @@ public class WebSQLDataFilter {
|
||||
this.limit = MAX_ROWS_NUMBER;
|
||||
}
|
||||
this.where = CommonUtils.toString(filterProps.get("where"), null);
|
||||
this.anyConstraint = CommonUtils.toBoolean(filterProps.get("anyConstraint"));
|
||||
Object constraints = filterProps.get("constraints");
|
||||
if (constraints instanceof Collection) {
|
||||
for (Object constrItem : (Collection<?>) constraints) {
|
||||
if (constraints instanceof Collection<?> col) {
|
||||
for (Object constrItem : col) {
|
||||
if (constrItem instanceof Map) {
|
||||
this.constraints.add(
|
||||
new WebSQLDataFilterConstraint((Map<String, Object>) constrItem));
|
||||
@@ -92,10 +94,18 @@ public class WebSQLDataFilter {
|
||||
return where;
|
||||
}
|
||||
|
||||
/**
|
||||
* When true constraints are combined with OR instead of AND
|
||||
*/
|
||||
public boolean isAnyConstraint() {
|
||||
return anyConstraint;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static WebSQLDataFilter from(@NotNull DBDDataFilter filter) {
|
||||
var webFilter = new WebSQLDataFilter();
|
||||
webFilter.where = filter.getWhere();
|
||||
webFilter.anyConstraint = filter.isAnyConstraint();
|
||||
for (DBDAttributeConstraint constraint : filter.getConstraints()) {
|
||||
webFilter.constraints.add(WebSQLDataFilterConstraint.from(constraint));
|
||||
}
|
||||
@@ -106,6 +116,7 @@ public class WebSQLDataFilter {
|
||||
public DBDDataFilter makeDataFilter(@Nullable WebSQLResultsInfo resultInfo) throws DBException {
|
||||
DBDDataFilter dataFilter = new DBDDataFilter();
|
||||
dataFilter.setWhere(where);
|
||||
dataFilter.setAnyConstraint(anyConstraint);
|
||||
if (CommonUtils.isEmpty(constraints)) {
|
||||
return dataFilter;
|
||||
}
|
||||
@@ -139,14 +150,42 @@ public class WebSQLDataFilter {
|
||||
|
||||
private void fillEmptyConstrains(@NotNull List<DBDAttributeConstraint> emptyConstraints) throws DBException {
|
||||
for (WebSQLDataFilterConstraint webConstr : constraints) {
|
||||
if (webConstr.getAttributePosition() >= emptyConstraints.size()) {
|
||||
throw new DBException(MessageFormat.format("Incorrect column position ''{0}'' in order clause", webConstr.getAttributePosition()));
|
||||
DBDAttributeConstraint dbConstr = findConstraintToFill(emptyConstraints, webConstr);
|
||||
if (dbConstr.hasCondition()) {
|
||||
// The attribute already has a condition from a previous constraint
|
||||
dbConstr = new DBDAttributeConstraint(dbConstr);
|
||||
dbConstr.reset();
|
||||
emptyConstraints.add(dbConstr);
|
||||
}
|
||||
DBDAttributeConstraint dbConstr = emptyConstraints.get(webConstr.getAttributePosition());
|
||||
fillEmptyConstraint(dbConstr, webConstr);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private DBDAttributeConstraint findConstraintToFill(
|
||||
@NotNull List<DBDAttributeConstraint> emptyConstraints,
|
||||
@NotNull WebSQLDataFilterConstraint webConstr
|
||||
) throws DBException {
|
||||
Integer attributePosition = webConstr.getAttributePosition();
|
||||
if (attributePosition != null) {
|
||||
if (attributePosition < 0 || attributePosition >= emptyConstraints.size()) {
|
||||
throw new DBException(MessageFormat.format("Incorrect column position ''{0}'' in order clause", attributePosition));
|
||||
}
|
||||
return emptyConstraints.get(attributePosition);
|
||||
}
|
||||
// try by attribute name
|
||||
String attributeName = webConstr.getAttributeName();
|
||||
if (!CommonUtils.isEmpty(attributeName)) {
|
||||
for (DBDAttributeConstraint dbConstr : emptyConstraints) {
|
||||
if (CommonUtils.equalObjects(dbConstr.getAttributeName(), attributeName)) {
|
||||
return dbConstr;
|
||||
}
|
||||
}
|
||||
throw new DBException(MessageFormat.format("Constraint attribute ''{0}'' not found in result set", attributeName));
|
||||
}
|
||||
throw new DBException("Constraint must specify either attributePosition or attributeName");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private DBDAttributeConstraint fillEmptyConstraint(@NotNull DBDAttributeConstraint dbConstr,
|
||||
@NotNull WebSQLDataFilterConstraint webConstr) {
|
||||
@@ -168,6 +207,7 @@ public class WebSQLDataFilter {
|
||||
return dbConstr;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<DBDAttributeConstraint> getDbdConstraints() {
|
||||
List<DBDAttributeConstraint> dbConstraints = new ArrayList<>();
|
||||
for (WebSQLDataFilterConstraint constraint : constraints) {
|
||||
@@ -182,7 +222,10 @@ public class WebSQLDataFilter {
|
||||
return dbConstraints;
|
||||
}
|
||||
|
||||
private static int getOriginalVisualPosition(WebSQLDataFilterConstraint constraint, String attributeName) {
|
||||
private static int getOriginalVisualPosition(
|
||||
@NotNull WebSQLDataFilterConstraint constraint,
|
||||
@NotNull String attributeName
|
||||
) {
|
||||
Integer attributePosition = constraint.getAttributePosition();
|
||||
|
||||
if (CommonUtils.isEmpty(attributeName)) {
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ package io.cloudbeaver.service.sql;
|
||||
|
||||
public record WebSQLGeneratorOptions(
|
||||
boolean useFullyQualifiedNames,
|
||||
boolean compactSql
|
||||
boolean compactSql,
|
||||
boolean showFullDdl
|
||||
) {
|
||||
}
|
||||
|
||||
+18
-5
@@ -479,7 +479,7 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
sendTransactionalEvent(contextInfo);
|
||||
}
|
||||
|
||||
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(webSession);
|
||||
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet();
|
||||
updatedResultSet.setResultsInfo(resultsInfo);
|
||||
updatedResultSet.setColumns(resultsInfo.getAttributes());
|
||||
|
||||
@@ -651,7 +651,7 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
DBDAttributeBinding[] allAttributes = resultsInfo.getAttributes();
|
||||
DBDAttributeBinding[] keyAttributes = rowIdentifier.getAttributes().toArray(new DBDAttributeBinding[0]);
|
||||
|
||||
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(webSession);
|
||||
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet();
|
||||
updatedResultSet.setResultsInfo(resultsInfo);
|
||||
updatedResultSet.setColumns(resultsInfo.getAttributes());
|
||||
|
||||
@@ -1131,7 +1131,7 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
@NotNull WebSQLExecuteInfo executeInfo,
|
||||
@NotNull DBDDataFilter filter
|
||||
) throws DBException {
|
||||
if (!filter.getConstraints().isEmpty() || !CommonUtils.isEmpty(filter.getWhere())) {
|
||||
if (filter.getConstraintsCount() > 0 || !CommonUtils.isEmpty(filter.getWhere())) {
|
||||
StringBuilder where = new StringBuilder();
|
||||
SQLUtils.appendConditionString(
|
||||
filter,
|
||||
@@ -1268,10 +1268,23 @@ public class WebSQLProcessor implements WebSessionProvider {
|
||||
throws DBException {
|
||||
if (cellRow instanceof Map<?, ?> variables) {
|
||||
if (variables.get(FILE_ID) != null) {
|
||||
Path path = WebAppUtils.getWebPlatform()
|
||||
String fileId = variables.get(FILE_ID).toString();
|
||||
try {
|
||||
// file id must be UUID
|
||||
UUID.fromString(fileId);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new DBException("File ID is invalid");
|
||||
}
|
||||
|
||||
Path uploadFolder = WebAppUtils.getWebPlatform()
|
||||
.getTempFolder(webSession.getProgressMonitor(), TEMP_FILE_FOLDER)
|
||||
.resolve(webSession.getSessionId())
|
||||
.resolve(variables.get(FILE_ID).toString());
|
||||
.normalize();
|
||||
Path path = uploadFolder.resolve(fileId).normalize();
|
||||
// ensure the resolved path stays within the session upload directory
|
||||
if (!path.startsWith(uploadFolder)) {
|
||||
throw new DBException("File ID is invalid");
|
||||
}
|
||||
|
||||
try {
|
||||
var file = Files.newInputStream(path);
|
||||
|
||||
+1
-2
@@ -54,7 +54,7 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver {
|
||||
private final WebDataFormat dataFormat;
|
||||
@Nullable
|
||||
private final DBDDataFilter dataFilter;
|
||||
private final WebSQLQueryResultSet webResultSet;
|
||||
private final WebSQLQueryResultSet webResultSet = new WebSQLQueryResultSet();
|
||||
|
||||
private DBDAttributeBinding[] bindings;
|
||||
private DBCTrace trace;
|
||||
@@ -71,7 +71,6 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver {
|
||||
this.dataContainer = dataContainer;
|
||||
this.dataFormat = dataFormat;
|
||||
this.dataFilter = dataFilter;
|
||||
this.webResultSet = new WebSQLQueryResultSet(contextInfo.getProcessor().getWebSession());
|
||||
rowLimit = ServletAppUtils.getServletApplication()
|
||||
.getAppConfiguration()
|
||||
.getResourceQuota(WebSQLConstants.QUOTA_PROP_ROW_LIMIT);
|
||||
|
||||
+37
-13
@@ -21,19 +21,20 @@ import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.DBPEvaluationContext;
|
||||
import org.jkiss.dbeaver.model.DBUtils;
|
||||
import org.jkiss.dbeaver.model.meta.Property;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNNode;
|
||||
import org.jkiss.dbeaver.model.struct.DBSEntity;
|
||||
import org.jkiss.dbeaver.model.struct.DBSEntityAssociation;
|
||||
import org.jkiss.dbeaver.model.struct.DBSEntityConstraint;
|
||||
import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog;
|
||||
import org.jkiss.dbeaver.model.struct.rdb.DBSSchema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WebSQLQueryResultReference {
|
||||
public class WebSQLQueryResultAssociation {
|
||||
|
||||
private static final Log log = Log.getLog(WebSQLQueryResultReference.class);
|
||||
private static final Log log = Log.getLog(WebSQLQueryResultAssociation.class);
|
||||
|
||||
@Nullable
|
||||
private final WebSession session;
|
||||
@@ -41,18 +42,23 @@ public class WebSQLQueryResultReference {
|
||||
private final DBSEntityAssociation association;
|
||||
private final boolean reverse;
|
||||
@NotNull
|
||||
private final List<Integer> columnIndexList;
|
||||
private final List<WebSQLReferenceColumnMapping> columnMapping;
|
||||
|
||||
public WebSQLQueryResultReference(
|
||||
public WebSQLQueryResultAssociation(
|
||||
@Nullable WebSession session,
|
||||
@NotNull DBSEntityAssociation association,
|
||||
boolean reverse,
|
||||
@NotNull List<Integer> columnIndexList
|
||||
@NotNull List<WebSQLReferenceColumnMapping> columnMapping
|
||||
) {
|
||||
this.session = session;
|
||||
this.association = association;
|
||||
this.reverse = reverse;
|
||||
this.columnIndexList = columnIndexList;
|
||||
this.columnMapping = columnMapping;
|
||||
}
|
||||
|
||||
@Property
|
||||
public boolean isReference() {
|
||||
return reverse;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -63,18 +69,36 @@ public class WebSQLQueryResultReference {
|
||||
|
||||
@Nullable
|
||||
@Property
|
||||
public String getTargetEntityName() {
|
||||
public String getTargetCatalogName() {
|
||||
DBSEntity targetEntity = getTargetEntity();
|
||||
if (targetEntity == null) {
|
||||
return null;
|
||||
}
|
||||
return DBUtils.getObjectFullName(targetEntity, DBPEvaluationContext.UI);
|
||||
DBSCatalog catalog = DBUtils.getParentOfType(DBSCatalog.class, targetEntity);
|
||||
return catalog == null ? null : catalog.getName();
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
@Property
|
||||
public String getNodePath() {
|
||||
public String getTargetSchemaName() {
|
||||
DBSEntity targetEntity = getTargetEntity();
|
||||
if (targetEntity == null) {
|
||||
return null;
|
||||
}
|
||||
DBSSchema schema = DBUtils.getParentOfType(DBSSchema.class, targetEntity);
|
||||
return schema == null ? null : schema.getName();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Property
|
||||
public String getTargetEntityName() {
|
||||
DBSEntity targetEntity = getTargetEntity();
|
||||
return targetEntity == null ? null : targetEntity.getName();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Property
|
||||
public String getTargetNodePath() {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -94,8 +118,8 @@ public class WebSQLQueryResultReference {
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public List<Integer> getColumnIndexList() {
|
||||
return columnIndexList;
|
||||
public List<WebSQLReferenceColumnMapping> getColumnMapping() {
|
||||
return columnMapping;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
+1
-24
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package io.cloudbeaver.service.sql;
|
||||
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.service.sql.WebSQLResultSetRowIdentifier.WebSQLResultSetRowIdentifierState;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
@@ -36,10 +35,6 @@ public class WebSQLQueryResultSet {
|
||||
|
||||
private static final Log log = Log.getLog(WebSQLQueryResultSet.class);
|
||||
|
||||
@Nullable
|
||||
private final WebSession session;
|
||||
@Nullable
|
||||
private DBDAttributeBinding[] referencesBindings;
|
||||
private WebSQLQueryResultColumn[] columns;
|
||||
private List<WebSQLQueryResultSetRow> rows = Collections.emptyList();
|
||||
private boolean hasMoreData;
|
||||
@@ -55,8 +50,7 @@ public class WebSQLQueryResultSet {
|
||||
private boolean readOnly;
|
||||
private String readOnlyStatus;
|
||||
|
||||
public WebSQLQueryResultSet(@Nullable WebSession session) {
|
||||
this.session = session;
|
||||
public WebSQLQueryResultSet() {
|
||||
}
|
||||
|
||||
@Property
|
||||
@@ -79,23 +73,6 @@ public class WebSQLQueryResultSet {
|
||||
columns[i] = new WebSQLQueryResultColumn(bindings[i]);
|
||||
}
|
||||
this.columns = columns;
|
||||
this.referencesBindings = bindings;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public List<WebSQLQueryResultReference> getAssociations() {
|
||||
return session != null && referencesBindings != null
|
||||
? WebSQLUtils.collectAssociations(session, referencesBindings)
|
||||
: Collections.emptyList();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public List<WebSQLQueryResultReference> getReferences() {
|
||||
return session != null && referencesBindings != null
|
||||
? WebSQLUtils.collectReferences(session, referencesBindings)
|
||||
: Collections.emptyList();
|
||||
}
|
||||
|
||||
@Property
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.service.sql;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.model.meta.Property;
|
||||
|
||||
public class WebSQLReferenceColumnMapping {
|
||||
|
||||
private final int sourceColumnIndex;
|
||||
@NotNull
|
||||
private final String sourceColumnName;
|
||||
private final int targetColumnIndex;
|
||||
@NotNull
|
||||
private final String targetColumnName;
|
||||
|
||||
public WebSQLReferenceColumnMapping(
|
||||
int sourceColumnIndex,
|
||||
@NotNull String sourceColumnName,
|
||||
int targetColumnIndex,
|
||||
@NotNull String targetColumnName
|
||||
) {
|
||||
this.sourceColumnIndex = sourceColumnIndex;
|
||||
this.sourceColumnName = sourceColumnName;
|
||||
this.targetColumnIndex = targetColumnIndex;
|
||||
this.targetColumnName = targetColumnName;
|
||||
}
|
||||
|
||||
@Property
|
||||
public int getSourceColumnIndex() {
|
||||
return sourceColumnIndex;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public String getSourceColumnName() {
|
||||
return sourceColumnName;
|
||||
}
|
||||
|
||||
@Property
|
||||
public int getTargetColumnIndex() {
|
||||
return targetColumnIndex;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public String getTargetColumnName() {
|
||||
return targetColumnName;
|
||||
}
|
||||
}
|
||||
+42
-31
@@ -331,23 +331,23 @@ public class WebSQLUtils {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<WebSQLQueryResultReference> collectAssociations(
|
||||
public static List<WebSQLQueryResultAssociation> collectAssociations(
|
||||
@NotNull WebSession session,
|
||||
@NotNull DBDAttributeBinding[] bindings
|
||||
) {
|
||||
return collectWebSQLQueryResultReference(session, bindings, false);
|
||||
return collectWebSQLQueryResultAssociation(session, bindings, false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<WebSQLQueryResultReference> collectReferences(
|
||||
public static List<WebSQLQueryResultAssociation> collectReferences(
|
||||
@NotNull WebSession session,
|
||||
@NotNull DBDAttributeBinding[] bindings
|
||||
) {
|
||||
return collectWebSQLQueryResultReference(session, bindings, true);
|
||||
return collectWebSQLQueryResultAssociation(session, bindings, true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<WebSQLQueryResultReference> collectWebSQLQueryResultReference(
|
||||
private static List<WebSQLQueryResultAssociation> collectWebSQLQueryResultAssociation(
|
||||
@NotNull WebSession session,
|
||||
@NotNull DBDAttributeBinding[] bindings,
|
||||
boolean reverse
|
||||
@@ -369,7 +369,7 @@ public class WebSQLUtils {
|
||||
return idx == null ? null : bindings[idx];
|
||||
};
|
||||
|
||||
List<WebSQLQueryResultReference> result = new ArrayList<>();
|
||||
List<WebSQLQueryResultAssociation> result = new ArrayList<>();
|
||||
DBRProgressMonitor monitor = session.getProgressMonitor();
|
||||
for (DBSEntity entity : entities) {
|
||||
try {
|
||||
@@ -377,9 +377,9 @@ public class WebSQLUtils {
|
||||
? DBStructUtils.readReferences(monitor, entity, attrToBinding)
|
||||
: DBStructUtils.readAssociations(monitor, entity, attrToBinding);
|
||||
for (DBSEntityAssociation association : source) {
|
||||
List<Integer> columnIndex = collectOwnColumnIndex(monitor, association, reverse, attrToIndex);
|
||||
if (columnIndex != null) {
|
||||
result.add(new WebSQLQueryResultReference(session, association, reverse, columnIndex));
|
||||
List<WebSQLReferenceColumnMapping> mapping = collectColumnMapping(monitor, association, reverse, attrToIndex);
|
||||
if (mapping != null) {
|
||||
result.add(new WebSQLQueryResultAssociation(session, association, reverse, mapping));
|
||||
}
|
||||
}
|
||||
} catch (DBException e) {
|
||||
@@ -389,38 +389,49 @@ public class WebSQLUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pairs source-side attributes (in our result set) with target-side attributes
|
||||
*/
|
||||
@Nullable
|
||||
private static List<Integer> collectOwnColumnIndex(
|
||||
private static List<WebSQLReferenceColumnMapping> collectColumnMapping(
|
||||
@NotNull DBRProgressMonitor monitor,
|
||||
@NotNull DBSEntityAssociation association,
|
||||
boolean reverse,
|
||||
@NotNull Map<DBSEntityAttribute, Integer> attrToIndex
|
||||
) throws DBException {
|
||||
DBSEntityReferrer ownSide;
|
||||
if (reverse) {
|
||||
DBSEntityConstraint refConstraint = association.getReferencedConstraint();
|
||||
if (!(refConstraint instanceof DBSEntityReferrer referrer)) {
|
||||
return null;
|
||||
}
|
||||
ownSide = referrer;
|
||||
} else {
|
||||
if (!(association instanceof DBSEntityReferrer associationRef)) {
|
||||
return null;
|
||||
}
|
||||
ownSide = associationRef;
|
||||
}
|
||||
List<? extends DBSEntityAttributeRef> attrs = ownSide.getAttributeReferences(monitor);
|
||||
if (attrs == null || attrs.isEmpty()) {
|
||||
if (!(association instanceof DBSEntityReferrer associationReferrer)) {
|
||||
return null;
|
||||
}
|
||||
List<Integer> indexList = new ArrayList<>(attrs.size());
|
||||
for (DBSEntityAttributeRef attrRef : attrs) {
|
||||
Integer idx = attrToIndex.get(attrRef.getAttribute());
|
||||
if (idx == null) {
|
||||
DBSEntityConstraint refConstraint = association.getReferencedConstraint();
|
||||
if (!(refConstraint instanceof DBSEntityReferrer constraintReferrer)) {
|
||||
return null;
|
||||
}
|
||||
DBSEntityReferrer sourceSide = reverse ? constraintReferrer : associationReferrer;
|
||||
DBSEntityReferrer targetSide = reverse ? associationReferrer : constraintReferrer;
|
||||
|
||||
List<? extends DBSEntityAttributeRef> sourceAttrs = sourceSide.getAttributeReferences(monitor);
|
||||
List<? extends DBSEntityAttributeRef> targetAttrs = targetSide.getAttributeReferences(monitor);
|
||||
if (sourceAttrs == null || targetAttrs == null
|
||||
|| sourceAttrs.isEmpty() || sourceAttrs.size() != targetAttrs.size()
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
List<WebSQLReferenceColumnMapping> mapping = new ArrayList<>(sourceAttrs.size());
|
||||
for (int i = 0; i < sourceAttrs.size(); i++) {
|
||||
DBSEntityAttribute sourceAttr = sourceAttrs.get(i).getAttribute();
|
||||
Integer sourceIdx = sourceAttr == null ? null : attrToIndex.get(sourceAttr);
|
||||
if (sourceIdx == null) {
|
||||
return null;
|
||||
}
|
||||
indexList.add(idx);
|
||||
DBSEntityAttribute targetAttr = targetAttrs.get(i).getAttribute();
|
||||
if (targetAttr == null) {
|
||||
return null;
|
||||
}
|
||||
// getOrdinalPosition() is 1-based. Normalize to 0-based to match sourceColumnIndex
|
||||
int targetIdx = targetAttr.getOrdinalPosition() - 1;
|
||||
mapping.add(new WebSQLReferenceColumnMapping(
|
||||
sourceIdx, sourceAttr.getName(), targetIdx, targetAttr.getName()));
|
||||
}
|
||||
return indexList;
|
||||
return mapping;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-14
@@ -54,6 +54,14 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL>
|
||||
.dataFetcher("sqlDialectInfo", env ->
|
||||
getService(env).getDialectInfo(getWebConnection(env))
|
||||
)
|
||||
.dataFetcher("sqlResultAssociations", env ->
|
||||
getService(env).getSqlResultAssociations(
|
||||
getWebSession(env),
|
||||
getSQLContext(env),
|
||||
getArgumentVal(env, "resultsId"),
|
||||
getArgument(env, "isReference")
|
||||
)
|
||||
)
|
||||
.dataFetcher("sqlListContexts", env ->
|
||||
getService(env).listContexts(getWebSession(env),
|
||||
getProjectReference(env),
|
||||
@@ -232,21 +240,17 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL>
|
||||
getDataFilter(env),
|
||||
getDataFormat(env)
|
||||
))
|
||||
.dataFetcher("asyncSqlNavigateForeignKey", env ->
|
||||
getService(env).asyncNavigateForeignKey(
|
||||
getWebSession(env),
|
||||
getSQLContext(env),
|
||||
getArgumentVal(env, "resultsId"),
|
||||
new WebSQLResultsRow(getArgument(env, "row")),
|
||||
getArgumentVal(env, "columnIndex"),
|
||||
getArgumentVal(env, "associationName"),
|
||||
CommonUtils.toBoolean(env.getArgument("isReference")),
|
||||
getDataFormat(env)
|
||||
))
|
||||
.dataFetcher("asyncSqlExecuteResults", env ->
|
||||
getService(env).asyncGetQueryResults(
|
||||
getWebSession(env), getArgumentVal(env, "taskId")
|
||||
))
|
||||
.dataFetcher("asyncSqlGenerateEntityQuery", env ->
|
||||
getService(env).asyncGenerateEntityQuery(
|
||||
getWebSession(env),
|
||||
getArgumentVal(env, "generatorId"),
|
||||
getArgumentVal(env, "nodePathList"),
|
||||
getGeneratorOptions(env)
|
||||
))
|
||||
.dataFetcher("asyncSqlExplainExecutionPlan", env ->
|
||||
getService(env).asyncSqlExplainExecutionPlan(
|
||||
getSQLContext(env),
|
||||
@@ -353,7 +357,7 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL>
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addServlets(ServletApplication application, DBWServletContext servletContext) throws DBException {
|
||||
public void addServlets(@NotNull ServletApplication application, @NotNull DBWServletContext servletContext) throws DBException {
|
||||
servletContext.addServlet(
|
||||
"sqlResultValueViewer",
|
||||
new WebSQLResultServlet(application, getServiceImpl()),
|
||||
@@ -432,11 +436,12 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL>
|
||||
private static WebSQLGeneratorOptions getGeneratorOptions(@NotNull DataFetchingEnvironment env) {
|
||||
Map<String, Object> optionsMap = getArgument(env, "generatorOptions");
|
||||
if (optionsMap == null) {
|
||||
return new WebSQLGeneratorOptions(true, false);
|
||||
return new WebSQLGeneratorOptions(true, false, false);
|
||||
}
|
||||
return new WebSQLGeneratorOptions(
|
||||
CommonUtils.toBoolean(optionsMap.get("useFullyQualifiedNames")),
|
||||
CommonUtils.toBoolean(optionsMap.get("compactSql"))
|
||||
CommonUtils.toBoolean(optionsMap.get("compactSql")),
|
||||
CommonUtils.toBoolean(optionsMap.get("showFullDdl"))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+77
-78
@@ -36,8 +36,6 @@ import org.jkiss.dbeaver.model.DBPDataSource;
|
||||
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
|
||||
import org.jkiss.dbeaver.model.DBUtils;
|
||||
import org.jkiss.dbeaver.model.data.DBDAttributeBinding;
|
||||
import org.jkiss.dbeaver.model.data.DBDReferenceNavigation;
|
||||
import org.jkiss.dbeaver.model.data.DBDReferenceUtils;
|
||||
import org.jkiss.dbeaver.model.exec.DBCException;
|
||||
import org.jkiss.dbeaver.model.exec.DBCLogicalOperator;
|
||||
import org.jkiss.dbeaver.model.exec.DBExecUtils;
|
||||
@@ -61,7 +59,9 @@ import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorDescriptor;
|
||||
import org.jkiss.dbeaver.model.sql.semantics.completion.SQLCompletionProposalComparator;
|
||||
import org.jkiss.dbeaver.model.sql.semantics.completion.SQLQueryCompletionAnalyzer;
|
||||
import org.jkiss.dbeaver.model.sql.semantics.completion.SQLQueryCompletionContext;
|
||||
import org.jkiss.dbeaver.model.struct.*;
|
||||
import org.jkiss.dbeaver.model.struct.DBSDataContainer;
|
||||
import org.jkiss.dbeaver.model.struct.DBSObject;
|
||||
import org.jkiss.dbeaver.model.struct.DBSWrapper;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
import org.jkiss.dbeaver.utils.RuntimeUtils;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
@@ -252,10 +252,33 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
@NotNull WebSQLGeneratorOptions options
|
||||
) throws DBWebException {
|
||||
List<DBSObject> objectList = getObjectListFromNodeIds(session, nodePathList);
|
||||
return createAndRunGenerator(
|
||||
session, generatorId, objectList,
|
||||
options.useFullyQualifiedNames(), options.compactSql()
|
||||
);
|
||||
return createAndRunGenerator(session, generatorId, objectList, options);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public WebAsyncTaskInfo asyncGenerateEntityQuery(
|
||||
@NotNull WebSession session,
|
||||
@NotNull String generatorId,
|
||||
@NotNull List<String> nodePathList,
|
||||
@NotNull WebSQLGeneratorOptions options
|
||||
) throws DBWebException {
|
||||
WebAsyncTaskProcessor<String> runnable = new WebAsyncTaskProcessor<>() {
|
||||
@Override
|
||||
public void run(@NotNull DBRProgressMonitor monitor) throws InvocationTargetException {
|
||||
try {
|
||||
monitor.beginTask("Generate SQL", 1);
|
||||
monitor.subTask("Generate '" + generatorId + "' SQL");
|
||||
List<DBSObject> objectList = getObjectListFromNodeIds(session, monitor, nodePathList);
|
||||
this.result = createAndRunGenerator(monitor, generatorId, objectList, options);
|
||||
} catch (Throwable e) {
|
||||
throw new InvocationTargetException(e);
|
||||
} finally {
|
||||
monitor.done();
|
||||
}
|
||||
}
|
||||
};
|
||||
return session.createAndRunAsyncTask("Generate SQL", runnable);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -270,10 +293,7 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
) throws DBWebException {
|
||||
checkAndFillTruncatedData(sqlContext, resultsId, selectedRows);
|
||||
WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider(resultsId, sqlContext, selectedRows);
|
||||
return createAndRunGenerator(
|
||||
webSession, generatorId, Collections.singletonList(dataProvider),
|
||||
options.useFullyQualifiedNames(), options.compactSql()
|
||||
);
|
||||
return createAndRunGenerator(webSession, generatorId, Collections.singletonList(dataProvider), options);
|
||||
}
|
||||
|
||||
private void checkAndFillTruncatedData(
|
||||
@@ -308,8 +328,17 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
@NotNull WebSession session,
|
||||
@NotNull String generatorId,
|
||||
@NotNull List<DBSObject> objectList,
|
||||
boolean useFullyQualifiedNames,
|
||||
boolean compactSql
|
||||
@NotNull WebSQLGeneratorOptions options
|
||||
) throws DBWebException {
|
||||
return createAndRunGenerator(session.getProgressMonitor(), generatorId, objectList, options);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String createAndRunGenerator(
|
||||
@NotNull DBRProgressMonitor monitor,
|
||||
@NotNull String generatorId,
|
||||
@NotNull List<DBSObject> objectList,
|
||||
@NotNull WebSQLGeneratorOptions options
|
||||
) throws DBWebException {
|
||||
SQLGeneratorDescriptor generator = SQLGeneratorConfigurationRegistry.getInstance().getGenerator(generatorId);
|
||||
if (generator == null) {
|
||||
@@ -317,9 +346,14 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
}
|
||||
try {
|
||||
SQLGenerator<DBSObject> generatorInstance = generator.createGenerator(objectList);
|
||||
generatorInstance.setFullyQualifiedNames(useFullyQualifiedNames);
|
||||
generatorInstance.setCompactSQL(compactSql);
|
||||
generatorInstance.run(session.getProgressMonitor());
|
||||
generatorInstance.setFullyQualifiedNames(options.useFullyQualifiedNames());
|
||||
generatorInstance.setCompactSQL(options.compactSql());
|
||||
if (options.showFullDdl()) {
|
||||
generatorInstance.setShowFullDdl(true);
|
||||
generatorInstance.setShowComments(true);
|
||||
generatorInstance.setShowPermissions(true);
|
||||
}
|
||||
generatorInstance.run(monitor);
|
||||
return generatorInstance.getResult();
|
||||
} catch (DBException e) {
|
||||
throw new DBWebException("Error creating SQL generator", e);
|
||||
@@ -331,12 +365,24 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<DBSObject> getObjectListFromNodeIds(@NotNull WebSession session, @NotNull List<String> nodePathList) throws DBWebException {
|
||||
private List<DBSObject> getObjectListFromNodeIds(
|
||||
@NotNull WebSession session,
|
||||
@NotNull List<String> nodePathList
|
||||
) throws DBWebException {
|
||||
return getObjectListFromNodeIds(session, session.getProgressMonitor(), nodePathList);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<DBSObject> getObjectListFromNodeIds(
|
||||
@NotNull WebSession session,
|
||||
@NotNull DBRProgressMonitor monitor,
|
||||
@NotNull List<String> nodePathList
|
||||
) throws DBWebException {
|
||||
try {
|
||||
List<DBSObject> objectList = new ArrayList<>(nodePathList.size());
|
||||
DBNModel navigatorModel = session.getNavigatorModelOrThrow();
|
||||
for (String nodePath : nodePathList) {
|
||||
DBNNode node = navigatorModel.getNodeByPath(session.getProgressMonitor(), nodePath);
|
||||
DBNNode node = navigatorModel.getNodeByPath(monitor, nodePath);
|
||||
if (node == null) {
|
||||
throw new DBException("Node '" + nodePath + "' not found");
|
||||
}
|
||||
@@ -603,72 +649,25 @@ public class WebServiceSQL implements DBWServiceSQL {
|
||||
return contextInfo.getProcessor().getWebSession().createAndRunAsyncTask("Read data from container " + nodePath, runnable);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public WebAsyncTaskInfo asyncNavigateForeignKey(
|
||||
public List<WebSQLQueryResultAssociation> getSqlResultAssociations(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull WebSQLContextInfo contextInfo,
|
||||
@NotNull String resultsId,
|
||||
@NotNull WebSQLResultsRow row,
|
||||
int columnIndex,
|
||||
@NotNull String associationName,
|
||||
boolean isReference,
|
||||
@Nullable WebDataFormat dataFormat
|
||||
) {
|
||||
WebAsyncTaskProcessor<String> runnable = new WebAsyncTaskProcessor<>() {
|
||||
@Override
|
||||
public void run(DBRProgressMonitor monitor) throws InvocationTargetException {
|
||||
try {
|
||||
monitor.beginTask("Navigate foreign key", 1);
|
||||
WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId);
|
||||
DBSEntity sourceEntity = resolveSourceEntity(resultsInfo.getAttributes(), columnIndex);
|
||||
DBSEntityAssociation association = isReference
|
||||
? DBStructUtils.findReverseAssociationByName(monitor, sourceEntity, associationName)
|
||||
: DBStructUtils.findForwardAssociationByName(monitor, sourceEntity, associationName);
|
||||
|
||||
WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider(
|
||||
resultsId,
|
||||
contextInfo,
|
||||
List.of(row)
|
||||
);
|
||||
DBDReferenceNavigation navigation = isReference
|
||||
? DBDReferenceUtils.resolveReferenceNavigation(monitor, dataProvider, association, dataProvider.getSelectedRows())
|
||||
: DBDReferenceUtils.resolveAssociationNavigation(monitor, dataProvider, association, dataProvider.getSelectedRows());
|
||||
if (!(navigation.getTargetEntity() instanceof DBSDataContainer targetDataContainer)) {
|
||||
throw new DBWebException("Referenced entity '" + navigation.getTargetEntity().getName() + "' is not a data container");
|
||||
}
|
||||
WebSQLExecuteInfo executeResults = contextInfo.getProcessor().readDataFromContainer(
|
||||
contextInfo,
|
||||
monitor,
|
||||
targetDataContainer,
|
||||
null,
|
||||
WebSQLDataFilter.from(navigation.getTargetFilter()),
|
||||
dataFormat
|
||||
);
|
||||
this.result = executeResults.getStatusMessage();
|
||||
this.extendedResults = executeResults;
|
||||
} catch (Throwable e) {
|
||||
throw new InvocationTargetException(e);
|
||||
} finally {
|
||||
monitor.done();
|
||||
}
|
||||
}
|
||||
};
|
||||
return webSession.createAndRunAsyncTask("Navigate foreign key from results " + resultsId, runnable);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private DBSEntity resolveSourceEntity(
|
||||
@NotNull DBDAttributeBinding[] attributes,
|
||||
int columnIndex
|
||||
@Nullable Boolean isReference
|
||||
) throws DBException {
|
||||
if (columnIndex < 0 || columnIndex >= attributes.length) {
|
||||
throw new DBWebException("Column index '" + columnIndex + "' is out of range");
|
||||
DBDAttributeBinding[] attributes = contextInfo.getResults(resultsId).getAttributes();
|
||||
if (isReference == null) {
|
||||
// Both forward associations and reverse references
|
||||
List<WebSQLQueryResultAssociation> associations =
|
||||
new ArrayList<>(WebSQLUtils.collectAssociations(webSession, attributes));
|
||||
associations.addAll(WebSQLUtils.collectReferences(webSession, attributes));
|
||||
return associations;
|
||||
}
|
||||
DBSEntityAttribute entityAttribute = attributes[columnIndex].getEntityAttribute();
|
||||
if (entityAttribute == null) {
|
||||
throw new DBException("Column [" + attributes[columnIndex].getName() + "] is not bound to any entity");
|
||||
}
|
||||
return entityAttribute.getParentObject();
|
||||
return isReference
|
||||
? WebSQLUtils.collectReferences(webSession, attributes)
|
||||
: WebSQLUtils.collectAssociations(webSession, attributes);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-Name: Cloudbeaver Web Service - Administration
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true
|
||||
Bundle-Version: 1.0.148.qualifier
|
||||
Bundle-Release-Date: 20260706
|
||||
Bundle-Version: 1.0.150.qualifier
|
||||
Bundle-Release-Date: 20260803
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Require-Bundle: io.cloudbeaver.server.ce,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.admin</artifactId>
|
||||
<version>1.0.148-SNAPSHOT</version>
|
||||
<version>1.0.150-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
|
||||
</project>
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,6 +25,7 @@ import io.cloudbeaver.service.DBWServletContext;
|
||||
import io.cloudbeaver.service.WebServiceBindingBase;
|
||||
import io.cloudbeaver.service.admin.impl.WebAdminLogsServlet;
|
||||
import io.cloudbeaver.service.admin.impl.WebServiceAdmin;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
@@ -205,7 +206,7 @@ public class WebServiceBindingAdmin extends WebServiceBindingBase<DBWServiceAdmi
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException {
|
||||
public void addServlets(@NotNull CBApplication application, @NotNull DBWServletContext servletContext) throws DBException {
|
||||
if(!application.isMultiuser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
+11
-96
@@ -19,20 +19,15 @@ package io.cloudbeaver.service.admin.impl;
|
||||
import io.cloudbeaver.*;
|
||||
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
|
||||
import io.cloudbeaver.model.WebPropertyInfo;
|
||||
import io.cloudbeaver.model.config.CBAppConfig;
|
||||
import io.cloudbeaver.model.config.CBServerConfig;
|
||||
import io.cloudbeaver.model.session.WebAuthInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.model.user.WebUser;
|
||||
import io.cloudbeaver.registry.*;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.server.CBConstants;
|
||||
import io.cloudbeaver.server.WebAppUtils;
|
||||
import io.cloudbeaver.server.*;
|
||||
import io.cloudbeaver.service.DBWServiceServerConfigurator;
|
||||
import io.cloudbeaver.service.admin.*;
|
||||
import io.cloudbeaver.service.security.SMUtils;
|
||||
import io.cloudbeaver.utils.ServletAppUtils;
|
||||
import io.cloudbeaver.utils.WebDataSourceUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
@@ -44,7 +39,6 @@ import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.auth.SMAuthConfiguration;
|
||||
import org.jkiss.dbeaver.model.auth.SMObjectType;
|
||||
import org.jkiss.dbeaver.model.connection.DBPDriver;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings;
|
||||
import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor;
|
||||
import org.jkiss.dbeaver.model.rm.RMProjectType;
|
||||
@@ -596,50 +590,12 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
@Override
|
||||
public boolean configureServer(@NotNull WebSession webSession, @NotNull Map<String, Object> params) throws DBWebException {
|
||||
try {
|
||||
CBAppConfig appConfig = new CBAppConfig(CBApplication.getInstance().getAppConfiguration());
|
||||
CBServerConfig serverConfig = new CBServerConfig();
|
||||
serverConfig.setServerName(CBApplication.getInstance().getServerName());
|
||||
serverConfig.setMaxSessionIdleTime(CBApplication.getInstance().getMaxSessionIdleTime());
|
||||
String adminName = null;
|
||||
String adminPassword = null;
|
||||
|
||||
if (!params.isEmpty()) { // FE can send an empty configuration
|
||||
var config = new AdminServerConfig(params);
|
||||
appConfig.setAnonymousAccessEnabled(config.isAnonymousAccessEnabled());
|
||||
appConfig.setSupportsCustomConnections(config.isCustomConnectionsEnabled());
|
||||
appConfig.setPublicCredentialsSaveEnabled(config.isPublicCredentialsSaveEnabled());
|
||||
appConfig.setAdminCredentialsSaveEnabled(config.isAdminCredentialsSaveEnabled());
|
||||
updateDisabledFeaturesConfig(appConfig, config.getEnabledFeatures());
|
||||
// custom logic for enabling embedded drivers
|
||||
updateDisabledDriversConfig(appConfig, config.getDisabledDrivers());
|
||||
appConfig.setResourceManagerEnabled(config.isResourceManagerEnabled());
|
||||
appConfig.setSecretManagerEnabled(config.isSecretManagerEnabled());
|
||||
|
||||
if (CommonUtils.isEmpty(config.getEnabledAuthProviders())) {
|
||||
// All of them
|
||||
appConfig.setEnabledAuthProviders(new String[0]);
|
||||
} else {
|
||||
appConfig.setEnabledAuthProviders(config.getEnabledAuthProviders().toArray(new String[0]));
|
||||
}
|
||||
|
||||
appConfig.setDefaultNavigatorSettings(
|
||||
CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings());
|
||||
|
||||
adminName = config.getAdminName();
|
||||
adminPassword = config.getAdminPassword();
|
||||
serverConfig.setServerName(config.getServerName());
|
||||
serverConfig.setMaxSessionIdleTime(config.getSessionExpireTime());
|
||||
if (config.getForceHttps() != null) {
|
||||
serverConfig.setForceHttps(config.getForceHttps());
|
||||
}
|
||||
if (config.getSupportedHosts() != null) {
|
||||
serverConfig.setSupportedHosts(config.getSupportedHosts());
|
||||
}
|
||||
if (config.getBindSessionToIp() != null) {
|
||||
serverConfig.setBindSessionToIp(config.getBindSessionToIp());
|
||||
}
|
||||
}
|
||||
|
||||
CBServerConfigurationMapper<?, ?> mapper = CBApplication.getInstance()
|
||||
.getServerConfigurationController().createServerConfigurationInputMapper();
|
||||
CBServerConfigurations configurations = mapper.loadServerConfigurationsFromParams(params);
|
||||
String adminName = configurations.adminName();
|
||||
String adminPassword = configurations.adminPassword();
|
||||
if (CommonUtils.isEmpty(adminName)) {
|
||||
// Grant admin permissions to the current user
|
||||
WebUser curUser = webSession.getUser();
|
||||
@@ -651,7 +607,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
if (CommonUtils.isEmpty(adminName)) {
|
||||
// Try to get admin name from existing authentications (first one)
|
||||
if (!authInfoList.isEmpty()) {
|
||||
adminName = authInfoList.get(0).getUserId();
|
||||
adminName = authInfoList.getFirst().getUserId();
|
||||
}
|
||||
}
|
||||
if (CommonUtils.isEmpty(adminName)) {
|
||||
@@ -666,7 +622,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
// Patch configuration by services
|
||||
for (DBWServiceServerConfigurator wsc : WebServiceRegistry.getInstance().getWebServices(DBWServiceServerConfigurator.class)) {
|
||||
try {
|
||||
wsc.configureServer(CBApplication.getInstance(), webSession, serverConfig, appConfig);
|
||||
wsc.configureServer(CBApplication.getInstance(), webSession, configurations.serverConfig(), configurations.appConfig());
|
||||
} catch (Exception e) {
|
||||
log.warn("Error configuring server by web service " + wsc.getClass().getName(), e);
|
||||
}
|
||||
@@ -678,8 +634,8 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
adminName,
|
||||
adminPassword,
|
||||
authInfos,
|
||||
serverConfig,
|
||||
appConfig,
|
||||
configurations.serverConfig(),
|
||||
configurations.appConfig(),
|
||||
webSession
|
||||
);
|
||||
|
||||
@@ -687,8 +643,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
if (configurationMode) {
|
||||
// In config mode we always refresh because admin user doesn't exist yet
|
||||
webSession.resetUserState();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Just reload session state
|
||||
webSession.refreshUserData();
|
||||
}
|
||||
@@ -700,46 +655,6 @@ public class WebServiceAdmin implements DBWServiceAdmin {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateDisabledFeaturesConfig(@NotNull CBAppConfig appConfig, @NotNull List<String> enabledFeatures) {
|
||||
Set<String> enabledIds = new LinkedHashSet<>(enabledFeatures);
|
||||
appConfig.setEnabledFeatures(enabledFeatures.toArray(new String[0]));
|
||||
String[] disabledFeatures = ServletAppUtils.getServletApplication().getFeatureRegistry().getWebFeatures().stream()
|
||||
.map(DBWFeatureSet::getId)
|
||||
.filter(id -> !enabledIds.contains(id))
|
||||
.toArray(String[]::new);
|
||||
appConfig.setDisabledFeatures(disabledFeatures);
|
||||
}
|
||||
|
||||
// we disable embedded drivers by default and enable it in enabled drivers list
|
||||
// that's why we need so complicated logic for disabling drivers
|
||||
private void updateDisabledDriversConfig(CBAppConfig appConfig, String[] disabledDriversConfig) {
|
||||
Set<String> disabledIds = new LinkedHashSet<>(Arrays.asList(disabledDriversConfig));
|
||||
Set<String> enabledIds = new LinkedHashSet<>(Arrays.asList(appConfig.getEnabledDrivers()));
|
||||
|
||||
// remove all disabled embedded drivers from enabled drivers list
|
||||
enabledIds.removeAll(disabledIds);
|
||||
|
||||
// enable embedded driver if it is not in disabled drivers list
|
||||
for (String driverId : appConfig.getDisabledDrivers()) {
|
||||
if (disabledIds.contains(driverId)) {
|
||||
// driver is also disabled
|
||||
continue;
|
||||
}
|
||||
// driver is removed from disabled list
|
||||
// we need to enable if it is embedded
|
||||
try {
|
||||
DBPDriver driver = WebDataSourceUtils.getDriverById(driverId);
|
||||
if (driver.isEmbedded()) {
|
||||
enabledIds.add(driverId);
|
||||
}
|
||||
} catch (DBWebException e) {
|
||||
log.error("Failed to find driver by id", e);
|
||||
}
|
||||
}
|
||||
appConfig.setDisabledDrivers(disabledDriversConfig);
|
||||
appConfig.setEnabledDrivers(enabledIds.toArray(String[]::new));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setDefaultNavigatorSettings(@NotNull WebSession webSession, @NotNull DBNBrowseSettings settings) throws DBWebException {
|
||||
CBApplication.getInstance().getAppConfiguration().setDefaultNavigatorSettings(settings);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
Manifest-Version: 1.0
|
||||
Bundle-ManifestVersion: 2
|
||||
Bundle-Name: Cloudbeaver AI Integration
|
||||
Bundle-SymbolicName: io.cloudbeaver.service.ai;singleton:=true
|
||||
Bundle-Version: 1.0.0.qualifier
|
||||
Bundle-Release-Date: 20260803
|
||||
Bundle-Vendor: DBeaver Corp
|
||||
Bundle-ActivationPolicy: lazy
|
||||
Automatic-Module-Name: io.cloudbeaver.service.ai
|
||||
Require-Bundle: org.jkiss.dbeaver.model.ai,
|
||||
io.cloudbeaver.model,
|
||||
io.cloudbeaver.server.ce
|
||||
Export-Package: io.cloudbeaver.service.ai,
|
||||
io.cloudbeaver.service.ai.model,
|
||||
io.cloudbeaver.service.ai.model.inputs
|
||||
@@ -0,0 +1,6 @@
|
||||
source..=src/
|
||||
output..=target/classes/
|
||||
bin.includes=.,\
|
||||
META-INF/,\
|
||||
schema/,\
|
||||
plugin.xml
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<plugin>
|
||||
<extension point="io.cloudbeaver.service">
|
||||
<service id="ai" label="AI Service" description="AI service"
|
||||
class="io.cloudbeaver.service.ai.gql.WebServiceBindingAI"/>
|
||||
<service id="ai.configurator" label="AI Configurator" description="AI configurator"
|
||||
class="io.cloudbeaver.service.ai.gql.WebServiceAIConfigurator">
|
||||
</service>
|
||||
</extension>
|
||||
|
||||
<extension point="io.cloudbeaver.object.feature.provider">
|
||||
<featureProvider id="AIFeatureProvider" class="io.cloudbeaver.service.ai.WebAIFeatureProvider"/>
|
||||
</extension>
|
||||
|
||||
<extension point="org.jkiss.dbeaver.ws.event">
|
||||
<clientEvent id="cb_client_ai_function_call_confirmation"
|
||||
class="io.cloudbeaver.service.ai.model.events.WSAiFunctionCallConfirmationClientEvent"
|
||||
handler="io.cloudbeaver.service.ai.model.events.WSAiFunctionCallConfirmationHandler"/>
|
||||
</extension>
|
||||
</plugin>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>io.cloudbeaver</groupId>
|
||||
<artifactId>bundles</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
<artifactId>io.cloudbeaver.service.ai</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>eclipse-plugin</packaging>
|
||||
</project>
|
||||
@@ -0,0 +1,58 @@
|
||||
extend enum CBServerEventId {
|
||||
cb_ai_chat_message_chunk @since(version: "25.1.1")
|
||||
cb_ai_chat_message_error @since(version: "25.1.1")
|
||||
cb_ai_chat_message @since(version: "25.3.5")
|
||||
}
|
||||
|
||||
extend enum CBClientEventId {
|
||||
cb_client_ai_function_call_confirmation @since(version: "26.0.3")
|
||||
}
|
||||
|
||||
extend enum CBEventTopic {
|
||||
cb_ai @since(version: "25.1.1")
|
||||
}
|
||||
|
||||
type WSAIChatMessageChunkEvent implements CBServerEvent @since(version: "25.1.1") {
|
||||
id: CBServerEventId!
|
||||
topicId: CBEventTopic
|
||||
conversationId: ID!
|
||||
messageId: ID!
|
||||
chunk: String
|
||||
completed: Boolean!
|
||||
}
|
||||
|
||||
type WSAIChatMessageErrorEvent implements CBServerEvent @since(version: "25.1.1") {
|
||||
id: CBServerEventId!
|
||||
topicId: CBEventTopic
|
||||
conversationId: ID!
|
||||
messageId: ID!
|
||||
errorMessage: String
|
||||
}
|
||||
|
||||
type WSAIChatMessageEvent implements CBServerEvent @since(version: "25.3.5") {
|
||||
id: CBServerEventId!
|
||||
topicId: CBEventTopic
|
||||
conversationId: ID!
|
||||
messageId: ID!
|
||||
role: String!
|
||||
content: String!
|
||||
displayMessage: String!
|
||||
time: String!
|
||||
"Present only with CONFIRMATION role"
|
||||
functionConfirmation: AIFunctionConfirmation
|
||||
"Present only with FUNCTION role"
|
||||
functionCall: AIFunctionCall
|
||||
"Present only with FUNCTION role"
|
||||
functionResult: AIFunctionResult
|
||||
}
|
||||
|
||||
type WSAIFunctionCallConfirmationEvent implements CBClientEvent @since(version: "26.0.3") {
|
||||
id: CBClientEventId!
|
||||
topicId: CBEventTopic
|
||||
conversationId: ID!
|
||||
messageId: ID!
|
||||
"Returns a list of confirmed functions"
|
||||
confirmedFunctionCalls: [ID!]
|
||||
"Returns a list of declined functions"
|
||||
declinedFunctionCalls: [ID!]
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
# AI admin queries
|
||||
enum AIMessageType {
|
||||
SYSTEM,
|
||||
USER,
|
||||
ASSISTANT,
|
||||
ERROR,
|
||||
WARNING,
|
||||
CONFIRMATION,
|
||||
ATTACHMENT,
|
||||
FUNCTION
|
||||
}
|
||||
|
||||
"""
|
||||
Scope of the AI chat conversation.
|
||||
Can be a database connection, a current database, a current schema or a custom scope.
|
||||
"""
|
||||
enum AIDatabaseScope @since(version: "25.1.1") {
|
||||
CURRENT_SCHEMA
|
||||
CURRENT_DATABASE
|
||||
CURRENT_DATASOURCE
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
type AISettingsInfo @since(version: "23.2.2") {
|
||||
activeEngine: ID!
|
||||
defaultConfiguration: ID
|
||||
language: String
|
||||
}
|
||||
|
||||
type AIEngineInfo @since(version: "23.2.2") {
|
||||
"Unique identifier of the AI engine."
|
||||
id: ID!
|
||||
"Name of the AI engine."
|
||||
name: String!
|
||||
}
|
||||
|
||||
type AIChatConversationInfo @since(version: "25.1.1") {
|
||||
dataSourceId: DataSourceId
|
||||
id: ID!
|
||||
promptGeneratorId: ID
|
||||
caption: String!
|
||||
"Last message creation time."
|
||||
time: DateTime!
|
||||
messages: [AIMessage!]!
|
||||
settings: AIChatConversationSettings
|
||||
waitingForResponse: Boolean! @since(version: "25.1.5")
|
||||
metrics: AIChatConversationMetrics @since(version: "25.3.5")
|
||||
"ID of the AI engine configuration profile used for this conversation."
|
||||
profile: ID @since(version: "26.1.3")
|
||||
}
|
||||
|
||||
type AIChatConversationMetrics @since(version: "25.3.5") {
|
||||
totalInputTokens: Int!
|
||||
totalOutputTokens: Int!
|
||||
}
|
||||
|
||||
type AIChatConversationSettings {
|
||||
metaTransferConfirmed: Boolean!
|
||||
scope: AIDatabaseScope
|
||||
"If the scope is set to CUSTOM, this field is required."
|
||||
customObjectIds: [ID!]
|
||||
}
|
||||
|
||||
type AIMessage {
|
||||
conversationId: ID!
|
||||
id: ID!
|
||||
role: AIMessageType!
|
||||
content: String!
|
||||
displayMessage: String!
|
||||
time: DateTime!
|
||||
"Present only with CONFIRMATION role"
|
||||
functionConfirmation: AIFunctionConfirmation
|
||||
"Present only with FUNCTION role"
|
||||
functionCall: AIFunctionCall
|
||||
"Present only with FUNCTION role"
|
||||
functionResult: AIFunctionResult
|
||||
}
|
||||
|
||||
type AISendChatMessageInfo @since(version: "25.1.1") {
|
||||
conversation: AIChatConversationInfo!
|
||||
userMessage: AIMessage!
|
||||
"Message that would be received via websockets."
|
||||
assistantMessage: AIMessage!
|
||||
}
|
||||
|
||||
type AIDataSourceSettingsInfo @since(version: "25.3.3") {
|
||||
"Indicates whether user confirmed meta transfer to AI for the current data source."
|
||||
metaTransferConfirmed: Boolean!
|
||||
"Default scope for AI operations in this data source."
|
||||
scope: AIDatabaseScope
|
||||
"If the scope is set to CUSTOM, this field is required."
|
||||
customObjectIds: [ID!]
|
||||
}
|
||||
|
||||
type DataSourceId @since(version: "25.1.1") {
|
||||
projectId: ID!
|
||||
connectionId: ID!
|
||||
}
|
||||
|
||||
type AIConfigurationProfileInfo @since(version: "26.1.3") {
|
||||
"Unique identifier of the AI engine profile."
|
||||
id: ID!
|
||||
"Name of the AI engine profile."
|
||||
name: String!
|
||||
"ID of the AI engine that this profile belongs to."
|
||||
engineId: ID!
|
||||
}
|
||||
|
||||
type AIAdminConfigurationProfileInfo @since(version: "26.1.3") {
|
||||
"Unique identifier of the AI engine profile."
|
||||
id: ID!
|
||||
"Name of the AI engine profile."
|
||||
name: String!
|
||||
"ID of the AI engine that this profile belongs to."
|
||||
engineId: ID!
|
||||
"Configuration of the AI engine profile."
|
||||
configuration: [ObjectPropertyInfo!]!
|
||||
}
|
||||
|
||||
type AIFunction @since(version: "26.0.4") {
|
||||
"Unique identifier of the function (combines toolbox ID and function ID)"
|
||||
id: ID!
|
||||
"Human-readable name of the function"
|
||||
name: String!
|
||||
"Detailed description of what the function does"
|
||||
description: String
|
||||
"URL or path to the function icon"
|
||||
icon: String
|
||||
"Indicates whether the function is a system function (can be auto-confirmed when required)"
|
||||
system: Boolean!
|
||||
}
|
||||
|
||||
type AIFunctionCall @since(version: "26.0.3") {
|
||||
id: ID!
|
||||
functionName: String!
|
||||
arguments: Object
|
||||
hint: String
|
||||
}
|
||||
|
||||
type AIFunctionConfirmation @since(version: "26.0.3") {
|
||||
functionCalls: [AIFunctionCall!]!
|
||||
}
|
||||
|
||||
enum AIFunctionType {
|
||||
ACTION
|
||||
INFORMATION
|
||||
}
|
||||
|
||||
type AIFunctionResult @since(version: "26.0.3") {
|
||||
type: AIFunctionType!
|
||||
value: Object!
|
||||
}
|
||||
|
||||
input AISettingsConfig @since(version: "23.2.2") {
|
||||
"Default configuration profile ID. May be null if AI wasn't configured"
|
||||
defaultConfiguration: ID
|
||||
"Language to be used as a default for AI responses"
|
||||
language: String
|
||||
}
|
||||
|
||||
input AIEngineConfig @since(version: "23.2.2") {
|
||||
properties: Object
|
||||
}
|
||||
|
||||
input AIDatabaseScopeInput {
|
||||
id: AIDatabaseScope!
|
||||
"If custom scope is selected, this field is required."
|
||||
customScopes: [ID!]
|
||||
}
|
||||
|
||||
input DataSourceIdInput @since(version: "25.1.1") {
|
||||
projectId: ID!
|
||||
connectionId: ID!
|
||||
}
|
||||
|
||||
input AIChatConversationInput {
|
||||
dataSourceId: DataSourceIdInput
|
||||
caption: String
|
||||
settings: AIChatConversationSettingsInput
|
||||
"ID of the prompt generator to use for this conversation (sql - by default)."
|
||||
promptGeneratorId: ID
|
||||
}
|
||||
|
||||
input AIChatConversationSettingsInput {
|
||||
metaTransferConfirmed: Boolean
|
||||
scope: AIDatabaseScope
|
||||
profile: ID
|
||||
customObjectIds: [ID!]
|
||||
}
|
||||
|
||||
input AIDataSourceSettingsInput @since(version: "25.3.3") {
|
||||
"Flag indicating whether MCP integration is enabled for the current data source."
|
||||
mcpEnabled: Boolean
|
||||
"Flag indicating whether user confirmed meta transfer to AI."
|
||||
metaTransferConfirmed: Boolean
|
||||
"Default scope for AI operations in this data source."
|
||||
scope: AIDatabaseScope
|
||||
"If custom scope is selected, this field is required."
|
||||
customObjectIds: [ID!]
|
||||
}
|
||||
|
||||
input AIConfigurationProfileInput @since(version: "26.1.3") {
|
||||
"Unique identifier of the AI engine profile."
|
||||
profileId: ID!
|
||||
"Name of the AI engine profile."
|
||||
profileName: String
|
||||
"ID of the AI engine that this profile belongs to."
|
||||
engineId: ID
|
||||
"Configuration of the AI engine profile."
|
||||
configuration: AIEngineConfig
|
||||
}
|
||||
|
||||
extend type Query @since(version: "23.2.2") {
|
||||
"Returns the list of available AI engines."
|
||||
aiListEngines: [AIEngineInfo!]
|
||||
"Returns the global AI settings."
|
||||
aiSettings: AISettingsInfo!
|
||||
"Returns the properties of the specified AI engine for displaying it in the UI. In case settings are passed, fills it with defaults and returns back."
|
||||
aiListEngineProperties(engineId: ID!, profileId: ID, settings : AIEngineConfig): [ObjectPropertyInfo!]!
|
||||
|
||||
"Return the list of available AI functions."
|
||||
aiListFunctions: [AIFunction!] @since(version: "26.0.4")
|
||||
|
||||
"Returns the list of available AI engine configuration profiles."
|
||||
aiListProfiles: [AIConfigurationProfileInfo!]! @since(version: "26.1.3")
|
||||
|
||||
"Returns AI chat conversation info for the specified conversation ID."
|
||||
aiChatConversationInfo(conversationId: ID!, loadMetrics: Boolean): AIChatConversationInfo! @since(version: "25.1.1")
|
||||
"Returns list of AI chat conversations for the specified connection."
|
||||
aiListChatConversations(dataSourceId: DataSourceIdInput): [AIChatConversationInfo!]! @since(version: "25.1.1")
|
||||
|
||||
"Returns AI settings for the specified connection."
|
||||
aiDataSourceSettings(dataSourceId: DataSourceIdInput!): AIDataSourceSettingsInfo! @since(version: "25.3.3")
|
||||
}
|
||||
|
||||
extend type Mutation @since(version: "23.2.2") {
|
||||
"Saves the global AI settings (active AI engine)."
|
||||
aiSaveSettings(settings: AISettingsConfig!): AISettingsInfo!
|
||||
"Saves the configuration of the specified AI engine."
|
||||
aiSaveEngineConfiguration(profileId: ID!, settings : AIEngineConfig!): Boolean! @deprecated(reason: "use profiles api")
|
||||
"Creates an anync task to perform AI query completion."
|
||||
asyncAiPerformQueryCompletion(projectId: ID, connectionId: ID!, contextId: ID!, request: String!): AsyncTaskInfo!
|
||||
"Retrieves the result of an async AI query completion task."
|
||||
asyncAiPerformQueryCompletionResult(taskId: ID!): String
|
||||
|
||||
"Creates a new AI engine configuration profile."
|
||||
aiCreateProfile(config: AIConfigurationProfileInput!): AIAdminConfigurationProfileInfo! @since(version: "26.1.3")
|
||||
"Updates the configuration of the specified AI engine configuration profile."
|
||||
aiUpdateProfile(config: AIConfigurationProfileInput!): AIAdminConfigurationProfileInfo! @since(version: "26.1.3")
|
||||
"Deletes the specified AI engine configuration profile."
|
||||
aiDeleteProfile(profileId: ID!): Boolean! @since(version: "26.1.3")
|
||||
|
||||
"""
|
||||
Creates a new AI chat conversation.
|
||||
Conversation is created in the context of a project and connection.
|
||||
If the connection and project are not specified, the conversation will be created without a context.
|
||||
"""
|
||||
aiCreateChatConversation(config: AIChatConversationInput!): AIChatConversationInfo! @since(version: "25.1.1")
|
||||
|
||||
"Updates the caption of the AI chat conversation"
|
||||
aiUpdateChatConversation(conversationId: ID!, config: AIChatConversationInput!): AIChatConversationInfo! @since(version: "25.1.1")
|
||||
|
||||
"Deletes the specified AI chat conversation"
|
||||
aiDeleteChatConversation(conversationId: ID!): Boolean! @since(version: "25.1.1")
|
||||
|
||||
"Sends a chat message to the AI chat conversation and returns a message ID from response. The response will be received partially via websockets."
|
||||
aiSendChatMessage(conversationId: ID!, prompt: String!): AISendChatMessageInfo! @since(version: "25.1.1")
|
||||
|
||||
"Clears the chat messages in the AI chat conversation. The messages will be removed from the conversation."
|
||||
aiClearLastChatMessages(conversationId: ID!, messageId: ID!): Boolean! @since(version: "25.1.1")
|
||||
|
||||
"Saves AI settings (e.g. supporting confirming metadata transfer) for the specified connection."
|
||||
aiSaveDataSourceSettings(dataSourceId: DataSourceIdInput!, settings: AIDataSourceSettingsInput!): AIDataSourceSettingsInfo! @since(version: "25.3.3")
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp
|
||||
*
|
||||
* All Rights Reserved.
|
||||
*
|
||||
* NOTICE: All information contained herein is, and remains
|
||||
* the property of DBeaver Corp and its suppliers, if any.
|
||||
* The intellectual and technical concepts contained
|
||||
* herein are proprietary to DBeaver Corp and its suppliers
|
||||
* and may be covered by U.S. and Foreign Patents,
|
||||
* patents in process, and are protected by trade secret or copyright law.
|
||||
* Dissemination of this information or reproduction of this material
|
||||
* is strictly forbidden unless prior written permission is obtained
|
||||
* from DBeaver Corp.
|
||||
*/
|
||||
package io.cloudbeaver.service.ai;
|
||||
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.service.navigator.DBWFeatureProvider;
|
||||
import io.cloudbeaver.utils.ServletAppUtils;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.model.ai.utils.AIUtils;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNDatabaseNode;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WebAIFeatureProvider implements DBWFeatureProvider {
|
||||
public static final String AI_FEATURE_ID = "ai";
|
||||
private static final String FEATURE_CAN_DESCRIBE_OBJECT = "canDescribeObject";
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<String> getNodeFeatures(@NotNull WebSession webSession, @NotNull DBNNode node) {
|
||||
List<String> features = new ArrayList<>();
|
||||
if (!ServletAppUtils.getServletApplication().getAppConfiguration().isFeatureEnabled(AI_FEATURE_ID)) {
|
||||
return features;
|
||||
}
|
||||
if (node instanceof DBNDatabaseNode element && AIUtils.isEligible(element.getObject())) {
|
||||
features.add(FEATURE_CAN_DESCRIBE_OBJECT);
|
||||
}
|
||||
return features;
|
||||
}
|
||||
}
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.service.ai;
|
||||
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.service.ai.model.WebAIChatConversation;
|
||||
import io.cloudbeaver.service.ai.model.WebAIMessage;
|
||||
import io.cloudbeaver.service.ai.model.WebAISendChatMessageInfo;
|
||||
import io.cloudbeaver.service.ai.model.WebAiChatResponseConsumer;
|
||||
import io.cloudbeaver.service.ai.model.events.WSAiChatMessageEvent;
|
||||
import io.cloudbeaver.utils.ServletAppUtils;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
|
||||
import org.jkiss.dbeaver.model.DBUtils;
|
||||
import org.jkiss.dbeaver.model.ai.*;
|
||||
import org.jkiss.dbeaver.model.ai.qm.AIChatStorage;
|
||||
import org.jkiss.dbeaver.model.ai.quota.UserTokenQuotaService;
|
||||
import org.jkiss.dbeaver.model.ai.registry.AIAssistantRegistry;
|
||||
import org.jkiss.dbeaver.model.ai.utils.AIUtils;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNDatabaseNode;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNNode;
|
||||
import org.jkiss.dbeaver.model.navigator.DBNUtils;
|
||||
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
|
||||
import org.jkiss.dbeaver.model.struct.DBSObject;
|
||||
import org.jkiss.dbeaver.utils.RuntimeUtils;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class WebAIUtils {
|
||||
private static final Log log = Log.getLog(WebAIUtils.class);
|
||||
private static final String AI_WAITING_ATTR = "ai.waiting.";
|
||||
public static final String AI_CHAT_ATTR = "ai_chat";
|
||||
public static final String AI_CHAT_USER_ATTR = "ai_chat_user";
|
||||
|
||||
@NotNull
|
||||
public static List<String> convertObjectIdsToNodePaths(
|
||||
@NotNull DBRProgressMonitor monitor,
|
||||
@NotNull DBPProject project,
|
||||
@NotNull String[] objectIds
|
||||
) {
|
||||
List<String> customObjectIdList = new ArrayList<>();
|
||||
for (String id : objectIds) {
|
||||
if (id != null && !id.isEmpty()) {
|
||||
try {
|
||||
DBSObject dbpObject = DBUtils.findObjectById(monitor, project, id);
|
||||
if (dbpObject != null) {
|
||||
DBNDatabaseNode dbNode = DBNUtils.getNodeByObject(monitor, dbpObject, false);
|
||||
if (dbNode != null) {
|
||||
customObjectIdList.add(dbNode.getNodeUri());
|
||||
}
|
||||
}
|
||||
} catch (DBException e) {
|
||||
log.error("Error finding object by ID: " + id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return customObjectIdList;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String[] convertNodePathsToObjectIds(
|
||||
@NotNull WebSession session,
|
||||
@NotNull DBPProject project,
|
||||
@NotNull String[] nodePaths
|
||||
) {
|
||||
List<DBNNode> nodes = new ArrayList<>();
|
||||
for (String nodePath : nodePaths) {
|
||||
if (nodePath != null && !nodePath.isEmpty()) {
|
||||
try {
|
||||
DBNNode node = session.getNavigatorModelOrThrow().getNodeByPath(session.getProgressMonitor(), project, nodePath);
|
||||
if (node != null) {
|
||||
nodes.add(node);
|
||||
}
|
||||
} catch (DBException e) {
|
||||
log.error("Error finding object by ID: " + nodePath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodes.stream()
|
||||
.map(DBNDatabaseNode.class::cast)
|
||||
.map(DBNDatabaseNode::getValueObject)
|
||||
.map(DBSObject.class::cast)
|
||||
.map(DBUtils::getObjectFullId)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AIChatSession findAiChatSession(@NotNull WebSession webSession) {
|
||||
return webSession.getAttribute(AI_CHAT_ATTR);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AIContextSettingsChatConversation getConversationSettings(
|
||||
@NotNull AIChatSession chatSession,
|
||||
@NotNull AIChatConversation conversation
|
||||
) {
|
||||
DBPDataSourceContainer container = conversation.getDataSource();
|
||||
if (container == null) {
|
||||
return null;
|
||||
}
|
||||
AIContextSettingsChatConversation chatContextSettings = conversation.getCustomSettings();
|
||||
if (chatContextSettings == null) {
|
||||
chatContextSettings = new AIContextSettingsChatConversation(chatSession, conversation);
|
||||
conversation.setCustomSettings(chatContextSettings);
|
||||
}
|
||||
chatContextSettings.loadDataSourceDefaults();
|
||||
chatSession.updateScopeSettingsIfNeeded(chatContextSettings, container);
|
||||
return chatContextSettings;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static CompletableFuture<AIChatConversation> scheduleConversationSubmission(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull AIChatSession aiChatSession,
|
||||
@NotNull AIChatConversation conversation,
|
||||
@Nullable AIConfirmation confirmation,
|
||||
@NotNull String jobName
|
||||
) {
|
||||
webSession.setAttribute(getWaitingAttr(conversation), true);
|
||||
CompletableFuture<AIChatConversation> result = new CompletableFuture<>();
|
||||
RuntimeUtils.scheduleJob(
|
||||
jobName, monitor -> {
|
||||
try {
|
||||
AIChatResponseConsumer subscriber = new WebAiChatResponseConsumer(conversation, webSession, aiChatSession);
|
||||
aiChatSession.processAICompletion(
|
||||
monitor,
|
||||
conversation,
|
||||
subscriber,
|
||||
WebAIUtils.getConversationSettings(aiChatSession, conversation),
|
||||
confirmation
|
||||
).whenComplete((submittedConversation, error) -> {
|
||||
if (error != null) {
|
||||
result.completeExceptionally(error);
|
||||
} else {
|
||||
result.complete(submittedConversation);
|
||||
}
|
||||
});
|
||||
} catch (DBException e) {
|
||||
log.error("Error processing AI completion", e);
|
||||
var errorMessage = conversation.addMessage(AIMessage.errorMessage(e));
|
||||
webSession.addSessionEvent(new WSAiChatMessageEvent(new WebAIMessage(errorMessage, conversation)));
|
||||
aiChatSession.notifyMessageAdd(conversation, errorMessage);
|
||||
} finally {
|
||||
webSession.removeAttribute(getWaitingAttr(conversation));
|
||||
}
|
||||
}
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String getWaitingAttr(@NotNull AIChatConversation conversation) {
|
||||
return AI_WAITING_ATTR + conversation.getId();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static AIChatSession getAiChatSession(@NotNull WebSession webSession) throws DBWebException {
|
||||
validateAiPluginEnabled();
|
||||
String aiChatSessionUser = webSession.getAttribute(AI_CHAT_USER_ATTR);
|
||||
boolean sameUser = webSession.isAuthorizedInSecurityManager() &&
|
||||
CommonUtils.equalObjects(webSession.getUserId(), aiChatSessionUser);
|
||||
if (sameUser || webSession.getUserId() == null) {
|
||||
AIChatSession aiChatSession = webSession.getAttribute(AI_CHAT_ATTR);
|
||||
if (aiChatSession != null) {
|
||||
return aiChatSession;
|
||||
}
|
||||
}
|
||||
return createChatSession(webSession);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static AIChatSession createChatSession(@NotNull WebSession webSession) {
|
||||
AIAssistant assistant = AIAssistantRegistry.getInstance().getAssistant(webSession.getWorkspace());
|
||||
AIChatStorage qmChatStorage = assistant.createChatStorage();
|
||||
|
||||
AIChatSession aiChatSession = new AIChatSession(
|
||||
webSession.getWorkspace(),
|
||||
dataSourceContainer -> DBUtils.getDefaultContext(dataSourceContainer, false),
|
||||
qmChatStorage,
|
||||
assistant.getChatSessionProvider(),
|
||||
new UserTokenQuotaService(Clock.systemUTC(), qmChatStorage)
|
||||
);
|
||||
aiChatSession.addListener(new AIChatListener() {
|
||||
@Override
|
||||
public void messageAdded(@NotNull AIChatConversation conversation, @NotNull AIChatMessage message) {
|
||||
if (message.message().getConfirmation() instanceof AIFunctionCallConfirmation fcc) {
|
||||
for (AIFunctionCall fc : fcc.getFunctionCalls()) {
|
||||
// functions are not resolved at the current step, we need to resolve them first
|
||||
// otherwise no information about a function will be sent
|
||||
AIFunctionDescriptor function = fc.getOrResolveFunction(aiChatSession.getAssistant().getToolboxManager());
|
||||
if (function == null) {
|
||||
log.warn("Function is not found for function call " + fc.getFunctionName());
|
||||
}
|
||||
}
|
||||
WebAIMessage confirmationMessage = new WebAIMessage(message, conversation);
|
||||
WSAiChatMessageEvent event = new WSAiChatMessageEvent(confirmationMessage);
|
||||
webSession.addSessionEvent(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
webSession.setAttribute(AI_CHAT_ATTR, aiChatSession);
|
||||
if (webSession.getUser() != null) {
|
||||
String userId = webSession.getUserId();
|
||||
webSession.setAttribute(AI_CHAT_USER_ATTR, userId);
|
||||
}
|
||||
return aiChatSession;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static AIChatConversation getAiChatConversation(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull String conversationId
|
||||
) throws DBWebException {
|
||||
try {
|
||||
AIChatSession aiChatSession = getAiChatSession(webSession);
|
||||
AIChatConversation conversation = aiChatSession.getConversation(conversationId);
|
||||
if (conversation == null) {
|
||||
throw new DBWebException("Invalid chat conversation ID " + conversationId);
|
||||
}
|
||||
return conversation;
|
||||
} catch (DBException e) {
|
||||
throw new DBWebException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static WebAISendChatMessageInfo submitPrompt(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull AIChatSession aiChatSession,
|
||||
@NotNull AIChatConversation conversation,
|
||||
@NotNull AIMessage message
|
||||
) throws DBException {
|
||||
if (!isMetaTransferConfirmed(conversation)) {
|
||||
throw new DBWebException("AI services restricted for '%s'. Please contact your administrator if you need it.".formatted(
|
||||
conversation.getDataSource()));
|
||||
}
|
||||
String caption = conversation.getCaption();
|
||||
AIChatMessage promptMessage = conversation.addMessage(message);
|
||||
webSession.addSessionEvent(new WSAiChatMessageEvent(new WebAIMessage(promptMessage, conversation)));
|
||||
aiChatSession.notifyMessageAdd(conversation, promptMessage);
|
||||
if (!CommonUtils.equalObjects(caption, conversation.getCaption())) {
|
||||
aiChatSession.notifyConversationRenamed(conversation, conversation.getCaption());
|
||||
}
|
||||
if (!AIUtils.hasValidConfiguration()) {
|
||||
throw new DBWebException("Invalid AI configuration");
|
||||
}
|
||||
if (webSession.getAttribute(WebAIUtils.getWaitingAttr(conversation)) != null) {
|
||||
throw new DBWebException("Conversation is already waiting for response");
|
||||
}
|
||||
AIChatMessage result = new AIChatMessage(conversation.getNextMessageId(), AIMessage.assistantMessage("", null));
|
||||
WebAIUtils.scheduleConversationSubmission(webSession, aiChatSession, conversation, null, "AI completion");
|
||||
return new WebAISendChatMessageInfo(
|
||||
new WebAIChatConversation(webSession, conversation),
|
||||
new WebAIMessage(promptMessage, conversation),
|
||||
new WebAIMessage(result, conversation)
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean isMetaTransferConfirmed(@NotNull AIChatConversation conversation) {
|
||||
DBPDataSourceContainer dataSourceContainer = conversation.getDataSource();
|
||||
if (dataSourceContainer == null) {
|
||||
return true;
|
||||
}
|
||||
AIContextSettingsDataSource dsSettings = new AIContextSettingsDataSource(dataSourceContainer);
|
||||
return dsSettings.isMetaTransferConfirmed();
|
||||
}
|
||||
|
||||
|
||||
public static void validateAiPluginEnabled() throws DBWebException {
|
||||
var isAiEnabled = ServletAppUtils.getServletApplication().getAppConfiguration()
|
||||
.isFeatureEnabled(WebAIFeatureProvider.AI_FEATURE_ID);
|
||||
if (!isAiEnabled) {
|
||||
throw new DBWebException("AI feature is disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.service.ai;
|
||||
|
||||
import io.cloudbeaver.DBWebException;
|
||||
import io.cloudbeaver.service.ai.gql.WebAISettingsConfig;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.model.ai.AIConstants;
|
||||
import org.jkiss.dbeaver.runtime.DBWorkbench;
|
||||
|
||||
public class WebServiceAIManager {
|
||||
|
||||
public WebAISettingsConfig getAiSettings() throws DBWebException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getAiLanguage() {
|
||||
return DBWorkbench.getPlatform().getPreferenceStore()
|
||||
.getString(AIConstants.AI_RESPONSE_LANGUAGE);
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.cloudbeaver.service.ai.gql;
|
||||
|
||||
import io.cloudbeaver.*;
|
||||
import io.cloudbeaver.model.WebAsyncTaskInfo;
|
||||
import io.cloudbeaver.model.WebPropertyInfo;
|
||||
import io.cloudbeaver.model.session.WebSession;
|
||||
import io.cloudbeaver.service.DBWService;
|
||||
import io.cloudbeaver.service.ai.model.*;
|
||||
import io.cloudbeaver.service.ai.model.inputs.DataSourceId;
|
||||
import io.cloudbeaver.service.ai.model.inputs.WebAIChatConversationInput;
|
||||
import io.cloudbeaver.service.ai.model.inputs.WebAIConfigurationProfileInput;
|
||||
import io.cloudbeaver.service.ai.model.inputs.WebAiChatCompletionSettingsInput;
|
||||
import io.cloudbeaver.service.sql.WebSQLContextInfo;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.model.rm.RMConstants;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Service for executing GraphQL functions for AI module.
|
||||
*/
|
||||
public interface DBWServiceAI extends DBWService {
|
||||
|
||||
@NotNull
|
||||
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
|
||||
WebAISettingsConfig getAiSettings() throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
|
||||
List<WebAIEngine> getEngineConfigurations() throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
|
||||
List<WebPropertyInfo> getEngineConfigurationParameters(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull String engineId,
|
||||
@Nullable String profileId,
|
||||
@Nullable Map<String, Object> engineSettings
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
List<WebAIFunctionInfo> getFunctions(@NotNull WebSession webSession) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
List<WebAIConfigurationProfile> getProfiles(@NotNull WebSession webSession) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
|
||||
WebAISettingsConfig saveAiSettings(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull WebAISettingsConfig config
|
||||
) throws DBWebException;
|
||||
|
||||
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
|
||||
boolean saveEngineConfiguration(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull String profileId,
|
||||
@WebParameterSecure @NotNull Map<String, Object> engineSettings
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
WebAsyncTaskInfo performQueryCompletion(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull WebSQLContextInfo contextInfo,
|
||||
@NotNull String request
|
||||
) throws DBWebException;
|
||||
|
||||
@Nullable
|
||||
@WebAction
|
||||
String performQueryCompletionResult(@NotNull WebSession webSession, @NotNull String taskId) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
WebAIChatConversation createChat(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull WebAIChatConversationInput conversationInput
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
WebAIChatConversation updateChatConversation(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull String conversationId,
|
||||
@NotNull WebAIChatConversationInput conversationInput
|
||||
) throws DBWebException;
|
||||
|
||||
@WebAction
|
||||
boolean deleteChatConversation(@NotNull WebSession webSession, @NotNull String conversationId) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
List<WebAIChatConversation> getChatConversations(
|
||||
@NotNull WebSession webSession,
|
||||
@Nullable DataSourceId dataSourceId
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
WebAIChatConversation getChatConversationInfo(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull String conversationId,
|
||||
@Nullable Boolean loadMetrics
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
WebAISendChatMessageInfo asyncSendChatMessage(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull String conversationId,
|
||||
@NotNull String prompt
|
||||
) throws DBWebException;
|
||||
|
||||
@WebAction
|
||||
boolean setLastChatMessage(
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull String conversationId,
|
||||
@NotNull String messageId
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction
|
||||
WebAIDataSourceSettings getDataSourceAiSettings(
|
||||
@NotNull HttpServletRequest request,
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull DataSourceId dataSourceId
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebProjectAction(requireProjectPermissions = RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT)
|
||||
WebAIDataSourceSettings saveDataSourceAiSettings(
|
||||
@NotNull HttpServletRequest request,
|
||||
@NotNull WebSession webSession,
|
||||
@NotNull @WebObjectId String projectId,
|
||||
@NotNull DataSourceId dataSourceId,
|
||||
@NotNull WebAiChatCompletionSettingsInput settings
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
|
||||
WebAIConfigurationProfile createProfile(
|
||||
@NotNull WebSession webSession,
|
||||
@WebParameterSecure @NotNull WebAIConfigurationProfileInput config
|
||||
) throws DBWebException;
|
||||
|
||||
@NotNull
|
||||
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
|
||||
WebAIConfigurationProfile updateProfile(
|
||||
@NotNull WebSession webSession,
|
||||
@WebParameterSecure @NotNull WebAIConfigurationProfileInput config
|
||||
) throws DBWebException;
|
||||
|
||||
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
|
||||
boolean deleteProfile(@NotNull WebSession webSession, @NotNull String profileId) throws DBWebException;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2024 DBeaver Corp
|
||||
*
|
||||
* All Rights Reserved.
|
||||
*
|
||||
* NOTICE: All information contained herein is, and remains
|
||||
* the property of DBeaver Corp and its suppliers, if any.
|
||||
* The intellectual and technical concepts contained
|
||||
* herein are proprietary to DBeaver Corp and its suppliers
|
||||
* and may be covered by U.S. and Foreign Patents,
|
||||
* patents in process, and are protected by trade secret or copyright law.
|
||||
* Dissemination of this information or reproduction of this material
|
||||
* is strictly forbidden unless prior written permission is obtained
|
||||
* from DBeaver Corp.
|
||||
*/
|
||||
package io.cloudbeaver.service.ai.gql;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.model.ai.registry.AIEngineDescriptor;
|
||||
import org.jkiss.dbeaver.model.meta.Property;
|
||||
|
||||
public class WebAIEngine {
|
||||
|
||||
@NotNull
|
||||
private final AIEngineDescriptor descriptor;
|
||||
|
||||
public WebAIEngine(@NotNull AIEngineDescriptor descriptor) {
|
||||
this.descriptor = descriptor;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public String getId() {
|
||||
return descriptor.getId();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Property
|
||||
public String getName() {
|
||||
return descriptor.getLabel();
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user