Merge branch 'master' into feature/mongosh

This commit is contained in:
SPRINX0\prochazka
2025-07-24 17:14:59 +02:00
48 changed files with 2248 additions and 280 deletions

View File

@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro path: dbgate-pro
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6 ref: 8a4dc2732a7097b5c4c48b4feb62609111cdf3e0
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

View File

@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro path: dbgate-pro
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6 ref: 8a4dc2732a7097b5c4c48b4feb62609111cdf3e0
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

View File

@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro path: dbgate-pro
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6 ref: 8a4dc2732a7097b5c4c48b4feb62609111cdf3e0
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

View File

@@ -44,7 +44,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro path: dbgate-pro
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6 ref: 8a4dc2732a7097b5c4c48b4feb62609111cdf3e0
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

View File

@@ -32,7 +32,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro path: dbgate-pro
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6 ref: 8a4dc2732a7097b5c4c48b4feb62609111cdf3e0
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro
@@ -98,3 +98,8 @@ jobs:
cd .. cd ..
cd dbgate-merged/plugins/dbgate-plugin-cosmosdb cd dbgate-merged/plugins/dbgate-plugin-cosmosdb
npm publish npm publish
- name: Publish dbgate-plugin-firestore
run: |
cd ..
cd dbgate-merged/plugins/dbgate-plugin-firestore
npm publish

View File

@@ -26,7 +26,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro path: dbgate-pro
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6 ref: 8a4dc2732a7097b5c4c48b4feb62609111cdf3e0
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

41
.vscode/launch.json vendored
View File

@@ -1,20 +1,41 @@
{ {
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Debug App",
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Launch API",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/packages/api/src/index.js", "program": "${workspaceFolder}/packages/api/src/index.js",
"outFiles": [ "envFile": "${workspaceFolder}/packages/api/.env",
"${workspaceFolder}/**/*.js" "args": ["--listen-api"],
] "console": "integratedTerminal",
"restart": true,
"runtimeExecutable": "node",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "Debug App (Break on Start)",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/packages/api/src/index.js",
"args": ["--listen-api"],
"envFile": "${workspaceFolder}/.env",
"console": "integratedTerminal",
"restart": true,
"runtimeExecutable": "node",
"skipFiles": ["<node_internals>/**"],
"stopOnEntry": true
},
{
"name": "Attach to Process",
"type": "node",
"request": "attach",
"port": 9229,
"restart": true,
"localRoot": "${workspaceFolder}",
"remoteRoot": "${workspaceFolder}",
"skipFiles": ["<node_internals>/**"]
} }
] ]
} }

View File

