Compare commits
260 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64c2faf538 | |||
| c741434e3c | |||
| 60fcb1a862 | |||
| 18dc6a3ff5 | |||
| da52dd006b | |||
| ed1655ed8f | |||
| 516c4e32be | |||
| 1e222d806e | |||
| bfb35b198d | |||
| 3a5f36155f | |||
| 82092fab76 | |||
| a432cb886d | |||
| 5fff8c6ba2 | |||
| e1de4f5c5f | |||
| b6145d6f1e | |||
| 3804a87cef | |||
| a7a6c664c8 | |||
| f075515607 | |||
| 84c15bbc69 | |||
| 54e70d490d | |||
| 67c3de1f5d | |||
| 7281b5b1d7 | |||
| 966eb01f1c | |||
| 5bdf072cdf | |||
| 59c3381962 | |||
| 1fa4216b18 | |||
| a98c953876 | |||
| bf995e5861 | |||
| 1b8470df38 | |||
| 98ded8ea30 | |||
| b72a50eb7e | |||
| 9c3f4fbb9d | |||
| 28c68308a9 | |||
| ea3b0c15ac | |||
| 38ce46adb0 | |||
| 0a9a0103dd | |||
| b7b370ff62 | |||
| 7b0c98ad2c | |||
| bfe25e70d6 | |||
| b2409df369 | |||
| e1d8549730 | |||
| 865cc081ce | |||
| 867d5a9eb5 | |||
| ac84b7604b | |||
| 8f860ad93e | |||
| f3b65700d7 | |||
| 8ec1856206 | |||
| 811d2162fc | |||
| 5e2cdca103 | |||
| 23fb5852ba | |||
| 75cbc0d29a | |||
| d08cd684c5 | |||
| 529b297ba6 | |||
| 32193eef49 | |||
| 1f97b90b2d | |||
| 0dd36260e9 | |||
| 3571d49987 | |||
| ad3489c491 | |||
| 2461fa2e25 | |||
| 60e49ba343 | |||
| a709381980 | |||
| c2805c8c1c | |||
| 0346cbe911 | |||
| 74a4d4455b | |||
| 3659e1c91f | |||
| 09da5c6968 | |||
| 2575efd28d | |||
| 78c1c8d2b1 | |||
| d5147f3dbb | |||
| 593580fbc1 | |||
| 67ca1cb638 | |||
| bcf5b64545 | |||
| e37ad663b3 | |||
| 4ce7582a46 | |||
| 1c371bb7bf | |||
| 17fdeb0734 | |||
| 5c6f0c32b3 | |||
| e630280673 | |||
| 7c87961adf | |||
| 0df5ceb7d2 | |||
| 54342f2592 | |||
| fbad558c37 | |||
| b27dfb290c | |||
| 063c930349 | |||
| c1f1e489a7 | |||
| 62960ed8de | |||
| 03305e04a7 | |||
| 4b294b1125 | |||
| 7122a21591 | |||
| 9682e571a2 | |||
| 2cefbfb8aa | |||
| 084062488c | |||
| 4dbe2b5297 | |||
| 0391e5bc3d | |||
| bcbd96c608 | |||
| 6a6633e151 | |||
| 5c4546a54c | |||
| 255e328340 | |||
| aaf9b085d7 | |||
| c7b14c9fab | |||
| 0436ba78e2 | |||
| 1085a1c221 | |||
| 494b33bd7a | |||
| 925e3a67da | |||
| 78026f7fa5 | |||
| 9d77cac4bb | |||
| 6747280964 | |||
| d7dbd79f7c | |||
| aec692c402 | |||
| 113bbead4a | |||
| 1361c196da | |||
| 987995ad68 | |||
| d24db7c053 | |||
| 25a9d52d86 | |||
| 946c632920 | |||
| 94bcbb80fd | |||
| ba58965770 | |||
| e50ddbf348 | |||
| e95f21fa9c | |||
| 7026b765bd | |||
| 53eedd2701 | |||
| 9886c58681 | |||
| 953f6da7d7 | |||
| da1efe880d | |||
| bd0b6dd4d2 | |||
| 1c049fe1fb | |||
| 10b1b87d55 | |||
| 3ec6a3b3f2 | |||
| 8da919d4cd | |||
| 5cd59b795b | |||
| e4dc30d1fb | |||
| 2013cee298 | |||
| 1f89a6304b | |||
| 580e0f9df7 | |||
| 2221c4548e | |||
| e06c226e84 | |||
| 11a4f0ef32 | |||
| ef15f299d2 | |||
| 1d333b9322 | |||
| 5302ed8653 | |||
| 2cf26a10c4 | |||
| deda1e4251 | |||
| c042bf2d15 | |||
| 8ced6aa205 | |||
| 3ca514c85b | |||
| 27b8e7d5ec | |||
| a2d77a3917 | |||
| c0549fe422 | |||
| 0dc8d6fd68 | |||
| cd97647818 | |||
| fcb5811f37 | |||
| a239ba2211 | |||
| e8dc96bcda | |||
| 096ad97a73 | |||
| a5a5517555 | |||
| d9b88a5d8d | |||
| 8493ea22eb | |||
| aebb87aa20 | |||
| 14e97cb24f | |||
| 26cc15b4a2 | |||
| 50ce606e12 | |||
| b052320f98 | |||
| ecb3cebc9f | |||
| 4a32dfc71b | |||
| c913929ff9 | |||
| d7d5b29b07 | |||
| 111a7f72f8 | |||
| 149abdef9b | |||
| 980848f35a | |||
| d1e0c86a71 | |||
| d3872ca8a3 | |||
| 003dec269a | |||
| e88092cde7 | |||
| 98422bd355 | |||
| af83b89812 | |||
| cffb1b8713 | |||
| 0b4895addf | |||
| 08646ea12a | |||
| 40beb7ceeb | |||
| e7963aa324 | |||
| d5894b9fb7 | |||
| 75c47a1113 | |||
| 97923b19bf | |||
| 879c89a285 | |||
| 718727462b | |||
| 63f2fd864a | |||
| 556dda5790 | |||
| aeaa8549e3 | |||
| 1a8a757912 | |||
| c69bb8acc9 | |||
| 4989d67b92 | |||
| b272d342b0 | |||
| 251b2853e0 | |||
| d381c9505f | |||
| eeb3b8f939 | |||
| ee40f32b0c | |||
| 02a69ea6d9 | |||
| d47bb5ecd4 | |||
| d2d6e2f554 | |||
| f48b4a6c62 | |||
| 07f7b7df1b | |||
| 26486f9d63 | |||
| 428aa970b9 | |||
| 2a8c532786 | |||
| 4381829d16 | |||
| 176d75768f | |||
| e28e363bd0 | |||
| 114ce1ea3a | |||
| 78215552bf | |||
| be4fe6ab77 | |||
| ab924f6b48 | |||
| e4bf2b4c9b | |||
| c49b1a46f8 | |||
| 6a56726734 | |||
| 8f6783792f | |||
| b5ab1d6b33 | |||
| 25fe1d03a7 | |||
| 90546ad4a7 | |||
| 939bbc3f2c | |||
| 02ee327595 | |||
| d8081277ee | |||
| 164a112e0c | |||
| 697bde7b53 | |||
| 2fd5244f85 | |||
| 354d925f94 | |||
| 88c74f020c | |||
| 9b7021b1cd | |||
| 30dbb23330 | |||
| b1696ed1cd | |||
| 61f1c99791 | |||
| bb9a559b80 | |||
| 5e20ea4975 | |||
| c5d7e30bed | |||
| de5f3a31ed | |||
| 7913c4135f | |||
| d49345de9c | |||
| 1dbfa71bde | |||
| d46b84f0d6 | |||
| 9d456992cf | |||
| 5dd62ad2aa | |||
| a293eeb398 | |||
| a6b6b5eb70 | |||
| 971af1df5f | |||
| 21641da0bf | |||
| a8d9c145e6 | |||
| bfafcb76ba | |||
| 52f74f1204 | |||
| de43880a1c | |||
| 32b1a5b22d | |||
| 795992fb42 | |||
| 339eab33c8 | |||
| 489f3aa19d | |||
| 888e284f84 | |||
| d151114f08 | |||
| 4f6a3c23ad | |||
| c20aec23a2 | |||
| a9cff01579 | |||
| 6af56a61b8 | |||
| 252db191a6 | |||
| 5e2776f264 |
@@ -53,12 +53,6 @@ jobs:
|
||||
run: |
|
||||
|
||||
yarn setCurrentVersion
|
||||
- name: printSecrets
|
||||
run: |
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
|
||||
|
||||
@@ -49,12 +49,6 @@ jobs:
|
||||
run: |
|
||||
|
||||
yarn setCurrentVersion
|
||||
- name: printSecrets
|
||||
run: |
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4b28757ade169ac0a1696351519bbaa4bbba5db9
|
||||
ref: 11203754aad94189b565c2816d37760b15c8e07f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -81,14 +81,6 @@ jobs:
|
||||
cd dbgate-merged
|
||||
|
||||
yarn setCurrentVersion
|
||||
- name: printSecrets
|
||||
run: |
|
||||
cd ..
|
||||
cd dbgate-merged
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
cd ..
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4b28757ade169ac0a1696351519bbaa4bbba5db9
|
||||
ref: 11203754aad94189b565c2816d37760b15c8e07f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -81,14 +81,6 @@ jobs:
|
||||
cd dbgate-merged
|
||||
|
||||
yarn setCurrentVersion
|
||||
- name: printSecrets
|
||||
run: |
|
||||
cd ..
|
||||
cd dbgate-merged
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
cd ..
|
||||
|
||||
@@ -49,12 +49,6 @@ jobs:
|
||||
run: |
|
||||
|
||||
yarn setCurrentVersion
|
||||
- name: printSecrets
|
||||
run: |
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: fillPackagedPlugins
|
||||
run: |
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4b28757ade169ac0a1696351519bbaa4bbba5db9
|
||||
ref: 11203754aad94189b565c2816d37760b15c8e07f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -66,13 +66,6 @@ jobs:
|
||||
cd ..
|
||||
cd dbgate-merged
|
||||
yarn setCurrentVersion
|
||||
- name: printSecrets
|
||||
run: |
|
||||
cd ..
|
||||
cd dbgate-merged
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: Prepare packer build
|
||||
run: |
|
||||
cd ..
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4b28757ade169ac0a1696351519bbaa4bbba5db9
|
||||
ref: 11203754aad94189b565c2816d37760b15c8e07f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -76,14 +76,6 @@ jobs:
|
||||
cd dbgate-merged
|
||||
|
||||
yarn setCurrentVersion
|
||||
- name: printSecrets
|
||||
run: |
|
||||
cd ..
|
||||
cd dbgate-merged
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: Prepare docker image
|
||||
run: |
|
||||
cd ..
|
||||
|
||||
@@ -65,12 +65,6 @@ jobs:
|
||||
run: |
|
||||
|
||||
yarn setCurrentVersion
|
||||
- name: printSecrets
|
||||
run: |
|
||||
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: Prepare docker image
|
||||
run: |
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4b28757ade169ac0a1696351519bbaa4bbba5db9
|
||||
ref: 11203754aad94189b565c2816d37760b15c8e07f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
@@ -71,13 +71,6 @@ jobs:
|
||||
cd ..
|
||||
cd dbgate-merged
|
||||
yarn setCurrentVersion
|
||||
- name: printSecrets
|
||||
run: |
|
||||
cd ..
|
||||
cd dbgate-merged
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: Publish dbgate-api-premium
|
||||
run: |
|
||||
cd ..
|
||||
|
||||
@@ -37,11 +37,6 @@ jobs:
|
||||
- name: setCurrentVersion
|
||||
run: |
|
||||
yarn setCurrentVersion
|
||||
- name: printSecrets
|
||||
run: |
|
||||
yarn printSecrets
|
||||
env:
|
||||
GIST_UPLOAD_SECRET: ${{secrets.GIST_UPLOAD_SECRET}}
|
||||
- name: Publish types
|
||||
working-directory: packages/types
|
||||
run: |
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4b28757ade169ac0a1696351519bbaa4bbba5db9
|
||||
ref: 11203754aad94189b565c2816d37760b15c8e07f
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
ports:
|
||||
- '15002:1433'
|
||||
clickhouse-integr:
|
||||
image: bitnami/clickhouse:24.8.4
|
||||
image: bitnamilegacy/clickhouse:24.8.4
|
||||
env:
|
||||
CLICKHOUSE_ADMIN_PASSWORD: Pwd2020Db
|
||||
ports:
|
||||
|
||||
@@ -8,6 +8,49 @@ Builds:
|
||||
- linux - application for linux
|
||||
- win - application for Windows
|
||||
|
||||
## 6.6.5
|
||||
- ADDED: SQL AI assistant - powered by database chat, could help you to write SQL queries (Premium)
|
||||
- ADDED: Explain SQL error (powered by AI) (Premium)
|
||||
- ADDED: Database chat (and SQL AI Assistant) now supports showing charts (Premium)
|
||||
- FIXED: Fxied editing new files and roles (Team Premium)
|
||||
- FIXED: Connection to standalone database could be now pinned
|
||||
|
||||
## 6.6.4
|
||||
- ADDED: AI Database chat now supports much more LLM models. (Premium)
|
||||
- ADDED: Possibility to use your own API key with OPENAI-compatible providers (OpenRouter, Antropic...)
|
||||
- ADDED: Possibility to use self-hosted own LLM (eg. Llama)
|
||||
- ADDED: Team files - save SQL files and define shared charts, assign roles and users to these objects (Team Premium)
|
||||
- FIXED: BUG: does no longer work with Cockroach DB #1202
|
||||
- FIXED: DbGate Web UI Connections do not display 'Databases' #1199
|
||||
- CHANGED: Redesign fof applications. Applications are now storted in single JSON file
|
||||
- ADDED: Application editor (Premium)
|
||||
- ADDED: Posibility to filter only tables with rows
|
||||
- FIXED: Fixed several issues with large Firebird databases
|
||||
- CHANGED: Community edition now supports shared folders in read-only mode
|
||||
|
||||
## 6.6.3
|
||||
- FIXED: Error “db.getCollection(…).renameCollection is not a function” when renaming collection in dbGate #1198
|
||||
- FIXED: Can't list databases from Azure SQL SERVER #1197
|
||||
- ADDED: Save zoom level in electron apps
|
||||
|
||||
## 6.6.2
|
||||
- ADDED: List of processes, ability to kill process (Server summary) #1178
|
||||
- ADDED: Database and table permissions (Team Premium edition)
|
||||
- ADDED: Redis search box - Scan all #1191
|
||||
- FIXED: Optimalized loading SQL server with descriptions #1187
|
||||
- CHANGED: Allow a much greater page size #1185
|
||||
- FIXED: Optimalized loading SQL server with descriptions #1187
|
||||
- FIXED: Executing queries for SQLite crash #1195
|
||||
|
||||
## 6.6.1
|
||||
- ADDED: Support for Mongo shell (Premium) - #1114
|
||||
- FIXED: Support for BLOB in Oracle #1181
|
||||
- ADDED: Connect to named SQL Server instance #340
|
||||
- ADDED: Support for SQL Server descriptions #1137
|
||||
- ADDED: Application log viewer
|
||||
- FIXED: Selecting default database in connection dialog
|
||||
- CHANGED: Improved logging system, added related database and connection to logs metadata
|
||||
|
||||
## 6.6.0
|
||||
- ADDED: Database chat - AI powered chatbot, which knows your database (Premium)
|
||||
- ADDED: Firestore support (Premium)
|
||||
|
||||
@@ -15,7 +15,7 @@ But there are also many advanced features like schema compare, visual query desi
|
||||
DbGate is licensed under GPL-3.0 license and is free to use for any purpose.
|
||||
|
||||
* Try it online - [demo.dbgate.org](https://demo.dbgate.org) - online demo application
|
||||
* **Download** application for Windows, Linux or Mac from [dbgate.io](https://dbgate.io/download/)
|
||||
* **Download** application for Windows, Linux or Mac from [dbgate.io](https://www.dbgate.io/download/)
|
||||
* Looking for DbGate Community? **Download** from [dbgate.org](https://dbgate.org/download/)
|
||||
* Run web version as [NPM package](https://www.npmjs.com/package/dbgate-serve) or as [docker image](https://hub.docker.com/r/dbgate/dbgate)
|
||||
* Use nodeJs [scripting interface](https://docs.dbgate.io/scripting) ([API documentation](https://docs.dbgate.io/apidoc))
|
||||
@@ -92,7 +92,7 @@ DbGate is licensed under GPL-3.0 license and is free to use for any purpose.
|
||||
Any contributions are welcome. If you want to contribute without coding, consider following:
|
||||
|
||||
* Tell your friends about DbGate or share on social networks - when more people will use DbGate, it will grow to be better
|
||||
* Purchase a [DbGate Premium](https://dbgate.io/purchase/premium/) liocense
|
||||
* Purchase a [DbGate Premium](https://www.dbgate.io/purchase/premium/) license
|
||||
* Write review on [Product Hunt](https://www.producthunt.com/products/dbgate) or [G2](https://www.g2.com/products/dbgate/reviews) - we offer [2-year PREMIUM license](https://dbgate.org/review/) for reviewers (time limited offer)
|
||||
* Create issue, if you find problem in app, or you have idea to new feature. If issue already exists, you could leave comment on it, to prioritise most wanted issues
|
||||
* Create some tutorial video on [youtube](https://www.youtube.com/playlist?list=PLCo7KjCVXhr0RfUSjM9wJMsp_ShL1q61A)
|
||||
|
||||
@@ -212,6 +212,10 @@ ipcMain.on('app-started', async (event, arg) => {
|
||||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
if (initialConfig['winZoomLevel'] != null) {
|
||||
mainWindow.webContents.zoomLevel = initialConfig['winZoomLevel'];
|
||||
}
|
||||
});
|
||||
ipcMain.on('window-action', async (event, arg) => {
|
||||
if (!mainWindow) {
|
||||
@@ -394,6 +398,7 @@ function createWindow() {
|
||||
JSON.stringify({
|
||||
winBounds: mainWindow.getBounds(),
|
||||
winIsMaximized: mainWindow.isMaximized(),
|
||||
winZoomLevel: mainWindow.webContents.zoomLevel,
|
||||
}),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ module.exports = ({ editMenu, isMac }) => [
|
||||
{ command: 'new.queryDesign', hideDisabled: true },
|
||||
{ command: 'new.diagram', hideDisabled: true },
|
||||
{ command: 'new.perspective', hideDisabled: true },
|
||||
{ command: 'new.application', hideDisabled: true },
|
||||
{ command: 'new.shell', hideDisabled: true },
|
||||
{ command: 'new.jsonl', hideDisabled: true },
|
||||
{ command: 'new.modelTransform', hideDisabled: true },
|
||||
@@ -86,7 +87,6 @@ module.exports = ({ editMenu, isMac }) => [
|
||||
{ divider: true },
|
||||
{ command: 'folder.showLogs', hideDisabled: true },
|
||||
{ command: 'folder.showData', hideDisabled: true },
|
||||
{ command: 'new.gist', hideDisabled: true },
|
||||
{ command: 'app.resetSettings', hideDisabled: true },
|
||||
{ divider: true },
|
||||
{ command: 'app.exportConnections', hideDisabled: true },
|
||||
@@ -108,7 +108,7 @@ module.exports = ({ editMenu, isMac }) => [
|
||||
{ command: 'app.openWeb', hideDisabled: true },
|
||||
{ command: 'app.openIssue', hideDisabled: true },
|
||||
{ command: 'app.openSponsoring', hideDisabled: true },
|
||||
{ command: 'app.giveFeedback', hideDisabled: true },
|
||||
// { command: 'app.giveFeedback', hideDisabled: true },
|
||||
{ divider: true },
|
||||
{ command: 'settings.commands', hideDisabled: true },
|
||||
{ command: 'tabs.changelog', hideDisabled: true },
|
||||
|
||||
@@ -119,4 +119,17 @@ describe('Add connection', () => {
|
||||
cy.contains('Export connections').click();
|
||||
cy.themeshot('export-connections');
|
||||
});
|
||||
|
||||
it('configure LLM provider', () => {
|
||||
cy.testid('WidgetIconPanel_settings').click();
|
||||
cy.contains('Settings').click();
|
||||
cy.contains('AI').click();
|
||||
cy.testid('AiSupportedProvidersInfo_add_OpenRouter').click();
|
||||
cy.testid('AiProviderCard_apikey_OpenRouter').clear().type('xxx');
|
||||
cy.testid('AiProviderCard_testButton_OpenRouter').click();
|
||||
cy.testid('AiProviderCard_statusValid_OpenRouter').should('exist');
|
||||
cy.testid('AiProviderCard_editButton_OpenRouter').click();
|
||||
cy.wait(1000);
|
||||
cy.themeshot('llm-providers-settings');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -277,6 +277,14 @@ describe('Data browser data', () => {
|
||||
cy.testid('CommandPalette_main').themeshot('command-palette', { padding: 50 });
|
||||
});
|
||||
|
||||
it('About window', () => {
|
||||
cy.contains('Connections');
|
||||
cy.testid('WidgetIconPanel_menu').click();
|
||||
cy.contains('Help').click();
|
||||
cy.contains('About').click();
|
||||
cy.testid('ModalBase_window').themeshot('about-window', { padding: 50 });
|
||||
});
|
||||
|
||||
it('Show map', () => {
|
||||
cy.contains('Postgres-connection').click();
|
||||
cy.contains('PgGeoData').click();
|
||||
@@ -381,27 +389,6 @@ describe('Data browser data', () => {
|
||||
cy.themeshot('compare-database-settings');
|
||||
});
|
||||
|
||||
it('Database chat', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('TabsPanel_buttonNewObject').click();
|
||||
cy.testid('NewObjectModal_databaseChat').click();
|
||||
cy.wait(1000);
|
||||
cy.get('body').realType('find most popular artist');
|
||||
cy.get('body').realPress('{enter}');
|
||||
cy.testid('DatabaseChatTab_executeAllQueries', { timeout: 20000 }).click();
|
||||
cy.wait(20000);
|
||||
// cy.contains('Iron Maiden');
|
||||
cy.themeshot('database-chat');
|
||||
|
||||
// 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', () => {
|
||||
// TODO FIX: delete references cascade not working
|
||||
cy.contains('MySql-connection').click();
|
||||
|
||||
@@ -109,4 +109,62 @@ describe('Charts', () => {
|
||||
cy.contains('Compare database');
|
||||
cy.themeshot('new-object-window');
|
||||
});
|
||||
|
||||
it('Database chat - charts', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('TabsPanel_buttonNewObject').click();
|
||||
cy.testid('NewObjectModal_databaseChat').click();
|
||||
cy.wait(1000);
|
||||
cy.get('body').realType('show me chart of most popular genres');
|
||||
cy.get('body').realPress('{enter}');
|
||||
cy.testid('DatabaseChatTab_executeAllQueries', { timeout: 30000 }).click();
|
||||
cy.wait(5000);
|
||||
cy.testid('chart-canvas').should($c => expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/));
|
||||
cy.themeshot('database-chat-chart');
|
||||
});
|
||||
|
||||
it('Database chat', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('TabsPanel_buttonNewObject').click();
|
||||
cy.testid('NewObjectModal_databaseChat').click();
|
||||
cy.wait(1000);
|
||||
cy.get('body').realType('find most popular artist');
|
||||
cy.get('body').realPress('{enter}');
|
||||
cy.testid('DatabaseChatTab_executeAllQueries', { timeout: 30000 }).click();
|
||||
cy.wait(30000);
|
||||
// cy.contains('Iron Maiden');
|
||||
cy.themeshot('database-chat');
|
||||
|
||||
// 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('Explain query error', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('TabsPanel_buttonNewObject').click();
|
||||
cy.testid('NewObjectModal_query').click();
|
||||
cy.wait(1000);
|
||||
cy.get('body').realType('select * from Invoice2');
|
||||
cy.contains('Execute').click();
|
||||
cy.testid('MessageViewRow-explainErrorButton-1').click();
|
||||
cy.testid('ChatCodeRenderer_useSqlButton', { timeout: 30000 });
|
||||
cy.themeshot('explain-query-error');
|
||||
|
||||
// cy.testid('TabsPanel_buttonNewObject').click();
|
||||
// cy.testid('NewObjectModal_databaseChat').click();
|
||||
// cy.wait(1000);
|
||||
// cy.get('body').realType('show me chart of most popular genres');
|
||||
// cy.get('body').realPress('{enter}');
|
||||
// cy.testid('DatabaseChatTab_executeAllQueries', { timeout: 30000 }).click();
|
||||
// cy.wait(5000);
|
||||
// cy.testid('chart-canvas').should($c => expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/));
|
||||
// cy.themeshot('database-chat-chart');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,14 +21,17 @@ describe('Team edition tests', () => {
|
||||
cy.testid('AdminMenuWidget_itemConnections').click();
|
||||
cy.contains('New connection').click();
|
||||
cy.testid('ConnectionDriverFields_connectionType').select('PostgreSQL');
|
||||
cy.contains('not granted').should('not.exist');
|
||||
cy.themeshot('connection-administration');
|
||||
|
||||
cy.testid('AdminMenuWidget_itemRoles').click();
|
||||
cy.contains('logged-user').click();
|
||||
cy.contains('not granted').should('not.exist');
|
||||
cy.themeshot('role-administration');
|
||||
|
||||
cy.testid('AdminMenuWidget_itemUsers').click();
|
||||
cy.contains('New user').click();
|
||||
cy.contains('not granted').should('not.exist');
|
||||
cy.themeshot('user-administration');
|
||||
|
||||
cy.testid('AdminMenuWidget_itemAuthentication').click();
|
||||
@@ -36,6 +39,7 @@ describe('Team edition tests', () => {
|
||||
cy.contains('Use database login').click();
|
||||
cy.contains('Add authentication').click();
|
||||
cy.contains('OAuth 2.0').click();
|
||||
cy.contains('not granted').should('not.exist');
|
||||
cy.themeshot('authentication-administration');
|
||||
});
|
||||
|
||||
@@ -119,4 +123,29 @@ describe('Team edition tests', () => {
|
||||
cy.contains('Exporting query').click();
|
||||
cy.themeshot('auditlog');
|
||||
});
|
||||
|
||||
it('Edit database permissions', () => {
|
||||
cy.testid('LoginPage_linkAdmin').click();
|
||||
cy.testid('LoginPage_password').type('adminpwd');
|
||||
cy.testid('LoginPage_submitLogin').click();
|
||||
|
||||
cy.testid('AdminMenuWidget_itemRoles').click();
|
||||
cy.testid('AdminRolesTab_table').contains('superadmin').click();
|
||||
cy.testid('AdminRolesTab_databases').click();
|
||||
|
||||
cy.testid('AdminDatabasesPermissionsGrid_addButton').click();
|
||||
cy.testid('AdminDatabasesPermissionsGrid_addButton').click();
|
||||
cy.testid('AdminDatabasesPermissionsGrid_addButton').click();
|
||||
|
||||
cy.testid('AdminListOrRegexEditor_1_regexInput').type('^Chinook[\\d]*$');
|
||||
cy.testid('AdminListOrRegexEditor_2_listSwitch').click();
|
||||
cy.testid('AdminListOrRegexEditor_2_listInput').type('Nortwind\nSales');
|
||||
cy.testid('AdminDatabasesPermissionsGrid_roleSelect_0').select('-2');
|
||||
cy.testid('AdminDatabasesPermissionsGrid_roleSelect_1').select('-3');
|
||||
cy.testid('AdminDatabasesPermissionsGrid_roleSelect_2').select('-4');
|
||||
|
||||
cy.contains('not granted').should('not.exist');
|
||||
|
||||
cy.themeshot('database-permissions');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,6 +118,31 @@ describe('Alter table', () => {
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.filter(i => i.supportTableComments).map(engine => [engine.label, engine]))(
|
||||
'Add comment to table - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableDiff(engine, conn, driver, tbl => {
|
||||
tbl.objectComment = 'Added table comment';
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.filter(i => i.supportColumnComments).map(engine => [engine.label, engine]))(
|
||||
'Add comment to column - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableDiff(engine, conn, driver, tbl => {
|
||||
tbl.columns.push({
|
||||
columnName: 'added',
|
||||
columnComment: 'Added column comment',
|
||||
dataType: 'int',
|
||||
pairingId: crypto.randomUUID(),
|
||||
notNull: false,
|
||||
autoIncrement: false,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(
|
||||
createEnginesColumnsSource(engines.filter(x => !x.skipDropColumn)).filter(
|
||||
([_label, col, engine]) => !engine.skipPkDrop || !col.endsWith('_pk')
|
||||
|
||||
@@ -64,6 +64,40 @@ describe('Table create', () => {
|
||||
})
|
||||
);
|
||||
|
||||
test.each(
|
||||
engines.filter(i => i.supportTableComments || i.supportColumnComments).map(engine => [engine.label, engine])
|
||||
)(
|
||||
'Simple table with comment - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testTableCreate(engine, conn, driver, {
|
||||
...(engine.supportTableComments && {
|
||||
schemaName: 'dbo',
|
||||
objectComment: 'table comment',
|
||||
}),
|
||||
...(engine.defaultSchemaName && {
|
||||
schemaName: engine.defaultSchemaName,
|
||||
}),
|
||||
columns: [
|
||||
{
|
||||
columnName: 'col1',
|
||||
dataType: 'int',
|
||||
pureName: 'tested',
|
||||
...(engine.skipNullability ? {} : { notNull: true }),
|
||||
...(engine.supportColumnComments && {
|
||||
columnComment: 'column comment',
|
||||
}),
|
||||
...(engine.defaultSchemaName && {
|
||||
schemaName: engine.defaultSchemaName,
|
||||
}),
|
||||
},
|
||||
],
|
||||
primaryKey: {
|
||||
columns: [{ columnName: 'col1' }],
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.filter(x => !x.skipIndexes).map(engine => [engine.label, engine]))(
|
||||
'Table with index - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
|
||||
@@ -443,6 +443,8 @@ const sqlServerEngine = {
|
||||
supportSchemas: true,
|
||||
supportRenameSqlObject: true,
|
||||
defaultSchemaName: 'dbo',
|
||||
supportTableComments: true,
|
||||
supportColumnComments: true,
|
||||
// skipSeparateSchemas: true,
|
||||
triggers: [
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "6.6.1-premium-beta.15",
|
||||
"version": "6.6.6-beta.13",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
|
||||
@@ -2,6 +2,7 @@ DEVMODE=1
|
||||
SHELL_SCRIPTING=1
|
||||
ALLOW_DBGATE_PRIVATE_CLOUD=1
|
||||
DEVWEB=1
|
||||
LOCAL_AUTH_PROXY=1
|
||||
# LOCAL_AI_GATEWAY=true
|
||||
|
||||
# REDIRECT_TO_DBGATE_CLOUD_LOGIN=1
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-datalib": "^6.0.0-alpha.1",
|
||||
"dbgate-query-splitter": "^4.11.5",
|
||||
"dbgate-query-splitter": "^4.11.7",
|
||||
"dbgate-sqltree": "^6.0.0-alpha.1",
|
||||
"dbgate-tools": "^6.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
|
||||
@@ -10,7 +10,13 @@ function getTokenSecret() {
|
||||
return tokenSecret;
|
||||
}
|
||||
|
||||
function getStaticTokenSecret() {
|
||||
// TODO static not fixed
|
||||
return '14813c43-a91b-4ad1-9dcd-a81bd7dbb05f';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTokenLifetime,
|
||||
getTokenSecret,
|
||||
getStaticTokenSecret,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ const logger = getLogger('authProvider');
|
||||
|
||||
class AuthProviderBase {
|
||||
amoid = 'none';
|
||||
skipInList = false;
|
||||
|
||||
async login(login, password, options = undefined, req = undefined) {
|
||||
return {
|
||||
@@ -36,12 +37,28 @@ class AuthProviderBase {
|
||||
return !!req?.user || !!req?.auth;
|
||||
}
|
||||
|
||||
getCurrentPermissions(req) {
|
||||
async getCurrentPermissions(req) {
|
||||
const login = this.getCurrentLogin(req);
|
||||
const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
|
||||
return permissions || process.env.PERMISSIONS;
|
||||
}
|
||||
|
||||
async checkCurrentConnectionPermission(req, conid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getCurrentDatabasePermissions(req) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getCurrentTablePermissions(req) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getCurrentFilePermissions(req) {
|
||||
return [];
|
||||
}
|
||||
|
||||
getLoginPageConnections() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,233 +1,99 @@
|
||||
const fs = require('fs-extra');
|
||||
const _ = require('lodash');
|
||||
const path = require('path');
|
||||
const { appdir } = require('../utility/directories');
|
||||
const { appdir, filesdir } = require('../utility/directories');
|
||||
const socket = require('../utility/socket');
|
||||
const connections = require('./connections');
|
||||
const {
|
||||
loadPermissionsFromRequest,
|
||||
loadFilePermissionsFromRequest,
|
||||
hasPermission,
|
||||
getFilePermissionRole,
|
||||
} = require('../utility/hasPermission');
|
||||
|
||||
module.exports = {
|
||||
folders_meta: true,
|
||||
async folders() {
|
||||
const folders = await fs.readdir(appdir());
|
||||
return [
|
||||
...folders.map(name => ({
|
||||
name,
|
||||
})),
|
||||
];
|
||||
},
|
||||
|
||||
createFolder_meta: true,
|
||||
async createFolder({ folder }) {
|
||||
const name = await this.getNewAppFolder({ name: folder });
|
||||
await fs.mkdir(path.join(appdir(), name));
|
||||
socket.emitChanged('app-folders-changed');
|
||||
this.emitChangedDbApp(folder);
|
||||
return name;
|
||||
},
|
||||
|
||||
files_meta: true,
|
||||
async files({ folder }) {
|
||||
if (!folder) return [];
|
||||
const dir = path.join(appdir(), folder);
|
||||
getAllApps_meta: true,
|
||||
async getAllApps({}, req) {
|
||||
const dir = path.join(filesdir(), 'apps');
|
||||
if (!(await fs.exists(dir))) return [];
|
||||
const files = await fs.readdir(dir);
|
||||
|
||||
function fileType(ext, type) {
|
||||
return files
|
||||
.filter(name => name.endsWith(ext))
|
||||
.map(name => ({
|
||||
name: name.slice(0, -ext.length),
|
||||
label: path.parse(name.slice(0, -ext.length)).base,
|
||||
type,
|
||||
}));
|
||||
}
|
||||
|
||||
return [
|
||||
...fileType('.command.sql', 'command.sql'),
|
||||
...fileType('.query.sql', 'query.sql'),
|
||||
...fileType('.config.json', 'config.json'),
|
||||
];
|
||||
},
|
||||
|
||||
async emitChangedDbApp(folder) {
|
||||
const used = await this.getUsedAppFolders();
|
||||
if (used.includes(folder)) {
|
||||
socket.emitChanged('used-apps-changed');
|
||||
}
|
||||
},
|
||||
|
||||
refreshFiles_meta: true,
|
||||
async refreshFiles({ folder }) {
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
},
|
||||
|
||||
refreshFolders_meta: true,
|
||||
async refreshFolders() {
|
||||
socket.emitChanged(`app-folders-changed`);
|
||||
},
|
||||
|
||||
deleteFile_meta: true,
|
||||
async deleteFile({ folder, file, fileType }) {
|
||||
await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
this.emitChangedDbApp(folder);
|
||||
},
|
||||
|
||||
renameFile_meta: true,
|
||||
async renameFile({ folder, file, newFile, fileType }) {
|
||||
await fs.rename(
|
||||
path.join(path.join(appdir(), folder), `${file}.${fileType}`),
|
||||
path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
|
||||
);
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
this.emitChangedDbApp(folder);
|
||||
},
|
||||
|
||||
renameFolder_meta: true,
|
||||
async renameFolder({ folder, newFolder }) {
|
||||
const uniqueName = await this.getNewAppFolder({ name: newFolder });
|
||||
await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
|
||||
socket.emitChanged(`app-folders-changed`);
|
||||
},
|
||||
|
||||
deleteFolder_meta: true,
|
||||
async deleteFolder({ folder }) {
|
||||
if (!folder) throw new Error('Missing folder parameter');
|
||||
await fs.rmdir(path.join(appdir(), folder), { recursive: true });
|
||||
socket.emitChanged(`app-folders-changed`);
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
socket.emitChanged('used-apps-changed');
|
||||
},
|
||||
|
||||
async getNewAppFolder({ name }) {
|
||||
if (!(await fs.exists(path.join(appdir(), name)))) return name;
|
||||
let index = 2;
|
||||
while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
|
||||
index += 1;
|
||||
}
|
||||
return `${name}${index}`;
|
||||
},
|
||||
|
||||
getUsedAppFolders_meta: true,
|
||||
async getUsedAppFolders() {
|
||||
const list = await connections.list();
|
||||
const apps = [];
|
||||
|
||||
for (const connection of list) {
|
||||
for (const db of connection.databases || []) {
|
||||
for (const key of _.keys(db || {})) {
|
||||
if (key.startsWith('useApp:') && db[key]) {
|
||||
apps.push(key.substring('useApp:'.length));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
|
||||
},
|
||||
|
||||
getUsedApps_meta: true,
|
||||
async getUsedApps() {
|
||||
const apps = await this.getUsedAppFolders();
|
||||
const res = [];
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
const filePermissions = await loadFilePermissionsFromRequest(req);
|
||||
|
||||
for (const folder of apps) {
|
||||
res.push(await this.loadApp({ folder }));
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
// getAppsForDb_meta: true,
|
||||
// async getAppsForDb({ conid, database }) {
|
||||
// const connection = await connections.get({ conid });
|
||||
// if (!connection) return [];
|
||||
// const db = (connection.databases || []).find(x => x.name == database);
|
||||
// const apps = [];
|
||||
// const res = [];
|
||||
// if (db) {
|
||||
// for (const key of _.keys(db || {})) {
|
||||
// if (key.startsWith('useApp:') && db[key]) {
|
||||
// apps.push(key.substring('useApp:'.length));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// for (const folder of apps) {
|
||||
// res.push(await this.loadApp({ folder }));
|
||||
// }
|
||||
// return res;
|
||||
// },
|
||||
|
||||
loadApp_meta: true,
|
||||
async loadApp({ folder }) {
|
||||
const res = {
|
||||
queries: [],
|
||||
commands: [],
|
||||
name: folder,
|
||||
};
|
||||
const dir = path.join(appdir(), folder);
|
||||
if (await fs.exists(dir)) {
|
||||
const files = await fs.readdir(dir);
|
||||
|
||||
async function processType(ext, field) {
|
||||
for (const file of files) {
|
||||
if (file.endsWith(ext)) {
|
||||
res[field].push({
|
||||
name: file.slice(0, -ext.length),
|
||||
sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const file of await fs.readdir(dir)) {
|
||||
if (!hasPermission(`all-disk-files`, loadedPermissions)) {
|
||||
const role = getFilePermissionRole('apps', file, filePermissions);
|
||||
if (role == 'deny') continue;
|
||||
}
|
||||
const content = await fs.readFile(path.join(dir, file), { encoding: 'utf-8' });
|
||||
const appJson = JSON.parse(content);
|
||||
// const app = {
|
||||
// appid: file,
|
||||
// name: appJson.applicationName,
|
||||
// usageRules: appJson.usageRules || [],
|
||||
// icon: appJson.applicationIcon || 'img app',
|
||||
// color: appJson.applicationColor,
|
||||
// queries: Object.values(appJson.files || {})
|
||||
// .filter(x => x.type == 'query')
|
||||
// .map(x => ({
|
||||
// name: x.label,
|
||||
// sql: x.sql,
|
||||
// })),
|
||||
// commands: Object.values(appJson.files || {})
|
||||
// .filter(x => x.type == 'command')
|
||||
// .map(x => ({
|
||||
// name: x.label,
|
||||
// sql: x.sql,
|
||||
// })),
|
||||
// virtualReferences: appJson.virtualReferences,
|
||||
// dictionaryDescriptions: appJson.dictionaryDescriptions,
|
||||
// };
|
||||
const app = {
|
||||
...appJson,
|
||||
appid: file,
|
||||
};
|
||||
|
||||
await processType('.command.sql', 'commands');
|
||||
await processType('.query.sql', 'queries');
|
||||
res.push(app);
|
||||
}
|
||||
|
||||
try {
|
||||
res.virtualReferences = JSON.parse(
|
||||
await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
|
||||
);
|
||||
} catch (err) {
|
||||
res.virtualReferences = [];
|
||||
}
|
||||
try {
|
||||
res.dictionaryDescriptions = JSON.parse(
|
||||
await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
|
||||
);
|
||||
} catch (err) {
|
||||
res.dictionaryDescriptions = [];
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
async saveConfigFile(appFolder, filename, filterFunc, newItem) {
|
||||
const file = path.join(appdir(), appFolder, filename);
|
||||
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
|
||||
} catch (err) {
|
||||
json = [];
|
||||
createAppFromDb_meta: true,
|
||||
async createAppFromDb({ appName, server, database }, req) {
|
||||
const appdir = path.join(filesdir(), 'apps');
|
||||
if (!fs.existsSync(appdir)) {
|
||||
await fs.mkdir(appdir);
|
||||
}
|
||||
|
||||
if (filterFunc) {
|
||||
json = json.filter(filterFunc);
|
||||
const appId = _.kebabCase(appName);
|
||||
let suffix = undefined;
|
||||
while (fs.existsSync(path.join(appdir, `${appId}${suffix || ''}`))) {
|
||||
if (!suffix) suffix = 2;
|
||||
else suffix++;
|
||||
}
|
||||
const finalAppId = `${appId}${suffix || ''}`;
|
||||
|
||||
json = [...json, newItem];
|
||||
const appJson = {
|
||||
applicationName: appName,
|
||||
usageRules: [
|
||||
{
|
||||
serverHostsList: server,
|
||||
databaseNamesList: database,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fs.writeFile(file, JSON.stringify(json, undefined, 2));
|
||||
await fs.writeFile(path.join(appdir, `${finalAppId}`), JSON.stringify(appJson, undefined, 2));
|
||||
|
||||
socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
socket.emitChanged('used-apps-changed');
|
||||
socket.emitChanged(`files-changed`, { folder: 'apps' });
|
||||
|
||||
return finalAppId;
|
||||
},
|
||||
|
||||
saveVirtualReference_meta: true,
|
||||
async saveVirtualReference({ appFolder, schemaName, pureName, refSchemaName, refTableName, columns }) {
|
||||
await this.saveConfigFile(
|
||||
appFolder,
|
||||
'virtual-references.config.json',
|
||||
async saveVirtualReference({ appid, schemaName, pureName, refSchemaName, refTableName, columns }) {
|
||||
await this.saveConfigItem(
|
||||
appid,
|
||||
'virtualReferences',
|
||||
columns.length == 1
|
||||
? x =>
|
||||
!(
|
||||
@@ -245,14 +111,17 @@ module.exports = {
|
||||
columns,
|
||||
}
|
||||
);
|
||||
|
||||
socket.emitChanged(`files-changed`, { folder: 'apps' });
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
saveDictionaryDescription_meta: true,
|
||||
async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
|
||||
await this.saveConfigFile(
|
||||
appFolder,
|
||||
'dictionary-descriptions.config.json',
|
||||
async saveDictionaryDescription({ appid, pureName, schemaName, expression, columns, delimiter }) {
|
||||
await this.saveConfigItem(
|
||||
appid,
|
||||
'dictionaryDescriptions',
|
||||
x => !(x.schemaName == schemaName && x.pureName == pureName),
|
||||
{
|
||||
schemaName,
|
||||
@@ -263,18 +132,271 @@ module.exports = {
|
||||
}
|
||||
);
|
||||
|
||||
socket.emitChanged(`files-changed`, { folder: 'apps' });
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
createConfigFile_meta: true,
|
||||
async createConfigFile({ appFolder, fileName, content }) {
|
||||
const file = path.join(appdir(), appFolder, fileName);
|
||||
if (!(await fs.exists(file))) {
|
||||
await fs.writeFile(file, JSON.stringify(content, undefined, 2));
|
||||
socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
socket.emitChanged('used-apps-changed');
|
||||
return true;
|
||||
async saveConfigItem(appid, fieldName, filterFunc, newItem) {
|
||||
const file = path.join(filesdir(), 'apps', appid);
|
||||
|
||||
const appJson = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
|
||||
let json = appJson[fieldName] || [];
|
||||
|
||||
if (filterFunc) {
|
||||
json = json.filter(filterFunc);
|
||||
}
|
||||
return false;
|
||||
|
||||
json = [...json, newItem];
|
||||
|
||||
await fs.writeFile(
|
||||
file,
|
||||
JSON.stringify(
|
||||
{
|
||||
...appJson,
|
||||
[fieldName]: json,
|
||||
},
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
socket.emitChanged('files-changed', { folder: 'apps' });
|
||||
},
|
||||
|
||||
// folders_meta: true,
|
||||
// async folders() {
|
||||
// const folders = await fs.readdir(appdir());
|
||||
// return [
|
||||
// ...folders.map(name => ({
|
||||
// name,
|
||||
// })),
|
||||
// ];
|
||||
// },
|
||||
|
||||
// createFolder_meta: true,
|
||||
// async createFolder({ folder }) {
|
||||
// const name = await this.getNewAppFolder({ name: folder });
|
||||
// await fs.mkdir(path.join(appdir(), name));
|
||||
// socket.emitChanged('app-folders-changed');
|
||||
// this.emitChangedDbApp(folder);
|
||||
// return name;
|
||||
// },
|
||||
|
||||
// files_meta: true,
|
||||
// async files({ folder }) {
|
||||
// if (!folder) return [];
|
||||
// const dir = path.join(appdir(), folder);
|
||||
// if (!(await fs.exists(dir))) return [];
|
||||
// const files = await fs.readdir(dir);
|
||||
|
||||
// function fileType(ext, type) {
|
||||
// return files
|
||||
// .filter(name => name.endsWith(ext))
|
||||
// .map(name => ({
|
||||
// name: name.slice(0, -ext.length),
|
||||
// label: path.parse(name.slice(0, -ext.length)).base,
|
||||
// type,
|
||||
// }));
|
||||
// }
|
||||
|
||||
// return [
|
||||
// ...fileType('.command.sql', 'command.sql'),
|
||||
// ...fileType('.query.sql', 'query.sql'),
|
||||
// ...fileType('.config.json', 'config.json'),
|
||||
// ];
|
||||
// },
|
||||
|
||||
// async emitChangedDbApp(folder) {
|
||||
// const used = await this.getUsedAppFolders();
|
||||
// if (used.includes(folder)) {
|
||||
// socket.emitChanged('used-apps-changed');
|
||||
// }
|
||||
// },
|
||||
|
||||
// refreshFiles_meta: true,
|
||||
// async refreshFiles({ folder }) {
|
||||
// socket.emitChanged('app-files-changed', { app: folder });
|
||||
// },
|
||||
|
||||
// refreshFolders_meta: true,
|
||||
// async refreshFolders() {
|
||||
// socket.emitChanged(`app-folders-changed`);
|
||||
// },
|
||||
|
||||
// deleteFile_meta: true,
|
||||
// async deleteFile({ folder, file, fileType }) {
|
||||
// await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
|
||||
// socket.emitChanged('app-files-changed', { app: folder });
|
||||
// this.emitChangedDbApp(folder);
|
||||
// },
|
||||
|
||||
// renameFile_meta: true,
|
||||
// async renameFile({ folder, file, newFile, fileType }) {
|
||||
// await fs.rename(
|
||||
// path.join(path.join(appdir(), folder), `${file}.${fileType}`),
|
||||
// path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
|
||||
// );
|
||||
// socket.emitChanged('app-files-changed', { app: folder });
|
||||
// this.emitChangedDbApp(folder);
|
||||
// },
|
||||
|
||||
// renameFolder_meta: true,
|
||||
// async renameFolder({ folder, newFolder }) {
|
||||
// const uniqueName = await this.getNewAppFolder({ name: newFolder });
|
||||
// await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
|
||||
// socket.emitChanged(`app-folders-changed`);
|
||||
// },
|
||||
|
||||
// deleteFolder_meta: true,
|
||||
// async deleteFolder({ folder }) {
|
||||
// if (!folder) throw new Error('Missing folder parameter');
|
||||
// await fs.rmdir(path.join(appdir(), folder), { recursive: true });
|
||||
// socket.emitChanged(`app-folders-changed`);
|
||||
// socket.emitChanged('app-files-changed', { app: folder });
|
||||
// socket.emitChanged('used-apps-changed');
|
||||
// },
|
||||
|
||||
// async getNewAppFolder({ name }) {
|
||||
// if (!(await fs.exists(path.join(appdir(), name)))) return name;
|
||||
// let index = 2;
|
||||
// while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
|
||||
// index += 1;
|
||||
// }
|
||||
// return `${name}${index}`;
|
||||
// },
|
||||
|
||||
// getUsedAppFolders_meta: true,
|
||||
// async getUsedAppFolders() {
|
||||
// const list = await connections.list();
|
||||
// const apps = [];
|
||||
|
||||
// for (const connection of list) {
|
||||
// for (const db of connection.databases || []) {
|
||||
// for (const key of _.keys(db || {})) {
|
||||
// if (key.startsWith('useApp:') && db[key]) {
|
||||
// apps.push(key.substring('useApp:'.length));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
|
||||
// },
|
||||
|
||||
// // getAppsForDb_meta: true,
|
||||
// // async getAppsForDb({ conid, database }) {
|
||||
// // const connection = await connections.get({ conid });
|
||||
// // if (!connection) return [];
|
||||
// // const db = (connection.databases || []).find(x => x.name == database);
|
||||
// // const apps = [];
|
||||
// // const res = [];
|
||||
// // if (db) {
|
||||
// // for (const key of _.keys(db || {})) {
|
||||
// // if (key.startsWith('useApp:') && db[key]) {
|
||||
// // apps.push(key.substring('useApp:'.length));
|
||||
// // }
|
||||
// // }
|
||||
// // }
|
||||
// // for (const folder of apps) {
|
||||
// // res.push(await this.loadApp({ folder }));
|
||||
// // }
|
||||
// // return res;
|
||||
// // },
|
||||
|
||||
// loadApp_meta: true,
|
||||
// async loadApp({ folder }) {
|
||||
// const res = {
|
||||
// queries: [],
|
||||
// commands: [],
|
||||
// name: folder,
|
||||
// };
|
||||
// const dir = path.join(appdir(), folder);
|
||||
// if (await fs.exists(dir)) {
|
||||
// const files = await fs.readdir(dir);
|
||||
|
||||
// async function processType(ext, field) {
|
||||
// for (const file of files) {
|
||||
// if (file.endsWith(ext)) {
|
||||
// res[field].push({
|
||||
// name: file.slice(0, -ext.length),
|
||||
// sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// await processType('.command.sql', 'commands');
|
||||
// await processType('.query.sql', 'queries');
|
||||
// }
|
||||
|
||||
// try {
|
||||
// res.virtualReferences = JSON.parse(
|
||||
// await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
|
||||
// );
|
||||
// } catch (err) {
|
||||
// res.virtualReferences = [];
|
||||
// }
|
||||
// try {
|
||||
// res.dictionaryDescriptions = JSON.parse(
|
||||
// await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
|
||||
// );
|
||||
// } catch (err) {
|
||||
// res.dictionaryDescriptions = [];
|
||||
// }
|
||||
|
||||
// return res;
|
||||
// },
|
||||
|
||||
// async saveConfigFile(appFolder, filename, filterFunc, newItem) {
|
||||
// const file = path.join(appdir(), appFolder, filename);
|
||||
|
||||
// let json;
|
||||
// try {
|
||||
// json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
|
||||
// } catch (err) {
|
||||
// json = [];
|
||||
// }
|
||||
|
||||
// if (filterFunc) {
|
||||
// json = json.filter(filterFunc);
|
||||
// }
|
||||
|
||||
// json = [...json, newItem];
|
||||
|
||||
// await fs.writeFile(file, JSON.stringify(json, undefined, 2));
|
||||
|
||||
// socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
// socket.emitChanged('used-apps-changed');
|
||||
// },
|
||||
|
||||
// saveDictionaryDescription_meta: true,
|
||||
// async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
|
||||
// await this.saveConfigFile(
|
||||
// appFolder,
|
||||
// 'dictionary-descriptions.config.json',
|
||||
// x => !(x.schemaName == schemaName && x.pureName == pureName),
|
||||
// {
|
||||
// schemaName,
|
||||
// pureName,
|
||||
// expression,
|
||||
// columns,
|
||||
// delimiter,
|
||||
// }
|
||||
// );
|
||||
|
||||
// return true;
|
||||
// },
|
||||
|
||||
// createConfigFile_meta: true,
|
||||
// async createConfigFile({ appFolder, fileName, content }) {
|
||||
// const file = path.join(appdir(), appFolder, fileName);
|
||||
// if (!(await fs.exists(file))) {
|
||||
// await fs.writeFile(file, JSON.stringify(content, undefined, 2));
|
||||
// socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
// socket.emitChanged('used-apps-changed');
|
||||
// return true;
|
||||
// }
|
||||
// return false;
|
||||
// },
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ function authMiddleware(req, res, next) {
|
||||
'/auth/oauth-token',
|
||||
'/auth/login',
|
||||
'/auth/redirect',
|
||||
'/redirect',
|
||||
'/stream',
|
||||
'/storage/get-connections-for-login-page',
|
||||
'/storage/set-admin-password',
|
||||
@@ -139,9 +140,9 @@ module.exports = {
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
login: 'superadmin',
|
||||
permissions: await storage.loadSuperadminPermissions(),
|
||||
roleId: -3,
|
||||
licenseUid,
|
||||
amoid: 'superadmin',
|
||||
},
|
||||
getTokenSecret(),
|
||||
{
|
||||
@@ -173,7 +174,9 @@ module.exports = {
|
||||
getProviders_meta: true,
|
||||
getProviders() {
|
||||
return {
|
||||
providers: getAuthProviders().map(x => x.toJson()),
|
||||
providers: getAuthProviders()
|
||||
.filter(x => !x.skipInList)
|
||||
.map(x => x.toJson()),
|
||||
default: getDefaultAuthProvider()?.amoid,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -8,6 +8,9 @@ const {
|
||||
getCloudContent,
|
||||
putCloudContent,
|
||||
removeCloudCachedConnection,
|
||||
getPromoWidgetData,
|
||||
getPromoWidgetList,
|
||||
getPromoWidgetPreview,
|
||||
} = require('../utility/cloudIntf');
|
||||
const connections = require('./connections');
|
||||
const socket = require('../utility/socket');
|
||||
@@ -283,6 +286,28 @@ module.exports = {
|
||||
return getAiGatewayServer();
|
||||
},
|
||||
|
||||
premiumPromoWidget_meta: true,
|
||||
async premiumPromoWidget() {
|
||||
const data = await getPromoWidgetData();
|
||||
if (data?.state != 'data') {
|
||||
return null;
|
||||
}
|
||||
if (data.validTo && new Date().getTime() > new Date(data.validTo).getTime()) {
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
promoWidgetList_meta: true,
|
||||
async promoWidgetList() {
|
||||
return getPromoWidgetList();
|
||||
},
|
||||
|
||||
promoWidgetPreview_meta: true,
|
||||
async promoWidgetPreview({ campaign, variant }) {
|
||||
return getPromoWidgetPreview(campaign, variant);
|
||||
},
|
||||
|
||||
// chatStream_meta: {
|
||||
// raw: true,
|
||||
// method: 'post',
|
||||
|
||||
@@ -3,7 +3,7 @@ const os = require('os');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const { datadir, getLogsFilePath } = require('../utility/directories');
|
||||
const { hasPermission } = require('../utility/hasPermission');
|
||||
const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
|
||||
const socket = require('../utility/socket');
|
||||
const _ = require('lodash');
|
||||
const AsyncLock = require('async-lock');
|
||||
@@ -46,7 +46,7 @@ module.exports = {
|
||||
async get(_params, req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
const login = authProvider.getCurrentLogin(req);
|
||||
const permissions = authProvider.getCurrentPermissions(req);
|
||||
const permissions = await authProvider.getCurrentPermissions(req);
|
||||
const isUserLoggedIn = authProvider.isUserLoggedIn(req);
|
||||
|
||||
const singleConid = authProvider.getSingleConnectionId(req);
|
||||
@@ -280,7 +280,8 @@ module.exports = {
|
||||
|
||||
updateSettings_meta: true,
|
||||
async updateSettings(values, req) {
|
||||
if (!hasPermission(`settings/change`, req)) return false;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`settings/change`, loadedPermissions)) return false;
|
||||
cachedSettingsValue = null;
|
||||
|
||||
const res = await lock.acquire('settings', async () => {
|
||||
@@ -392,7 +393,8 @@ module.exports = {
|
||||
|
||||
exportConnectionsAndSettings_meta: true,
|
||||
async exportConnectionsAndSettings(_params, req) {
|
||||
if (!hasPermission(`admin/config`, req)) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`admin/config`, loadedPermissions)) {
|
||||
throw new Error('Permission denied: admin/config');
|
||||
}
|
||||
|
||||
@@ -416,7 +418,8 @@ module.exports = {
|
||||
|
||||
importConnectionsAndSettings_meta: true,
|
||||
async importConnectionsAndSettings({ db }, req) {
|
||||
if (!hasPermission(`admin/config`, req)) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`admin/config`, loadedPermissions)) {
|
||||
throw new Error('Permission denied: admin/config');
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { safeJsonParse, getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
|
||||
const { connectionHasPermission, testConnectionPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
|
||||
const pipeForkLogs = require('../utility/pipeForkLogs');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { getAuthProviderById } = require('../auth/authProvider');
|
||||
@@ -227,6 +227,7 @@ module.exports = {
|
||||
list_meta: true,
|
||||
async list(_params, req) {
|
||||
const storage = require('./storage');
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
|
||||
const storageConnections = await storage.connections(req);
|
||||
if (storageConnections) {
|
||||
@@ -234,9 +235,9 @@ module.exports = {
|
||||
}
|
||||
if (portalConnections) {
|
||||
if (platformInfo.allowShellConnection) return portalConnections;
|
||||
return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, req));
|
||||
return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, loadedPermissions));
|
||||
}
|
||||
return (await this.datastore.find()).filter(x => connectionHasPermission(x, req));
|
||||
return (await this.datastore.find()).filter(x => connectionHasPermission(x, loadedPermissions));
|
||||
},
|
||||
|
||||
async getUsedEngines() {
|
||||
@@ -375,7 +376,7 @@ module.exports = {
|
||||
update_meta: true,
|
||||
async update({ _id, values }, req) {
|
||||
if (portalConnections) return;
|
||||
testConnectionPermission(_id, req);
|
||||
await testConnectionPermission(_id, req);
|
||||
const res = await this.datastore.patch(_id, values);
|
||||
socket.emitChanged('connection-list-changed');
|
||||
return res;
|
||||
@@ -392,7 +393,7 @@ module.exports = {
|
||||
updateDatabase_meta: true,
|
||||
async updateDatabase({ conid, database, values }, req) {
|
||||
if (portalConnections) return;
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const conn = await this.datastore.get(conid);
|
||||
let databases = (conn && conn.databases) || [];
|
||||
if (databases.find(x => x.name == database)) {
|
||||
@@ -410,7 +411,7 @@ module.exports = {
|
||||
delete_meta: true,
|
||||
async delete(connection, req) {
|
||||
if (portalConnections) return;
|
||||
testConnectionPermission(connection, req);
|
||||
await testConnectionPermission(connection, req);
|
||||
const res = await this.datastore.remove(connection._id);
|
||||
socket.emitChanged('connection-list-changed');
|
||||
return res;
|
||||
@@ -452,7 +453,7 @@ module.exports = {
|
||||
_id: '__model',
|
||||
};
|
||||
}
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.getCore({ conid, mask: true });
|
||||
},
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const generateDeploySql = require('../shell/generateDeploySql');
|
||||
const { createTwoFilesPatch } = require('diff');
|
||||
const diff2htmlPage = require('../utility/diff2htmlPage');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { testConnectionPermission } = require('../utility/hasPermission');
|
||||
const { testConnectionPermission, hasPermission, loadPermissionsFromRequest, loadTablePermissionsFromRequest, getTablePermissionRole, loadDatabasePermissionsFromRequest, getDatabasePermissionRole, getTablePermissionRoleLevelIndex, testDatabaseRolePermission } = require('../utility/hasPermission');
|
||||
const { MissingCredentialsError } = require('../utility/exceptions');
|
||||
const pipeForkLogs = require('../utility/pipeForkLogs');
|
||||
const crypto = require('crypto');
|
||||
@@ -100,7 +100,7 @@ module.exports = {
|
||||
socket.emitChanged(`database-status-changed`, { conid, database });
|
||||
},
|
||||
|
||||
handle_ping() {},
|
||||
handle_ping() { },
|
||||
|
||||
// session event handlers
|
||||
|
||||
@@ -235,7 +235,7 @@ module.exports = {
|
||||
|
||||
queryData_meta: true,
|
||||
async queryData({ conid, database, sql }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
logger.info({ conid, database, sql }, 'DBGM-00007 Processing query');
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
// if (opened && opened.status && opened.status.name == 'error') {
|
||||
@@ -247,7 +247,7 @@ module.exports = {
|
||||
|
||||
sqlSelect_meta: true,
|
||||
async sqlSelect({ conid, database, select, auditLogSessionGroup }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(
|
||||
opened,
|
||||
@@ -256,24 +256,23 @@ module.exports = {
|
||||
auditLogger:
|
||||
auditLogSessionGroup && select?.from?.name?.pureName
|
||||
? response => {
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
component: 'DatabaseConnectionsController',
|
||||
event: 'sql.select',
|
||||
action: 'select',
|
||||
severity: 'info',
|
||||
conid,
|
||||
database,
|
||||
schemaName: select?.from?.name?.schemaName,
|
||||
pureName: select?.from?.name?.pureName,
|
||||
sumint1: response?.rows?.length,
|
||||
sessionParam: `${conid}::${database}::${select?.from?.name?.schemaName || '0'}::${
|
||||
select?.from?.name?.pureName
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
component: 'DatabaseConnectionsController',
|
||||
event: 'sql.select',
|
||||
action: 'select',
|
||||
severity: 'info',
|
||||
conid,
|
||||
database,
|
||||
schemaName: select?.from?.name?.schemaName,
|
||||
pureName: select?.from?.name?.pureName,
|
||||
sumint1: response?.rows?.length,
|
||||
sessionParam: `${conid}::${database}::${select?.from?.name?.schemaName || '0'}::${select?.from?.name?.pureName
|
||||
}`,
|
||||
sessionGroup: auditLogSessionGroup,
|
||||
message: `Loaded table data from ${select?.from?.name?.pureName}`,
|
||||
});
|
||||
}
|
||||
sessionGroup: auditLogSessionGroup,
|
||||
message: `Loaded table data from ${select?.from?.name?.pureName}`,
|
||||
});
|
||||
}
|
||||
: null,
|
||||
}
|
||||
);
|
||||
@@ -282,7 +281,9 @@ module.exports = {
|
||||
|
||||
runScript_meta: true,
|
||||
async runScript({ conid, database, sql, useTransaction, logMessage }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
await testConnectionPermission(conid, req, loadedPermissions);
|
||||
await testDatabaseRolePermission(conid, database, 'run_script', req);
|
||||
logger.info({ conid, database, sql }, 'DBGM-00008 Processing script');
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
sendToAuditLog(req, {
|
||||
@@ -303,7 +304,7 @@ module.exports = {
|
||||
|
||||
runOperation_meta: true,
|
||||
async runOperation({ conid, database, operation, useTransaction }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
logger.info({ conid, database, operation }, 'DBGM-00009 Processing operation');
|
||||
|
||||
sendToAuditLog(req, {
|
||||
@@ -325,7 +326,7 @@ module.exports = {
|
||||
|
||||
collectionData_meta: true,
|
||||
async collectionData({ conid, database, options, auditLogSessionGroup }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(
|
||||
opened,
|
||||
@@ -334,21 +335,21 @@ module.exports = {
|
||||
auditLogger:
|
||||
auditLogSessionGroup && options?.pureName
|
||||
? response => {
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
component: 'DatabaseConnectionsController',
|
||||
event: 'nosql.collectionData',
|
||||
action: 'select',
|
||||
severity: 'info',
|
||||
conid,
|
||||
database,
|
||||
pureName: options?.pureName,
|
||||
sumint1: response?.result?.rows?.length,
|
||||
sessionParam: `${conid}::${database}::${options?.pureName}`,
|
||||
sessionGroup: auditLogSessionGroup,
|
||||
message: `Loaded collection data ${options?.pureName}`,
|
||||
});
|
||||
}
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
component: 'DatabaseConnectionsController',
|
||||
event: 'nosql.collectionData',
|
||||
action: 'select',
|
||||
severity: 'info',
|
||||
conid,
|
||||
database,
|
||||
pureName: options?.pureName,
|
||||
sumint1: response?.result?.rows?.length,
|
||||
sessionParam: `${conid}::${database}::${options?.pureName}`,
|
||||
sessionGroup: auditLogSessionGroup,
|
||||
message: `Loaded collection data ${options?.pureName}`,
|
||||
});
|
||||
}
|
||||
: null,
|
||||
}
|
||||
);
|
||||
@@ -356,7 +357,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
async loadDataCore(msgtype, { conid, database, ...args }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype, ...args });
|
||||
if (res.errorMessage) {
|
||||
@@ -371,7 +372,7 @@ module.exports = {
|
||||
|
||||
schemaList_meta: true,
|
||||
async schemaList({ conid, database }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('schemaList', { conid, database });
|
||||
},
|
||||
|
||||
@@ -383,43 +384,43 @@ module.exports = {
|
||||
|
||||
loadKeys_meta: true,
|
||||
async loadKeys({ conid, database, root, filter, limit }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadKeys', { conid, database, root, filter, limit });
|
||||
},
|
||||
|
||||
scanKeys_meta: true,
|
||||
async scanKeys({ conid, database, root, pattern, cursor, count }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('scanKeys', { conid, database, root, pattern, cursor, count });
|
||||
},
|
||||
|
||||
exportKeys_meta: true,
|
||||
async exportKeys({ conid, database, options }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('exportKeys', { conid, database, options });
|
||||
},
|
||||
|
||||
loadKeyInfo_meta: true,
|
||||
async loadKeyInfo({ conid, database, key }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadKeyInfo', { conid, database, key });
|
||||
},
|
||||
|
||||
loadKeyTableRange_meta: true,
|
||||
async loadKeyTableRange({ conid, database, key, cursor, count }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadKeyTableRange', { conid, database, key, cursor, count });
|
||||
},
|
||||
|
||||
loadFieldValues_meta: true,
|
||||
async loadFieldValues({ conid, database, schemaName, pureName, field, search, dataType }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadFieldValues', { conid, database, schemaName, pureName, field, search, dataType });
|
||||
},
|
||||
|
||||
callMethod_meta: true,
|
||||
async callMethod({ conid, database, method, args }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('callMethod', { conid, database, method, args });
|
||||
|
||||
// const opened = await this.ensureOpened(conid, database);
|
||||
@@ -432,7 +433,8 @@ module.exports = {
|
||||
|
||||
updateCollection_meta: true,
|
||||
async updateCollection({ conid, database, changeSet }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet });
|
||||
if (res.errorMessage) {
|
||||
@@ -443,6 +445,36 @@ module.exports = {
|
||||
return res.result || null;
|
||||
},
|
||||
|
||||
saveTableData_meta: true,
|
||||
async saveTableData({ conid, database, changeSet }, req) {
|
||||
await testConnectionPermission(conid, req);
|
||||
|
||||
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
|
||||
const tablePermissions = await loadTablePermissionsFromRequest(req);
|
||||
const fieldsAndRoles = [
|
||||
[changeSet.inserts, 'create_update_delete'],
|
||||
[changeSet.deletes, 'create_update_delete'],
|
||||
[changeSet.updates, 'update_only'],
|
||||
]
|
||||
for (const [operations, requiredRole] of fieldsAndRoles) {
|
||||
for (const operation of operations) {
|
||||
const role = getTablePermissionRole(conid, database, 'tables', operation.schemaName, operation.pureName, tablePermissions, databasePermissions);
|
||||
if (getTablePermissionRoleLevelIndex(role) < getTablePermissionRoleLevelIndex(requiredRole)) {
|
||||
throw new Error('DBGM-00262 Permission not granted');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'saveTableData', changeSet });
|
||||
if (res.errorMessage) {
|
||||
return {
|
||||
errorMessage: res.errorMessage,
|
||||
};
|
||||
}
|
||||
return res.result || null;
|
||||
},
|
||||
|
||||
status_meta: true,
|
||||
async status({ conid, database }, req) {
|
||||
if (!conid) {
|
||||
@@ -451,7 +483,7 @@ module.exports = {
|
||||
message: 'No connection',
|
||||
};
|
||||
}
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) {
|
||||
return {
|
||||
@@ -474,7 +506,7 @@ module.exports = {
|
||||
|
||||
ping_meta: true,
|
||||
async ping({ conid, database }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
let existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
|
||||
if (existing) {
|
||||
@@ -502,7 +534,7 @@ module.exports = {
|
||||
|
||||
refresh_meta: true,
|
||||
async refresh({ conid, database, keepOpen }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
if (!keepOpen) this.close(conid, database);
|
||||
|
||||
await this.ensureOpened(conid, database);
|
||||
@@ -516,7 +548,7 @@ module.exports = {
|
||||
return { status: 'ok' };
|
||||
}
|
||||
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const conn = await this.ensureOpened(conid, database);
|
||||
conn.subprocess.send({ msgtype: 'syncModel', isFullRefresh });
|
||||
return { status: 'ok' };
|
||||
@@ -553,7 +585,7 @@ module.exports = {
|
||||
|
||||
disconnect_meta: true,
|
||||
async disconnect({ conid, database }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
await this.close(conid, database, true);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
@@ -563,8 +595,9 @@ module.exports = {
|
||||
if (!conid || !database) {
|
||||
return {};
|
||||
}
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req, loadedPermissions);
|
||||
if (conid == '__model') {
|
||||
const model = await importDbModel(database);
|
||||
const trans = await loadModelTransform(modelTransFile);
|
||||
@@ -586,6 +619,38 @@ module.exports = {
|
||||
message: `Loaded database structure for ${database}`,
|
||||
});
|
||||
|
||||
if (process.env.STORAGE_DATABASE && !hasPermission(`all-tables`, loadedPermissions)) {
|
||||
// filter databases by permissions
|
||||
const tablePermissions = await loadTablePermissionsFromRequest(req);
|
||||
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
|
||||
const databasePermissionRole = getDatabasePermissionRole(conid, database, databasePermissions);
|
||||
|
||||
function applyTablePermissionRole(list, objectTypeField) {
|
||||
const res = [];
|
||||
for (const item of list ?? []) {
|
||||
const tablePermissionRole = getTablePermissionRole(conid, database, objectTypeField, item.schemaName, item.pureName, tablePermissions, databasePermissionRole);
|
||||
if (tablePermissionRole != 'deny') {
|
||||
res.push({
|
||||
...item,
|
||||
tablePermissionRole,
|
||||
});
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const res = {
|
||||
...opened.structure,
|
||||
tables: applyTablePermissionRole(opened.structure.tables, 'tables'),
|
||||
views: applyTablePermissionRole(opened.structure.views, 'views'),
|
||||
procedures: applyTablePermissionRole(opened.structure.procedures, 'procedures'),
|
||||
functions: applyTablePermissionRole(opened.structure.functions, 'functions'),
|
||||
triggers: applyTablePermissionRole(opened.structure.triggers, 'triggers'),
|
||||
collections: applyTablePermissionRole(opened.structure.collections, 'collections'),
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return opened.structure;
|
||||
// const existing = this.opened.find((x) => x.conid == conid && x.database == database);
|
||||
// if (existing) return existing.status;
|
||||
@@ -600,7 +665,7 @@ module.exports = {
|
||||
if (!conid) {
|
||||
return null;
|
||||
}
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
if (!conid) return null;
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
return opened.serverVersion || null;
|
||||
@@ -608,7 +673,7 @@ module.exports = {
|
||||
|
||||
sqlPreview_meta: true,
|
||||
async sqlPreview({ conid, database, objects, options }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
// wait for structure
|
||||
await this.structure({ conid, database });
|
||||
|
||||
@@ -619,7 +684,7 @@ module.exports = {
|
||||
|
||||
exportModel_meta: true,
|
||||
async exportModel({ conid, database, outputFolder, schema }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
|
||||
const realFolder = outputFolder.startsWith('archive:')
|
||||
? resolveArchiveFolder(outputFolder.substring('archive:'.length))
|
||||
@@ -637,7 +702,7 @@ module.exports = {
|
||||
|
||||
exportModelSql_meta: true,
|
||||
async exportModelSql({ conid, database, outputFolder, outputFile, schema }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
|
||||
const connection = await connections.getCore({ conid });
|
||||
const driver = requireEngineDriver(connection);
|
||||
@@ -651,7 +716,7 @@ module.exports = {
|
||||
|
||||
generateDeploySql_meta: true,
|
||||
async generateDeploySql({ conid, database, archiveFolder }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, {
|
||||
msgtype: 'generateDeploySql',
|
||||
@@ -816,17 +881,17 @@ module.exports = {
|
||||
return {
|
||||
...(command == 'backup'
|
||||
? driver.backupDatabaseCommand(
|
||||
connection,
|
||||
{ outputFile, database, options, selectedTables, skippedTables, argsFormat },
|
||||
// @ts-ignore
|
||||
externalTools
|
||||
)
|
||||
connection,
|
||||
{ outputFile, database, options, selectedTables, skippedTables, argsFormat },
|
||||
// @ts-ignore
|
||||
externalTools
|
||||
)
|
||||
: driver.restoreDatabaseCommand(
|
||||
connection,
|
||||
{ inputFile, database, options, argsFormat },
|
||||
// @ts-ignore
|
||||
externalTools
|
||||
)),
|
||||
connection,
|
||||
{ inputFile, database, options, argsFormat },
|
||||
// @ts-ignore
|
||||
externalTools
|
||||
)),
|
||||
transformMessage: driver.transformNativeCommandMessage
|
||||
? message => driver.transformNativeCommandMessage(message, command)
|
||||
: null,
|
||||
@@ -923,7 +988,7 @@ module.exports = {
|
||||
|
||||
executeSessionQuery_meta: true,
|
||||
async executeSessionQuery({ sesid, conid, database, sql }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
logger.info({ sesid, sql }, 'DBGM-00010 Processing query');
|
||||
sessions.dispatchMessage(sesid, 'Query execution started');
|
||||
|
||||
@@ -935,7 +1000,7 @@ module.exports = {
|
||||
|
||||
evalJsonScript_meta: true,
|
||||
async evalJsonScript({ conid, database, script, runid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
|
||||
opened.subprocess.send({ msgtype: 'evalJsonScript', script, runid });
|
||||
|
||||
@@ -3,7 +3,12 @@ const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir, jsldir } = require('../utility/directories');
|
||||
const getChartExport = require('../utility/getChartExport');
|
||||
const { hasPermission } = require('../utility/hasPermission');
|
||||
const {
|
||||
hasPermission,
|
||||
loadPermissionsFromRequest,
|
||||
loadFilePermissionsFromRequest,
|
||||
getFilePermissionRole,
|
||||
} = require('../utility/hasPermission');
|
||||
const socket = require('../utility/socket');
|
||||
const scheduler = require('./scheduler');
|
||||
const getDiagramExport = require('../utility/getDiagramExport');
|
||||
@@ -31,7 +36,8 @@ function deserialize(format, text) {
|
||||
module.exports = {
|
||||
list_meta: true,
|
||||
async list({ folder }, req) {
|
||||
if (!hasPermission(`files/${folder}/read`, req)) return [];
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return [];
|
||||
const dir = path.join(filesdir(), folder);
|
||||
if (!(await fs.exists(dir))) return [];
|
||||
const files = (await fs.readdir(dir)).map(file => ({ folder, file }));
|
||||
@@ -40,10 +46,11 @@ module.exports = {
|
||||
|
||||
listAll_meta: true,
|
||||
async listAll(_params, req) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
const folders = await fs.readdir(filesdir());
|
||||
const res = [];
|
||||
for (const folder of folders) {
|
||||
if (!hasPermission(`files/${folder}/read`, req)) continue;
|
||||
if (!hasPermission(`files/${folder}/read`, loadedPermissions)) continue;
|
||||
const dir = path.join(filesdir(), folder);
|
||||
const files = (await fs.readdir(dir)).map(file => ({ folder, file }));
|
||||
res.push(...files);
|
||||
@@ -53,7 +60,8 @@ module.exports = {
|
||||
|
||||
delete_meta: true,
|
||||
async delete({ folder, file }, req) {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false;
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -65,7 +73,8 @@ module.exports = {
|
||||
|
||||
rename_meta: true,
|
||||
async rename({ folder, file, newFile }, req) {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false;
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file, newFile)) {
|
||||
return false;
|
||||
}
|
||||
@@ -86,10 +95,11 @@ module.exports = {
|
||||
|
||||
copy_meta: true,
|
||||
async copy({ folder, file, newFile }, req) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file, newFile)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false;
|
||||
await fs.copyFile(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile));
|
||||
socket.emitChanged(`files-changed`, { folder });
|
||||
socket.emitChanged(`all-files-changed`);
|
||||
@@ -113,7 +123,8 @@ module.exports = {
|
||||
});
|
||||
return deserialize(format, text);
|
||||
} else {
|
||||
if (!hasPermission(`files/${folder}/read`, req)) return null;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return null;
|
||||
const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' });
|
||||
return deserialize(format, text);
|
||||
}
|
||||
@@ -131,18 +142,19 @@ module.exports = {
|
||||
|
||||
save_meta: true,
|
||||
async save({ folder, file, data, format }, req) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!checkSecureFilePathsWithoutDirectory(folder, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (folder.startsWith('archive:')) {
|
||||
if (!hasPermission(`archive/write`, req)) return false;
|
||||
if (!hasPermission(`archive/write`, loadedPermissions)) return false;
|
||||
const dir = resolveArchiveFolder(folder.substring('archive:'.length));
|
||||
await fs.writeFile(path.join(dir, file), serialize(format, data));
|
||||
socket.emitChanged(`archive-files-changed`, { folder: folder.substring('archive:'.length) });
|
||||
return true;
|
||||
} else if (folder.startsWith('app:')) {
|
||||
if (!hasPermission(`apps/write`, req)) return false;
|
||||
if (!hasPermission(`apps/write`, loadedPermissions)) return false;
|
||||
const app = folder.substring('app:'.length);
|
||||
await fs.writeFile(path.join(appdir(), app, file), serialize(format, data));
|
||||
socket.emitChanged(`app-files-changed`, { app });
|
||||
@@ -150,7 +162,7 @@ module.exports = {
|
||||
apps.emitChangedDbApp(folder);
|
||||
return true;
|
||||
} else {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false;
|
||||
const dir = path.join(filesdir(), folder);
|
||||
if (!(await fs.exists(dir))) {
|
||||
await fs.mkdir(dir);
|
||||
@@ -177,7 +189,8 @@ module.exports = {
|
||||
|
||||
favorites_meta: true,
|
||||
async favorites(_params, req) {
|
||||
if (!hasPermission(`files/favorites/read`, req)) return [];
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/favorites/read`, loadedPermissions)) return [];
|
||||
const dir = path.join(filesdir(), 'favorites');
|
||||
if (!(await fs.exists(dir))) return [];
|
||||
const files = await fs.readdir(dir);
|
||||
@@ -234,16 +247,17 @@ module.exports = {
|
||||
|
||||
getFileRealPath_meta: true,
|
||||
async getFileRealPath({ folder, file }, req) {
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (folder.startsWith('archive:')) {
|
||||
if (!hasPermission(`archive/write`, req)) return false;
|
||||
if (!hasPermission(`archive/write`, loadedPermissions)) return false;
|
||||
const dir = resolveArchiveFolder(folder.substring('archive:'.length));
|
||||
return path.join(dir, file);
|
||||
} else if (folder.startsWith('app:')) {
|
||||
if (!hasPermission(`apps/write`, req)) return false;
|
||||
if (!hasPermission(`apps/write`, loadedPermissions)) return false;
|
||||
const app = folder.substring('app:'.length);
|
||||
return path.join(appdir(), app, file);
|
||||
} else {
|
||||
if (!hasPermission(`files/${folder}/write`, req)) return false;
|
||||
if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false;
|
||||
const dir = path.join(filesdir(), folder);
|
||||
if (!(await fs.exists(dir))) {
|
||||
await fs.mkdir(dir);
|
||||
@@ -297,7 +311,8 @@ module.exports = {
|
||||
|
||||
exportFile_meta: true,
|
||||
async exportFile({ folder, file, filePath }, req) {
|
||||
if (!hasPermission(`files/${folder}/read`, req)) return false;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return false;
|
||||
await fs.copyFile(path.join(filesdir(), folder, file), filePath);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ const socket = require('../utility/socket');
|
||||
const compareVersions = require('compare-versions');
|
||||
const requirePlugin = require('../shell/requirePlugin');
|
||||
const downloadPackage = require('../utility/downloadPackage');
|
||||
const { hasPermission } = require('../utility/hasPermission');
|
||||
const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
|
||||
const _ = require('lodash');
|
||||
const packagedPluginsContent = require('../packagedPluginsContent');
|
||||
|
||||
@@ -118,7 +118,8 @@ module.exports = {
|
||||
|
||||
install_meta: true,
|
||||
async install({ packageName }, req) {
|
||||
if (!hasPermission(`plugins/install`, req)) return;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`plugins/install`, loadedPermissions)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
// @ts-ignore
|
||||
if (!(await fs.exists(dir))) {
|
||||
@@ -132,7 +133,8 @@ module.exports = {
|
||||
|
||||
uninstall_meta: true,
|
||||
async uninstall({ packageName }, req) {
|
||||
if (!hasPermission(`plugins/install`, req)) return;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`plugins/install`, loadedPermissions)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
await fs.rmdir(dir, { recursive: true });
|
||||
socket.emitChanged(`installed-plugins-changed`);
|
||||
@@ -143,7 +145,8 @@ module.exports = {
|
||||
|
||||
upgrade_meta: true,
|
||||
async upgrade({ packageName }, req) {
|
||||
if (!hasPermission(`plugins/install`, req)) return;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission(`plugins/install`, loadedPermissions)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
// @ts-ignore
|
||||
if (await fs.exists(dir)) {
|
||||
|
||||
@@ -21,6 +21,7 @@ const processArgs = require('../utility/processArgs');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const { checkSecureDirectories, checkSecureDirectoriesInScript } = require('../utility/security');
|
||||
const { sendToAuditLog, logJsonRunnerScript } = require('../utility/auditlog');
|
||||
const { testStandardPermission } = require('../utility/hasPermission');
|
||||
const logger = getLogger('runners');
|
||||
|
||||
function extractPlugins(script) {
|
||||
@@ -288,6 +289,8 @@ module.exports = {
|
||||
return this.startCore(runid, scriptTemplate(js, false));
|
||||
}
|
||||
|
||||
await testStandardPermission('run-shell-script', req);
|
||||
|
||||
if (!platformInfo.allowShellScripting) {
|
||||
sendToAuditLog(req, {
|
||||
category: 'shell',
|
||||
|
||||
@@ -3,7 +3,7 @@ const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const cron = require('node-cron');
|
||||
const runners = require('./runners');
|
||||
const { hasPermission } = require('../utility/hasPermission');
|
||||
const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
|
||||
const { getLogger } = require('dbgate-tools');
|
||||
|
||||
const logger = getLogger('scheduler');
|
||||
@@ -30,7 +30,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
async reload(_params, req) {
|
||||
if (!hasPermission('files/shell/read', req)) return;
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (!hasPermission('files/shell/read', loadedPermissions)) return;
|
||||
const shellDir = path.join(filesdir(), 'shell');
|
||||
await this.unload();
|
||||
if (!(await fs.exists(shellDir))) return;
|
||||
|
||||
@@ -8,7 +8,13 @@ const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const lock = new AsyncLock();
|
||||
const config = require('./config');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { testConnectionPermission } = require('../utility/hasPermission');
|
||||
const {
|
||||
testConnectionPermission,
|
||||
loadPermissionsFromRequest,
|
||||
hasPermission,
|
||||
loadDatabasePermissionsFromRequest,
|
||||
getDatabasePermissionRole,
|
||||
} = require('../utility/hasPermission');
|
||||
const { MissingCredentialsError } = require('../utility/exceptions');
|
||||
const pipeForkLogs = require('../utility/pipeForkLogs');
|
||||
const { getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||
@@ -135,7 +141,7 @@ module.exports = {
|
||||
|
||||
disconnect_meta: true,
|
||||
async disconnect({ conid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
await this.close(conid, true);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
@@ -144,7 +150,9 @@ module.exports = {
|
||||
async listDatabases({ conid }, req) {
|
||||
if (!conid) return [];
|
||||
if (conid == '__model') return [];
|
||||
testConnectionPermission(conid, req);
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
|
||||
await testConnectionPermission(conid, req, loadedPermissions);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
sendToAuditLog(req, {
|
||||
category: 'serverop',
|
||||
@@ -157,12 +165,29 @@ module.exports = {
|
||||
sessionGroup: 'listDatabases',
|
||||
message: `Loaded databases for connection`,
|
||||
});
|
||||
|
||||
if (process.env.STORAGE_DATABASE && !hasPermission(`all-databases`, loadedPermissions)) {
|
||||
// filter databases by permissions
|
||||
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
|
||||
const res = [];
|
||||
for (const db of opened?.databases ?? []) {
|
||||
const databasePermissionRole = getDatabasePermissionRole(db.id, db.name, databasePermissions);
|
||||
if (databasePermissionRole != 'deny') {
|
||||
res.push({
|
||||
...db,
|
||||
databasePermissionRole,
|
||||
});
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return opened?.databases ?? [];
|
||||
},
|
||||
|
||||
version_meta: true,
|
||||
async version({ conid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
return opened?.version ?? null;
|
||||
},
|
||||
@@ -184,11 +209,11 @@ module.exports = {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.lastPinged[conid] = new Date().getTime();
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
try {
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
opened.subprocess.send({ msgtype: 'ping' });
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'DBGM-00121 Error pinging server connection');
|
||||
@@ -202,7 +227,7 @@ module.exports = {
|
||||
|
||||
refresh_meta: true,
|
||||
async refresh({ conid, keepOpen }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
if (!keepOpen) this.close(conid);
|
||||
|
||||
await this.ensureOpened(conid);
|
||||
@@ -210,7 +235,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
async sendDatabaseOp({ conid, msgtype, name }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return null;
|
||||
@@ -252,7 +277,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
async loadDataCore(msgtype, { conid, ...args }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return null;
|
||||
@@ -270,13 +295,43 @@ module.exports = {
|
||||
|
||||
serverSummary_meta: true,
|
||||
async serverSummary({ conid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
logger.info({ conid }, 'DBGM-00260 Processing server summary');
|
||||
return this.loadDataCore('serverSummary', { conid });
|
||||
},
|
||||
|
||||
listDatabaseProcesses_meta: true,
|
||||
async listDatabaseProcesses(ctx, req) {
|
||||
const { conid } = ctx;
|
||||
// logger.info({ conid }, 'DBGM-00261 Listing processes of database server');
|
||||
testConnectionPermission(conid, req);
|
||||
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return null;
|
||||
}
|
||||
if (opened.connection.isReadOnly) return false;
|
||||
|
||||
return this.sendRequest(opened, { msgtype: 'listDatabaseProcesses' });
|
||||
},
|
||||
|
||||
killDatabaseProcess_meta: true,
|
||||
async killDatabaseProcess(ctx, req) {
|
||||
const { conid, pid } = ctx;
|
||||
testConnectionPermission(conid, req);
|
||||
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return null;
|
||||
}
|
||||
if (opened.connection.isReadOnly) return false;
|
||||
|
||||
return this.sendRequest(opened, { msgtype: 'killDatabaseProcess', pid });
|
||||
},
|
||||
|
||||
summaryCommand_meta: true,
|
||||
async summaryCommand({ conid, command, row }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (!opened) {
|
||||
return null;
|
||||
|
||||
@@ -8,10 +8,13 @@ const path = require('path');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { appdir } = require('../utility/directories');
|
||||
const { getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||
const { getLogger, extractErrorLogData, removeSqlFrontMatter } = require('dbgate-tools');
|
||||
const pipeForkLogs = require('../utility/pipeForkLogs');
|
||||
const config = require('./config');
|
||||
const { sendToAuditLog } = require('../utility/auditlog');
|
||||
const { testStandardPermission, testDatabaseRolePermission } = require('../utility/hasPermission');
|
||||
const { getStaticTokenSecret } = require('../auth/authCommon');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const logger = getLogger('sessions');
|
||||
|
||||
@@ -148,10 +151,23 @@ module.exports = {
|
||||
|
||||
executeQuery_meta: true,
|
||||
async executeQuery({ sesid, sql, autoCommit, autoDetectCharts, limitRows, frontMatter }, req) {
|
||||
let useTokenIsOk = false;
|
||||
if (frontMatter?.useToken) {
|
||||
const decoded = jwt.verify(frontMatter.useToken, getStaticTokenSecret());
|
||||
if (decoded?.['contentHash'] == crypto.createHash('md5').update(removeSqlFrontMatter(sql)).digest('hex')) {
|
||||
useTokenIsOk = true;
|
||||
}
|
||||
}
|
||||
if (!useTokenIsOk) {
|
||||
await testStandardPermission('dbops/query', req);
|
||||
}
|
||||
const session = this.opened.find(x => x.sesid == sesid);
|
||||
if (!session) {
|
||||
throw new Error('Invalid session');
|
||||
}
|
||||
if (!useTokenIsOk) {
|
||||
await testDatabaseRolePermission(session.conid, session.database, 'run_script', req);
|
||||
}
|
||||
|
||||
sendToAuditLog(req, {
|
||||
category: 'dbop',
|
||||
|
||||
@@ -13,10 +13,6 @@ module.exports = {
|
||||
return null;
|
||||
},
|
||||
|
||||
async loadSuperadminPermissions() {
|
||||
return [];
|
||||
},
|
||||
|
||||
getConnectionsForLoginPage_meta: true,
|
||||
async getConnectionsForLoginPage() {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
list_meta: true,
|
||||
async list(req) {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
@@ -1,19 +1,8 @@
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
const { uploadsdir, getLogsFilePath, filesdir } = require('../utility/directories');
|
||||
const { getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||
const { uploadsdir } = require('../utility/directories');
|
||||
const { getLogger } = require('dbgate-tools');
|
||||
const logger = getLogger('uploads');
|
||||
const axios = require('axios');
|
||||
const os = require('os');
|
||||
const fs = require('fs/promises');
|
||||
const { read } = require('./queryHistory');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const _ = require('lodash');
|
||||
const serverConnections = require('./serverConnections');
|
||||
const config = require('./config');
|
||||
const gistSecret = require('../gistSecret');
|
||||
const currentVersion = require('../currentVersion');
|
||||
const socket = require('../utility/socket');
|
||||
|
||||
module.exports = {
|
||||
upload_meta: {
|
||||
@@ -51,88 +40,70 @@ module.exports = {
|
||||
res.sendFile(path.join(uploadsdir(), req.query.file));
|
||||
},
|
||||
|
||||
async getGistToken() {
|
||||
const settings = await config.getSettings();
|
||||
// uploadErrorToGist_meta: true,
|
||||
// async uploadErrorToGist() {
|
||||
// const logs = await fs.readFile(getLogsFilePath(), { encoding: 'utf-8' });
|
||||
// const connections = await serverConnections.getOpenedConnectionReport();
|
||||
// try {
|
||||
// const response = await axios.default.post(
|
||||
// 'https://api.github.com/gists',
|
||||
// {
|
||||
// description: `DbGate ${currentVersion.version} error report`,
|
||||
// public: false,
|
||||
// files: {
|
||||
// 'logs.jsonl': {
|
||||
// content: logs,
|
||||
// },
|
||||
// 'os.json': {
|
||||
// content: JSON.stringify(
|
||||
// {
|
||||
// release: os.release(),
|
||||
// arch: os.arch(),
|
||||
// machine: os.machine(),
|
||||
// platform: os.platform(),
|
||||
// type: os.type(),
|
||||
// },
|
||||
// null,
|
||||
// 2
|
||||
// ),
|
||||
// },
|
||||
// 'platform.json': {
|
||||
// content: JSON.stringify(
|
||||
// _.omit(
|
||||
// {
|
||||
// ...platformInfo,
|
||||
// },
|
||||
// ['defaultKeyfile', 'sshAuthSock']
|
||||
// ),
|
||||
// null,
|
||||
// 2
|
||||
// ),
|
||||
// },
|
||||
// 'connections.json': {
|
||||
// content: JSON.stringify(connections, null, 2),
|
||||
// },
|
||||
// 'version.json': {
|
||||
// content: JSON.stringify(currentVersion, null, 2),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// headers: {
|
||||
// Authorization: `token ${await this.getGistToken()}`,
|
||||
// 'Content-Type': 'application/json',
|
||||
// Accept: 'application/vnd.github.v3+json',
|
||||
// },
|
||||
// }
|
||||
// );
|
||||
|
||||
return settings['other.gistCreateToken'] || gistSecret;
|
||||
},
|
||||
// return response.data;
|
||||
// } catch (err) {
|
||||
// logger.error(extractErrorLogData(err), 'DBGM-00148 Error uploading gist');
|
||||
|
||||
uploadErrorToGist_meta: true,
|
||||
async uploadErrorToGist() {
|
||||
const logs = await fs.readFile(getLogsFilePath(), { encoding: 'utf-8' });
|
||||
const connections = await serverConnections.getOpenedConnectionReport();
|
||||
try {
|
||||
const response = await axios.default.post(
|
||||
'https://api.github.com/gists',
|
||||
{
|
||||
description: `DbGate ${currentVersion.version} error report`,
|
||||
public: false,
|
||||
files: {
|
||||
'logs.jsonl': {
|
||||
content: logs,
|
||||
},
|
||||
'os.json': {
|
||||
content: JSON.stringify(
|
||||
{
|
||||
release: os.release(),
|
||||
arch: os.arch(),
|
||||
machine: os.machine(),
|
||||
platform: os.platform(),
|
||||
type: os.type(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
'platform.json': {
|
||||
content: JSON.stringify(
|
||||
_.omit(
|
||||
{
|
||||
...platformInfo,
|
||||
},
|
||||
['defaultKeyfile', 'sshAuthSock']
|
||||
),
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
'connections.json': {
|
||||
content: JSON.stringify(connections, null, 2),
|
||||
},
|
||||
'version.json': {
|
||||
content: JSON.stringify(currentVersion, null, 2),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${await this.getGistToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'DBGM-00148 Error uploading gist');
|
||||
|
||||
return {
|
||||
apiErrorMessage: err.message,
|
||||
};
|
||||
// console.error('Error creating gist:', error.response ? error.response.data : error.message);
|
||||
}
|
||||
},
|
||||
|
||||
deleteGist_meta: true,
|
||||
async deleteGist({ url }) {
|
||||
const response = await axios.default.delete(url, {
|
||||
headers: {
|
||||
Authorization: `token ${await this.getGistToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
});
|
||||
return true;
|
||||
},
|
||||
// return {
|
||||
// apiErrorMessage: err.message,
|
||||
// };
|
||||
// // console.error('Error creating gist:', error.response ? error.response.data : error.message);
|
||||
// }
|
||||
// },
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = process.env.GIST_UPLOAD_SECRET;
|
||||
@@ -5,6 +5,7 @@ const moment = require('moment');
|
||||
const path = require('path');
|
||||
const { logsdir, setLogsFilePath, getLogsFilePath } = require('./utility/directories');
|
||||
const currentVersion = require('./currentVersion');
|
||||
const _ = require('lodash');
|
||||
|
||||
const logger = getLogger('apiIndex');
|
||||
|
||||
@@ -68,7 +69,7 @@ function configureLogger() {
|
||||
}
|
||||
const additionals = {};
|
||||
const finalMsg =
|
||||
msg.msg && msg.msg.match(/^DBGM-\d\d\d\d\d/)
|
||||
_.isString(msg.msg) && msg.msg.match(/^DBGM-\d\d\d\d\d/)
|
||||
? {
|
||||
...msg,
|
||||
msg: msg.msg.substring(10).trimStart(),
|
||||
|
||||
@@ -29,6 +29,8 @@ const files = require('./controllers/files');
|
||||
const scheduler = require('./controllers/scheduler');
|
||||
const queryHistory = require('./controllers/queryHistory');
|
||||
const cloud = require('./controllers/cloud');
|
||||
const teamFiles = require('./controllers/teamFiles');
|
||||
|
||||
const onFinished = require('on-finished');
|
||||
const processArgs = require('./utility/processArgs');
|
||||
|
||||
@@ -264,6 +266,7 @@ function useAllControllers(app, electron) {
|
||||
useController(app, electron, '/apps', apps);
|
||||
useController(app, electron, '/auth', auth);
|
||||
useController(app, electron, '/cloud', cloud);
|
||||
useController(app, electron, '/team-files', teamFiles);
|
||||
}
|
||||
|
||||
function setElectronSender(electronSender) {
|
||||
|
||||
@@ -17,13 +17,14 @@ const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { connectUtility } = require('../utility/connectUtility');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const generateDeploySql = require('../shell/generateDeploySql');
|
||||
const { dumpSqlSelect } = require('dbgate-sqltree');
|
||||
const { dumpSqlSelect, scriptToSql } = require('dbgate-sqltree');
|
||||
const { allowExecuteCustomScript, handleQueryStream } = require('../utility/handleQueryStream');
|
||||
const dbgateApi = require('../shell');
|
||||
const requirePlugin = require('../shell/requirePlugin');
|
||||
const path = require('path');
|
||||
const { rundir } = require('../utility/directories');
|
||||
const fs = require('fs-extra');
|
||||
const { changeSetToSql } = require('dbgate-datalib');
|
||||
|
||||
const logger = getLogger('dbconnProcess');
|
||||
|
||||
@@ -348,6 +349,25 @@ async function handleUpdateCollection({ msgid, changeSet }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveTableData({ msgid, changeSet }) {
|
||||
await waitStructure();
|
||||
try {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
const script = driver.createSaveChangeSetScript(changeSet, analysedStructure, () =>
|
||||
changeSetToSql(changeSet, analysedStructure, driver.dialect)
|
||||
);
|
||||
const sql = scriptToSql(driver, script);
|
||||
await driver.script(dbhan, sql, { useTransaction: true });
|
||||
process.send({ msgtype: 'response', msgid });
|
||||
} catch (err) {
|
||||
process.send({
|
||||
msgtype: 'response',
|
||||
msgid,
|
||||
errorMessage: extractErrorMessage(err, 'Error executing SQL script'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSqlPreview({ msgid, objects, options }) {
|
||||
await waitStructure();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
@@ -464,6 +484,7 @@ const messageHandlers = {
|
||||
runScript: handleRunScript,
|
||||
runOperation: handleRunOperation,
|
||||
updateCollection: handleUpdateCollection,
|
||||
saveTableData: handleSaveTableData,
|
||||
collectionData: handleCollectionData,
|
||||
loadKeys: handleLoadKeys,
|
||||
scanKeys: handleScanKeys,
|
||||
|
||||
@@ -146,6 +146,30 @@ async function handleServerSummary({ msgid }) {
|
||||
return handleDriverDataCore(msgid, driver => driver.serverSummary(dbhan));
|
||||
}
|
||||
|
||||
async function handleKillDatabaseProcess({ msgid, pid }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
|
||||
try {
|
||||
const result = await driver.killProcess(dbhan, Number(pid));
|
||||
process.send({ msgtype: 'response', msgid, result });
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleListDatabaseProcesses({ msgid }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
|
||||
try {
|
||||
const result = await driver.listProcesses(dbhan);
|
||||
process.send({ msgtype: 'response', msgid, result });
|
||||
} catch (err) {
|
||||
process.send({ msgtype: 'response', msgid, errorMessage: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSummaryCommand({ msgid, command, row }) {
|
||||
return handleDriverDataCore(msgid, driver => driver.summaryCommand(dbhan, command, row));
|
||||
}
|
||||
@@ -154,6 +178,8 @@ const messageHandlers = {
|
||||
connect: handleConnect,
|
||||
ping: handlePing,
|
||||
serverSummary: handleServerSummary,
|
||||
killDatabaseProcess: handleKillDatabaseProcess,
|
||||
listDatabaseProcesses: handleListDatabaseProcesses,
|
||||
summaryCommand: handleSummaryCommand,
|
||||
createDatabase: props => handleDatabaseOp('createDatabase', props),
|
||||
dropDatabase: props => handleDatabaseOp('dropDatabase', props),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,11 +13,12 @@ const socket = require('./socket');
|
||||
const config = require('../controllers/config');
|
||||
const simpleEncryptor = require('simple-encryptor');
|
||||
const currentVersion = require('../currentVersion');
|
||||
const { getPublicIpInfo } = require('./hardwareFingerprint');
|
||||
|
||||
const logger = getLogger('cloudIntf');
|
||||
|
||||
let cloudFiles = null;
|
||||
let promoWidgetData = null;
|
||||
let promoWidgetDataLoaded = false;
|
||||
|
||||
const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY
|
||||
? 'http://localhost:3103'
|
||||
@@ -200,8 +201,6 @@ async function updateCloudFiles(isRefresh) {
|
||||
lastCloudFilesTags = '';
|
||||
}
|
||||
|
||||
const ipInfo = await getPublicIpInfo();
|
||||
|
||||
const tags = (await collectCloudFilesSearchTags()).join(',');
|
||||
let lastCheckedTm = 0;
|
||||
if (tags == lastCloudFilesTags && cloudFiles.length > 0) {
|
||||
@@ -213,7 +212,7 @@ async function updateCloudFiles(isRefresh) {
|
||||
const resp = await axios.default.get(
|
||||
`${DBGATE_CLOUD_URL}/public-cloud-updates?lastCheckedTm=${lastCheckedTm}&tags=${tags}&isRefresh=${
|
||||
isRefresh ? 1 : 0
|
||||
}&country=${ipInfo?.country || ''}`,
|
||||
}`,
|
||||
{
|
||||
headers: {
|
||||
...getLicenseHttpHeaders(),
|
||||
@@ -262,6 +261,44 @@ async function getPublicFileData(path) {
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
async function ensurePromoWidgetDataLoaded() {
|
||||
if (promoWidgetDataLoaded) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const fileContent = await fs.readFile(path.join(datadir(), 'promo-widget.json'), 'utf-8');
|
||||
promoWidgetData = JSON.parse(fileContent);
|
||||
} catch (err) {
|
||||
promoWidgetData = null;
|
||||
}
|
||||
promoWidgetDataLoaded = true;
|
||||
}
|
||||
|
||||
async function updatePremiumPromoWidget() {
|
||||
await ensurePromoWidgetDataLoaded();
|
||||
|
||||
const tags = (await collectCloudFilesSearchTags()).join(',');
|
||||
|
||||
const resp = await axios.default.get(
|
||||
`${DBGATE_CLOUD_URL}/premium-promo-widget?identifier=${promoWidgetData?.identifier ?? 'empty'}&tags=${tags}`,
|
||||
{
|
||||
headers: {
|
||||
...(await getCloudInstanceHeaders()),
|
||||
'x-app-version': currentVersion.version,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!resp.data || resp.data?.state == 'unchanged') {
|
||||
return;
|
||||
}
|
||||
|
||||
promoWidgetData = resp.data;
|
||||
await fs.writeFile(path.join(datadir(), 'promo-widget.json'), JSON.stringify(promoWidgetData, null, 2));
|
||||
|
||||
socket.emitChanged(`promo-widget-changed`);
|
||||
}
|
||||
|
||||
async function refreshPublicFiles(isRefresh) {
|
||||
if (!cloudFiles) {
|
||||
await loadCloudFiles();
|
||||
@@ -271,6 +308,9 @@ async function refreshPublicFiles(isRefresh) {
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'DBGM-00166 Error updating cloud files');
|
||||
}
|
||||
if (!isProApp()) {
|
||||
await updatePremiumPromoWidget();
|
||||
}
|
||||
}
|
||||
|
||||
async function callCloudApiGet(endpoint, signinHolder = null, additionalHeaders = {}) {
|
||||
@@ -423,6 +463,33 @@ function removeCloudCachedConnection(folid, cntid) {
|
||||
delete cloudConnectionCache[cacheKey];
|
||||
}
|
||||
|
||||
async function getPublicIpInfo() {
|
||||
try {
|
||||
const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/ipinfo`);
|
||||
if (!resp.data?.ip) {
|
||||
return { ip: 'unknown-ip' };
|
||||
}
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
return { ip: 'unknown-ip' };
|
||||
}
|
||||
}
|
||||
|
||||
async function getPromoWidgetData() {
|
||||
await ensurePromoWidgetDataLoaded();
|
||||
return promoWidgetData;
|
||||
}
|
||||
|
||||
async function getPromoWidgetPreview(campaign, variant) {
|
||||
const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/premium-promo-widget-preview/${campaign}/${variant}`);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
async function getPromoWidgetList() {
|
||||
const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/promo-widget-list`);
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDbGateIdentitySession,
|
||||
startCloudTokenChecking,
|
||||
@@ -439,4 +506,8 @@ module.exports = {
|
||||
removeCloudCachedConnection,
|
||||
readCloudTokenHolder,
|
||||
readCloudTestTokenHolder,
|
||||
getPublicIpInfo,
|
||||
getPromoWidgetData,
|
||||
getPromoWidgetPreview,
|
||||
getPromoWidgetList,
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ const getChartExport = (title, config, imageFile, plugins) => {
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Exported from <a href='https://dbgate.io/' target='_blank'>DbGate</a>, powered by <a href='https://www.chartjs.org/' target='_blank'>Chart.js</a>
|
||||
Exported from <a href='https://www.dbgate.io/' target='_blank'>DbGate</a>, powered by <a href='https://www.chartjs.org/' target='_blank'>Chart.js</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const getMapExport = (geoJson) => {
|
||||
leaflet
|
||||
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '<a href="https://dbgate.io" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
|
||||
attribution: '<a href="https://www.dbgate.io" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
|
||||
@@ -3,18 +3,6 @@ const os = require('os');
|
||||
const crypto = require('crypto');
|
||||
const platformInfo = require('./platformInfo');
|
||||
|
||||
async function getPublicIpInfo() {
|
||||
try {
|
||||
const resp = await axios.default.get('https://ipinfo.io/json');
|
||||
if (!resp.data?.ip) {
|
||||
return { ip: 'unknown-ip' };
|
||||
}
|
||||
return resp.data;
|
||||
} catch (err) {
|
||||
return { ip: 'unknown-ip' };
|
||||
}
|
||||
}
|
||||
|
||||
function getMacAddress() {
|
||||
try {
|
||||
const interfaces = os.networkInterfaces();
|
||||
@@ -32,6 +20,7 @@ function getMacAddress() {
|
||||
}
|
||||
|
||||
async function getHardwareFingerprint() {
|
||||
const { getPublicIpInfo } = require('./cloudIntf');
|
||||
const publicIpInfo = await getPublicIpInfo();
|
||||
const macAddress = getMacAddress();
|
||||
const platform = os.platform();
|
||||
@@ -42,8 +31,6 @@ async function getHardwareFingerprint() {
|
||||
return {
|
||||
publicIp: publicIpInfo.ip,
|
||||
country: publicIpInfo.country,
|
||||
region: publicIpInfo.region,
|
||||
city: publicIpInfo.city,
|
||||
macAddress,
|
||||
platform,
|
||||
release,
|
||||
@@ -68,9 +55,7 @@ async function getPublicHardwareFingerprint() {
|
||||
hash,
|
||||
payload: {
|
||||
platform: fingerprint.platform,
|
||||
city: fingerprint.city,
|
||||
country: fingerprint.country,
|
||||
region: fingerprint.region,
|
||||
isDocker: platformInfo.isDocker,
|
||||
isAwsUbuntuLayout: platformInfo.isAwsUbuntuLayout,
|
||||
isAzureUbuntuLayout: platformInfo.isAzureUbuntuLayout,
|
||||
@@ -87,5 +72,4 @@ module.exports = {
|
||||
getHardwareFingerprint,
|
||||
getHardwareFingerprintHash,
|
||||
getPublicHardwareFingerprint,
|
||||
getPublicIpInfo,
|
||||
};
|
||||
|
||||
@@ -1,96 +1,350 @@
|
||||
const { compilePermissions, testPermission } = require('dbgate-tools');
|
||||
const { compilePermissions, testPermission, getPermissionsCacheKey } = require('dbgate-tools');
|
||||
const _ = require('lodash');
|
||||
const { getAuthProviderFromReq } = require('../auth/authProvider');
|
||||
|
||||
const cachedPermissions = {};
|
||||
|
||||
function hasPermission(tested, req) {
|
||||
async function loadPermissionsFromRequest(req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
// request object not available, allow all
|
||||
return null;
|
||||
}
|
||||
|
||||
const loadedPermissions = await authProvider.getCurrentPermissions(req);
|
||||
return loadedPermissions;
|
||||
}
|
||||
|
||||
function hasPermission(tested, loadedPermissions) {
|
||||
if (!loadedPermissions) {
|
||||
// not available, allow all
|
||||
return true;
|
||||
}
|
||||
|
||||
const permissions = getAuthProviderFromReq(req).getCurrentPermissions(req);
|
||||
|
||||
if (!cachedPermissions[permissions]) {
|
||||
cachedPermissions[permissions] = compilePermissions(permissions);
|
||||
const permissionsKey = getPermissionsCacheKey(loadedPermissions);
|
||||
if (!cachedPermissions[permissionsKey]) {
|
||||
cachedPermissions[permissionsKey] = compilePermissions(loadedPermissions);
|
||||
}
|
||||
|
||||
return testPermission(tested, cachedPermissions[permissions]);
|
||||
|
||||
// const { user } = (req && req.auth) || {};
|
||||
// const { login } = (process.env.OAUTH_PERMISSIONS && req && req.user) || {};
|
||||
// const key = user || login || '';
|
||||
// const logins = getLogins();
|
||||
|
||||
// if (!userPermissions[key]) {
|
||||
// if (logins) {
|
||||
// const login = logins.find(x => x.login == user);
|
||||
// userPermissions[key] = compilePermissions(login ? login.permissions : null);
|
||||
// } else {
|
||||
// userPermissions[key] = compilePermissions(process.env.PERMISSIONS);
|
||||
// }
|
||||
// }
|
||||
// return testPermission(tested, userPermissions[key]);
|
||||
return testPermission(tested, cachedPermissions[permissionsKey]);
|
||||
}
|
||||
|
||||
// let loginsCache = null;
|
||||
// let loginsLoaded = false;
|
||||
|
||||
// function getLogins() {
|
||||
// if (loginsLoaded) {
|
||||
// return loginsCache;
|
||||
// }
|
||||
|
||||
// const res = [];
|
||||
// if (process.env.LOGIN && process.env.PASSWORD) {
|
||||
// res.push({
|
||||
// login: process.env.LOGIN,
|
||||
// password: process.env.PASSWORD,
|
||||
// permissions: process.env.PERMISSIONS,
|
||||
// });
|
||||
// }
|
||||
// if (process.env.LOGINS) {
|
||||
// const logins = _.compact(process.env.LOGINS.split(',').map(x => x.trim()));
|
||||
// for (const login of logins) {
|
||||
// const password = process.env[`LOGIN_PASSWORD_${login}`];
|
||||
// const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
|
||||
// if (password) {
|
||||
// res.push({
|
||||
// login,
|
||||
// password,
|
||||
// permissions,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// } else if (process.env.OAUTH_PERMISSIONS) {
|
||||
// const login_permission_keys = Object.keys(process.env).filter(key => _.startsWith(key, 'LOGIN_PERMISSIONS_'));
|
||||
// for (const permissions_key of login_permission_keys) {
|
||||
// const login = permissions_key.replace('LOGIN_PERMISSIONS_', '');
|
||||
// const permissions = process.env[permissions_key];
|
||||
// userPermissions[login] = compilePermissions(permissions);
|
||||
// }
|
||||
// }
|
||||
|
||||
// loginsCache = res.length > 0 ? res : null;
|
||||
// loginsLoaded = true;
|
||||
// return loginsCache;
|
||||
// }
|
||||
|
||||
function connectionHasPermission(connection, req) {
|
||||
function connectionHasPermission(connection, loadedPermissions) {
|
||||
if (!connection) {
|
||||
return true;
|
||||
}
|
||||
if (_.isString(connection)) {
|
||||
return hasPermission(`connections/${connection}`, req);
|
||||
return hasPermission(`connections/${connection}`, loadedPermissions);
|
||||
} else {
|
||||
return hasPermission(`connections/${connection._id}`, req);
|
||||
return hasPermission(`connections/${connection._id}`, loadedPermissions);
|
||||
}
|
||||
}
|
||||
|
||||
function testConnectionPermission(connection, req) {
|
||||
if (!connectionHasPermission(connection, req)) {
|
||||
throw new Error('Connection permission not granted');
|
||||
async function testConnectionPermission(connection, req, loadedPermissions) {
|
||||
if (!loadedPermissions) {
|
||||
loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
}
|
||||
if (process.env.STORAGE_DATABASE) {
|
||||
if (hasPermission(`all-connections`, loadedPermissions)) {
|
||||
return;
|
||||
}
|
||||
const conid = _.isString(connection) ? connection : connection?._id;
|
||||
if (hasPermission('internal-storage', loadedPermissions) && conid == '__storage') {
|
||||
return;
|
||||
}
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
return;
|
||||
}
|
||||
if (!(await authProvider.checkCurrentConnectionPermission(req, conid))) {
|
||||
throw new Error('DBGM-00263 Connection permission not granted');
|
||||
}
|
||||
} else {
|
||||
if (!connectionHasPermission(connection, loadedPermissions)) {
|
||||
throw new Error('DBGM-00264 Connection permission not granted');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDatabasePermissionsFromRequest(req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const databasePermissions = await authProvider.getCurrentDatabasePermissions(req);
|
||||
return databasePermissions;
|
||||
}
|
||||
|
||||
async function loadTablePermissionsFromRequest(req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tablePermissions = await authProvider.getCurrentTablePermissions(req);
|
||||
return tablePermissions;
|
||||
}
|
||||
|
||||
async function loadFilePermissionsFromRequest(req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filePermissions = await authProvider.getCurrentFilePermissions(req);
|
||||
return filePermissions;
|
||||
}
|
||||
|
||||
function matchDatabasePermissionRow(conid, database, permissionRow) {
|
||||
if (permissionRow.connection_id) {
|
||||
if (conid != permissionRow.connection_id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.database_names_list) {
|
||||
const items = permissionRow.database_names_list.split('\n');
|
||||
if (!items.find(item => item.trim()?.toLowerCase() === database?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.database_names_regex) {
|
||||
const regex = new RegExp(permissionRow.database_names_regex, 'i');
|
||||
if (!regex.test(database)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchTablePermissionRow(objectTypeField, schemaName, pureName, permissionRow) {
|
||||
if (permissionRow.table_names_list) {
|
||||
const items = permissionRow.table_names_list.split('\n');
|
||||
if (!items.find(item => item.trim()?.toLowerCase() === pureName?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.table_names_regex) {
|
||||
const regex = new RegExp(permissionRow.table_names_regex, 'i');
|
||||
if (!regex.test(pureName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.schema_names_list) {
|
||||
const items = permissionRow.schema_names_list.split('\n');
|
||||
if (!items.find(item => item.trim()?.toLowerCase() === schemaName?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.schema_names_regex) {
|
||||
const regex = new RegExp(permissionRow.schema_names_regex, 'i');
|
||||
if (!regex.test(schemaName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchFilePermissionRow(folder, file, permissionRow) {
|
||||
if (permissionRow.folder_name) {
|
||||
if (folder != permissionRow.folder_name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.file_names_list) {
|
||||
const items = permissionRow.file_names_list.split('\n');
|
||||
if (!items.find(item => item.trim()?.toLowerCase() === file?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.file_names_regex) {
|
||||
const regex = new RegExp(permissionRow.file_names_regex, 'i');
|
||||
if (!regex.test(file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const DATABASE_ROLE_ID_NAMES = {
|
||||
'-1': 'view',
|
||||
'-2': 'read_content',
|
||||
'-3': 'write_data',
|
||||
'-4': 'run_script',
|
||||
'-5': 'deny',
|
||||
};
|
||||
|
||||
const FILE_ROLE_ID_NAMES = {
|
||||
'-1': 'allow',
|
||||
'-2': 'deny',
|
||||
};
|
||||
|
||||
function getDatabaseRoleLevelIndex(roleName) {
|
||||
if (!roleName) {
|
||||
return 6;
|
||||
}
|
||||
if (roleName == 'run_script') {
|
||||
return 5;
|
||||
}
|
||||
if (roleName == 'write_data') {
|
||||
return 4;
|
||||
}
|
||||
if (roleName == 'read_content') {
|
||||
return 3;
|
||||
}
|
||||
if (roleName == 'view') {
|
||||
return 2;
|
||||
}
|
||||
if (roleName == 'deny') {
|
||||
return 1;
|
||||
}
|
||||
return 6;
|
||||
}
|
||||
|
||||
function getTablePermissionRoleLevelIndex(roleName) {
|
||||
if (!roleName) {
|
||||
return 6;
|
||||
}
|
||||
if (roleName == 'run_script') {
|
||||
return 5;
|
||||
}
|
||||
if (roleName == 'create_update_delete') {
|
||||
return 4;
|
||||
}
|
||||
if (roleName == 'update_only') {
|
||||
return 3;
|
||||
}
|
||||
if (roleName == 'read') {
|
||||
return 2;
|
||||
}
|
||||
if (roleName == 'deny') {
|
||||
return 1;
|
||||
}
|
||||
return 6;
|
||||
}
|
||||
|
||||
function getDatabasePermissionRole(conid, database, loadedDatabasePermissions) {
|
||||
let res = 'deny';
|
||||
for (const permissionRow of loadedDatabasePermissions) {
|
||||
if (!matchDatabasePermissionRow(conid, database, permissionRow)) {
|
||||
continue;
|
||||
}
|
||||
res = DATABASE_ROLE_ID_NAMES[permissionRow.database_permission_role_id];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getFilePermissionRole(folder, file, loadedFilePermissions) {
|
||||
let res = 'deny';
|
||||
for (const permissionRow of loadedFilePermissions) {
|
||||
if (!matchFilePermissionRow(folder, file, permissionRow)) {
|
||||
continue;
|
||||
}
|
||||
res = FILE_ROLE_ID_NAMES[permissionRow.file_permission_role_id];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const TABLE_ROLE_ID_NAMES = {
|
||||
'-1': 'read',
|
||||
'-2': 'update_only',
|
||||
'-3': 'create_update_delete',
|
||||
'-4': 'run_script',
|
||||
'-5': 'deny',
|
||||
};
|
||||
|
||||
const TABLE_SCOPE_ID_NAMES = {
|
||||
'-1': 'all_objects',
|
||||
'-2': 'tables',
|
||||
'-3': 'views',
|
||||
'-4': 'tables_views_collections',
|
||||
'-5': 'procedures',
|
||||
'-6': 'functions',
|
||||
'-7': 'triggers',
|
||||
'-8': 'sql_objects',
|
||||
'-9': 'collections',
|
||||
};
|
||||
|
||||
function getTablePermissionRole(
|
||||
conid,
|
||||
database,
|
||||
objectTypeField,
|
||||
schemaName,
|
||||
pureName,
|
||||
loadedTablePermissions,
|
||||
databasePermissionRole
|
||||
) {
|
||||
let res =
|
||||
databasePermissionRole == 'read_content'
|
||||
? 'read'
|
||||
: databasePermissionRole == 'write_data'
|
||||
? 'create_update_delete'
|
||||
: databasePermissionRole == 'run_script'
|
||||
? 'run_script'
|
||||
: 'deny';
|
||||
for (const permissionRow of loadedTablePermissions) {
|
||||
if (!matchDatabasePermissionRow(conid, database, permissionRow)) {
|
||||
continue;
|
||||
}
|
||||
if (!matchTablePermissionRow(objectTypeField, schemaName, pureName, permissionRow)) {
|
||||
continue;
|
||||
}
|
||||
const scope = TABLE_SCOPE_ID_NAMES[permissionRow.table_permission_scope_id];
|
||||
switch (scope) {
|
||||
case 'tables':
|
||||
if (objectTypeField != 'tables') continue;
|
||||
break;
|
||||
case 'views':
|
||||
if (objectTypeField != 'views') continue;
|
||||
break;
|
||||
case 'tables_views_collections':
|
||||
if (objectTypeField != 'tables' && objectTypeField != 'views' && objectTypeField != 'collections') continue;
|
||||
break;
|
||||
case 'procedures':
|
||||
if (objectTypeField != 'procedures') continue;
|
||||
break;
|
||||
case 'functions':
|
||||
if (objectTypeField != 'functions') continue;
|
||||
break;
|
||||
case 'triggers':
|
||||
if (objectTypeField != 'triggers') continue;
|
||||
break;
|
||||
case 'sql_objects':
|
||||
if (objectTypeField != 'procedures' && objectTypeField != 'functions' && objectTypeField != 'triggers')
|
||||
continue;
|
||||
break;
|
||||
case 'collections':
|
||||
if (objectTypeField != 'collections') continue;
|
||||
break;
|
||||
}
|
||||
res = TABLE_ROLE_ID_NAMES[permissionRow.table_permission_role_id];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function testStandardPermission(permission, req, loadedPermissions) {
|
||||
if (!loadedPermissions) {
|
||||
loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
}
|
||||
if (!hasPermission(permission, loadedPermissions)) {
|
||||
throw new Error(`DBGM-00265 Permission ${permission} not granted`);
|
||||
}
|
||||
}
|
||||
|
||||
async function testDatabaseRolePermission(conid, database, requiredRole, req) {
|
||||
if (!process.env.STORAGE_DATABASE) {
|
||||
return;
|
||||
}
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
if (hasPermission(`all-databases`, loadedPermissions)) {
|
||||
return;
|
||||
}
|
||||
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
|
||||
const role = getDatabasePermissionRole(conid, database, databasePermissions);
|
||||
const requiredIndex = getDatabaseRoleLevelIndex(requiredRole);
|
||||
const roleIndex = getDatabaseRoleLevelIndex(role);
|
||||
if (roleIndex < requiredIndex) {
|
||||
throw new Error(`DBGM-00266 Permission ${requiredRole} not granted`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +352,14 @@ module.exports = {
|
||||
hasPermission,
|
||||
connectionHasPermission,
|
||||
testConnectionPermission,
|
||||
loadPermissionsFromRequest,
|
||||
loadDatabasePermissionsFromRequest,
|
||||
loadTablePermissionsFromRequest,
|
||||
loadFilePermissionsFromRequest,
|
||||
getDatabasePermissionRole,
|
||||
getTablePermissionRole,
|
||||
getFilePermissionRole,
|
||||
testStandardPermission,
|
||||
testDatabaseRolePermission,
|
||||
getTablePermissionRoleLevelIndex,
|
||||
};
|
||||
|
||||
@@ -31,6 +31,8 @@ export interface GridConfig extends GridConfigColumns {
|
||||
formFilterColumns: string[];
|
||||
multiColumnFilter?: string;
|
||||
searchInColumns?: string;
|
||||
disabledFilterColumns: string[];
|
||||
disabledMultiColumnFilter?: boolean;
|
||||
}
|
||||
|
||||
export interface GridCache {
|
||||
@@ -48,6 +50,7 @@ export function createGridConfig(): GridConfig {
|
||||
focusedColumns: null,
|
||||
grouping: {},
|
||||
formFilterColumns: [],
|
||||
disabledFilterColumns: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
FilterBehaviour,
|
||||
} from 'dbgate-types';
|
||||
import { parseFilter } from 'dbgate-filterparser';
|
||||
import { filterName } from 'dbgate-tools';
|
||||
import { filterName, shortenIdentifier } from 'dbgate-tools';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
import { Expression, Select, treeToSql, dumpSqlSelect, Condition, CompoudCondition } from 'dbgate-sqltree';
|
||||
import { isTypeLogical, standardFilterBehaviours, detectSqlFilterBehaviour, stringFilterBehaviour } from 'dbgate-tools';
|
||||
@@ -24,6 +24,7 @@ export interface DisplayColumn {
|
||||
columnName: string;
|
||||
headerText: string;
|
||||
uniqueName: string;
|
||||
uniqueNameShorten?: string;
|
||||
uniquePath: string[];
|
||||
notNull?: boolean;
|
||||
autoIncrement?: boolean;
|
||||
@@ -232,6 +233,7 @@ export abstract class GridDisplay {
|
||||
if (!filter) continue;
|
||||
const column = displayedColumnInfo[uniqueName];
|
||||
if (!column) continue;
|
||||
if (this.isFilterDisabled(uniqueName)) continue;
|
||||
try {
|
||||
const condition = parseFilter(
|
||||
filter,
|
||||
@@ -258,7 +260,7 @@ export abstract class GridDisplay {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.baseTableOrView && this.config.multiColumnFilter) {
|
||||
if (this.baseTableOrView && this.config.multiColumnFilter && !this.isMultiColumnFilterDisabled()) {
|
||||
const orCondition: CompoudCondition = {
|
||||
conditionType: 'or',
|
||||
conditions: [],
|
||||
@@ -415,6 +417,7 @@ export abstract class GridDisplay {
|
||||
[uniqueName]: value,
|
||||
},
|
||||
formViewRecordNumber: 0,
|
||||
disabledFilterColumns: cfg.disabledFilterColumns.filter(x => x != uniqueName),
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
@@ -424,6 +427,7 @@ export abstract class GridDisplay {
|
||||
...cfg,
|
||||
multiColumnFilter: value,
|
||||
formViewRecordNumber: 0,
|
||||
disabledMultiColumnFilter: false,
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
@@ -447,6 +451,7 @@ export abstract class GridDisplay {
|
||||
...cfg,
|
||||
filters: _.omit(cfg.filters, [uniqueName]),
|
||||
formFilterColumns: (cfg.formFilterColumns || []).filter(x => x != uniqueName),
|
||||
disabledFilterColumns: (cfg.disabledFilterColumns).filter(x => x != uniqueName),
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
@@ -462,6 +467,37 @@ export abstract class GridDisplay {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
toggleFilterEnabled(uniqueName) {
|
||||
if (this.isFilterDisabled(uniqueName)) {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
disabledFilterColumns: cfg.disabledFilterColumns.filter(x => x != uniqueName),
|
||||
}));
|
||||
} else {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
disabledFilterColumns: [...cfg.disabledFilterColumns, uniqueName],
|
||||
}));
|
||||
}
|
||||
this.reload();
|
||||
}
|
||||
|
||||
isFilterDisabled(uniqueName: string) {
|
||||
return this.config.disabledFilterColumns.includes(uniqueName);
|
||||
}
|
||||
|
||||
toggleMultiColumnFilterEnabled() {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
disabledMultiColumnFilter: !cfg.disabledMultiColumnFilter,
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
|
||||
isMultiColumnFilterDisabled() {
|
||||
return this.config.disabledMultiColumnFilter;
|
||||
}
|
||||
|
||||
setSort(uniqueName, order) {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
@@ -606,7 +642,9 @@ export abstract class GridDisplay {
|
||||
}
|
||||
return {
|
||||
exprType: 'column',
|
||||
...(!this.dialect.omitTableAliases && { alias: alias || col.columnName }),
|
||||
...(!this.dialect.omitTableAliases && {
|
||||
alias: alias ?? col.columnName,
|
||||
}),
|
||||
source,
|
||||
...col,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { filterName, isTableColumnUnique } from 'dbgate-tools';
|
||||
import { filterName, isTableColumnUnique, shortenIdentifier } from 'dbgate-tools';
|
||||
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
||||
import type {
|
||||
TableInfo,
|
||||
@@ -93,7 +93,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
);
|
||||
}
|
||||
|
||||
getDisplayColumns(table: TableInfo, parentPath: string[]) {
|
||||
getDisplayColumns(table: TableInfo, parentPath: string[]): DisplayColumn[] {
|
||||
return (
|
||||
table?.columns
|
||||
?.map(col => this.getDisplayColumn(table, col, parentPath))
|
||||
@@ -101,11 +101,12 @@ export class TableGridDisplay extends GridDisplay {
|
||||
...col,
|
||||
isChecked: this.isColumnChecked(col),
|
||||
hintColumnNames:
|
||||
this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)?.columns?.map(
|
||||
columnName => `hint_${col.uniqueName}_${columnName}`
|
||||
this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)?.columns?.map(columnName =>
|
||||
shortenIdentifier(`hint_${col.uniqueName}_${columnName}`, this.driver.dialect.maxIdentifierLength)
|
||||
) || null,
|
||||
hintColumnDelimiter: this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)
|
||||
?.delimiter,
|
||||
uniqueNameShorten: shortenIdentifier(col.uniqueName, this.driver.dialect.maxIdentifierLength),
|
||||
isExpandable: !!col.foreignKey,
|
||||
})) || []
|
||||
);
|
||||
@@ -116,7 +117,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
if (this.isExpandedColumn(column.uniqueName)) {
|
||||
const table = this.getFkTarget(column);
|
||||
if (table) {
|
||||
const childAlias = `${column.uniqueName}_ref`;
|
||||
const childAlias = shortenIdentifier(`${column.uniqueName}_ref`, this.driver.dialect.maxIdentifierLength);
|
||||
const subcolumns = this.getDisplayColumns(table, column.uniquePath);
|
||||
|
||||
this.addReferenceToSelect(select, parentAlias, column);
|
||||
@@ -129,7 +130,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
}
|
||||
|
||||
addReferenceToSelect(select: Select, parentAlias: string, column: DisplayColumn) {
|
||||
const childAlias = `${column.uniqueName}_ref`;
|
||||
const childAlias = shortenIdentifier(`${column.uniqueName}_ref`, this.driver.dialect.maxIdentifierLength);
|
||||
if ((select.from.relations || []).find(x => x.alias == childAlias)) return;
|
||||
const table = this.getFkTarget(column);
|
||||
if (table && table.primaryKey) {
|
||||
@@ -191,15 +192,24 @@ export class TableGridDisplay extends GridDisplay {
|
||||
const hintDescription = this.getDictionaryDescription(table);
|
||||
if (hintDescription) {
|
||||
const parentUniqueName = column.uniquePath.slice(0, -1).join('.');
|
||||
this.addReferenceToSelect(select, parentUniqueName ? `${parentUniqueName}_ref` : 'basetbl', column);
|
||||
const childAlias = `${column.uniqueName}_ref`;
|
||||
this.addReferenceToSelect(
|
||||
select,
|
||||
parentUniqueName
|
||||
? shortenIdentifier(`${parentUniqueName}_ref`, this.driver.dialect.maxIdentifierLength)
|
||||
: 'basetbl',
|
||||
column
|
||||
);
|
||||
const childAlias = shortenIdentifier(`${column.uniqueName}_ref`, this.driver.dialect.maxIdentifierLength);
|
||||
select.columns.push(
|
||||
...hintDescription.columns.map(
|
||||
columnName =>
|
||||
({
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
alias: `hint_${column.uniqueName}_${columnName}`,
|
||||
alias: shortenIdentifier(
|
||||
`hint_${column.uniqueName}_${columnName}`,
|
||||
this.driver.dialect.maxIdentifierLength
|
||||
),
|
||||
source: { alias: childAlias },
|
||||
} as ColumnRefExpression)
|
||||
)
|
||||
@@ -230,7 +240,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
}
|
||||
|
||||
getFkTarget(column: DisplayColumn) {
|
||||
const { uniqueName, foreignKey, isForeignKeyUnique } = column;
|
||||
const { foreignKey, isForeignKeyUnique } = column;
|
||||
if (!isForeignKeyUnique) return null;
|
||||
const pureName = foreignKey.refTableName;
|
||||
const schemaName = foreignKey.refSchemaName;
|
||||
@@ -298,7 +308,12 @@ export class TableGridDisplay extends GridDisplay {
|
||||
for (const column of columns) {
|
||||
if (this.addAllExpandedColumnsToSelected || this.config.addedColumns.includes(column.uniqueName)) {
|
||||
select.columns.push(
|
||||
this.createColumnExpression(column, { name: column, alias: parentAlias }, column.uniqueName, 'view')
|
||||
this.createColumnExpression(
|
||||
column,
|
||||
{ name: column, alias: parentAlias },
|
||||
column.uniqueNameShorten ?? column.uniqueName,
|
||||
'view'
|
||||
)
|
||||
);
|
||||
displayedColumnInfo[column.uniqueName] = {
|
||||
...column,
|
||||
|
||||
@@ -4,6 +4,7 @@ export type ChartXTransformFunction =
|
||||
| 'date:minute'
|
||||
| 'date:hour'
|
||||
| 'date:day'
|
||||
| 'date:week'
|
||||
| 'date:month'
|
||||
| 'date:year';
|
||||
export type ChartYAggregateFunction = 'sum' | 'first' | 'last' | 'min' | 'max' | 'count' | 'avg';
|
||||
@@ -70,6 +71,7 @@ export interface ChartDateParsed {
|
||||
minute?: number;
|
||||
second?: number;
|
||||
fraction?: string;
|
||||
week?: number;
|
||||
}
|
||||
|
||||
export interface ChartAvailableColumn {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ChartYFieldDefinition,
|
||||
ProcessedChart,
|
||||
} from './chartDefinitions';
|
||||
import { addMinutes, addHours, addDays, addMonths, addYears } from 'date-fns';
|
||||
import { addMinutes, addHours, addDays, addMonths, addWeeks, addYears, getWeek } from 'date-fns';
|
||||
|
||||
export function getChartDebugPrint(chart: ProcessedChart) {
|
||||
let res = '';
|
||||
@@ -29,6 +29,7 @@ export function tryParseChartDate(dateInput: any): ChartDateParsed | null {
|
||||
return {
|
||||
year: dateInput.getFullYear(),
|
||||
month: dateInput.getMonth() + 1,
|
||||
week: getWeek(dateInput),
|
||||
day: dateInput.getDate(),
|
||||
hour: dateInput.getHours(),
|
||||
minute: dateInput.getMinutes(),
|
||||
@@ -42,15 +43,21 @@ export function tryParseChartDate(dateInput: any): ChartDateParsed | null {
|
||||
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?)?$/
|
||||
);
|
||||
const monthMatch = dateInput.match(/^(\d{4})-(\d{2})$/);
|
||||
const weekMatch = dateInput.match(/^(\d{4})\@(\d{2})$/);
|
||||
// const yearMatch = dateInput.match(/^(\d{4})$/);
|
||||
|
||||
if (dateMatch) {
|
||||
const [_notUsed, year, month, day, hour, minute, second, fraction] = dateMatch;
|
||||
const [_notUsed, yearStr, monthStr, dayStr, hour, minute, second, fraction] = dateMatch;
|
||||
|
||||
const year = parseInt(yearStr, 10);
|
||||
const month = parseInt(monthStr, 10);
|
||||
const day = parseInt(dayStr, 10);
|
||||
|
||||
return {
|
||||
year: parseInt(year, 10),
|
||||
month: parseInt(month, 10),
|
||||
day: parseInt(day, 10),
|
||||
year,
|
||||
month,
|
||||
week: getWeek(new Date(year, month - 1, day)),
|
||||
day,
|
||||
hour: parseInt(hour, 10) || 0,
|
||||
minute: parseInt(minute, 10) || 0,
|
||||
second: parseInt(second, 10) || 0,
|
||||
@@ -71,6 +78,19 @@ export function tryParseChartDate(dateInput: any): ChartDateParsed | null {
|
||||
};
|
||||
}
|
||||
|
||||
if (weekMatch) {
|
||||
const [_notUsed, year, week] = weekMatch;
|
||||
return {
|
||||
year: parseInt(year, 10),
|
||||
week: parseInt(week, 10),
|
||||
day: 1,
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
fraction: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// if (yearMatch) {
|
||||
// const [_notUsed, year] = yearMatch;
|
||||
// return {
|
||||
@@ -97,6 +117,8 @@ export function stringifyChartDate(value: ChartDateParsed, transform: ChartXTran
|
||||
return `${value.year}`;
|
||||
case 'date:month':
|
||||
return `${value.year}-${pad2Digits(value.month)}`;
|
||||
case 'date:week':
|
||||
return `${value.year}@${pad2Digits(getWeek(new Date(value.year, (value.month ?? 1) - 1, value.day ?? 1)))}`;
|
||||
case 'date:day':
|
||||
return `${value.year}-${pad2Digits(value.month)}-${pad2Digits(value.day)}`;
|
||||
case 'date:hour':
|
||||
@@ -126,6 +148,9 @@ export function incrementChartDate(value: ChartDateParsed, transform: ChartXTran
|
||||
case 'date:month':
|
||||
newDateRepresentation = addMonths(dateRepresentation, 1);
|
||||
break;
|
||||
case 'date:week':
|
||||
newDateRepresentation = addWeeks(dateRepresentation, 1);
|
||||
break;
|
||||
case 'date:day':
|
||||
newDateRepresentation = addDays(dateRepresentation, 1);
|
||||
break;
|
||||
@@ -144,6 +169,11 @@ export function incrementChartDate(value: ChartDateParsed, transform: ChartXTran
|
||||
year: newDateRepresentation.getFullYear(),
|
||||
month: newDateRepresentation.getMonth() + 1,
|
||||
};
|
||||
case 'date:week':
|
||||
return {
|
||||
year: newDateRepresentation.getFullYear(),
|
||||
week: getWeek(newDateRepresentation),
|
||||
};
|
||||
case 'date:day':
|
||||
return {
|
||||
year: newDateRepresentation.getFullYear(),
|
||||
@@ -175,6 +205,8 @@ export function runTransformFunction(value: string, transformFunction: ChartXTra
|
||||
return dateParsed ? `${dateParsed.year}` : null;
|
||||
case 'date:month':
|
||||
return dateParsed ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}` : null;
|
||||
case 'date:week':
|
||||
return dateParsed ? `${dateParsed.year}@${pad2Digits(dateParsed.week)}` : null;
|
||||
case 'date:day':
|
||||
return dateParsed ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}-${pad2Digits(dateParsed.day)}` : null;
|
||||
case 'date:hour':
|
||||
@@ -211,6 +243,14 @@ export function computeChartBucketKey(
|
||||
month: dateParsed.month,
|
||||
},
|
||||
];
|
||||
case 'date:week':
|
||||
return [
|
||||
dateParsed ? `${dateParsed.year}@${pad2Digits(dateParsed.week)}` : null,
|
||||
{
|
||||
year: dateParsed.year,
|
||||
week: dateParsed.week,
|
||||
},
|
||||
];
|
||||
case 'date:day':
|
||||
return [
|
||||
dateParsed ? `${dateParsed.year}-${pad2Digits(dateParsed.month)}-${pad2Digits(dateParsed.day)}` : null,
|
||||
@@ -265,6 +305,8 @@ export function computeDateBucketDistance(
|
||||
return end.year - begin.year;
|
||||
case 'date:month':
|
||||
return (end.year - begin.year) * 12 + (end.month - begin.month);
|
||||
case 'date:week':
|
||||
return (end.year - begin.year) * 52 + (end.week - begin.week);
|
||||
case 'date:day':
|
||||
return (
|
||||
(end.year - begin.year) * 365 +
|
||||
@@ -302,6 +344,8 @@ export function compareChartDatesParsed(
|
||||
return a.year - b.year;
|
||||
case 'date:month':
|
||||
return a.year === b.year ? a.month - b.month : a.year - b.year;
|
||||
case 'date:week':
|
||||
return a.year === b.year ? a.week - b.week : a.year - b.year;
|
||||
case 'date:day':
|
||||
return a.year === b.year && a.month === b.month
|
||||
? a.day - b.day
|
||||
@@ -356,6 +400,8 @@ function getParentDateBucketKey(
|
||||
return null; // no parent for year
|
||||
case 'date:month':
|
||||
return bucketKey.slice(0, 4);
|
||||
case 'date:week':
|
||||
return bucketKey.slice(0, 4);
|
||||
case 'date:day':
|
||||
return bucketKey.slice(0, 7);
|
||||
case 'date:hour':
|
||||
@@ -371,6 +417,8 @@ function getParentDateBucketTransform(transform: ChartXTransformFunction): Chart
|
||||
return null; // no parent for year
|
||||
case 'date:month':
|
||||
return 'date:year';
|
||||
case 'date:week':
|
||||
return 'date:year';
|
||||
case 'date:day':
|
||||
return 'date:month';
|
||||
case 'date:hour':
|
||||
@@ -388,6 +436,8 @@ function getParentKeyParsed(date: ChartDateParsed, transform: ChartXTransformFun
|
||||
return null; // no parent for year
|
||||
case 'date:month':
|
||||
return { year: date.year };
|
||||
case 'date:week':
|
||||
return { year: date.week };
|
||||
case 'date:day':
|
||||
return { year: date.year, month: date.month };
|
||||
case 'date:hour':
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"dbgate-query-splitter": "^4.11.5",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"dbgate-query-splitter": "^4.11.7",
|
||||
"dbgate-sqltree": "^6.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
|
||||
@@ -49,6 +49,8 @@ export class DatabaseAnalyser<TClient = any> {
|
||||
singleObjectId: string = null;
|
||||
dialect: SqlDialect;
|
||||
logger: Logger;
|
||||
startedTm = Date.now();
|
||||
analyseIdentifier = Math.random().toString().substring(2);
|
||||
|
||||
constructor(public dbhan: DatabaseHandle<TClient>, public driver: EngineDriver, version) {
|
||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(version)) || driver?.dialect;
|
||||
@@ -78,14 +80,24 @@ export class DatabaseAnalyser<TClient = any> {
|
||||
}
|
||||
|
||||
getLogDbInfo() {
|
||||
return this.driver.getLogDbInfo(this.dbhan);
|
||||
return {
|
||||
...this.driver.getLogDbInfo(this.dbhan),
|
||||
analyserTime: Date.now() - this.startedTm,
|
||||
analyseIdentifier: this.analyseIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
async fullAnalysis() {
|
||||
logger.debug(this.getLogDbInfo(), 'DBGM-00126 Performing full analysis');
|
||||
const res = this.addEngineField(await this._runAnalysis());
|
||||
try {
|
||||
const res = this.addEngineField(await this._runAnalysis());
|
||||
logger.debug(this.getLogDbInfo(), 'DBGM-00271 Full analysis finished successfully');
|
||||
return res;
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err, this.getLogDbInfo()), 'DBGM-00272 Error during full analysis');
|
||||
throw err;
|
||||
}
|
||||
// console.log('FULL ANALYSIS', res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async singleObjectAnalysis(name, typeField) {
|
||||
@@ -106,31 +118,40 @@ export class DatabaseAnalyser<TClient = any> {
|
||||
logger.info(this.getLogDbInfo(), 'DBGM-00127 Performing incremental analysis');
|
||||
this.structure = structure;
|
||||
|
||||
const modifications = await this.getModifications();
|
||||
if (modifications == null) {
|
||||
// modifications not implemented, perform full analysis
|
||||
this.structure = null;
|
||||
return this.addEngineField(await this._runAnalysis());
|
||||
}
|
||||
const structureModifications = modifications.filter(x => x.action != 'setTableRowCounts');
|
||||
const setTableRowCounts = modifications.find(x => x.action == 'setTableRowCounts');
|
||||
|
||||
let structureWithRowCounts = null;
|
||||
if (setTableRowCounts) {
|
||||
const newStructure = mergeTableRowCounts(structure, setTableRowCounts.rowCounts);
|
||||
if (areDifferentRowCounts(structure, newStructure)) {
|
||||
structureWithRowCounts = newStructure;
|
||||
try {
|
||||
const modifications = await this.getModifications();
|
||||
if (modifications == null) {
|
||||
// modifications not implemented, perform full analysis
|
||||
this.structure = null;
|
||||
return this.addEngineField(await this._runAnalysis());
|
||||
}
|
||||
}
|
||||
const structureModifications = modifications.filter(x => x.action != 'setTableRowCounts');
|
||||
const setTableRowCounts = modifications.find(x => x.action == 'setTableRowCounts');
|
||||
|
||||
if (structureModifications.length == 0) {
|
||||
return structureWithRowCounts ? this.addEngineField(structureWithRowCounts) : null;
|
||||
}
|
||||
let structureWithRowCounts = null;
|
||||
if (setTableRowCounts) {
|
||||
const newStructure = mergeTableRowCounts(structure, setTableRowCounts.rowCounts);
|
||||
if (areDifferentRowCounts(structure, newStructure)) {
|
||||
structureWithRowCounts = newStructure;
|
||||
}
|
||||
}
|
||||
|
||||
this.modifications = structureModifications;
|
||||
if (structureWithRowCounts) this.structure = structureWithRowCounts;
|
||||
logger.info({ ...this.getLogDbInfo(), modifications: this.modifications }, 'DBGM-00128 DB modifications detected');
|
||||
return this.addEngineField(this.mergeAnalyseResult(await this._runAnalysis()));
|
||||
if (structureModifications.length == 0) {
|
||||
logger.debug(this.getLogDbInfo(), 'DBGM-00267 No changes in database structure detected');
|
||||
return structureWithRowCounts ? this.addEngineField(structureWithRowCounts) : null;
|
||||
}
|
||||
|
||||
this.modifications = structureModifications;
|
||||
if (structureWithRowCounts) this.structure = structureWithRowCounts;
|
||||
logger.info(
|
||||
{ ...this.getLogDbInfo(), modifications: this.modifications },
|
||||
'DBGM-00128 DB modifications detected'
|
||||
);
|
||||
return this.addEngineField(this.mergeAnalyseResult(await this._runAnalysis()));
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err, this.getLogDbInfo()), 'DBGM-00273 Error during incremental analysis');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
mergeAnalyseResult(newlyAnalysed) {
|
||||
|
||||
@@ -292,6 +292,16 @@ export class AlterPlan {
|
||||
}
|
||||
}
|
||||
|
||||
_hasOnlyCommentChange(op: AlterOperation): boolean {
|
||||
if (op.operationType === 'changeColumn') {
|
||||
return _.isEqual(
|
||||
_.omit(op.oldObject, ['columnComment', 'ordinal']),
|
||||
_.omit(op.newObject, ['columnComment', 'ordinal'])
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_getDependendColumnConstraints(column: ColumnInfo, dependencyDefinition) {
|
||||
const table = this.wholeOldDb.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName);
|
||||
if (!table) return [];
|
||||
@@ -337,31 +347,42 @@ export class AlterPlan {
|
||||
]) {
|
||||
if (op.operationType == testedOperationType) {
|
||||
const constraints = this._getDependendColumnConstraints(testedObject as ColumnInfo, testedDependencies);
|
||||
const ignoreContraints = this.dialect.safeCommentChanges && this._hasOnlyCommentChange(op);
|
||||
|
||||
// if (constraints.length > 0 && this.opts.noDropConstraint) {
|
||||
// return [];
|
||||
// }
|
||||
|
||||
const res: AlterOperation[] = [
|
||||
...constraints.map(oldObject => {
|
||||
const opRes: AlterOperation = {
|
||||
operationType: 'dropConstraint',
|
||||
oldObject,
|
||||
isRecreate: true,
|
||||
};
|
||||
return opRes;
|
||||
}),
|
||||
op,
|
||||
..._.reverse([...constraints]).map(newObject => {
|
||||
const opRes: AlterOperation = {
|
||||
operationType: 'createConstraint',
|
||||
newObject,
|
||||
};
|
||||
return opRes;
|
||||
}),
|
||||
];
|
||||
const res: AlterOperation[] = [];
|
||||
|
||||
if (constraints.length > 0) {
|
||||
if (!ignoreContraints) {
|
||||
res.push(
|
||||
...constraints.map(oldObject => {
|
||||
const opRes: AlterOperation = {
|
||||
operationType: 'dropConstraint',
|
||||
oldObject,
|
||||
isRecreate: true,
|
||||
};
|
||||
return opRes;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
res.push(op);
|
||||
|
||||
if (!ignoreContraints) {
|
||||
res.push(
|
||||
..._.reverse([...constraints]).map(newObject => {
|
||||
const opRes: AlterOperation = {
|
||||
operationType: 'createConstraint',
|
||||
newObject,
|
||||
};
|
||||
return opRes;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!ignoreContraints && constraints.length > 0) {
|
||||
this.recreates.constraints += 1;
|
||||
}
|
||||
return res;
|
||||
|
||||
@@ -60,4 +60,4 @@ export function chooseTopTables(tables: TableInfo[], count: number, tableFilter:
|
||||
|
||||
export const DIAGRAM_ZOOMS = [0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1, 1.25, 1.5, 1.75, 2];
|
||||
|
||||
export const DIAGRAM_DEFAULT_WATERMARK = 'Powered by [dbgate.io](https://dbgate.io)';
|
||||
export const DIAGRAM_DEFAULT_WATERMARK = 'Powered by [dbgate.io](https://www.dbgate.io)';
|
||||
|
||||
@@ -59,6 +59,7 @@ export interface DbDiffOptions {
|
||||
|
||||
ignoreForeignKeyActions?: boolean;
|
||||
ignoreDataTypes?: boolean;
|
||||
ignoreComments?: boolean;
|
||||
}
|
||||
|
||||
export function generateTablePairingId(table: TableInfo): TableInfo {
|
||||
@@ -322,11 +323,14 @@ export function testEqualColumns(
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ((a.columnComment || '') != (b.columnComment || '')) {
|
||||
console.debug(
|
||||
`Column ${a.pureName}.${a.columnName}, ${b.pureName}.${b.columnName}: different comment: ${a.columnComment}, ${b.columnComment}`
|
||||
);
|
||||
return false;
|
||||
|
||||
if (!opts.ignoreComments) {
|
||||
if ((a.columnComment || '') != (b.columnComment || '')) {
|
||||
console.debug(
|
||||
`Column ${a.pureName}.${a.columnName}, ${b.pureName}.${b.columnName}: different comment: ${a.columnComment}, ${b.columnComment}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!testEqualTypes(a, b, opts)) {
|
||||
|
||||
@@ -111,3 +111,20 @@ export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export const DATA_FOLDER_NAMES = [
|
||||
{ name: 'sql', label: 'SQL scripts' },
|
||||
{ name: 'shell', label: 'Shell scripts' },
|
||||
{ name: 'markdown', label: 'Markdown files' },
|
||||
{ name: 'charts', label: 'Charts' },
|
||||
{ name: 'query', label: 'Query designs' },
|
||||
{ name: 'sqlite', label: 'SQLite files' },
|
||||
{ name: 'duckdb', label: 'DuckDB files' },
|
||||
{ name: 'diagrams', label: 'Diagrams' },
|
||||
{ name: 'perspectives', label: 'Perspectives' },
|
||||
{ name: 'impexp', label: 'Import/Export jobs' },
|
||||
{ name: 'modtrans', label: 'Model transforms' },
|
||||
{ name: 'datadeploy', label: 'Data deploy jobs' },
|
||||
{ name: 'dbcompare', label: 'Database compare jobs' },
|
||||
{ name: 'apps', label: 'Applications' },
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@ import _isEmpty from 'lodash/isEmpty';
|
||||
import _omitBy from 'lodash/omitBy';
|
||||
import { DataEditorTypesBehaviour } from 'dbgate-types';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import md5 from 'blueimp-md5';
|
||||
|
||||
export const MAX_GRID_TEXT_LENGTH = 1000; // maximum length of text in grid cell, longer text is truncated
|
||||
|
||||
@@ -386,6 +387,9 @@ export function safeJsonParse(json, defaultValue?, logError = false) {
|
||||
if (_isArray(json) || _isPlainObject(json)) {
|
||||
return json;
|
||||
}
|
||||
if (!json) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch (err) {
|
||||
@@ -734,3 +738,12 @@ export function setSqlFrontMatter(text: string, data: { [key: string]: any }, ya
|
||||
const frontMatterContent = `-- >>>\n${yamlContentMapped}\n-- <<<\n`;
|
||||
return frontMatterContent + (textClean || '');
|
||||
}
|
||||
|
||||
export function shortenIdentifier(s: string, maxLength?: number) {
|
||||
if (!maxLength || maxLength < 10) return s;
|
||||
if (s.length <= maxLength) return s;
|
||||
const hash = md5(s).substring(0, 8);
|
||||
const partLength = Math.floor((maxLength - 9) / 2);
|
||||
const restLength = maxLength - 10 - partLength;
|
||||
return s.substring(0, partLength) + '_' + hash + '_' + s.substring(s.length - restLength);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,12 @@ export function compilePermissions(permissions: string[] | string): CompiledPerm
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getPermissionsCacheKey(permissions: string[] | string) {
|
||||
if (!permissions) return null;
|
||||
if (_isString(permissions)) return permissions;
|
||||
return permissions.join('|');
|
||||
}
|
||||
|
||||
export function testPermission(tested: string, permissions: CompiledPermissions) {
|
||||
let allow = true;
|
||||
|
||||
@@ -103,9 +109,25 @@ export function getPredefinedPermissions(predefinedRoleName: string) {
|
||||
case 'superadmin':
|
||||
return ['*', '~widgets/*', 'widgets/admin', 'widgets/database', '~all-connections'];
|
||||
case 'logged-user':
|
||||
return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections'];
|
||||
return [
|
||||
'*',
|
||||
'~widgets/admin',
|
||||
'~admin/*',
|
||||
'~internal-storage',
|
||||
'~all-connections',
|
||||
'~run-shell-script',
|
||||
'~all-team-files/*',
|
||||
];
|
||||
case 'anonymous-user':
|
||||
return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections'];
|
||||
return [
|
||||
'*',
|
||||
'~widgets/admin',
|
||||
'~admin/*',
|
||||
'~internal-storage',
|
||||
'~all-connections',
|
||||
'~run-shell-script',
|
||||
'~all-team-files/*',
|
||||
];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
Vendored
+35
-15
@@ -1,12 +1,12 @@
|
||||
interface ApplicationCommand {
|
||||
name: string;
|
||||
sql: string;
|
||||
}
|
||||
// interface ApplicationCommand {
|
||||
// name: string;
|
||||
// sql: string;
|
||||
// }
|
||||
|
||||
interface ApplicationQuery {
|
||||
name: string;
|
||||
sql: string;
|
||||
}
|
||||
// interface ApplicationQuery {
|
||||
// name: string;
|
||||
// sql: string;
|
||||
// }
|
||||
|
||||
interface VirtualReferenceDefinition {
|
||||
pureName: string;
|
||||
@@ -27,11 +27,31 @@ interface DictionaryDescriptionDefinition {
|
||||
delimiter: string;
|
||||
}
|
||||
|
||||
export interface ApplicationDefinition {
|
||||
name: string;
|
||||
|
||||
queries: ApplicationQuery[];
|
||||
commands: ApplicationCommand[];
|
||||
virtualReferences: VirtualReferenceDefinition[];
|
||||
dictionaryDescriptions: DictionaryDescriptionDefinition[];
|
||||
interface ApplicationUsageRule {
|
||||
conditionGroup?: string;
|
||||
serverHostsRegex?: string;
|
||||
serverHostsList?: string[];
|
||||
databaseNamesRegex?: string;
|
||||
databaseNamesList?: string[];
|
||||
tableNamesRegex?: string;
|
||||
tableNamesList?: string[];
|
||||
columnNamesRegex?: string;
|
||||
columnNamesList?: string[];
|
||||
}
|
||||
|
||||
export interface ApplicationDefinition {
|
||||
appid: string;
|
||||
applicationName: string;
|
||||
applicationIcon?: string;
|
||||
applicationColor?: string;
|
||||
usageRules?: ApplicationUsageRule[];
|
||||
files?: {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
sql: string;
|
||||
type: 'query' | 'command';
|
||||
};
|
||||
};
|
||||
virtualReferences?: VirtualReferenceDefinition[];
|
||||
dictionaryDescriptions?: DictionaryDescriptionDefinition[];
|
||||
}
|
||||
|
||||
Vendored
+15
-14
@@ -22,7 +22,7 @@ export interface ColumnsConstraintInfo extends ConstraintInfo {
|
||||
columns: ColumnReference[];
|
||||
}
|
||||
|
||||
export interface PrimaryKeyInfo extends ColumnsConstraintInfo {}
|
||||
export interface PrimaryKeyInfo extends ColumnsConstraintInfo { }
|
||||
|
||||
export interface ForeignKeyInfo extends ColumnsConstraintInfo {
|
||||
refSchemaName?: string;
|
||||
@@ -39,7 +39,7 @@ export interface IndexInfo extends ColumnsConstraintInfo {
|
||||
filterDefinition?: string;
|
||||
}
|
||||
|
||||
export interface UniqueInfo extends ColumnsConstraintInfo {}
|
||||
export interface UniqueInfo extends ColumnsConstraintInfo { }
|
||||
|
||||
export interface CheckInfo extends ConstraintInfo {
|
||||
definition: string;
|
||||
@@ -77,6 +77,7 @@ export interface DatabaseObjectInfo extends NamedObjectInfo {
|
||||
hashCode?: string;
|
||||
objectTypeField?: string;
|
||||
objectComment?: string;
|
||||
tablePermissionRole?: 'read' | 'update_only' | 'create_update_delete' | 'deny';
|
||||
}
|
||||
|
||||
export interface SqlObjectInfo extends DatabaseObjectInfo {
|
||||
@@ -134,7 +135,7 @@ export interface CallableObjectInfo extends SqlObjectInfo {
|
||||
parameters?: ParameterInfo[];
|
||||
}
|
||||
|
||||
export interface ProcedureInfo extends CallableObjectInfo {}
|
||||
export interface ProcedureInfo extends CallableObjectInfo { }
|
||||
|
||||
export interface FunctionInfo extends CallableObjectInfo {
|
||||
returnType?: string;
|
||||
@@ -145,17 +146,17 @@ export interface TriggerInfo extends SqlObjectInfo {
|
||||
functionName?: string;
|
||||
tableName?: string;
|
||||
triggerTiming?:
|
||||
| 'BEFORE'
|
||||
| 'AFTER'
|
||||
| 'INSTEAD OF'
|
||||
| 'BEFORE EACH ROW'
|
||||
| 'INSTEAD OF'
|
||||
| 'AFTER EACH ROW'
|
||||
| 'AFTER STATEMENT'
|
||||
| 'BEFORE STATEMENT'
|
||||
| 'AFTER EVENT'
|
||||
| 'BEFORE EVENT'
|
||||
| null;
|
||||
| 'BEFORE'
|
||||
| 'AFTER'
|
||||
| 'INSTEAD OF'
|
||||
| 'BEFORE EACH ROW'
|
||||
| 'INSTEAD OF'
|
||||
| 'AFTER EACH ROW'
|
||||
| 'AFTER STATEMENT'
|
||||
| 'BEFORE STATEMENT'
|
||||
| 'AFTER EVENT'
|
||||
| 'BEFORE EVENT'
|
||||
| null;
|
||||
triggerLevel?: 'ROW' | 'STATEMENT';
|
||||
eventType?: 'INSERT' | 'UPDATE' | 'DELETE' | 'TRUNCATE';
|
||||
}
|
||||
|
||||
Vendored
+9
@@ -22,6 +22,7 @@ export interface SqlDialect {
|
||||
requireStandaloneSelectForScopeIdentity?: boolean;
|
||||
allowMultipleValuesInsert?: boolean;
|
||||
useServerDatabaseFile?: boolean;
|
||||
maxIdentifierLength?: number;
|
||||
|
||||
dropColumnDependencies?: string[];
|
||||
changeColumnDependencies?: string[];
|
||||
@@ -74,6 +75,14 @@ export interface SqlDialect {
|
||||
|
||||
predefinedDataTypes: string[];
|
||||
|
||||
columnProperties?: {
|
||||
columnName?: boolean;
|
||||
isSparse?: true;
|
||||
isPersisted?: true;
|
||||
};
|
||||
|
||||
safeCommentChanges?: boolean;
|
||||
|
||||
// create sql-tree expression
|
||||
createColumnViewExpression(
|
||||
columnName: string,
|
||||
|
||||
Vendored
+101
-44
@@ -99,19 +99,46 @@ export interface SupportedDbKeyType {
|
||||
showItemList?: boolean;
|
||||
}
|
||||
|
||||
export type DatabaseProcess = {
|
||||
processId: number;
|
||||
connectionId: number;
|
||||
client: string;
|
||||
operation?: string;
|
||||
namespace?: string;
|
||||
command?: any;
|
||||
runningTime: number;
|
||||
state?: any;
|
||||
waitingFor?: boolean;
|
||||
locks?: any;
|
||||
progress?: any;
|
||||
};
|
||||
|
||||
export type DatabaseVariable = {
|
||||
variable: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export interface SqlBackupDumper {
|
||||
run();
|
||||
}
|
||||
|
||||
export interface SummaryColumn {
|
||||
fieldName: string;
|
||||
header: string;
|
||||
dataType: 'string' | 'number' | 'bytes';
|
||||
export interface ServerSummaryDatabases {
|
||||
rows: any[];
|
||||
columns: SummaryDatabaseColumn[];
|
||||
}
|
||||
export interface ServerSummaryDatabase {}
|
||||
|
||||
export type SummaryDatabaseColumn = {
|
||||
header: string;
|
||||
fieldName: string;
|
||||
type: 'data' | 'fileSize';
|
||||
filterable?: boolean;
|
||||
sortable?: boolean;
|
||||
};
|
||||
|
||||
export interface ServerSummary {
|
||||
columns: SummaryColumn[];
|
||||
databases: ServerSummaryDatabase[];
|
||||
processes: DatabaseProcess[];
|
||||
variables: DatabaseVariable[];
|
||||
databases: ServerSummaryDatabases;
|
||||
}
|
||||
|
||||
export type CollectionAggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max';
|
||||
@@ -161,12 +188,12 @@ export interface FilterBehaviourProvider {
|
||||
getFilterBehaviour(dataType: string, standardFilterBehaviours: { [id: string]: FilterBehaviour }): FilterBehaviour;
|
||||
}
|
||||
|
||||
export interface DatabaseHandle<TClient = any> {
|
||||
export interface DatabaseHandle<TClient = any, TDataBase = any> {
|
||||
client: TClient;
|
||||
database?: string;
|
||||
conid?: string;
|
||||
feedback?: (message: any) => void;
|
||||
getDatabase?: () => any;
|
||||
getDatabase?: () => TDataBase;
|
||||
connectionType?: string;
|
||||
treeKeySeparator?: string;
|
||||
}
|
||||
@@ -196,7 +223,7 @@ export interface RestoreDatabaseSettings extends BackupRestoreSettingsBase {
|
||||
inputFile: string;
|
||||
}
|
||||
|
||||
export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
export interface EngineDriver<TClient = any, TDataBase = any> extends FilterBehaviourProvider {
|
||||
engine: string;
|
||||
title: string;
|
||||
defaultPort?: number;
|
||||
@@ -242,61 +269,88 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
defaultSocketPath?: string;
|
||||
authTypeLabel?: string;
|
||||
importExportArgs?: any[];
|
||||
connect({ server, port, user, password, database, connectionDefinition }): Promise<DatabaseHandle<TClient>>;
|
||||
close(dbhan: DatabaseHandle<TClient>): Promise<any>;
|
||||
query(dbhan: DatabaseHandle<TClient>, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
||||
stream(dbhan: DatabaseHandle<TClient>, sql: string, options: StreamOptions);
|
||||
readQuery(dbhan: DatabaseHandle<TClient>, sql: string, structure?: TableInfo): Promise<StreamResult>;
|
||||
readJsonQuery(dbhan: DatabaseHandle<TClient>, query: any, structure?: TableInfo): Promise<StreamResult>;
|
||||
connect({
|
||||
server,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
database,
|
||||
connectionDefinition,
|
||||
}): Promise<DatabaseHandle<TClient, TDataBase>>;
|
||||
close(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<any>;
|
||||
query(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
||||
stream(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, options: StreamOptions);
|
||||
readQuery(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, structure?: TableInfo): Promise<StreamResult>;
|
||||
readJsonQuery(dbhan: DatabaseHandle<TClient, TDataBase>, query: any, structure?: TableInfo): Promise<StreamResult>;
|
||||
// eg. PostgreSQL COPY FROM stdin
|
||||
writeQueryFromStream(dbhan: DatabaseHandle<TClient>, sql: string): Promise<StreamResult>;
|
||||
writeTable(dbhan: DatabaseHandle<TClient>, name: NamedObjectInfo, options: WriteTableOptions): Promise<StreamResult>;
|
||||
writeQueryFromStream(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string): Promise<StreamResult>;
|
||||
writeTable(
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
name: NamedObjectInfo,
|
||||
options: WriteTableOptions
|
||||
): Promise<StreamResult>;
|
||||
analyseSingleObject(
|
||||
dbhan: DatabaseHandle<TClient>,
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
name: NamedObjectInfo,
|
||||
objectTypeField: keyof DatabaseInfo
|
||||
): Promise<TableInfo | ViewInfo | ProcedureInfo | FunctionInfo | TriggerInfo>;
|
||||
analyseSingleTable(dbhan: DatabaseHandle<TClient>, name: NamedObjectInfo): Promise<TableInfo>;
|
||||
getVersion(dbhan: DatabaseHandle<TClient>): Promise<{ version: string; versionText?: string }>;
|
||||
listDatabases(dbhan: DatabaseHandle<TClient>): Promise<
|
||||
analyseSingleTable(dbhan: DatabaseHandle<TClient, TDataBase>, name: NamedObjectInfo): Promise<TableInfo>;
|
||||
getVersion(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<{ version: string; versionText?: string }>;
|
||||
listDatabases(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<
|
||||
{
|
||||
name: string;
|
||||
sizeOnDisk?: number;
|
||||
empty?: boolean;
|
||||
}[]
|
||||
>;
|
||||
loadKeys(dbhan: DatabaseHandle<TClient>, root: string, filter?: string): Promise;
|
||||
scanKeys(dbhan: DatabaseHandle<TClient>, root: string, pattern: string, cursor: string, count: number): Promise;
|
||||
exportKeys(dbhan: DatabaseHandle<TClient>, options: {}): Promise;
|
||||
loadKeyInfo(dbhan: DatabaseHandle<TClient>, key): Promise;
|
||||
loadKeyTableRange(dbhan: DatabaseHandle<TClient>, key, cursor, count): Promise;
|
||||
loadKeys(dbhan: DatabaseHandle<TClient, TDataBase>, root: string, filter?: string): Promise;
|
||||
scanKeys(
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
root: string,
|
||||
pattern: string,
|
||||
cursor: string,
|
||||
count: number
|
||||
): Promise;
|
||||
exportKeys(dbhan: DatabaseHandle<TClient, TDataBase>, options: {}): Promise;
|
||||
loadKeyInfo(dbhan: DatabaseHandle<TClient, TDataBase>, key): Promise;
|
||||
loadKeyTableRange(dbhan: DatabaseHandle<TClient, TDataBase>, key, cursor, count): Promise;
|
||||
loadFieldValues(
|
||||
dbhan: DatabaseHandle<TClient>,
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
name: NamedObjectInfo,
|
||||
field: string,
|
||||
search: string,
|
||||
dataType: string
|
||||
): Promise;
|
||||
analyseFull(dbhan: DatabaseHandle<TClient>, serverVersion): Promise<DatabaseInfo>;
|
||||
analyseIncremental(dbhan: DatabaseHandle<TClient>, structure: DatabaseInfo, serverVersion): Promise<DatabaseInfo>;
|
||||
analyseFull(dbhan: DatabaseHandle<TClient, TDataBase>, serverVersion): Promise<DatabaseInfo>;
|
||||
analyseIncremental(
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
structure: DatabaseInfo,
|
||||
serverVersion
|
||||
): Promise<DatabaseInfo>;
|
||||
dialect: SqlDialect;
|
||||
dialectByVersion(version): SqlDialect;
|
||||
createDumper(options = null): SqlDumper;
|
||||
createBackupDumper(dbhan: DatabaseHandle<TClient>, options): Promise<SqlBackupDumper>;
|
||||
createBackupDumper(dbhan: DatabaseHandle<TClient, TDataBase>, options): Promise<SqlBackupDumper>;
|
||||
getAuthTypes(): EngineAuthType[];
|
||||
readCollection(dbhan: DatabaseHandle<TClient>, options: ReadCollectionOptions): Promise<any>;
|
||||
updateCollection(dbhan: DatabaseHandle<TClient>, changeSet: any): Promise<any>;
|
||||
readCollection(dbhan: DatabaseHandle<TClient, TDataBase>, options: ReadCollectionOptions): Promise<any>;
|
||||
updateCollection(dbhan: DatabaseHandle<TClient, TDataBase>, changeSet: any): Promise<any>;
|
||||
getCollectionUpdateScript(changeSet: any, collectionInfo: CollectionInfo): string;
|
||||
createDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
|
||||
dropDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
|
||||
createDatabase(dbhan: DatabaseHandle<TClient, TDataBase>, name: string): Promise;
|
||||
dropDatabase(dbhan: DatabaseHandle<TClient, TDataBase>, name: string): Promise;
|
||||
getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any;
|
||||
script(dbhan: DatabaseHandle<TClient>, sql: string, options?: RunScriptOptions): Promise;
|
||||
operation(dbhan: DatabaseHandle<TClient>, operation: CollectionOperationInfo, options?: RunScriptOptions): Promise;
|
||||
script(dbhan: DatabaseHandle<TClient, TDataBase>, sql: string, options?: RunScriptOptions): Promise;
|
||||
operation(
|
||||
dbhan: DatabaseHandle<TClient, TDataBase>,
|
||||
operation: CollectionOperationInfo,
|
||||
options?: RunScriptOptions
|
||||
): Promise;
|
||||
getNewObjectTemplates(): NewObjectTemplate[];
|
||||
// direct call of dbhan.client method, only some methods could be supported, on only some drivers
|
||||
callMethod(dbhan: DatabaseHandle<TClient>, method, args);
|
||||
serverSummary(dbhan: DatabaseHandle<TClient>): Promise<ServerSummary>;
|
||||
summaryCommand(dbhan: DatabaseHandle<TClient>, command, row): Promise<void>;
|
||||
startProfiler(dbhan: DatabaseHandle<TClient>, options): Promise<any>;
|
||||
stopProfiler(dbhan: DatabaseHandle<TClient>, profiler): Promise<void>;
|
||||
callMethod(dbhan: DatabaseHandle<TClient, TDataBase>, method, args);
|
||||
serverSummary(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<ServerSummary>;
|
||||
summaryCommand(dbhan: DatabaseHandle<TClient, TDataBase>, command, row): Promise<void>;
|
||||
startProfiler(dbhan: DatabaseHandle<TClient, TDataBase>, options): Promise<any>;
|
||||
stopProfiler(dbhan: DatabaseHandle<TClient, TDataBase>, profiler): Promise<void>;
|
||||
getRedirectAuthUrl(connection, options): Promise<{ url: string; sid: string }>;
|
||||
getAuthTokenFromCode(connection, options): Promise<string>;
|
||||
getAccessTokenFromAuth(connection, req): Promise<string | null>;
|
||||
@@ -313,7 +367,10 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
adaptTableInfo(table: TableInfo): TableInfo;
|
||||
// simple data type adapter
|
||||
adaptDataType(dataType: string): string;
|
||||
listSchemas(dbhan: DatabaseHandle<TClient>): Promise<SchemaInfo[] | null>;
|
||||
listSchemas(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<SchemaInfo[] | null>;
|
||||
listProcesses(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<DatabaseProcess[] | null>;
|
||||
listVariables(dbhan: DatabaseHandle<TClient, TDataBase>): Promise<DatabaseVariable[] | null>;
|
||||
killProcess(dbhan: DatabaseHandle<TClient, TDataBase>, pid: number): Promise<any>;
|
||||
backupDatabaseCommand(
|
||||
connection: any,
|
||||
settings: BackupDatabaseSettings,
|
||||
@@ -337,7 +394,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
analyserClass?: any;
|
||||
dumperClass?: any;
|
||||
singleConnectionOnly?: boolean;
|
||||
getLogDbInfo(dbhan: DatabaseHandle<TClient>): {
|
||||
getLogDbInfo(dbhan: DatabaseHandle<TClient, TDataBase>): {
|
||||
database?: string;
|
||||
engine: string;
|
||||
conid?: string;
|
||||
|
||||
Vendored
+3
@@ -56,6 +56,9 @@ export type TestEngineInfo = {
|
||||
|
||||
useTextTypeForStrings?: boolean;
|
||||
|
||||
supportTableComments?: boolean;
|
||||
supportColumnComments?: boolean;
|
||||
|
||||
supportRenameSqlObject?: boolean;
|
||||
supportSchemas?: boolean;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"@ant-design/colors": "^5.0.0",
|
||||
"@energiency/chartjs-plugin-piechart-outlabels": "^1.3.4",
|
||||
"@mdi/font": "^7.1.96",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-commonjs": "^20.0.0",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.5",
|
||||
@@ -29,7 +28,7 @@
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dbgate-datalib": "^6.0.0-alpha.1",
|
||||
"dbgate-query-splitter": "^4.11.5",
|
||||
"dbgate-query-splitter": "^4.11.7",
|
||||
"dbgate-sqltree": "^6.0.0-alpha.1",
|
||||
"dbgate-tools": "^6.0.0-alpha.1",
|
||||
"dbgate-types": "^6.0.0-alpha.1",
|
||||
@@ -61,9 +60,10 @@
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@langchain/core": "^0.3.72",
|
||||
"@langchain/langgraph": "^0.4.9",
|
||||
"@langchain/openai": "^0.6.9",
|
||||
"@messageformat/core": "^3.4.0",
|
||||
"babel-preset-solid": "^1.9.8",
|
||||
"chartjs-plugin-zoom": "^1.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -73,8 +73,8 @@
|
||||
"interval-operations": "^1.0.7",
|
||||
"leaflet": "^1.8.0",
|
||||
"openai": "^5.10.1",
|
||||
"solid-js": "^1.9.8",
|
||||
"wellknown": "^0.5.0",
|
||||
"xml-formatter": "^3.6.4"
|
||||
"xml-formatter": "^3.6.4",
|
||||
"zod": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,20 @@
|
||||
.color-icon-inv-red {
|
||||
color: var(--theme-icon-inv-red);
|
||||
}
|
||||
|
||||
.premium-background-gradient {
|
||||
background: linear-gradient(135deg, #1686c8, #8a25b1);
|
||||
}
|
||||
|
||||
.premium-gradient {
|
||||
background: linear-gradient(135deg, #1686c8, #8a25b1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.web-color-primary {
|
||||
background: #1686c8;
|
||||
}
|
||||
|
||||
.web-color-secondary {
|
||||
background: #8a25b1;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import typescript from '@rollup/plugin-typescript';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
import json from '@rollup/plugin-json';
|
||||
import babel from '@rollup/plugin-babel';
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
@@ -34,13 +33,11 @@ function serve() {
|
||||
};
|
||||
}
|
||||
|
||||
const BABEL_EXTENSIONS = ['.js', '.ts', '.tsx'];
|
||||
|
||||
export default [
|
||||
{
|
||||
input: 'src/query/QueryParserWorker.js',
|
||||
output: {
|
||||
sourcemap: true,
|
||||
sourcemap: !production,
|
||||
format: 'iife',
|
||||
file: 'public/build/query-parser-worker.js',
|
||||
},
|
||||
@@ -59,7 +56,7 @@ export default [
|
||||
{
|
||||
input: 'src/main.ts',
|
||||
output: {
|
||||
sourcemap: true,
|
||||
sourcemap: !production,
|
||||
format: 'iife',
|
||||
name: 'app',
|
||||
file: 'public/build/bundle.js',
|
||||
@@ -125,16 +122,6 @@ export default [
|
||||
sourceMap: !production,
|
||||
inlineSources: !production,
|
||||
}),
|
||||
babel({
|
||||
// +++
|
||||
extensions: BABEL_EXTENSIONS,
|
||||
babelHelpers: 'bundled',
|
||||
presets: [
|
||||
['solid', { generate: 'dom', hydratable: false }], // Solid JSX transform
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
}),
|
||||
json(),
|
||||
|
||||
// In dev mode, call `npm run start` once
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
installNewVolatileConnectionListener,
|
||||
refreshPublicCloudFiles,
|
||||
} from './utility/api';
|
||||
import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders';
|
||||
import { getAllApps, getConfig, getSettings } from './utility/metadataLoaders';
|
||||
import AppTitleProvider from './utility/AppTitleProvider.svelte';
|
||||
import getElectron from './utility/getElectron';
|
||||
import AppStartInfo from './widgets/AppStartInfo.svelte';
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
const connections = await apiCall('connections/list');
|
||||
const settings = await getSettings();
|
||||
const apps = await getUsedApps();
|
||||
const apps = await getAllApps();
|
||||
const loadedApiValue = !!(settings && connections && config && apps);
|
||||
|
||||
if (loadedApiValue) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang='ts'>
|
||||
import PermissionCheckBox from './PermissionCheckBox.svelte';
|
||||
import { getFormContext } from '../forms/FormProviderCore.svelte';
|
||||
|
||||
const { values } = getFormContext();
|
||||
|
||||
export let onSetPermission;
|
||||
export let label;
|
||||
export let folder;
|
||||
</script>
|
||||
|
||||
<PermissionCheckBox
|
||||
{label}
|
||||
permission={`files/${folder}/*`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
/>
|
||||
|
||||
<div class="ml-4">
|
||||
<PermissionCheckBox
|
||||
label="Read"
|
||||
permission={`files/${folder}/read`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
/>
|
||||
<PermissionCheckBox
|
||||
label="Write"
|
||||
permission={`files/${folder}/write`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
This component is only for Premium edition
|
||||
@@ -36,6 +36,7 @@
|
||||
export let filter = null;
|
||||
export let disableHover = false;
|
||||
export let divProps = {};
|
||||
export let additionalIcons = null;
|
||||
|
||||
$: isChecked =
|
||||
checkedObjectsStore && $checkedObjectsStore.find(x => module?.extractKey(data) == module?.extractKey(x));
|
||||
@@ -160,6 +161,11 @@
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
{#if additionalIcons}
|
||||
{#each additionalIcons as ic}
|
||||
<FontIcon icon={ic.icon} title={ic.title} colorClass={ic.colorClass} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if extInfo}
|
||||
<span class="ext-info">
|
||||
<TokenizedFilteredText text={extInfo} {filter} />
|
||||
|
||||
@@ -167,7 +167,7 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
isProApp() && { text: 'Data deployer', onClick: handleOpenDataDeployTab },
|
||||
$currentDatabase && [
|
||||
{ text: 'Generate deploy DB SQL', onClick: handleGenerateDeploySql },
|
||||
{ text: 'Shell: Deploy DB', onClick: handleGenerateDeployScript },
|
||||
hasPermission(`run-shell-script`) && { text: 'Shell: Deploy DB', onClick: handleGenerateDeployScript },
|
||||
],
|
||||
data.name != 'default' &&
|
||||
isProApp() &&
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
getOpenedTabs,
|
||||
openedConnections,
|
||||
openedSingleDatabaseConnections,
|
||||
pinnedDatabases,
|
||||
} from '../stores';
|
||||
import { filterName, filterNameCompoud } from 'dbgate-tools';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
@@ -130,7 +131,7 @@
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getDatabaseList, useAllApps } from '../utility/metadataLoaders';
|
||||
import { getLocalStorage } from '../utility/storageCache';
|
||||
import { apiCall, removeVolatileMapping } from '../utility/api';
|
||||
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
|
||||
@@ -152,6 +153,8 @@
|
||||
let engineStatusIcon = null;
|
||||
let engineStatusTitle = null;
|
||||
|
||||
$: isPinned = data.singleDatabase && !!$pinnedDatabases.find(x => x?.connection?._id == data?._id);
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
const handleConnect = (disableExpand = false) => {
|
||||
@@ -382,7 +385,8 @@
|
||||
$extensions,
|
||||
$currentDatabase,
|
||||
$apps,
|
||||
$openedSingleDatabaseConnections
|
||||
$openedSingleDatabaseConnections,
|
||||
data.databasePermissionRole
|
||||
),
|
||||
],
|
||||
|
||||
@@ -426,7 +430,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
@@ -454,6 +458,19 @@
|
||||
.find(x => x.isNewQuery)
|
||||
.onClick();
|
||||
}}
|
||||
onPin={!isPinned && data.singleDatabase
|
||||
? () =>
|
||||
pinnedDatabases.update(list => [
|
||||
...list,
|
||||
{
|
||||
name: data.defaultDatabase,
|
||||
connection: data,
|
||||
},
|
||||
])
|
||||
: null}
|
||||
onUnpin={isPinned && data.singleDatabase
|
||||
? () => pinnedDatabases.update(list => list.filter(x => x?.connection?._id != data?._id))
|
||||
: null}
|
||||
isChoosed={data._id == $focusedConnectionOrDatabase?.conid &&
|
||||
(data.singleDatabase
|
||||
? $focusedConnectionOrDatabase?.database == data.defaultDatabase
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
$extensions,
|
||||
$currentDatabase,
|
||||
$apps,
|
||||
$openedSingleDatabaseConnections
|
||||
$openedSingleDatabaseConnections,
|
||||
databasePermissionRole
|
||||
) {
|
||||
const apps = filterAppsForDatabase(connection, name, $apps);
|
||||
const handleNewQuery = () => {
|
||||
@@ -404,19 +405,36 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateNewApp = () => {
|
||||
showModal(InputTextModal, {
|
||||
header: 'New application',
|
||||
label: 'Application name',
|
||||
value: _.startCase(name),
|
||||
onConfirm: async appName => {
|
||||
const newAppId = await apiCall('apps/create-app-from-db', {
|
||||
appName,
|
||||
server: connection?.server,
|
||||
database: name,
|
||||
});
|
||||
openApplicationEditor(newAppId);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const driver = findEngineDriver(connection, getExtensions());
|
||||
|
||||
const commands = _.flatten((apps || []).map(x => x.commands || []));
|
||||
const commands = _.flatten((apps || []).map(x => Object.values(x.files || {}).filter(x => x.type == 'command')));
|
||||
|
||||
const isSqlOrDoc =
|
||||
driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document');
|
||||
|
||||
return [
|
||||
hasPermission(`dbops/query`) && {
|
||||
onClick: handleNewQuery,
|
||||
text: _t('database.newQuery', { defaultMessage: 'New query' }),
|
||||
isNewQuery: true,
|
||||
},
|
||||
hasPermission(`dbops/query`) &&
|
||||
isAllowedDatabaseRunScript(databasePermissionRole) && {
|
||||
onClick: handleNewQuery,
|
||||
text: _t('database.newQuery', { defaultMessage: 'New query' }),
|
||||
isNewQuery: true,
|
||||
},
|
||||
hasPermission(`dbops/model/edit`) &&
|
||||
!connection.isReadOnly &&
|
||||
driver?.databaseEngineTypes?.includes('sql') && {
|
||||
@@ -545,12 +563,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
{ divider: true },
|
||||
|
||||
driver?.databaseEngineTypes?.includes('sql') &&
|
||||
hasPermission(`run-shell-script`) &&
|
||||
hasPermission(`dbops/dropdb`) && {
|
||||
onClick: handleGenerateDropAllObjectsScript,
|
||||
text: _t('database.shellDropAllObjects', { defaultMessage: 'Shell: Drop all objects' }),
|
||||
},
|
||||
|
||||
{
|
||||
hasPermission(`run-shell-script`) && {
|
||||
onClick: handleGenerateRunScript,
|
||||
text: _t('database.shellRunScript', { defaultMessage: 'Shell: Run script' }),
|
||||
},
|
||||
@@ -561,11 +580,26 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
text: _t('database.dataDeployer', { defaultMessage: 'Data deployer' }),
|
||||
},
|
||||
|
||||
isProApp() &&
|
||||
hasPermission(`files/apps/write`) && {
|
||||
onClick: handleCreateNewApp,
|
||||
text: _t('database.createNewApplication', { defaultMessage: 'Create new application' }),
|
||||
},
|
||||
|
||||
isProApp() &&
|
||||
apps?.length > 0 && {
|
||||
text: _t('database.editApplications', { defaultMessage: 'Edit application' }),
|
||||
submenu: apps.map((app: any) => ({
|
||||
text: app.applicationName,
|
||||
onClick: () => openApplicationEditor(app.appid),
|
||||
})),
|
||||
},
|
||||
|
||||
{ divider: true },
|
||||
|
||||
commands.length > 0 && [
|
||||
commands.map((cmd: any) => ({
|
||||
text: cmd.name,
|
||||
text: cmd.label,
|
||||
onClick: () => {
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql: cmd.sql,
|
||||
@@ -615,17 +649,17 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
getConnectionLabel,
|
||||
} from 'dbgate-tools';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getDatabaseInfo, useAllApps, useDatabaseInfoPeek } from '../utility/metadataLoaders';
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import ConfirmSqlModal, { runOperationOnDatabase, saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';
|
||||
import { filterAppsForDatabase } from '../utility/appTools';
|
||||
import { filterAppsForDatabase, openApplicationEditor } from '../utility/appTools';
|
||||
import newQuery from '../query/newQuery';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
|
||||
import NewCollectionModal from '../modals/NewCollectionModal.svelte';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import hasPermission, { isAllowedDatabaseRunScript } from '../utility/hasPermission';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
import newTable from '../tableeditor/newTable';
|
||||
import { loadSchemaList, switchCurrentDatabase } from '../utility/common';
|
||||
@@ -636,6 +670,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
import { getNumberIcon } from '../icons/FontIcon.svelte';
|
||||
import { getDatabaseClickActionSetting } from '../settings/settingsTools';
|
||||
import { _t } from '../translations';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -647,13 +682,19 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
$extensions,
|
||||
$currentDatabase,
|
||||
$apps,
|
||||
$openedSingleDatabaseConnections
|
||||
$openedSingleDatabaseConnections,
|
||||
data.databasePermissionRole
|
||||
);
|
||||
}
|
||||
|
||||
$: isPinned = !!$pinnedDatabases.find(x => x?.name == data.name && x?.connection?._id == data.connection?._id);
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
$: isLoadingSchemas = $loadingSchemaLists[`${data?.connection?._id}::${data?.name}`];
|
||||
$: dbInfo = useDatabaseInfoPeek({ conid: data?.connection?._id, database: data?.name });
|
||||
|
||||
$: appsForDb = filterAppsForDatabase(data?.connection, data?.name, $apps, $dbInfo);
|
||||
|
||||
// $: console.log('AppsForDB:', data?.name, appsForDb);
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
@@ -676,6 +717,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
switchCurrentDatabase(data);
|
||||
}
|
||||
}}
|
||||
additionalIcons={appsForDb?.length > 0
|
||||
? appsForDb.map(ic => ({
|
||||
icon: ic.applicationIcon || 'img app',
|
||||
title: ic.applicationName,
|
||||
colorClass: ic.applicationColor ? `color-icon-${ic.applicationColor}` : undefined,
|
||||
}))
|
||||
: null}
|
||||
on:mousedown={() => {
|
||||
$focusedConnectionOrDatabase = { conid: data.connection?._id, database: data.name, connection: data.connection };
|
||||
}}
|
||||
@@ -697,6 +745,9 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
).length
|
||||
)
|
||||
: ''}
|
||||
statusIconBefore={data.databasePermissionRole == 'read_content' || data.databasePermissionRole == 'view'
|
||||
? 'icon lock'
|
||||
: null}
|
||||
menu={createMenu}
|
||||
showPinnedInsteadOfUnpin={passProps?.showPinnedInsteadOfUnpin}
|
||||
onPin={isPinned ? null : () => pinnedDatabases.update(list => [...list, data])}
|
||||
|
||||
@@ -4,7 +4,17 @@
|
||||
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
|
||||
export const createMatcher =
|
||||
(filter, cfg = DEFAULT_OBJECT_SEARCH_SETTINGS) =>
|
||||
({ schemaName, pureName, objectComment, tableEngine, columns, objectTypeField, tableName, createSql }) => {
|
||||
({
|
||||
schemaName,
|
||||
pureName,
|
||||
objectComment,
|
||||
tableEngine,
|
||||
columns,
|
||||
objectTypeField,
|
||||
tableName,
|
||||
createSql,
|
||||
tableRowCount,
|
||||
}) => {
|
||||
const mainArgs = [];
|
||||
const childArgs = [];
|
||||
if (cfg.schemaName) mainArgs.push(schemaName);
|
||||
@@ -12,6 +22,7 @@
|
||||
if (objectTypeField == 'tables') {
|
||||
if (cfg.tableComment) mainArgs.push(objectComment);
|
||||
if (cfg.tableEngine) mainArgs.push(tableEngine);
|
||||
if (cfg.tablesWithRows && !tableRowCount) return 'none';
|
||||
|
||||
for (const column of columns || []) {
|
||||
if (cfg.columnName) childArgs.push(column.columnName);
|
||||
@@ -45,16 +56,16 @@
|
||||
schedulerEvents: 'icon scheduler-event',
|
||||
};
|
||||
|
||||
const defaultTabs = {
|
||||
tables: 'TableDataTab',
|
||||
collections: 'CollectionDataTab',
|
||||
views: 'ViewDataTab',
|
||||
matviews: 'ViewDataTab',
|
||||
queries: 'QueryDataTab',
|
||||
procedures: 'SqlObjectTab',
|
||||
functions: 'SqlObjectTab',
|
||||
triggers: 'SqlObjectTab',
|
||||
};
|
||||
// const defaultTabs = {
|
||||
// tables: 'TableDataTab',
|
||||
// collections: 'CollectionDataTab',
|
||||
// views: 'ViewDataTab',
|
||||
// matviews: 'ViewDataTab',
|
||||
// queries: 'QueryDataTab',
|
||||
// procedures: 'SqlObjectTab',
|
||||
// functions: 'SqlObjectTab',
|
||||
// triggers: 'SqlObjectTab',
|
||||
// };
|
||||
|
||||
function createScriptTemplatesSubmenu(objectTypeField) {
|
||||
return {
|
||||
@@ -703,15 +714,29 @@
|
||||
}
|
||||
|
||||
function createMenus(objectTypeField, driver, data): ReturnType<typeof createMenusCore> {
|
||||
return createMenusCore(objectTypeField, driver, data).filter(x => {
|
||||
if (x.scriptTemplate) {
|
||||
return hasPermission(`dbops/sql-template/${x.scriptTemplate}`);
|
||||
const coreMenus = createMenusCore(objectTypeField, driver, data);
|
||||
|
||||
const filteredSumenus = coreMenus.map(item => {
|
||||
if (!item.submenu) {
|
||||
return item;
|
||||
}
|
||||
if (x.sqlGeneratorProps) {
|
||||
return hasPermission(`dbops/sql-generator`);
|
||||
}
|
||||
return true;
|
||||
return {
|
||||
...item,
|
||||
submenu: item.submenu.filter(x => {
|
||||
if (x.scriptTemplate) {
|
||||
return hasPermission(`dbops/sql-template/${x.scriptTemplate}`);
|
||||
}
|
||||
if (x.sqlGeneratorProps) {
|
||||
return hasPermission(`dbops/sql-generator`);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const filteredNoEmptySubmenus = filteredSumenus.filter(x => !x.submenu || x.submenu.length > 0);
|
||||
|
||||
return filteredNoEmptySubmenus;
|
||||
}
|
||||
|
||||
function getObjectTitle(connection, schemaName, pureName) {
|
||||
@@ -727,7 +752,7 @@
|
||||
export async function openDatabaseObjectDetail(
|
||||
tabComponent,
|
||||
scriptTemplate,
|
||||
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode },
|
||||
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode, sql },
|
||||
forceNewTab?,
|
||||
initialData?,
|
||||
icon?,
|
||||
@@ -762,6 +787,7 @@
|
||||
initialArgs: scriptTemplate ? { scriptTemplate } : null,
|
||||
defaultActionId,
|
||||
isRawMode,
|
||||
sql,
|
||||
},
|
||||
},
|
||||
initialData,
|
||||
@@ -783,7 +809,7 @@
|
||||
data,
|
||||
{ forceNewTab = false, tabPreviewMode = false, focusTab = false } = {}
|
||||
) {
|
||||
const { schemaName, pureName, conid, database, objectTypeField } = data;
|
||||
const { schemaName, pureName, conid, database, objectTypeField, sql } = data;
|
||||
const driver = findEngineDriver(data, getExtensions());
|
||||
|
||||
const activeTab = getActiveTab();
|
||||
@@ -829,6 +855,7 @@
|
||||
objectTypeField,
|
||||
defaultActionId: prefferedAction.defaultActionId,
|
||||
isRawMode: prefferedAction?.isRawMode ?? false,
|
||||
sql,
|
||||
},
|
||||
forceNewTab,
|
||||
prefferedAction?.initialData,
|
||||
@@ -1062,6 +1089,7 @@
|
||||
: null}
|
||||
extInfo={getExtInfo(data)}
|
||||
isChoosed={matchDatabaseObjectAppObject($selectedDatabaseObjectAppObject, data)}
|
||||
statusIconBefore={data.tablePermissionRole == 'read' ? 'icon lock' : null}
|
||||
on:click={() => handleObjectClick(data, 'leftClick')}
|
||||
on:middleclick={() => handleObjectClick(data, 'middleClick')}
|
||||
on:dblclick={() => handleObjectClick(data, 'dblClick')}
|
||||
|
||||
@@ -142,6 +142,18 @@
|
||||
label: 'Model transform file',
|
||||
};
|
||||
|
||||
const apps: FileTypeHandler = isProApp()
|
||||
? {
|
||||
icon: 'img app',
|
||||
format: 'json',
|
||||
tabComponent: 'AppEditorTab',
|
||||
folder: 'apps',
|
||||
currentConnection: false,
|
||||
extension: 'json',
|
||||
label: 'Application file',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
export const SAVED_FILE_HANDLERS = {
|
||||
sql,
|
||||
shell,
|
||||
@@ -154,6 +166,7 @@
|
||||
modtrans,
|
||||
datadeploy,
|
||||
dbcompare,
|
||||
apps,
|
||||
};
|
||||
|
||||
export const extractKey = data => data.file;
|
||||
@@ -179,6 +192,7 @@
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { saveFileToDisk } from '../utility/exportFileTools';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import { showSnackbarError } from '../utility/snackbar';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -201,11 +215,20 @@
|
||||
function createMenu() {
|
||||
return [
|
||||
handler?.tabComponent && { text: 'Open', onClick: openTab },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Rename', onClick: handleRename },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Create copy', onClick: handleCopy },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Delete', onClick: handleDelete },
|
||||
|
||||
!data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: 'Rename', onClick: handleRename },
|
||||
!data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: 'Create copy', onClick: handleCopy },
|
||||
!data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: 'Delete', onClick: handleDelete },
|
||||
|
||||
data.teamFileId && data.allowWrite && { text: 'Rename', onClick: handleRename },
|
||||
data.teamFileId &&
|
||||
data.allowRead &&
|
||||
hasPermission('all-team-files/create') && { text: 'Create copy', onClick: handleCopy },
|
||||
data.teamFileId && data.allowWrite && { text: 'Delete', onClick: handleDelete },
|
||||
|
||||
folder == 'markdown' && { text: 'Show page', onClick: showMarkdownPage },
|
||||
{ text: 'Download', onClick: handleDownload },
|
||||
!data.teamFileId && { text: 'Download', onClick: handleDownload },
|
||||
data.teamFileId && data.allowRead && { text: 'Download', onClick: handleDownload },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -213,7 +236,9 @@
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete file ${data.file}?`,
|
||||
onConfirm: () => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
apiCall('team-files/delete', { teamFileId: data.teamFileId });
|
||||
} else if (data.folid && data.cntid) {
|
||||
apiCall('cloud/delete-content', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -231,7 +256,9 @@
|
||||
label: 'New file name',
|
||||
header: 'Rename file',
|
||||
onConfirm: newFile => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
apiCall('team-files/update', { teamFileId: data.teamFileId, name: newFile });
|
||||
} else if (data.folid && data.cntid) {
|
||||
apiCall('cloud/rename-content', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -250,7 +277,9 @@
|
||||
label: 'New file name',
|
||||
header: 'Copy file',
|
||||
onConfirm: newFile => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
apiCall('team-files/copy', { teamFileId: data.teamFileId, newName: newFile });
|
||||
} else if (data.folid && data.cntid) {
|
||||
apiCall('cloud/copy-file', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -266,7 +295,12 @@
|
||||
const handleDownload = () => {
|
||||
saveFileToDisk(
|
||||
async filePath => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
await apiCall('team-files/export-file', {
|
||||
teamFileId: data.teamFileId,
|
||||
filePath,
|
||||
});
|
||||
} else if (data.folid && data.cntid) {
|
||||
await apiCall('cloud/export-file', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -286,7 +320,23 @@
|
||||
|
||||
async function openTab() {
|
||||
let dataContent;
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
if (data?.metadata?.autoExecute) {
|
||||
if (!data.allowUse) {
|
||||
showSnackbarError('You do not have permission to use this team file');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!data.allowRead) {
|
||||
showSnackbarError('You do not have permission to read this team file');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const resp = await apiCall('team-files/get-content', {
|
||||
teamFileId: data.teamFileId,
|
||||
});
|
||||
dataContent = resp.content;
|
||||
} else if (data.folid && data.cntid) {
|
||||
const resp = await apiCall('cloud/get-content', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -311,6 +361,11 @@
|
||||
tooltip = `${getConnectionLabel(connection)}\n${database}`;
|
||||
}
|
||||
|
||||
if (data?.metadata?.connectionId) {
|
||||
connProps.conid = data.metadata.connectionId;
|
||||
connProps.database = data.metadata.databaseName;
|
||||
}
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: data.file,
|
||||
@@ -323,6 +378,8 @@
|
||||
savedFormat: handler.format,
|
||||
savedCloudFolderId: data.folid,
|
||||
savedCloudContentId: data.cntid,
|
||||
savedTeamFileId: data.teamFileId,
|
||||
hideEditor: data.teamFileId && data?.metadata?.autoExecute && !data.allowRead,
|
||||
...connProps,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -100,4 +100,12 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
icon: 'img sql-file',
|
||||
},
|
||||
],
|
||||
queries: [
|
||||
{
|
||||
label: 'Show query',
|
||||
tab: 'QueryDataTab',
|
||||
defaultActionId: 'showAppQuery',
|
||||
icon: 'img app-query',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
export let disabled = false;
|
||||
export let title = null;
|
||||
|
||||
let domButton;
|
||||
|
||||
export function getBoundingClientRect() {
|
||||
return domButton.getBoundingClientRect();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="cta-button"
|
||||
{title}
|
||||
{disabled}
|
||||
on:click
|
||||
bind:this={domButton}
|
||||
data-testid={$$props['data-testid']}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.cta-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: var(--theme-font-link);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.cta-button:hover:not(:disabled) {
|
||||
color: var(--theme-font-hover);
|
||||
}
|
||||
|
||||
.cta-button:disabled {
|
||||
color: var(--theme-font-3);
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@
|
||||
export let title = null;
|
||||
export let skipWidth = false;
|
||||
export let outline = false;
|
||||
export let colorClass = '';
|
||||
|
||||
function handleClick() {
|
||||
if (!disabled) dispatch('click');
|
||||
@@ -31,6 +32,8 @@
|
||||
bind:this={domButton}
|
||||
class:skipWidth
|
||||
class:outline
|
||||
class={colorClass}
|
||||
class:setBackgroundColor={!colorClass}
|
||||
/>
|
||||
|
||||
<style>
|
||||
@@ -38,19 +41,26 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.setBackgroundColor {
|
||||
background-color: var(--theme-bg-button-inv);
|
||||
}
|
||||
|
||||
input:not(.skipWidth) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
input:hover:not(.disabled):not(.outline) {
|
||||
input:not(.setBackgroundColor) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input.setBackgroundColor:hover:not(.disabled):not(.outline) {
|
||||
background-color: var(--theme-bg-button-inv-2);
|
||||
}
|
||||
input:active:not(.disabled):not(.outline) {
|
||||
input.setBackgroundColor:active:not(.disabled):not(.outline) {
|
||||
background-color: var(--theme-bg-button-inv-3);
|
||||
}
|
||||
input.disabled {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export let square = false;
|
||||
export let narrow = false;
|
||||
export let title = null;
|
||||
export let inlineBlock=false;
|
||||
|
||||
let domButton;
|
||||
|
||||
@@ -17,6 +18,7 @@
|
||||
class:disabled
|
||||
class:square
|
||||
class:narrow
|
||||
class:inlineBlock
|
||||
on:click
|
||||
bind:this={domButton}
|
||||
data-testid={$$props['data-testid']}
|
||||
@@ -71,4 +73,8 @@
|
||||
.square {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.inlineBlock {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
|
||||
const thisInstance = get_current_component();
|
||||
|
||||
export const activator = createActivator('ToolStripContainer', true);
|
||||
|
||||
$: isComponentActive = $isComponentActiveStore('ToolStripContainer', thisInstance);
|
||||
export let showAlways = false;
|
||||
export const activator = showAlways ? null : createActivator('ToolStripContainer', true);
|
||||
|
||||
export function activate() {
|
||||
activator?.activate();
|
||||
}
|
||||
|
||||
export let scrollContent = false;
|
||||
export let hideToolStrip = false;
|
||||
|
||||
$: isComponentActive = showAlways || ($isComponentActiveStore('ToolStripContainer', thisInstance) && !hideToolStrip);
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:component this={component} {title} {icon} on:click={handleClick}>
|
||||
<svelte:component this={component} {title} {icon} on:click={handleClick} {...$$restProps}>
|
||||
{label}
|
||||
<FontIcon icon="icon chevron-down" />
|
||||
</svelte:component>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getCloudSigninTokenHolder,
|
||||
getExtensions,
|
||||
getVisibleToolbar,
|
||||
promoWidgetPreview,
|
||||
visibleToolbar,
|
||||
visibleWidgetSideBar,
|
||||
} from '../stores';
|
||||
@@ -50,6 +51,7 @@ import { isProApp } from '../utility/proTools';
|
||||
import { openWebLink } from '../utility/simpleTools';
|
||||
import { _t } from '../translations';
|
||||
import ExportImportConnectionsModal from '../modals/ExportImportConnectionsModal.svelte';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
// function themeCommand(theme: ThemeDefinition) {
|
||||
// return {
|
||||
@@ -70,7 +72,7 @@ registerCommand({
|
||||
category: 'Theme',
|
||||
name: 'Change',
|
||||
toolbarName: 'Change theme',
|
||||
onClick: () => showModal(SettingsModal, { selectedTab: 2 }),
|
||||
onClick: () => showModal(SettingsModal, { selectedTab: 'theme' }),
|
||||
// getSubCommands: () => get(extensions).themes.map(themeCommand),
|
||||
});
|
||||
|
||||
@@ -268,6 +270,23 @@ if (isProApp()) {
|
||||
});
|
||||
}
|
||||
|
||||
if (isProApp()) {
|
||||
registerCommand({
|
||||
id: 'new.application',
|
||||
category: 'New',
|
||||
icon: 'img app',
|
||||
name: 'Application',
|
||||
menuName: 'New application',
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Application #',
|
||||
icon: 'img app',
|
||||
tabComponent: 'AppEditorTab',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registerCommand({
|
||||
id: 'new.diagram',
|
||||
category: 'New',
|
||||
@@ -297,22 +316,22 @@ registerCommand({
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.application',
|
||||
category: 'New',
|
||||
icon: 'img app',
|
||||
name: 'Application',
|
||||
onClick: () => {
|
||||
showModal(InputTextModal, {
|
||||
value: '',
|
||||
label: 'New application name',
|
||||
header: 'Create application',
|
||||
onConfirm: async folder => {
|
||||
apiCall('apps/create-folder', { folder });
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
// registerCommand({
|
||||
// id: 'new.application',
|
||||
// category: 'New',
|
||||
// icon: 'img app',
|
||||
// name: 'Application',
|
||||
// onClick: () => {
|
||||
// showModal(InputTextModal, {
|
||||
// value: '',
|
||||
// label: 'New application name',
|
||||
// header: 'Create application',
|
||||
// onConfirm: async folder => {
|
||||
// apiCall('apps/create-folder', { folder });
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
|
||||
registerCommand({
|
||||
id: 'new.table',
|
||||
@@ -322,6 +341,7 @@ registerCommand({
|
||||
toolbar: true,
|
||||
toolbarName: 'New table',
|
||||
testEnabled: () => {
|
||||
if (!hasPermission('dbops/model/edit')) return false;
|
||||
const driver = findEngineDriver(get(currentDatabase)?.connection, getExtensions());
|
||||
return !!get(currentDatabase) && driver?.databaseEngineTypes?.includes('sql');
|
||||
},
|
||||
@@ -671,7 +691,7 @@ registerCommand({
|
||||
name: 'Export database',
|
||||
toolbar: true,
|
||||
icon: 'icon export',
|
||||
testEnabled: () => getCurrentDatabase() != null,
|
||||
testEnabled: () => getCurrentDatabase() != null && hasPermission(`dbops/export`),
|
||||
onClick: () => {
|
||||
openImportExportTab({
|
||||
targetStorageType: getDefaultFileFormat(getExtensions()).storageType,
|
||||
@@ -691,7 +711,8 @@ if (isProApp()) {
|
||||
icon: 'icon compare',
|
||||
testEnabled: () =>
|
||||
getCurrentDatabase() != null &&
|
||||
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql'),
|
||||
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql') &&
|
||||
hasPermission(`dbops/export`),
|
||||
onClick: () => {
|
||||
openNewTab(
|
||||
{
|
||||
@@ -752,6 +773,7 @@ if (hasPermission('settings/change')) {
|
||||
props: {},
|
||||
});
|
||||
},
|
||||
testEnabled: () => hasPermission('settings/change'),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
@@ -760,6 +782,7 @@ if (hasPermission('settings/change')) {
|
||||
name: 'Change',
|
||||
toolbarName: 'Settings',
|
||||
onClick: () => showModal(SettingsModal),
|
||||
testEnabled: () => hasPermission('settings/change'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1025,7 +1048,7 @@ registerCommand({
|
||||
id: 'app.openWeb',
|
||||
category: 'Application',
|
||||
name: 'DbGate web',
|
||||
onClick: () => openWebLink('https://dbgate.io/'),
|
||||
onClick: () => openWebLink('https://www.dbgate.io/'),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
@@ -1043,12 +1066,12 @@ registerCommand({
|
||||
onClick: () => openWebLink('https://opencollective.com/dbgate'),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'app.giveFeedback',
|
||||
category: 'Application',
|
||||
name: 'Give us feedback',
|
||||
onClick: () => openWebLink('https://dbgate.org/feedback'),
|
||||
});
|
||||
// registerCommand({
|
||||
// id: 'app.giveFeedback',
|
||||
// category: 'Application',
|
||||
// name: 'Give us feedback',
|
||||
// onClick: () => openWebLink('https://dbgate.org/feedback'),
|
||||
// });
|
||||
|
||||
registerCommand({
|
||||
id: 'app.zoomIn',
|
||||
@@ -1135,13 +1158,6 @@ registerCommand({
|
||||
onClick: () => getElectron().send('window-action', 'selectAll'),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.gist',
|
||||
category: 'New',
|
||||
name: 'Upload error to gist',
|
||||
onClick: () => showModal(UploadErrorModal),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'app.unsetCurrentDatabase',
|
||||
category: 'Application',
|
||||
@@ -1150,6 +1166,41 @@ registerCommand({
|
||||
onClick: () => currentDatabase.set(null),
|
||||
});
|
||||
|
||||
let loadedCampaignList = [];
|
||||
|
||||
registerCommand({
|
||||
id: 'internal.loadCampaigns',
|
||||
category: 'Internal',
|
||||
name: 'Load campaign list',
|
||||
testEnabled: () => getBoolSettingsValue('internal.showCampaigns', false),
|
||||
onClick: async () => {
|
||||
const resp = await apiCall('cloud/promo-widget-list', {});
|
||||
loadedCampaignList = resp;
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'internal.showCampaigns',
|
||||
category: 'Internal',
|
||||
name: 'Show campaigns',
|
||||
testEnabled: () => getBoolSettingsValue('internal.showCampaigns', false) && loadedCampaignList?.length > 0,
|
||||
getSubCommands: () => {
|
||||
return loadedCampaignList.map(campaign => ({
|
||||
text: `${campaign.campaignName} (${campaign.countries || 'Global'}) - #${campaign.quantileRank ?? '*'}/${
|
||||
campaign.quantileGroupCount ?? '*'
|
||||
} - ${campaign.variantIdentifier}`,
|
||||
onClick: async () => {
|
||||
promoWidgetPreview.set(
|
||||
await apiCall('cloud/promo-widget-preview', {
|
||||
campaign: campaign.campaignIdentifier,
|
||||
variant: campaign.variantIdentifier,
|
||||
})
|
||||
);
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
const electron = getElectron();
|
||||
if (electron) {
|
||||
electron.addEventListener('run-command', (e, commandId) => runCommand(commandId));
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import DefineDictionaryDescriptionModal from '../modals/DefineDictionaryDescriptionModal.svelte';
|
||||
import { sleep } from '../utility/common';
|
||||
|
||||
export let column;
|
||||
export let conid = undefined;
|
||||
@@ -24,6 +26,7 @@
|
||||
export let allowDefineVirtualReferences = false;
|
||||
export let setGrouping;
|
||||
export let seachInColumns = '';
|
||||
export let onReload = undefined;
|
||||
|
||||
const openReferencedTable = () => {
|
||||
openDatabaseObjectDetail('TableDataTab', null, {
|
||||
@@ -45,6 +48,19 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleCustomizeDescriptions = () => {
|
||||
showModal(DefineDictionaryDescriptionModal, {
|
||||
conid,
|
||||
database,
|
||||
schemaName: column.foreignKey.refSchemaName,
|
||||
pureName: column.foreignKey.refTableName,
|
||||
onConfirm: async () => {
|
||||
await sleep(100);
|
||||
onReload?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function getMenu() {
|
||||
return [
|
||||
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
||||
@@ -72,10 +88,13 @@
|
||||
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
|
||||
],
|
||||
|
||||
allowDefineVirtualReferences && [
|
||||
{ divider: true },
|
||||
{ onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
|
||||
],
|
||||
{ divider: true },
|
||||
|
||||
allowDefineVirtualReferences && { onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
|
||||
column.foreignKey && {
|
||||
onClick: handleCustomizeDescriptions,
|
||||
text: 'Customize description',
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
export let onCustomCommand = null;
|
||||
export let customCommandTooltip = null;
|
||||
export let formatterFunction = null;
|
||||
export let filterDisabled = false;
|
||||
|
||||
export let pureName = null;
|
||||
export let schemaName = null;
|
||||
@@ -47,6 +48,7 @@
|
||||
let isError;
|
||||
let isOk;
|
||||
let domInput;
|
||||
let isDisabled;
|
||||
|
||||
$: if (onGetReference && domInput) onGetReference(domInput);
|
||||
|
||||
@@ -257,6 +259,7 @@
|
||||
try {
|
||||
isOk = false;
|
||||
isError = false;
|
||||
isDisabled = filterDisabled;
|
||||
if (value) {
|
||||
parseFilter(value, filterBehaviour);
|
||||
isOk = true;
|
||||
@@ -287,6 +290,7 @@
|
||||
on:paste={handlePaste}
|
||||
class:isError
|
||||
class:isOk
|
||||
class:isDisabled
|
||||
{placeholder}
|
||||
data-testid={`DataFilterControl_input_${uniqueName}`}
|
||||
/>
|
||||
@@ -345,4 +349,8 @@
|
||||
input.isOk {
|
||||
background-color: var(--theme-bg-green);
|
||||
}
|
||||
|
||||
input.isDisabled {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
export let overlayValue = null;
|
||||
export let isMissingOverlayField = false;
|
||||
|
||||
$: value = col.isStructured ? _.get(rowData || {}, col.uniquePath) : (rowData || {})[col.uniqueName];
|
||||
$: value = col.isStructured
|
||||
? _.get(rowData || {}, col.uniquePath)
|
||||
: (rowData || {})[col.uniqueNameShorten ?? col.uniqueName];
|
||||
|
||||
function computeStyle(maxWidth, col) {
|
||||
let res = '';
|
||||
|
||||
@@ -2003,6 +2003,7 @@
|
||||
grouping={display.getGrouping(col.uniqueName)}
|
||||
{allowDefineVirtualReferences}
|
||||
seachInColumns={display.config?.searchInColumns}
|
||||
onReload={refresh}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
@@ -2058,6 +2059,7 @@
|
||||
selectTopmostCell(col.uniqueName);
|
||||
}}
|
||||
dataType={col.dataType}
|
||||
filterDisabled={display.isFilterDisabled(col.uniqueName)}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
const nextRows = await loadDataPage(
|
||||
$$props,
|
||||
loadedRows.length,
|
||||
getIntSettingsValue('dataGrid.pageSize', 100, 5, 1000)
|
||||
getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000)
|
||||
);
|
||||
if (loadedTimeRef.get() !== loadStart) {
|
||||
// new load was dispatched
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
{/if}
|
||||
|
||||
{#if dependencies.length > 0}
|
||||
<div class="bold nowrap ml-1">Dependend tables ({dependencies.length})</div>
|
||||
<div class="bold nowrap ml-1">Dependent tables ({dependencies.length})</div>
|
||||
{#each dependencies.filter(fk => filterName(filter, fk.pureName)) as fk}
|
||||
<div
|
||||
class="link"
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
}
|
||||
|
||||
function openQueryOnError() {
|
||||
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 1000)));
|
||||
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000)));
|
||||
}
|
||||
|
||||
const quickExportHandler = fmt => async () => {
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
|
||||
import {
|
||||
useAllApps,
|
||||
useConnectionInfo,
|
||||
useConnectionList,
|
||||
useDatabaseInfo,
|
||||
useDatabaseServerVersion,
|
||||
useServerVersion,
|
||||
useSettings,
|
||||
useUsedApps,
|
||||
} from '../utility/metadataLoaders';
|
||||
|
||||
import DataGrid from './DataGrid.svelte';
|
||||
@@ -53,7 +53,7 @@
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: dbinfo = useDatabaseInfo({ conid, database });
|
||||
$: serverVersion = useDatabaseServerVersion({ conid, database });
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
$: extendedDbInfo = extendDatabaseInfoFromApps($dbinfo, $apps);
|
||||
$: connections = useConnectionList();
|
||||
const settingsValue = useSettings();
|
||||
@@ -77,7 +77,10 @@
|
||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||
$serverVersion,
|
||||
table => getDictionaryDescription(table, conid, database, $apps, $connections),
|
||||
forceReadOnly || $connection?.isReadOnly,
|
||||
forceReadOnly ||
|
||||
$connection?.isReadOnly ||
|
||||
extendedDbInfo?.tables?.find(x => x.pureName == pureName && x.schemaName == schemaName)
|
||||
?.tablePermissionRole == 'read',
|
||||
isRawMode,
|
||||
$settingsValue
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
import DesignerTable from './DesignerTable.svelte';
|
||||
import { isConnectedByReference } from './designerTools';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import { getTableInfo, useDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getTableInfo, useAllApps, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import cleanupDesignColumns from './cleanupDesignColumns';
|
||||
import _ from 'lodash';
|
||||
import { writable } from 'svelte/store';
|
||||
@@ -108,7 +108,7 @@
|
||||
ref => tables.find(x => x.designerId == ref.sourceId) && tables.find(x => x.designerId == ref.targetId)
|
||||
) as any[];
|
||||
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
|
||||
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user