@@ -303,7 +303,8 @@ describe('Data browser data', () => {
}); });
it('Plugin tab', () => { it('Plugin tab', () => {
cy.testid('WidgetIconPanel_plugins').click(); cy.testid('WidgetIconPanel_settings').click();
cy.contains('Manage plugins').click();
cy.contains('dbgate-plugin-theme-total-white').click(); cy.contains('dbgate-plugin-theme-total-white').click();
// text from plugin markdown // text from plugin markdown
cy.contains('Total white theme'); cy.contains('Total white theme');
@@ -380,20 +381,25 @@ describe('Data browser data', () => {
cy.themeshot('compare-database-settings'); cy.themeshot('compare-database-settings');
}); });
it('Query editor - AI assistant', () => { it('Database chat', () => {
cy.contains('MySql-connection').click(); cy.contains('MySql-connection').click();
cy.contains('MyChinook').click(); cy.contains('MyChinook').click();
cy.testid('TabsPanel_buttonNewObject').click(); cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_query').click(); cy.testid('NewObjectModal_databaseChat').click();
cy.testid('QueryTab_switchAiAssistantButton').click(); cy.wait(1000);
cy.testid('QueryAiAssistant_allowSendToAiServiceButton').click(); cy.get('body').realType('find most popular artist');
cy.testid('ConfirmModal_okButton').click(); cy.get('body').realPress('{enter}');
cy.testid('QueryAiAssistant_promptInput').type('album names'); cy.testid('DatabaseChatTab_executeAllQueries', { timeout: 20000 }).click();
cy.testid('QueryAiAssistant_queryFromQuestionButton').click(); cy.wait(4000);
cy.contains('Use this', { timeout: 10000 }).click(); // cy.contains('Iron Maiden');
cy.testid('QueryTab_executeButton').click(); cy.themeshot('database-chat');
cy.contains('Balls to the Wall');
cy.themeshot('ai-assistant'); // cy.testid('DatabaseChatTab_promptInput').click();
// cy.get('body').realType('I need top 10 songs with the biggest income');
// cy.get('body').realPress('{enter}');
// cy.contains('Hot Girl', { timeout: 20000 });
// cy.wait(1000);
// cy.themeshot('database-chat');
}); });
it('Modify data', () => { it('Modify data', () => {

View File

@@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "6.5.6", "version": "6.5.7-premium-beta.3",
"name": "dbgate-all", "name": "dbgate-all",
"workspaces": [ "workspaces": [
"packages/*", "packages/*",

View File

@@ -2,6 +2,8 @@ DEVMODE=1
SHELL_SCRIPTING=1 SHELL_SCRIPTING=1
ALLOW_DBGATE_PRIVATE_CLOUD=1 ALLOW_DBGATE_PRIVATE_CLOUD=1
DEVWEB=1 DEVWEB=1
# LOCAL_AI_GATEWAY=true
# REDIRECT_TO_DBGATE_CLOUD_LOGIN=1 # REDIRECT_TO_DBGATE_CLOUD_LOGIN=1
# PROD_DBGATE_CLOUD=1 # PROD_DBGATE_CLOUD=1
# PROD_DBGATE_IDENTITY=1 # PROD_DBGATE_IDENTITY=1
@@ -14,7 +16,6 @@ DEVWEB=1
# DISABLE_SHELL=1 # DISABLE_SHELL=1
# HIDE_APP_EDITOR=1 # HIDE_APP_EDITOR=1
# DEVWEB=1 # DEVWEB=1
# LOGINS=admin,test # LOGINS=admin,test

View File

@@ -68,6 +68,7 @@
}, },
"scripts": { "scripts": {
"start": "env-cmd -f .env node src/index.js --listen-api", "start": "env-cmd -f .env node src/index.js --listen-api",
"start:debug": "env-cmd -f .env node --inspect src/index.js --listen-api",
"start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api", "start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api",
"start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api", "start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api",
"start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api", "start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api",

View File

@@ -16,6 +16,7 @@ const { getConnectionLabel, getLogger, extractErrorLogData } = require('dbgate-t
const logger = getLogger('cloud'); const logger = getLogger('cloud');
const _ = require('lodash'); const _ = require('lodash');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { getAiGatewayServer } = require('../utility/authProxy');
module.exports = { module.exports = {
publicFiles_meta: true, publicFiles_meta: true,
@@ -276,4 +277,17 @@ module.exports = {
const resp = await callCloudApiPost(`content-folders/remove-user/${folid}`, { email }); const resp = await callCloudApiPost(`content-folders/remove-user/${folid}`, { email });
return resp; return resp;
}, },
getAiGateway_meta: true,
async getAiGateway() {
return getAiGatewayServer();
},
// chatStream_meta: {
// raw: true,
// method: 'post',
// },
// chatStream(req, res) {
// callChatStream(req.body, res);
// },
}; };

View File

@@ -46,6 +46,10 @@ async function tryToGetRefreshedLicense(oldLicenseKey) {
}; };
} }
function getAiGatewayServer() {
return {};
}
module.exports = { module.exports = {
isAuthProxySupported, isAuthProxySupported,
authProxyGetRedirectUrl, authProxyGetRedirectUrl,
@@ -59,4 +63,5 @@ module.exports = {
callRefactorSqlQueryApi, callRefactorSqlQueryApi,
getLicenseHttpHeaders, getLicenseHttpHeaders,
tryToGetRefreshedLicense, tryToGetRefreshedLicense,
getAiGatewayServer,
}; };

View File

@@ -70,6 +70,7 @@ function getDisplayColumn(basePath, columnName, display: CollectionGridDisplay)
isPartitionKey: !!display?.collection?.partitionKey?.find(x => x.columnName == uniqueName), isPartitionKey: !!display?.collection?.partitionKey?.find(x => x.columnName == uniqueName),
isClusterKey: !!display?.collection?.clusterKey?.find(x => x.columnName == uniqueName), isClusterKey: !!display?.collection?.clusterKey?.find(x => x.columnName == uniqueName),
isUniqueKey: !!display?.collection?.uniqueKey?.find(x => x.columnName == uniqueName), isUniqueKey: !!display?.collection?.uniqueKey?.find(x => x.columnName == uniqueName),
hasAutoValue: !!display?.collection?.autoValueColumns?.find(x => x.columnName == uniqueName),
}; };
} }

View File

@@ -6,11 +6,13 @@ import { hexStringToArray, parseNumberSafe } from 'dbgate-tools';
import { FilterBehaviour, TransformType } from 'dbgate-types'; import { FilterBehaviour, TransformType } from 'dbgate-types';
const binaryCondition = const binaryCondition =
(operator, numberDualTesting = false) => (operator, filterBehaviour: FilterBehaviour = {}) =>
value => { value => {
const { passNumbers, allowNumberDualTesting } = filterBehaviour;
const numValue = parseNumberSafe(value); const numValue = parseNumberSafe(value);
if ( if (
numberDualTesting && allowNumberDualTesting &&
// @ts-ignore // @ts-ignore
!isNaN(numValue) !isNaN(numValue)
) { ) {
@@ -43,6 +45,21 @@ const binaryCondition =
}; };
} }
// @ts-ignore
if (passNumbers && !isNaN(numValue)) {
return {
conditionType: 'binary',
operator,
left: {
exprType: 'placeholder',
},
right: {
exprType: 'value',
value: numValue,
},
};
}
return { return {
conditionType: 'binary', conditionType: 'binary',
operator, operator,
@@ -462,18 +479,18 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
null: () => word('NULL').map(unaryCondition('isNull')), null: () => word('NULL').map(unaryCondition('isNull')),
isEmpty: r => r.empty.map(unaryCondition('isEmpty')), isEmpty: r => r.empty.map(unaryCondition('isEmpty')),
isNotEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')), isNotEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')),
true: () => P.regexp(/true/i).map(binaryFixedValueCondition('1')), true: () => P.regexp(/true/i).map(binaryFixedValueCondition(filterBehaviour.passBooleans ? true : '1')),
false: () => P.regexp(/false/i).map(binaryFixedValueCondition('0')), false: () => P.regexp(/false/i).map(binaryFixedValueCondition(filterBehaviour.passBooleans ? false : '0')),
trueNum: () => word('1').map(binaryFixedValueCondition('1')), trueNum: () => word('1').map(binaryFixedValueCondition('1')),
falseNum: () => word('0').map(binaryFixedValueCondition('0')), falseNum: () => word('0').map(binaryFixedValueCondition('0')),
eq: r => word('=').then(r.value).map(binaryCondition('=', filterBehaviour.allowNumberDualTesting)), eq: r => word('=').then(r.value).map(binaryCondition('=', filterBehaviour)),
ne: r => word('!=').then(r.value).map(binaryCondition('<>', filterBehaviour.allowNumberDualTesting)), ne: r => word('!=').then(r.value).map(binaryCondition('<>', filterBehaviour)),
ne2: r => word('<>').then(r.value).map(binaryCondition('<>', filterBehaviour.allowNumberDualTesting)), ne2: r => word('<>').then(r.value).map(binaryCondition('<>', filterBehaviour)),
le: r => word('<=').then(r.value).map(binaryCondition('<=', filterBehaviour.allowNumberDualTesting)), le: r => word('<=').then(r.value).map(binaryCondition('<=', filterBehaviour)),
ge: r => word('>=').then(r.value).map(binaryCondition('>=', filterBehaviour.allowNumberDualTesting)), ge: r => word('>=').then(r.value).map(binaryCondition('>=', filterBehaviour)),
lt: r => word('<').then(r.value).map(binaryCondition('<', filterBehaviour.allowNumberDualTesting)), lt: r => word('<').then(r.value).map(binaryCondition('<', filterBehaviour)),
gt: r => word('>').then(r.value).map(binaryCondition('>', filterBehaviour.allowNumberDualTesting)), gt: r => word('>').then(r.value).map(binaryCondition('>', filterBehaviour)),
startsWith: r => word('^').then(r.value).map(likeCondition('like', '#VALUE#%')), startsWith: r => word('^').then(r.value).map(likeCondition('like', '#VALUE#%')),
endsWith: r => word('$').then(r.value).map(likeCondition('like', '%#VALUE#')), endsWith: r => word('$').then(r.value).map(likeCondition('like', '%#VALUE#')),
contains: r => word('+').then(r.value).map(likeCondition('like', '%#VALUE#%')), contains: r => word('+').then(r.value).map(likeCondition('like', '%#VALUE#%')),
@@ -526,8 +543,12 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
allowedElements.push('exists', 'notExists'); allowedElements.push('exists', 'notExists');
} }
if (filterBehaviour.supportArrayTesting) { if (filterBehaviour.supportEmptyArrayTesting) {
allowedElements.push('emptyArray', 'notEmptyArray'); allowedElements.push('emptyArray');
}
if (filterBehaviour.supportNotEmptyArrayTesting) {
allowedElements.push('notEmptyArray');
} }
if (filterBehaviour.supportNullTesting) { if (filterBehaviour.supportNullTesting) {

View File

@@ -42,8 +42,7 @@ function areDifferentRowCounts(db1: DatabaseInfo, db2: DatabaseInfo) {
} }
return false; return false;
} }
export class DatabaseAnalyser<TClient = any> {
export class DatabaseAnalyser {
structure: DatabaseInfo; structure: DatabaseInfo;
modifications: DatabaseModification[]; modifications: DatabaseModification[];
singleObjectFilter: any; singleObjectFilter: any;
@@ -51,7 +50,7 @@ export class DatabaseAnalyser {
dialect: SqlDialect; dialect: SqlDialect;
logger: Logger; logger: Logger;
constructor(public dbhan: DatabaseHandle, public driver: EngineDriver, version) { constructor(public dbhan: DatabaseHandle<TClient>, public driver: EngineDriver, version) {
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(version)) || driver?.dialect; this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(version)) || driver?.dialect;
this.logger = logger; this.logger = logger;
} }

View File

@@ -24,6 +24,7 @@ export const stringFilterBehaviour: FilterBehaviour = {
export const logicalFilterBehaviour: FilterBehaviour = { export const logicalFilterBehaviour: FilterBehaviour = {
supportBooleanValues: true, supportBooleanValues: true,
supportNullTesting: true, supportNullTesting: true,
supportBooleanOrNull: true,
supportSqlCondition: true, supportSqlCondition: true,
}; };
@@ -36,7 +37,8 @@ export const datetimeFilterBehaviour: FilterBehaviour = {
export const mongoFilterBehaviour: FilterBehaviour = { export const mongoFilterBehaviour: FilterBehaviour = {
supportEquals: true, supportEquals: true,
supportArrayTesting: true, supportEmptyArrayTesting: true,
supportNotEmptyArrayTesting: true,
supportNumberLikeComparison: true, supportNumberLikeComparison: true,
supportStringInclusion: true, supportStringInclusion: true,
supportBooleanValues: true, supportBooleanValues: true,
@@ -57,11 +59,38 @@ export const evalFilterBehaviour: FilterBehaviour = {
allowStringToken: true, allowStringToken: true,
}; };
export const firestoreFilterBehaviours: FilterBehaviour = {
supportEquals: true,
supportEmpty: false,
supportNumberLikeComparison: true,
supportDatetimeComparison: false,
supportNullTesting: true,
supportBooleanValues: true,
supportEmptyArrayTesting: true,
supportStringInclusion: false,
supportDatetimeSymbols: false,
supportExistsTesting: false,
supportSqlCondition: false,
allowStringToken: true,
allowNumberToken: true,
allowHexString: true,
allowNumberDualTesting: false,
allowObjectIdTesting: false,
passBooleans: true,
passNumbers: true,
disableOr: true,
};
export const standardFilterBehaviours: { [id: string]: FilterBehaviour } = { export const standardFilterBehaviours: { [id: string]: FilterBehaviour } = {
numberFilterBehaviour, numberFilterBehaviour,
stringFilterBehaviour, stringFilterBehaviour,
logicalFilterBehaviour, logicalFilterBehaviour,
datetimeFilterBehaviour, datetimeFilterBehaviour,
mongoFilterBehaviour, mongoFilterBehaviour,
firestoreFilterBehaviours,
evalFilterBehaviour, evalFilterBehaviour,
}; };

View File

@@ -75,6 +75,37 @@ export function parseCellValue(value, editorTypes?: DataEditorTypesBehaviour) {
} }
} }
if (editorTypes?.parseGeopointAsDollar) {
const m = value.match(/^([\d\.]+)\s*°\s*([NS]),\s*([\d\.]+)\s*°\s*([EW])$/i);
if (m) {
let latitude = parseFloat(m[1]);
const latDir = m[2].toUpperCase();
let longitude = parseFloat(m[3]);
const lonDir = m[4].toUpperCase();
if (latDir === 'S') latitude = -latitude;
if (lonDir === 'W') longitude = -longitude;
return {
$geoPoint: {
latitude,
longitude,
},
};
}
}
if (editorTypes?.parseFsDocumentRefAsDollar) {
const trimmedValue = value.replace(/\s/g, '');
if (trimmedValue.startsWith('$ref:')) {
return {
$fsDocumentRef: {
documentPath: trimmedValue.slice(5),
},
};
}
}
if (editorTypes?.parseJsonNull) { if (editorTypes?.parseJsonNull) {
if (value == 'null') return null; if (value == 'null') return null;
} }
@@ -246,6 +277,32 @@ export function stringifyCellValue(
} }
} }
if (editorTypes?.parseGeopointAsDollar) {
if (value?.$geoPoint) {
const { latitude, longitude } = value.$geoPoint;
if (_isNumber(latitude) && _isNumber(longitude)) {
const latAbs = Math.abs(latitude);
const lonAbs = Math.abs(longitude);
const latDir = latitude >= 0 ? 'N' : 'S';
const lonDir = longitude >= 0 ? 'E' : 'W';
return {
value: `${latAbs}° ${latDir}, ${lonAbs}° ${lonDir}`,
gridStyle: 'valueCellStyle',
};
}
}
}
if (editorTypes?.parseFsDocumentRefAsDollar) {
if (value?.$fsDocumentRef) {
return {
value: `$ref: ${value.$fsDocumentRef.documentPath ?? ''}`,
gridStyle: 'valueCellStyle',
};
}
}
if (_isArray(value)) { if (_isArray(value)) {
switch (intent) { switch (intent) {
case 'gridCellIntent': case 'gridCellIntent':

View File

@@ -108,6 +108,8 @@ export interface CollectionInfo extends DatabaseObjectInfo {
// unique combination of columns (should be contatenation of partitionKey and clusterKey) // unique combination of columns (should be contatenation of partitionKey and clusterKey)
uniqueKey?: ColumnReference[]; uniqueKey?: ColumnReference[];
autoValueColumns?: ColumnReference[];
// partition key columns // partition key columns
partitionKey?: ColumnReference[]; partitionKey?: ColumnReference[];

View File

@@ -23,6 +23,28 @@ export interface StreamOptions {
info?: (info) => void; info?: (info) => void;
} }
export type CollectionOperationInfo =
| {
type: 'createCollection';
collection: {
name: string;
};
}
| {
type: 'dropCollection';
collection: string;
}
| {
type: 'renameCollection';
collection: string;
newName: string;
}
| {
type: 'cloneCollection';
collection: string;
newName: string;
};
export interface RunScriptOptions { export interface RunScriptOptions {
useTransaction: boolean; useTransaction: boolean;
logScriptItems?: boolean; logScriptItems?: boolean;
@@ -120,6 +142,8 @@ export interface DataEditorTypesBehaviour {
parseHexAsBuffer?: boolean; parseHexAsBuffer?: boolean;
parseObjectIdAsDollar?: boolean; parseObjectIdAsDollar?: boolean;
parseDateAsDollar?: boolean; parseDateAsDollar?: boolean;
parseGeopointAsDollar?: boolean;
parseFsDocumentRefAsDollar?: boolean;
explicitDataType?: boolean; explicitDataType?: boolean;
supportNumberType?: boolean; supportNumberType?: boolean;
@@ -217,7 +241,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
defaultSocketPath?: string; defaultSocketPath?: string;
authTypeLabel?: string; authTypeLabel?: string;
importExportArgs?: any[]; importExportArgs?: any[];
connect({ server, port, user, password, database }): Promise<DatabaseHandle<TClient>>; connect({ server, port, user, password, database, certificateJson }): Promise<DatabaseHandle<TClient>>;
close(dbhan: DatabaseHandle<TClient>): Promise<any>; close(dbhan: DatabaseHandle<TClient>): Promise<any>;
query(dbhan: DatabaseHandle<TClient>, sql: string, options?: QueryOptions): Promise<QueryResult>; query(dbhan: DatabaseHandle<TClient>, sql: string, options?: QueryOptions): Promise<QueryResult>;
stream(dbhan: DatabaseHandle<TClient>, sql: string, options: StreamOptions); stream(dbhan: DatabaseHandle<TClient>, sql: string, options: StreamOptions);
@@ -264,7 +288,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
dropDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise; dropDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any; getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any;
script(dbhan: DatabaseHandle<TClient>, sql: string, options?: RunScriptOptions): Promise; script(dbhan: DatabaseHandle<TClient>, sql: string, options?: RunScriptOptions): Promise;
operation(dbhan: DatabaseHandle<TClient>, operation: {}, options?: RunScriptOptions): Promise; operation(dbhan: DatabaseHandle<TClient>, operation: CollectionOperationInfo, options?: RunScriptOptions): Promise;
getNewObjectTemplates(): NewObjectTemplate[]; getNewObjectTemplates(): NewObjectTemplate[];
// direct call of dbhan.client method, only some methods could be supported, on only some drivers // direct call of dbhan.client method, only some methods could be supported, on only some drivers
callMethod(dbhan: DatabaseHandle<TClient>, method, args); callMethod(dbhan: DatabaseHandle<TClient>, method, args);

View File

@@ -9,11 +9,18 @@ export interface FilterBehaviour {
supportExistsTesting?: boolean; supportExistsTesting?: boolean;
supportBooleanValues?: boolean; supportBooleanValues?: boolean;
supportSqlCondition?: boolean; supportSqlCondition?: boolean;
supportArrayTesting?: boolean; supportEmptyArrayTesting?: boolean;
supportNotEmptyArrayTesting?: boolean;
supportBooleanOrNull?: boolean;
allowStringToken?: boolean; allowStringToken?: boolean;
allowNumberToken?: boolean; allowNumberToken?: boolean;
allowHexString?: boolean; allowHexString?: boolean;
allowNumberDualTesting?: boolean; allowNumberDualTesting?: boolean;
allowObjectIdTesting?: boolean; allowObjectIdTesting?: boolean;
passBooleans?: boolean;
passNumbers?: boolean;
disableOr?: boolean;
} }

View File

@@ -15,3 +15,92 @@ export interface QueryResult {
columns?: QueryResultColumn[]; columns?: QueryResultColumn[];
rowsAffected?: number; rowsAffected?: number;
} }
export type LeftOperand = {
exprType: 'placeholder' | 'column';
columnName?: string;
};
export type RightOperand = {
exprType: 'value';
value: any;
};
export type BinaryCondition = {
conditionType: 'binary';
operator: '=' | '!=' | '<>' | '<' | '<=' | '>' | '>=';
left: LeftOperand;
right: RightOperand;
};
export type AndCondition = {
conditionType: 'and';
conditions: FilterCondition[];
};
export type OrCondition = {
conditionType: 'or';
conditions: FilterCondition[];
};
export type NullCondition = {
conditionType: 'isNull' | 'isNotNull';
expr: LeftOperand;
};
export type NotCondition = {
conditionType: 'not';
condition: FilterCondition;
};
export type LikeCondition = {
conditionType: 'like';
left: LeftOperand;
right: RightOperand;
};
export type PredicateCondition = {
conditionType: 'specificPredicate';
predicate: 'exists' | 'notExists' | 'emptyArray' | 'notEmptyArray';
expr: LeftOperand;
};
export type InCondition = {
conditionType: 'in';
expr: LeftOperand;
values: any[];
};
export type FilterCondition =
| BinaryCondition
| AndCondition
| OrCondition
| NullCondition
| NotCondition
| LikeCondition
| PredicateCondition
| InCondition;
export type SortItem = {
columnName: string;
direction?: 'ASC' | 'DESC';
};
export type AggregateColumn = {
aggregateFunction: 'count' | 'sum' | 'avg' | 'min' | 'max';
columnArgument?: string;
alias: string;
};
export type CollectionAggregate = {
condition?: FilterCondition;
groupByColumns: string[];
aggregateColumns: AggregateColumn[];
};
export type FullQueryOptions = {
condition?: FilterCondition;
sort?: SortItem[];
limit?: number;
skip?: number;
};

View File

@@ -69,6 +69,7 @@
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"interval-operations": "^1.0.7", "interval-operations": "^1.0.7",
"leaflet": "^1.8.0", "leaflet": "^1.8.0",
"openai": "^5.10.1",
"wellknown": "^0.5.0", "wellknown": "^0.5.0",
"xml-formatter": "^3.6.4" "xml-formatter": "^3.6.4"
} }

View File

@@ -71,7 +71,8 @@
const handleDropDatabase = () => { const handleDropDatabase = () => {
showModal(ConfirmModal, { showModal(ConfirmModal, {
message: _t('database.dropConfirm', { message: _t('database.dropConfirm', {
defaultMessage: 'Really drop database {name}? All opened sessions with this database will be forcefully closed.', defaultMessage:
'Really drop database {name}? All opened sessions with this database will be forcefully closed.',
values: { name }, values: { name },
}), }),
onConfirm: () => onConfirm: () =>
@@ -207,6 +208,18 @@
// showSnackbarSuccess(`Saved to archive ${resp.archiveFolder}`); // showSnackbarSuccess(`Saved to archive ${resp.archiveFolder}`);
}; };
const handleDatabaseChat = () => {
openNewTab({
title: 'Chat',
icon: 'img ai',
tabComponent: 'DatabaseChatTab',
props: {
conid: connection._id,
database: name,
},
});
};
const handleCompareWithCurrentDb = () => { const handleCompareWithCurrentDb = () => {
openNewTab( openNewTab(
{ {
@@ -312,7 +325,8 @@
const handleGenerateDropAllObjectsScript = () => { const handleGenerateDropAllObjectsScript = () => {
showModal(ConfirmModal, { showModal(ConfirmModal, {
message: _t('database.dropAllObjectsConfirm', { message: _t('database.dropAllObjectsConfirm', {
defaultMessage: 'This will generate script, after executing this script all objects in {name} will be dropped. Continue?', defaultMessage:
'This will generate script, after executing this script all objects in {name} will be dropped. Continue?',
values: { name }, values: { name },
}), }),
@@ -364,7 +378,9 @@ await dbgateApi.executeQuery(${JSON.stringify(
const handleShowDataDeployer = () => { const handleShowDataDeployer = () => {
showModal(ChooseArchiveFolderModal, { showModal(ChooseArchiveFolderModal, {
message: _t('database.chooseArchiveFolderForDataDeployer', { defaultMessage: 'Choose archive folder for data deployer' }), message: _t('database.chooseArchiveFolderForDataDeployer', {
defaultMessage: 'Choose archive folder for data deployer',
}),
onConfirm: archiveFolder => { onConfirm: archiveFolder => {
openNewTab( openNewTab(
{ {
@@ -396,57 +412,109 @@ await dbgateApi.executeQuery(${JSON.stringify(
driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document'); driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document');
return [ return [
hasPermission(`dbops/query`) && { onClick: handleNewQuery, text: _t('database.newQuery', { defaultMessage: 'New query' }), isNewQuery: true }, hasPermission(`dbops/query`) && {
onClick: handleNewQuery,
text: _t('database.newQuery', { defaultMessage: 'New query' }),
isNewQuery: true,
},
hasPermission(`dbops/model/edit`) && hasPermission(`dbops/model/edit`) &&
!connection.isReadOnly && !connection.isReadOnly &&
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleNewTable, text: _t('database.newTable', { defaultMessage: 'New table' }) }, driver?.databaseEngineTypes?.includes('sql') && {
onClick: handleNewTable,
text: _t('database.newTable', { defaultMessage: 'New table' }),
},
!connection.isReadOnly && !connection.isReadOnly &&
hasPermission(`dbops/model/edit`) && hasPermission(`dbops/model/edit`) &&
driver?.databaseEngineTypes?.includes('document') && { driver?.databaseEngineTypes?.includes('document') && {
onClick: handleNewCollection, onClick: handleNewCollection,
text: _t('database.newCollection', { defaultMessage: 'New {collectionLabel}', values: { collectionLabel: driver?.collectionSingularLabel ?? 'collection/container' } }), text: _t('database.newCollection', {
defaultMessage: 'New {collectionLabel}',
values: { collectionLabel: driver?.collectionSingularLabel ?? 'collection/container' },
}),
}, },
hasPermission(`dbops/query`) && hasPermission(`dbops/query`) &&
driver?.databaseEngineTypes?.includes('sql') && driver?.databaseEngineTypes?.includes('sql') &&
isProApp() && { onClick: handleQueryDesigner, text: _t('database.designQuery', { defaultMessage: 'Design query' }) }, isProApp() && {
onClick: handleQueryDesigner,
text: _t('database.designQuery', { defaultMessage: 'Design query' }),
},
driver?.databaseEngineTypes?.includes('sql') && driver?.databaseEngineTypes?.includes('sql') &&
isProApp() && { isProApp() && {
onClick: handleNewPerspective, onClick: handleNewPerspective,
text: _t('database.designPerspectiveQuery', { defaultMessage: 'Design perspective query' }), text: _t('database.designPerspectiveQuery', { defaultMessage: 'Design perspective query' }),
}, },
connection.useSeparateSchemas && { onClick: handleRefreshSchemas, text: _t('database.refreshSchemas', { defaultMessage: 'Refresh schemas' }) }, connection.useSeparateSchemas && {
onClick: handleRefreshSchemas,
text: _t('database.refreshSchemas', { defaultMessage: 'Refresh schemas' }),
},
{ divider: true }, { divider: true },
isSqlOrDoc && isSqlOrDoc &&
!connection.isReadOnly && !connection.isReadOnly &&
hasPermission(`dbops/import`) && { onClick: handleImport, text: _t('database.import', { defaultMessage: 'Import' }) }, hasPermission(`dbops/import`) && {
isSqlOrDoc && hasPermission(`dbops/export`) && { onClick: handleExport, text: _t('database.export', { defaultMessage: 'Export' }) }, onClick: handleImport,
text: _t('database.import', { defaultMessage: 'Import' }),
},
isSqlOrDoc &&
hasPermission(`dbops/export`) && {
onClick: handleExport,
text: _t('database.export', { defaultMessage: 'Export' }),
},
driver?.supportsDatabaseRestore && driver?.supportsDatabaseRestore &&
isProApp() && isProApp() &&
hasPermission(`dbops/sql-dump/import`) && hasPermission(`dbops/sql-dump/import`) &&
!connection.isReadOnly && { onClick: handleRestoreDatabase, text: _t('database.restoreDatabaseBackup', { defaultMessage: 'Restore database backup' }) }, !connection.isReadOnly && {
onClick: handleRestoreDatabase,
text: _t('database.restoreDatabaseBackup', { defaultMessage: 'Restore database backup' }),
},
driver?.supportsDatabaseBackup && driver?.supportsDatabaseBackup &&
isProApp() && isProApp() &&
hasPermission(`dbops/sql-dump/export`) && { onClick: handleBackupDatabase, text: _t('database.createDatabaseBackup', { defaultMessage: 'Create database backup' }) }, hasPermission(`dbops/sql-dump/export`) && {
onClick: handleBackupDatabase,
text: _t('database.createDatabaseBackup', { defaultMessage: 'Create database backup' }),
},
isSqlOrDoc && isSqlOrDoc &&
!connection.isReadOnly && !connection.isReadOnly &&
!connection.singleDatabase && !connection.singleDatabase &&
isSqlOrDoc && isSqlOrDoc &&
hasPermission(`dbops/dropdb`) && { onClick: handleDropDatabase, text: _t('database.dropDatabase', { defaultMessage: 'Drop database' }) }, hasPermission(`dbops/dropdb`) && {
onClick: handleDropDatabase,
text: _t('database.dropDatabase', { defaultMessage: 'Drop database' }),
},
{ divider: true }, { divider: true },
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleCopyName, text: _t('database.copyDatabaseName', { defaultMessage: 'Copy database name' }) }, driver?.databaseEngineTypes?.includes('sql') && {
driver?.databaseEngineTypes?.includes('sql') && { onClick: handleShowDiagram, text: _t('database.showDiagram', { defaultMessage: 'Show diagram' }) }, onClick: handleCopyName,
text: _t('database.copyDatabaseName', { defaultMessage: 'Copy database name' }),
},
driver?.databaseEngineTypes?.includes('sql') && {
onClick: handleShowDiagram,
text: _t('database.showDiagram', { defaultMessage: 'Show diagram' }),
},
driver?.databaseEngineTypes?.includes('sql') && driver?.databaseEngineTypes?.includes('sql') &&
hasPermission(`dbops/sql-generator`) && { onClick: handleSqlGenerator, text: _t('database.sqlGenerator', { defaultMessage: 'SQL Generator' }) }, hasPermission(`dbops/sql-generator`) && {
onClick: handleSqlGenerator,
text: _t('database.sqlGenerator', { defaultMessage: 'SQL Generator' }),
},
driver?.supportsDatabaseProfiler && driver?.supportsDatabaseProfiler &&
isProApp() && isProApp() &&
hasPermission(`dbops/profiler`) && { onClick: handleDatabaseProfiler, text: _t('database.databaseProfiler', { defaultMessage: 'Database profiler' }) }, hasPermission(`dbops/profiler`) && {
onClick: handleDatabaseProfiler,
text: _t('database.databaseProfiler', { defaultMessage: 'Database profiler' }),
},
// isSqlOrDoc && // isSqlOrDoc &&
// isSqlOrDoc && // isSqlOrDoc &&
// hasPermission(`dbops/model/view`) && { onClick: handleOpenJsonModel, text: 'Open model as JSON' }, // hasPermission(`dbops/model/view`) && { onClick: handleOpenJsonModel, text: 'Open model as JSON' },
isSqlOrDoc && isSqlOrDoc &&
isProApp() && isProApp() &&
hasPermission(`dbops/model/view`) && { onClick: handleExportModel, text: _t('database.exportDbModel', { defaultMessage: 'Export DB model' }) }, hasPermission(`dbops/model/view`) && {
onClick: handleExportModel,
text: _t('database.exportDbModel', { defaultMessage: 'Export DB model' }),
},
isProApp() &&
driver?.databaseEngineTypes?.includes('sql') && {
onClick: handleDatabaseChat,
text: _t('database.databaseChat', { defaultMessage: 'Database chat' }),
},
isSqlOrDoc && isSqlOrDoc &&
_.get($currentDatabase, 'connection._id') && _.get($currentDatabase, 'connection._id') &&
hasPermission('dbops/model/compare') && hasPermission('dbops/model/compare') &&
@@ -455,14 +523,23 @@ await dbgateApi.executeQuery(${JSON.stringify(
(_.get($currentDatabase, 'connection._id') == _.get(connection, '_id') && (_.get($currentDatabase, 'connection._id') == _.get(connection, '_id') &&
_.get($currentDatabase, 'name') != _.get(connection, 'name'))) && { _.get($currentDatabase, 'name') != _.get(connection, 'name'))) && {
onClick: handleCompareWithCurrentDb, onClick: handleCompareWithCurrentDb,
text: _t('database.compareWithCurrentDb', { defaultMessage: 'Compare with {name}', values: { name: _.get($currentDatabase, 'name') } }), text: _t('database.compareWithCurrentDb', {
defaultMessage: 'Compare with {name}',
values: { name: _.get($currentDatabase, 'name') },
}),
}, },
driver?.databaseEngineTypes?.includes('keyvalue') && { onClick: handleGenerateScript, text: _t('database.generateScript', { defaultMessage: 'Generate script' }) }, driver?.databaseEngineTypes?.includes('keyvalue') && {
onClick: handleGenerateScript,
text: _t('database.generateScript', { defaultMessage: 'Generate script' }),
},
($openedSingleDatabaseConnections.includes(connection._id) || ($openedSingleDatabaseConnections.includes(connection._id) ||
(_.get($currentDatabase, 'connection._id') == _.get(connection, '_id') && (_.get($currentDatabase, 'connection._id') == _.get(connection, '_id') &&
_.get($currentDatabase, 'name') == name)) && { onClick: handleDisconnect, text: _t('database.disconnect', { defaultMessage: 'Disconnect' }) }, _.get($currentDatabase, 'name') == name)) && {
onClick: handleDisconnect,
text: _t('database.disconnect', { defaultMessage: 'Disconnect' }),
},
{ divider: true }, { divider: true },

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import XmlHighlighter from './XmlHighlighter.svelte'; import XmlHighlighter from '../elements/XmlHighlighter.svelte';
export let selection; export let selection;
</script> </script>

View File

@@ -714,6 +714,28 @@ if (isProApp()) {
); );
}, },
}); });
registerCommand({
id: 'database.chat',
category: 'Database',
name: 'Database chat',
toolbar: true,
icon: 'icon ai',
testEnabled: () =>
getCurrentDatabase() != null &&
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql'),
onClick: () => {
openNewTab({
title: 'Chat',
icon: 'img ai',
tabComponent: 'DatabaseChatTab',
props: {
conid: getCurrentDatabase()?.connection?._id,
database: getCurrentDatabase()?.name,
},
});
},
});
} }
if (hasPermission('settings/change')) { if (hasPermission('settings/change')) {

View File

@@ -122,7 +122,11 @@
import _ from 'lodash'; import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte'; import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import registerCommand from '../commands/registerCommand'; import registerCommand from '../commands/registerCommand';
import { extractShellConnection, extractShellConnectionHostable, extractShellHostConnection } from '../impexp/createImpExpScript'; import {
extractShellConnection,
extractShellConnectionHostable,
extractShellHostConnection,
} from '../impexp/createImpExpScript';
import { apiCall } from '../utility/api'; import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu'; import { registerMenu } from '../utility/contextMenu';

View File

@@ -80,11 +80,12 @@
); );
} }
if (filterBehaviour.supportArrayTesting) { if (filterBehaviour.supportNotEmptyArrayTesting) {
res.push( res.push({ onClick: () => setFilter('NOT EMPTY ARRAY'), text: 'Array is not empty' });
{ onClick: () => setFilter('NOT EMPTY ARRAY'), text: 'Array is not empty' }, }
{ onClick: () => setFilter('EMPTY ARRAY'), text: 'Array is empty' }
); if (filterBehaviour.supportEmptyArrayTesting) {
res.push({ onClick: () => setFilter('EMPTY ARRAY'), text: 'Array is empty' });
} }
if (filterBehaviour.supportNullTesting) { if (filterBehaviour.supportNullTesting) {
@@ -132,7 +133,7 @@
); );
} }
if (filterBehaviour.supportBooleanValues && filterBehaviour.supportNullTesting) { if (filterBehaviour.supportBooleanOrNull) {
res.push( res.push(
{ onClick: () => setFilter('TRUE, NULL'), text: 'Is True or NULL' }, { onClick: () => setFilter('TRUE, NULL'), text: 'Is True or NULL' },
{ onClick: () => setFilter('FALSE, NULL'), text: 'Is False or NULL' } { onClick: () => setFilter('FALSE, NULL'), text: 'Is False or NULL' }

View File

@@ -0,0 +1,41 @@
<script>
/* npm i highlight.js sql-formatter */
import hljs from 'highlight.js/lib/core';
import sqlGrammar from './sqlGrammar';
import { onMount, afterUpdate } from 'svelte';
export let code = '';
let domCode;
onMount(() => {
hljs.registerLanguage('sql', sqlGrammar);
// first paint
if (domCode) {
hljs.highlightElement(domCode);
}
});
afterUpdate(() => {
if (domCode) {
hljs.highlightElement(domCode);
}
});
</script>
{#key code}
<!--
The `sql` class hints the language; highlight.js will
read it even though we register the grammar explicitly.
-->
<pre bind:this={domCode} class="sql">{code}</pre>
{/key}
<style>
pre {
margin: 0;
padding: 0;
padding: 0.5em;
}
</style>

View File

@@ -6,7 +6,7 @@
export let code = ''; export let code = '';
$: formattedCode = xmlFormat(code, { indentation: ' ' }); $: formattedCode = xmlFormat(code, { indentation: ' ', throwOnFailure: false });
onMount(() => { onMount(() => {
hljs.registerLanguage('xml', xmlGrammar); hljs.registerLanguage('xml', xmlGrammar);

View File

@@ -0,0 +1,691 @@
/*
Language: SQL
Website: https://en.wikipedia.org/wiki/SQL
Category: common, database
*/
/*
Goals:
SQL is intended to highlight basic/common SQL keywords and expressions
- If pretty much every single SQL server includes supports, then it's a canidate.
- It is NOT intended to include tons of vendor specific keywords (Oracle, MySQL,
PostgreSQL) although the list of data types is purposely a bit more expansive.
- For more specific SQL grammars please see:
- PostgreSQL and PL/pgSQL - core
- T-SQL - https://github.com/highlightjs/highlightjs-tsql
- sql_more (core)
*/
export default function(hljs) {
const regex = hljs.regex;
const COMMENT_MODE = hljs.COMMENT('--', '$');
const STRING = {
scope: 'string',
variants: [
{
begin: /'/,
end: /'/,
contains: [ { match: /''/ } ]
}
]
};
const QUOTED_IDENTIFIER = {
begin: /"/,
end: /"/,
contains: [ { match: /""/ } ]
};
const LITERALS = [
"true",
"false",
// Not sure it's correct to call NULL literal, and clauses like IS [NOT] NULL look strange that way.
// "null",
"unknown"
];
const MULTI_WORD_TYPES = [
"double precision",
"large object",
"with timezone",
"without timezone"
];
const TYPES = [
'bigint',
'binary',
'blob',
'boolean',
'char',
'character',
'clob',
'date',
'dec',
'decfloat',
'decimal',
'float',
'int',
'integer',
'interval',
'nchar',
'nclob',
'national',
'numeric',
'real',
'row',
'smallint',
'time',
'timestamp',
'varchar',
'varying', // modifier (character varying)
'varbinary'
];
const NON_RESERVED_WORDS = [
"add",
"asc",
"collation",
"desc",
"final",
"first",
"last",
"view"
];
// https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#reserved-word
const RESERVED_WORDS = [
"abs",
"acos",
"all",
"allocate",
"alter",
"and",
"any",
"are",
"array",
"array_agg",
"array_max_cardinality",
"as",
"asensitive",
"asin",
"asymmetric",
"at",
"atan",
"atomic",
"authorization",
"avg",
"begin",
"begin_frame",
"begin_partition",
"between",
"bigint",
"binary",
"blob",
"boolean",
"both",
"by",
"call",
"called",
"cardinality",
"cascaded",
"case",
"cast",
"ceil",
"ceiling",
"char",
"char_length",
"character",
"character_length",
"check",
"classifier",
"clob",
"close",
"coalesce",
"collate",
"collect",
"column",
"commit",
"condition",
"connect",
"constraint",
"contains",
"convert",
"copy",
"corr",
"corresponding",
"cos",
"cosh",
"count",
"covar_pop",
"covar_samp",
"create",
"cross",
"cube",
"cume_dist",
"current",
"current_catalog",
"current_date",
"current_default_transform_group",
"current_path",
"current_role",
"current_row",
"current_schema",
"current_time",
"current_timestamp",
"current_path",
"current_role",
"current_transform_group_for_type",
"current_user",
"cursor",
"cycle",
"date",
"day",
"deallocate",
"dec",
"decimal",
"decfloat",
"declare",
"default",
"define",
"delete",
"dense_rank",
"deref",
"describe",
"deterministic",
"disconnect",
"distinct",
"double",
"drop",
"dynamic",
"each",
"element",
"else",
"empty",
"end",
"end_frame",
"end_partition",
"end-exec",
"equals",
"escape",
"every",
"except",
"exec",
"execute",
"exists",
"exp",
"external",
"extract",
"false",
"fetch",
"filter",
"first_value",
"float",
"floor",
"for",
"foreign",
"frame_row",
"free",
"from",
"full",
"function",
"fusion",
"get",
"global",
"grant",
"group",
"grouping",
"groups",
"having",
"hold",
"hour",
"identity",
"in",
"indicator",
"initial",
"inner",
"inout",
"insensitive",
"insert",
"int",
"integer",
"intersect",
"intersection",
"interval",
"into",
"is",
"join",
"json_array",
"json_arrayagg",
"json_exists",
"json_object",
"json_objectagg",
"json_query",
"json_table",
"json_table_primitive",
"json_value",
"lag",
"language",
"large",
"last_value",
"lateral",
"lead",
"leading",
"left",
"like",
"like_regex",
"listagg",
"ln",
"local",
"localtime",
"localtimestamp",
"log",
"log10",
"lower",
"match",
"match_number",
"match_recognize",
"matches",
"max",
"member",
"merge",
"method",
"min",
"minute",
"mod",
"modifies",
"module",
"month",
"multiset",
"national",
"natural",
"nchar",
"nclob",
"new",
"no",
"none",
"normalize",
"not",
"nth_value",
"ntile",
"null",
"nullif",
"numeric",
"octet_length",
"occurrences_regex",
"of",
"offset",
"old",
"omit",
"on",
"one",
"only",
"open",
"or",
"order",
"out",
"outer",
"over",
"overlaps",
"overlay",
"parameter",
"partition",
"pattern",
"per",
"percent",
"percent_rank",
"percentile_cont",
"percentile_disc",
"period",
"portion",
"position",
"position_regex",
"power",
"precedes",
"precision",
"prepare",
"primary",
"procedure",
"ptf",
"range",
"rank",
"reads",
"real",
"recursive",
"ref",
"references",
"referencing",
"regr_avgx",
"regr_avgy",
"regr_count",
"regr_intercept",
"regr_r2",
"regr_slope",
"regr_sxx",
"regr_sxy",
"regr_syy",
"release",
"result",
"return",
"returns",
"revoke",
"right",
"rollback",
"rollup",
"row",
"row_number",
"rows",
"running",
"savepoint",
"scope",
"scroll",
"search",
"second",
"seek",
"select",
"sensitive",
"session_user",
"set",
"show",
"similar",
"sin",
"sinh",
"skip",
"smallint",
"some",
"specific",
"specifictype",
"sql",
"sqlexception",
"sqlstate",
"sqlwarning",
"sqrt",
"start",
"static",
"stddev_pop",
"stddev_samp",
"submultiset",
"subset",
"substring",
"substring_regex",
"succeeds",
"sum",
"symmetric",
"system",
"system_time",
"system_user",
"table",
"tablesample",
"tan",
"tanh",
"then",
"time",
"timestamp",
"timezone_hour",
"timezone_minute",
"to",
"trailing",
"translate",
"translate_regex",
"translation",
"treat",
"trigger",
"trim",
"trim_array",
"true",
"truncate",
"uescape",
"union",
"unique",
"unknown",
"unnest",
"update",
"upper",
"user",
"using",
"value",
"values",
"value_of",
"var_pop",
"var_samp",
"varbinary",
"varchar",
"varying",
"versioning",
"when",
"whenever",
"where",
"width_bucket",
"window",
"with",
"within",
"without",
"year",
];
// these are reserved words we have identified to be functions
// and should only be highlighted in a dispatch-like context
// ie, array_agg(...), etc.
const RESERVED_FUNCTIONS = [
"abs",
"acos",
"array_agg",
"asin",
"atan",
"avg",
"cast",
"ceil",
"ceiling",
"coalesce",
"corr",
"cos",
"cosh",
"count",
"covar_pop",
"covar_samp",
"cume_dist",
"dense_rank",
"deref",
"element",
"exp",
"extract",
"first_value",
"floor",
"json_array",
"json_arrayagg",
"json_exists",
"json_object",
"json_objectagg",
"json_query",
"json_table",
"json_table_primitive",
"json_value",
"lag",
"last_value",
"lead",
"listagg",
"ln",
"log",
"log10",
"lower",
"max",
"min",
"mod",
"nth_value",
"ntile",
"nullif",
"percent_rank",
"percentile_cont",
"percentile_disc",
"position",
"position_regex",
"power",
"rank",
"regr_avgx",
"regr_avgy",
"regr_count",
"regr_intercept",
"regr_r2",
"regr_slope",
"regr_sxx",
"regr_sxy",
"regr_syy",
"row_number",
"sin",
"sinh",
"sqrt",
"stddev_pop",
"stddev_samp",
"substring",
"substring_regex",
"sum",
"tan",
"tanh",
"translate",
"translate_regex",
"treat",
"trim",
"trim_array",
"unnest",
"upper",
"value_of",
"var_pop",
"var_samp",
"width_bucket",
];
// these functions can
const POSSIBLE_WITHOUT_PARENS = [
"current_catalog",
"current_date",
"current_default_transform_group",
"current_path",
"current_role",
"current_schema",
"current_transform_group_for_type",
"current_user",
"session_user",
"system_time",
"system_user",
"current_time",
"localtime",
"current_timestamp",
"localtimestamp"
];
// those exist to boost relevance making these very
// "SQL like" keyword combos worth +1 extra relevance
const COMBOS = [
"create table",
"insert into",
"primary key",
"foreign key",
"not null",
"alter table",
"add constraint",
"grouping sets",
"on overflow",
"character set",
"respect nulls",
"ignore nulls",
"nulls first",
"nulls last",
"depth first",
"breadth first"
];
const FUNCTIONS = RESERVED_FUNCTIONS;
const KEYWORDS = [
...RESERVED_WORDS,
...NON_RESERVED_WORDS
].filter((keyword) => {
return !RESERVED_FUNCTIONS.includes(keyword);
});
const VARIABLE = {
scope: "variable",
match: /@[a-z0-9][a-z0-9_]*/,
};
const OPERATOR = {
scope: "operator",
match: /[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/,
relevance: 0,
};
const FUNCTION_CALL = {
match: regex.concat(/\b/, regex.either(...FUNCTIONS), /\s*\(/),
relevance: 0,
keywords: { built_in: FUNCTIONS }
};
// turns a multi-word keyword combo into a regex that doesn't
// care about extra whitespace etc.
// input: "START QUERY"
// output: /\bSTART\s+QUERY\b/
function kws_to_regex(list) {
return regex.concat(
/\b/,
regex.either(...list.map((kw) => {
return kw.replace(/\s+/, "\\s+")
})),
/\b/
)
}
const MULTI_WORD_KEYWORDS = {
scope: "keyword",
match: kws_to_regex(COMBOS),
relevance: 0,
};
// keywords with less than 3 letters are reduced in relevancy
function reduceRelevancy(list, {
exceptions, when
} = {}) {
const qualifyFn = when;
exceptions = exceptions || [];
return list.map((item) => {
if (item.match(/\|\d+$/) || exceptions.includes(item)) {
return item;
} else if (qualifyFn(item)) {
return `${item}|0`;
} else {
return item;
}
});
}
return {
name: 'SQL',
case_insensitive: true,
// does not include {} or HTML tags `</`
illegal: /[{}]|<\//,
keywords: {
$pattern: /\b[\w\.]+/,
keyword:
reduceRelevancy(KEYWORDS, { when: (x) => x.length < 3 }),
literal: LITERALS,
type: TYPES,
built_in: POSSIBLE_WITHOUT_PARENS
},
contains: [
{
scope: "type",
match: kws_to_regex(MULTI_WORD_TYPES)
},
MULTI_WORD_KEYWORDS,
FUNCTION_CALL,
VARIABLE,
STRING,
QUOTED_IDENTIFIER,
hljs.C_NUMBER_MODE,
hljs.C_BLOCK_COMMENT_MODE,
COMMENT_MODE,
OPERATOR
]
};
}

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import SimpleFilesInput, { ProcessedFile } from '../impexp/SimpleFilesInput.svelte';
import { FileParseResult, parseFileAsJson } from '../utility/parseFileAsJson';
import { getFormContext } from './FormProviderCore.svelte';
import { createEventDispatcher } from 'svelte';
export let label: string;
export let buttonLabel: string = 'Choose File';
export let name: string;
export let disabled: boolean = false;
export let accept: string = '.json,application/json';
export let templateProps = {};
const { template, setFieldValue, values } = getFormContext();
const dispatch = createEventDispatcher();
let fileName: string | null = null;
$: hasValue = $values?.[name] != null;
$: displayLabel = getDisplayLabel(buttonLabel, hasValue, fileName);
async function handleFileChange(fileData: ProcessedFile): Promise<void> {
const parseResult: FileParseResult = await parseFileAsJson(fileData.file);
if (parseResult.success) {
fileName = fileData.name;
setFieldValue(name, parseResult.data);
dispatch('change', {
success: true,
data: parseResult.data,
fileName: fileData.name,
});
} else {
fileName = null;
setFieldValue(name, null);
dispatch('change', {
success: false,
error: parseResult.error,
fileName: fileData.name,
});
}
}
function getDisplayLabel(baseLabel: string, hasValue: boolean, fileName: string | null): string {
if (!hasValue) {
return baseLabel;
}
if (fileName) {
return `${baseLabel} (${fileName})`;
}
return `${baseLabel} (JSON loaded)`;
}
</script>
<svelte:component this={template} type="file" {label} {disabled} {...templateProps}>
<SimpleFilesInput label={displayLabel} {accept} {disabled} onChange={handleFileChange} {...$$restProps} />
</svelte:component>

View File

@@ -73,6 +73,7 @@
'icon scheduler-event': 'mdi mdi-calendar-blank', 'icon scheduler-event': 'mdi mdi-calendar-blank',
'icon arrow-link': 'mdi mdi-arrow-top-right-thick', 'icon arrow-link': 'mdi mdi-arrow-top-right-thick',
'icon reset': 'mdi mdi-cancel', 'icon reset': 'mdi mdi-cancel',
'icon send': 'mdi mdi-send',
'icon window-restore': 'mdi mdi-window-restore', 'icon window-restore': 'mdi mdi-window-restore',
'icon window-maximize': 'mdi mdi-window-maximize', 'icon window-maximize': 'mdi mdi-window-maximize',
@@ -163,8 +164,10 @@
'icon wait': 'mdi mdi-timer-sand', 'icon wait': 'mdi mdi-timer-sand',
'icon more': 'mdi mdi-more', 'icon more': 'mdi mdi-more',
'icon copy': 'mdi mdi-content-copy', 'icon copy': 'mdi mdi-content-copy',
'icon arrow-start-here': 'mdi mdi-arrow-down-bold-circle',
'icon run': 'mdi mdi-play', 'icon run': 'mdi mdi-play',
'icon run-settings': 'mdi mdi-cog-play',
'icon chevron-down': 'mdi mdi-chevron-down', 'icon chevron-down': 'mdi mdi-chevron-down',
'icon chevron-left': 'mdi mdi-chevron-left', 'icon chevron-left': 'mdi mdi-chevron-left',
'icon chevron-right': 'mdi mdi-chevron-right', 'icon chevron-right': 'mdi mdi-chevron-right',
@@ -280,6 +283,8 @@
'img admin': 'mdi mdi-security color-icon-blue', 'img admin': 'mdi mdi-security color-icon-blue',
'img auth': 'mdi mdi-account-key color-icon-blue', 'img auth': 'mdi mdi-account-key color-icon-blue',
'img cloud-connection': 'mdi mdi-cloud-lock color-icon-blue', 'img cloud-connection': 'mdi mdi-cloud-lock color-icon-blue',
'img ai': 'mdi mdi-head-lightbulb color-icon-yellow',
'img run': 'mdi mdi-play color-icon-blue',
'img add': 'mdi mdi-plus-circle color-icon-green', 'img add': 'mdi mdi-plus-circle color-icon-green',
'img minus': 'mdi mdi-minus-circle color-icon-red', 'img minus': 'mdi mdi-minus-circle color-icon-red',
@@ -338,6 +343,7 @@
'img db-restore': 'mdi mdi-database-import color-icon-red', 'img db-restore': 'mdi mdi-database-import color-icon-red',
'img settings': 'mdi mdi-cog color-icon-blue', 'img settings': 'mdi mdi-cog color-icon-blue',
'img data-deploy': 'mdi mdi-database-settings color-icon-green', 'img data-deploy': 'mdi mdi-database-settings color-icon-green',
'img arrow-start-here': 'mdi mdi-arrow-down-bold-circle color-icon-green',
}; };
</script> </script>

View File

@@ -0,0 +1,51 @@
<!-- Chatbot “thinking” indicator -->
<div class="typing-indicator" aria-label="Chatbot is thinking">
<span></span><span></span><span></span>
</div>
<style>
/* Container keeps the dots neatly aligned */
.typing-indicator {
display: inline-flex;
gap: 0.4rem;
align-items: center;
justify-content: center;
/* Optional tweaks */
height: 1.25rem; /* keeps layout height consistent */
}
/* Each dot */
.typing-indicator span {
width: 0.5rem;
height: 0.5rem;
background: currentColor; /* inherits text color */
border-radius: 50%;
opacity: 0.2; /* start slightly faded */
animation: pulse 1s infinite ease-in-out;
}
/* Delay each dot so they animate sequentially */
.typing-indicator span:nth-child(1) {
animation-delay: 0s;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.15s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.3s;
}
/* Keyframes for the pulsing effect */
@keyframes pulse {
0%,
80%,
100% {
opacity: 0.2;
transform: translateY(0);
}
40% {
opacity: 1;
transform: translateY(-0.2rem);
}
}
</style>

View File

@@ -0,0 +1,105 @@
<script context="module" lang="ts">
export type ProcessedFile = {
name: string;
size: number;
type: string;
lastModified: number;
content: string | ArrayBuffer | null;
file: File;
};
</script>
<script lang="ts">
export let disabled: boolean = false;
export let label: string = 'Choose File';
export let onChange: ((fileData: ProcessedFile | ProcessedFile[]) => void) | null = null;
export let accept: string = '*';
export let multiple: boolean = false;
let fileInput: HTMLInputElement;
function handleFileChange(event: Event): void {
const target = event.target as HTMLInputElement;
const files = target.files;
if (!files || files.length < 0 || !onChange) return;
if (multiple) {
const processedFiles = Array.from(files).map(processFile);
Promise.all(processedFiles).then((results: ProcessedFile[]) => {
onChange(results);
});
} else {
processFile(files[0]).then((result: ProcessedFile) => {
onChange(result);
});
}
}
function processFile(file: File): Promise<ProcessedFile> {
return new Promise(resolve => {
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
resolve({
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
content: e.target?.result || null,
file: file,
});
};
reader.readAsDataURL(file);
});
}
function triggerFileInput(): void {
fileInput.click();
}
</script>
<input
{disabled}
bind:this={fileInput}
type="file"
{accept}
{multiple}
on:change={handleFileChange}
style="display: none;"
/>
<button {disabled} on:click={triggerFileInput} class="file-input-btn">
{label}
</button>
<style>
.file-input-btn {
border: 1px solid var(--theme-bg-button-inv-2);
padding: 5px;
margin: 2px;
background-color: var(--theme-bg-button-inv);
color: var(--theme-font-inv-1);
border-radius: 2px;
cursor: pointer;
}
.file-input-btn:hover:not(:disabled) {
background-color: var(--theme-bg-button-inv-2);
}
.file-input-btn:active:not(:disabled) {
background-color: var(--theme-bg-button-inv-3);
}
.file-input-btn:focus {
outline: 2px solid var(--theme-bg-button-inv-2);
outline-offset: 2px;
}
.file-input-btn:disabled {
background-color: var(--theme-bg-button-inv-3);
color: var(--theme-font-inv-3);
cursor: not-allowed;
}
</style>

View File

@@ -173,8 +173,10 @@
{#if storageType == 'database' || storageType == 'query'} {#if storageType == 'database' || storageType == 'query'}
<FormConnectionSelect name={connectionIdField} label="Server" {direction} /> <FormConnectionSelect name={connectionIdField} label="Server" {direction} />
{#if !$connectionInfo?.singleDatabase}
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label="Database" /> <FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label="Database" />
{/if} {/if}
{/if}
{#if storageType == 'database'} {#if storageType == 'database'}
<FormSchemaSelect <FormSchemaSelect
conidName={connectionIdField} conidName={connectionIdField}

View File

@@ -105,6 +105,16 @@
disabledMessage: 'Database comparison is not available for current database', disabledMessage: 'Database comparison is not available for current database',
isProFeature: true, isProFeature: true,
}, },
{
icon: 'icon ai',
colorClass: 'color-icon-blue',
title: 'Database Chat',
description: 'Chat with your database using AI',
command: 'database.chat',
isProFeature: true,
disabledMessage: 'Database chat is not available for current database',
testid: 'NewObjectModal_databaseChat',
}
]; ];
</script> </script>

View File

@@ -63,7 +63,9 @@
<div class="row"> <div class="row">
<FormRadioGroupItem name="joinOperator" value=" " text="And" /> <FormRadioGroupItem name="joinOperator" value=" " text="And" />
{#if !filterBehaviour.disableOr}
<FormRadioGroupItem name="joinOperator" value="," text="Or" /> <FormRadioGroupItem name="joinOperator" value="," text="Or" />
{/if}
</div> </div>
<div class="row"> <div class="row">

View File

@@ -1 +0,0 @@
AI Assistant

View File

@@ -18,6 +18,9 @@
import FormDropDownTextField from '../forms/FormDropDownTextField.svelte'; import FormDropDownTextField from '../forms/FormDropDownTextField.svelte';
import { getConnectionLabel } from 'dbgate-tools'; import { getConnectionLabel } from 'dbgate-tools';
import { _t } from '../translations'; import { _t } from '../translations';
import FilesInput from '../impexp/FilesInput.svelte';
import SimpleFilesInput from '../impexp/SimpleFilesInput.svelte';
import FormJsonFileInputField from '../forms/FormJsonFileInputField.svelte';
export let getDatabaseList; export let getDatabaseList;
export let currentConnection; export let currentConnection;
@@ -462,6 +465,10 @@
/> />
{/if} {/if}
{#if driver?.showConnectionField('certificateJson', $values, showConnectionFieldArgs)}
<FormJsonFileInputField disabled={isConnected} label="Service account key JSON" name="certificateJson" />
{/if}
{#if driver} {#if driver}
<div class="row"> <div class="row">
<div class="col-6 mr-1"> <div class="col-6 mr-1">

View File

@@ -13,15 +13,6 @@
testEnabled: () => getCurrentEditor()?.isSqlEditor(), testEnabled: () => getCurrentEditor()?.isSqlEditor(),
onClick: () => getCurrentEditor().formatCode(), onClick: () => getCurrentEditor().formatCode(),
}); });
registerCommand({
id: 'query.switchAiAssistant',
category: 'Query',
name: 'AI Assistant',
keyText: 'Shift+Alt+A',
icon: 'icon ai',
testEnabled: () => isProApp(),
onClick: () => getCurrentEditor().toggleAiAssistant(),
});
registerCommand({ registerCommand({
id: 'query.insertSqlJoin', id: 'query.insertSqlJoin',
category: 'Query', category: 'Query',
@@ -157,7 +148,6 @@
import QueryParametersModal from '../modals/QueryParametersModal.svelte'; import QueryParametersModal from '../modals/QueryParametersModal.svelte';
import { isProApp } from '../utility/proTools'; import { isProApp } from '../utility/proTools';
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte'; import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
import QueryAiAssistant from '../query/QueryAiAssistant.svelte';
import uuidv1 from 'uuid/v1'; import uuidv1 from 'uuid/v1';
import ToolStripButton from '../buttons/ToolStripButton.svelte'; import ToolStripButton from '../buttons/ToolStripButton.svelte';
import { getIntSettingsValue } from '../settings/settingsTools'; import { getIntSettingsValue } from '../settings/settingsTools';
@@ -212,8 +202,6 @@
let domEditor; let domEditor;
let domToolStrip; let domToolStrip;
let intervalId; let intervalId;
let isAiAssistantVisible = isProApp() && localStorage.getItem(`tabdata_isAiAssistantVisible_${tabid}`) == 'true';
let domAiAssistant;
let isInTransaction = false; let isInTransaction = false;
let isAutocommit = false; let isAutocommit = false;
let splitterInitialValue = undefined; let splitterInitialValue = undefined;
@@ -287,12 +275,6 @@
domEditor?.getEditor()?.focus(); domEditor?.getEditor()?.focus();
} }
$: {
if (!isAiAssistantVisible && domEditor) {
domEditor?.getEditor()?.focus();
}
}
export function isSqlEditor() { export function isSqlEditor() {
return driver?.databaseEngineTypes?.includes('sql'); return driver?.databaseEngineTypes?.includes('sql');
} }
@@ -317,10 +299,6 @@
visibleResultTabs = !visibleResultTabs; visibleResultTabs = !visibleResultTabs;
} }
export function toggleAiAssistant() {
isAiAssistantVisible = !isAiAssistantVisible;
}
function getParameterSplitterOptions() { function getParameterSplitterOptions() {
if (!queryParameterStyle) { if (!queryParameterStyle) {
return null; return null;
@@ -631,29 +609,6 @@
); );
} }
async function handleKeyDown(event) {
if (isProApp()) {
if (event.code == 'Space' && event.shiftKey && event.ctrlKey && !isAiAssistantVisible) {
event.preventDefault();
event.stopPropagation();
toggleAiAssistant();
await sleep(100);
if (domAiAssistant) {
domAiAssistant.handleCompleteOnCursor();
domEditor?.getEditor()?.focus();
}
} else if (event.code == 'Space' && event.shiftKey && event.ctrlKey && isAiAssistantVisible && domAiAssistant) {
event.preventDefault();
event.stopPropagation();
domAiAssistant.handleCompleteOnCursor();
} else if (event.code?.startsWith('Digit') && event.altKey && isAiAssistantVisible && domAiAssistant) {
event.preventDefault();
event.stopPropagation();
domAiAssistant.insertCompletion(parseInt(event.code.substring(5)) - 1);
}
}
}
function createMenu() { function createMenu() {
return [ return [
{ command: 'query.execute' }, { command: 'query.execute' },
@@ -675,7 +630,6 @@
{ command: 'query.replace' }, { command: 'query.replace' },
{ divider: true }, { divider: true },
{ command: 'query.toggleVisibleResultTabs' }, { command: 'query.toggleVisibleResultTabs' },
{ command: 'query.switchAiAssistant', hideDisabled: true },
]; ];
} }
@@ -695,13 +649,9 @@
localStorage.getItem(`tabdata_queryParamStyle_${tabid}`) ?? localStorage.getItem(`tabdata_queryParamStyle_${tabid}`) ??
initialArgs?.queryParameterStyle ?? initialArgs?.queryParameterStyle ??
(initialArgs?.scriptTemplate == 'CALL OBJECT' ? ':' : ''); (initialArgs?.scriptTemplate == 'CALL OBJECT' ? ':' : '');
$: localStorage.setItem(`tabdata_isAiAssistantVisible_${tabid}`, isAiAssistantVisible ? 'true' : 'false');
</script> </script>
<ToolStripContainer bind:this={domToolStrip}> <ToolStripContainer bind:this={domToolStrip}>
<HorizontalSplitter isSplitter={isAiAssistantVisible} initialSizeRight={300}>
<svelte:fragment slot="1">
<VerticalSplitter isSplitter={visibleResultTabs} initialValue={splitterInitialValue}> <VerticalSplitter isSplitter={visibleResultTabs} initialValue={splitterInitialValue}>
<svelte:fragment slot="1"> <svelte:fragment slot="1">
{#if driver?.databaseEngineTypes?.includes('sql')} {#if driver?.databaseEngineTypes?.includes('sql')}
@@ -736,7 +686,6 @@
bind:this={domEditor} bind:this={domEditor}
onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)} onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)}
{errorMessages} {errorMessages}
onKeyDown={handleKeyDown}
/> />
{:else} {:else}
<AceEditor <AceEditor
@@ -785,37 +734,6 @@
</ResultTabs> </ResultTabs>
</svelte:fragment> </svelte:fragment>
</VerticalSplitter> </VerticalSplitter>
</svelte:fragment>
<svelte:fragment slot="2">
<QueryAiAssistant
bind:this={domAiAssistant}
{conid}
{database}
{driver}
onClose={() => {
isAiAssistantVisible = false;
}}
text={$editorValue}
getLine={() => domEditor.getEditor().getSelectionRange().start.row}
onInsertAtCursor={text => {
const editor = domEditor.getEditor();
editor.session.insert(editor.getCursorPosition(), text);
domEditor?.getEditor()?.focus();
}}
getTextOrSelectedText={() => domEditor.getEditor().getSelectedText() || $editorValue}
onSetSelectedText={text => {
const editor = domEditor.getEditor();
if (editor.getSelectedText()) {
const range = editor.selection.getRange();
editor.session.replace(range, text);
} else {
editor.setValue(text);
}
}}
{tabid}
/>
</svelte:fragment>
</HorizontalSplitter>
<svelte:fragment slot="toolstrip"> <svelte:fragment slot="toolstrip">
<ToolStripCommandSplitButton <ToolStripCommandSplitButton
commands={['query.execute', 'query.executeCurrent']} commands={['query.execute', 'query.executeCurrent']}
@@ -854,13 +772,6 @@
icon="icon at" icon="icon at"
title="Query parameter style" title="Query parameter style"
/> />
<ToolStripCommandButton
command="query.switchAiAssistant"
hideDisabled
data-testid="QueryTab_switchAiAssistantButton"
>
AI Assistant
</ToolStripCommandButton>
<ToolStripCommandButton <ToolStripCommandButton
command="query.beginTransaction" command="query.beginTransaction"
data-testid="QueryTab_beginTransactionButton" data-testid="QueryTab_beginTransactionButton"

View File

@@ -0,0 +1,38 @@
/**
* @template [T = any]
* @typedef {Object} FileParseResultSuccess
* @property {true} success
* @property {T} data
*/
/**
* @typedef {Object} FileParseResultError
* @property {false} success
* @property {string} error
*/
/**
* @template [T = any]
* @typedef {FileParseResultSuccess<T> | FileParseResultError} FileParseResult
*/
/**
* @template [T = any]
* @param {File} file
* @returns {Promise<FileParseResult<T>>}
*/
export async function parseFileAsJson(file) {
try {
const text = await file.text();
const data = JSON.parse(text);
return {
success: true,
data,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown parsing error',
};
}
}

View File

@@ -60,11 +60,11 @@
name: 'archive', name: 'archive',
title: 'Archive (saved tabular data)', title: 'Archive (saved tabular data)',
}, },
{ // {
icon: 'icon plugin', // icon: 'icon plugin',
name: 'plugins', // name: 'plugins',
title: 'Extensions & Plugins', // title: 'Extensions & Plugins',
}, // },
{ {
icon: 'icon cell-data', icon: 'icon cell-data',
name: 'cell-data', name: 'cell-data',
@@ -116,6 +116,13 @@
$visibleWidgetSideBar = true; $visibleWidgetSideBar = true;
}, },
}, },
{
text: 'Manage plugins',
onClick: () => {
$selectedWidget = 'plugins';
$visibleWidgetSideBar = true;
},
},
]; ];
currentDropDownMenu.set({ left, top, items }); currentDropDownMenu.set({ left, top, items });
} }
@@ -159,6 +166,7 @@
{#each widgets {#each widgets
.filter(x => x && hasPermission(`widgets/${x.name}`)) .filter(x => x && hasPermission(`widgets/${x.name}`))
.filter(x => !x.isPremiumPromo || !isProApp()) .filter(x => !x.isPremiumPromo || !isProApp())
// .filter(x => !x.isPremiumOnly || isProApp())
.filter(x => x.name != 'cloud-private' || $cloudSigninTokenHolder) as item} .filter(x => x.name != 'cloud-private' || $cloudSigninTokenHolder) as item}
<div <div
class="wrapper" class="wrapper"

20
silent Normal file
View File

@@ -0,0 +1,20 @@
DEVMODE=1
SHELL_SCRIPTING=1
# LOCAL_DBGATE_CLOUD=1
# LOCAL_DBGATE_IDENTITY=1
# CLOUD_UPGRADE_FILE=c:\test\upg\upgrade.zip
# PERMISSIONS=~widgets/app,~widgets/plugins
# DISABLE_SHELL=1
# HIDE_APP_EDITOR=1
# DEVWEB=1
# LOGINS=admin,test
# LOGIN_PASSWORD_admin=admin
# LOGIN_PERMISSIONS_admin=*
# LOGIN_PASSWORD_test=test
# LOGIN_PERMISSIONS_test=~*, widgets/database
# WORKSPACE_DIR=/home/jena/dbgate-data-2

View File

@@ -100,3 +100,9 @@ jobs:
cd .. cd ..
cd dbgate-merged/plugins/dbgate-plugin-cosmosdb cd dbgate-merged/plugins/dbgate-plugin-cosmosdb
npm publish npm publish
- name: Publish dbgate-plugin-firestore
run: |
cd ..
cd dbgate-merged/plugins/dbgate-plugin-firestore
npm publish

View File

@@ -7,7 +7,7 @@ checkout-and-merge-pro:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro path: dbgate-pro
ref: 458d8843318c2f65aae6524bbea30513d88f4bf6 ref: 8a4dc2732a7097b5c4c48b4feb62609111cdf3e0
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

658
yarn.lock

File diff suppressed because it is too large Load Diff