Compare commits
278 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a75e463ef5 | |||
| 7eb59ad3a0 | |||
| 7a9f8a460f | |||
| 289752c023 | |||
| 98f2c06c21 | |||
| 530b1cade3 | |||
| 65aa8fb4e3 | |||
| 4c0f17a0b2 | |||
| e4371c526b | |||
| e39f0a1f4b | |||
| 842f77d02b | |||
| 2571e6ac7e | |||
| 1599a7ea01 | |||
| cb1d81b586 | |||
| 339588b8a0 | |||
| 1731b7e4a3 | |||
| 5418bb932c | |||
| 6154b4c780 | |||
| 3f9bd100e1 | |||
| b5c6ddce59 | |||
| 51c72efb34 | |||
| 00df20e350 | |||
| f3a7e3af74 | |||
| 04c37c2b4f | |||
| 12df0993c0 | |||
| ac3ec5c11e | |||
| b565e981e4 | |||
| f7ada698e4 | |||
| bc4c146389 | |||
| 7c80ca1374 | |||
| 8c5cc7dcc1 | |||
| 1974243ed5 | |||
| 71c9071cb8 | |||
| c28e55132a | |||
| 2b2a4debd4 | |||
| 563a35560b | |||
| cc019281d4 | |||
| 86d7d61cc5 | |||
| aff1fe0b3d | |||
| 137631b5b5 | |||
| 090ffa064d | |||
| f77cc1023b | |||
| c6dbb31748 | |||
| ae6c486db5 | |||
| 9a2c12d558 | |||
| 1ed01e9839 | |||
| 25d2c129cd | |||
| 7dc7af0cdb | |||
| 80fea3b01b | |||
| 97dc92e413 | |||
| 9051ba2ee1 | |||
| 7dcbe6c7c1 | |||
| e6fe8a6379 | |||
| b793e4131d | |||
| b737eaac13 | |||
| cb5cce2ea3 | |||
| b05d260caa | |||
| 091e91556d | |||
| 2b4120435b | |||
| c8d031e2c4 | |||
| ac07b7e1ba | |||
| bf51f45934 | |||
| fe31cfb552 | |||
| d505be09ca | |||
| 44668b8017 | |||
| 452dba7f32 | |||
| 7694864fe7 | |||
| 37d5c6fbf9 | |||
| 802f231e43 | |||
| 53c39e6a43 | |||
| 65f550023a | |||
| abe7a20960 | |||
| d686206fe2 | |||
| 27b2fdb507 | |||
| 88f522084d | |||
| 8472c8be79 | |||
| 03f8a93dd0 | |||
| 2889f79120 | |||
| 8a312181a3 | |||
| e7236de078 | |||
| 1fe2269b11 | |||
| 10ea8ca3a6 | |||
| 491d24984d | |||
| b0279dd315 | |||
| 9d6b581809 | |||
| 3f748df1ec | |||
| 7ca835765c | |||
| a76530155d | |||
| 96b82b690e | |||
| d3a40e52fc | |||
| 513b2ba42f | |||
| d23371f642 | |||
| 5ac6e12c3e | |||
| 4468c0ed3b | |||
| 06bd9bcabe | |||
| 66d15abcab | |||
| 3bdb5c0152 | |||
| f504283002 | |||
| f07c7909ef | |||
| c809f58349 | |||
| 3e91ecd141 | |||
| 857185a78b | |||
| c189c12cae | |||
| 96106e6aac | |||
| 088ca231f3 | |||
| 5395d1343b | |||
| d48c34a4a5 | |||
| 53ee1d87c2 | |||
| b5d97c8181 | |||
| 28e06166e0 | |||
| 8f1343bc42 | |||
| 2080a23b69 | |||
| d71294621b | |||
| 0f6ec420d2 | |||
| 35152a2796 | |||
| 1abfab950e | |||
| 6e6d0bb616 | |||
| 93e264e9ec | |||
| 29257f9bf9 | |||
| 8dd90ce5e4 | |||
| f2f7421971 | |||
| 8a10beef52 | |||
| df33b43e90 | |||
| 153cba3779 | |||
| 8f110355c4 | |||
| b570f873fe | |||
| c07e26c036 | |||
| 995bc6f16a | |||
| 5b4339889f | |||
| ae963d7a3b | |||
| c426cd825f | |||
| 62c2b3f5f4 | |||
| ab3584dc23 | |||
| 3a5301af6b | |||
| 55efdef181 | |||
| e9ea1edd21 | |||
| d9b91f2122 | |||
| 15da5fb95e | |||
| d563a40d0f | |||
| a4e5630f89 | |||
| c368ad8d54 | |||
| 01d1f08597 | |||
| 8c934355ab | |||
| c6e3b52bc6 | |||
| e117caf708 | |||
| 2b4d5c026e | |||
| 93a736f1f8 | |||
| 1f8ef8e20e | |||
| bef8cdbee4 | |||
| 763391e73b | |||
| b1cd16b095 | |||
| 2ee1b3105f | |||
| 51fa652851 | |||
| 755781bca6 | |||
| 1a90729f66 | |||
| 9e520e04b2 | |||
| ded0c8398c | |||
| dc31552f9e | |||
| e0376a708c | |||
| 1becb89ff0 | |||
| 4d7365828e | |||
| 29ccb09ba6 | |||
| eadd3feba0 | |||
| 93269fe314 | |||
| 34ca4c501a | |||
| 34084d0e94 | |||
| 07fc551383 | |||
| b0eed05a1a | |||
| 8228afd725 | |||
| 301222d118 | |||
| 9b741b415a | |||
| cc8438ef66 | |||
| 179bd1f6b1 | |||
| 08b7b1870c | |||
| 2c7da1d3f8 | |||
| 2a8a2c8652 | |||
| b6b75f0743 | |||
| aca92f3889 | |||
| 4672540f82 | |||
| 261cec7ec2 | |||
| de444e8485 | |||
| f4fb92be91 | |||
| 571c928234 | |||
| 2fcc4b1ff0 | |||
| c0b0ca22aa | |||
| d862762758 | |||
| 7ca8880c3c | |||
| 21ccc55e3f | |||
| 8662353071 | |||
| faedcfa64d | |||
| 7ad1796db5 | |||
| 717ec5293b | |||
| d437e171fb | |||
| 97ae7ae0d6 | |||
| e9a8f3ee84 | |||
| 1fb237417a | |||
| cd65fa16ed | |||
| 1e5a740a52 | |||
| 42badf17eb | |||
| 2ec3c2c24f | |||
| f3ab06d3b8 | |||
| 2b78a8dcae | |||
| 389ef98c66 | |||
| 75bf0e53fc | |||
| ff4dd18c1b | |||
| 4c535289a4 | |||
| d24886c73b | |||
| 9883a2982a | |||
| 24191870e8 | |||
| b9dae8928e | |||
| 7bed880003 | |||
| e2b95ad372 | |||
| 18710bc67d | |||
| 02e8bba999 | |||
| e770ca3eef | |||
| aaa72426c3 | |||
| 53e5f1378c | |||
| 773abc6dff | |||
| 8abb311623 | |||
| 2d83fb7dc4 | |||
| ae69ca9ebd | |||
| 0cb4ec54bc | |||
| d34cff234c | |||
| 50abead104 | |||
| 3b0ed7df8b | |||
| ce925337f1 | |||
| a911f5048f | |||
| 096cbc13d8 | |||
| a2cf1cd340 | |||
| 44827ea504 | |||
| 13b549ca2c | |||
| c104122a50 | |||
| 6794b79d0e | |||
| 42200ec04a | |||
| 2944d0fa39 | |||
| 34496ced0e | |||
| fa0680a8ee | |||
| f2402cadb0 | |||
| ffe82a82fa | |||
| 6e1a1edac0 | |||
| 427e25b3c0 | |||
| fca2bf8ddb | |||
| f65c15d2e5 | |||
| 343cf84a58 | |||
| e67a94b5d7 | |||
| cc1916eba3 | |||
| 0a0ce6ad98 | |||
| fd21157c2d | |||
| 8b3697e71e | |||
| f3bebcfa8f | |||
| 4c145f1f0a | |||
| cfce4e6ece | |||
| 13d778586e | |||
| 77b85fa42b | |||
| fb89c47563 | |||
| 8ffbdfa01d | |||
| 94788454a9 | |||
| a92bd1c840 | |||
| 610e9f4e60 | |||
| 6e9dace360 | |||
| 148222e239 | |||
| 5e2279cd10 | |||
| b54026b039 | |||
| 6f3076fddb | |||
| 92c336624a | |||
| 07d4b248bf | |||
| 1534099dc4 | |||
| d483869aa6 | |||
| 8bb40e991b | |||
| 5c6989bf91 | |||
| 5b503ae802 | |||
| 5feb018e22 | |||
| 97d259cd1e | |||
| fa357cf8ce | |||
| 7a0f5e171e | |||
| 24cfb23b39 | |||
| 06b6a5d3ae | |||
| 301ba1df60 |
@@ -27,6 +27,9 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: yarn adjustPackageJson
|
||||
run: |
|
||||
yarn adjustPackageJson
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
|
||||
@@ -31,6 +31,9 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
- name: yarn adjustPackageJson
|
||||
run: |
|
||||
yarn adjustPackageJson
|
||||
- name: yarn install
|
||||
run: |
|
||||
# yarn --version
|
||||
|
||||
@@ -31,6 +31,11 @@ jobs:
|
||||
run: |
|
||||
cd packages/filterparser
|
||||
yarn test:ci
|
||||
- name: Datalib (perspective) tests
|
||||
if: always()
|
||||
run: |
|
||||
cd packages/datalib
|
||||
yarn test:ci
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
@@ -43,6 +48,12 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-file: packages/filterparser/result.json
|
||||
action-name: Filter parser test results
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-file: packages/datalib/result.json
|
||||
action-name: Datalib (perspectives) test results
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
||||
Vendored
+4
-1
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"jestrunner.jestCommand": "node_modules/.bin/cross-env DEVMODE=1 LOCALTEST=1 node_modules/.bin/jest"
|
||||
"jestrunner.jestCommand": "node_modules/.bin/cross-env DEVMODE=1 LOCALTEST=1 node_modules/.bin/jest",
|
||||
"cSpell.words": [
|
||||
"dbgate"
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,67 @@ Builds:
|
||||
- linux - application for linux
|
||||
- win - application for Windows
|
||||
|
||||
### 5.1.0
|
||||
- ADDED: Perspectives (docs: https://dbgate.org/docs/perspectives.html )
|
||||
- CHANGED: Upgraded SQLite engine version (driver better-sqlite3: 7.6.2)
|
||||
- CHANGED: Upgraded ElectronJS version (from version 13 to version 17)
|
||||
- CHANGED: Upgraded all dependencies with current available minor version updates
|
||||
- CHANGED: By deffault, connect on click #332˝
|
||||
- CHANGED: Improved keyboard navigation, when editing table data #331
|
||||
- ADDED: Option to skip Save changes dialog #329
|
||||
- FIXED: Unsigned column doesn't work correctly. #324
|
||||
- FIXED: Connect to MS SQL with doamin user now works also under Linux and Mac #305
|
||||
|
||||
### 5.0.9
|
||||
- FIXED: Fixed problem with SSE events on web version
|
||||
- ADDED: Added menu command "New query designer"
|
||||
- ADDED: Added menu command "New ER diagram"
|
||||
|
||||
### 5.0.8
|
||||
- ADDED: SQL Server - support using domain logins under Linux and Mac #305
|
||||
- ADDED: Permissions for connections #318
|
||||
- ADDED: Ability to change editor front #308
|
||||
- ADDED: Custom expression in query designer #306
|
||||
- ADDED: OR conditions in query designer #321
|
||||
- ADDED: Ability to configure settings view environment variables #304
|
||||
|
||||
### 5.0.7
|
||||
- FIXED: Fixed some problems with SSH tunnel (upgraded SSH client) #315
|
||||
- FIXED: Fixed MognoDB executing find query #312
|
||||
- ADDED: Interval filters for date/time columns #311
|
||||
- ADDED: Ability to clone rows #309
|
||||
- ADDED: connecting option Trust server certificate for SQL Server #305
|
||||
- ADDED: Autorefresh, reload table every x second #303
|
||||
- FIXED(app): Changing editor theme and font size in Editor Themes #300
|
||||
|
||||
### 5.0.6
|
||||
- ADDED: Search in columns
|
||||
- CHANGED: Upgraded mongodb driver
|
||||
- ADDED: Ability to reset view, when data load fails
|
||||
- FIXED: Filtering works for complex types (geography, xml under MSSQL)
|
||||
- FIXED: Fixed some NPM package problems
|
||||
|
||||
### 5.0.5
|
||||
- ADDED: Visualisation geographics objects on map #288
|
||||
- ADDED: Support for native SQL as default value inside yaml files #296
|
||||
- FIXED: Postgres boolean columns don't filter correctly #298
|
||||
- FIXED: Importing dbgate-api as NPM package now works correctly
|
||||
- FIXED: Handle error when reading deleted archive
|
||||
|
||||
### 5.0.3
|
||||
- CHANGED: Optimalization of loading DB structure for PostgreSQL, MySQL #273
|
||||
- CHANGED: Upgraded mysql driver #293
|
||||
- CHANGED: Better UX when defining SSH port #291
|
||||
- ADDED: Database object menu from tab
|
||||
- CHANGED: Ability to close file uploader
|
||||
- FIXED: Correct handling of NUL values in update keys
|
||||
- CHANGED: Upgraded MS SQL tedious driver
|
||||
- ADDED: Change order of pinned tables & databases #227
|
||||
- FIXED: #294 Statusbar doesn't match active tab
|
||||
- CHANGED: Improved connection worklflow, disconnecting shws confirmations, when it leads to close any tabs
|
||||
- ADDED: Configurable object actions #255
|
||||
- ADDED: Multiple sort criteria #235
|
||||
- ADDED(app): Open JSON file
|
||||
### 5.0.2
|
||||
- FIXED: Cannot use SSH Tunnel after update #291
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
const fs = require('fs');
|
||||
|
||||
function adjustFile(file) {
|
||||
const json = JSON.parse(fs.readFileSync(file, { encoding: 'utf-8' }));
|
||||
if (process.platform != 'win32') {
|
||||
delete json.optionalDependencies.msnodesqlv8;
|
||||
}
|
||||
fs.writeFileSync(file, JSON.stringify(json, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
adjustFile('packages/api/package.json');
|
||||
adjustFile('app/package.json');
|
||||
+5
-5
@@ -107,12 +107,12 @@
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"electron": "13.6.3",
|
||||
"electron-builder": "22.14.5",
|
||||
"electron-builder-notarize": "^1.4.0"
|
||||
"electron": "17.4.10",
|
||||
"electron-builder": "23.1.0",
|
||||
"electron-builder-notarize": "^1.5.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"better-sqlite3": "7.5.0",
|
||||
"msnodesqlv8": "^2.4.4"
|
||||
"better-sqlite3": "7.6.2",
|
||||
"msnodesqlv8": "^2.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
+8
-14
@@ -22,7 +22,6 @@ const configRootPath = path.join(app.getPath('userData'), 'config-root.json');
|
||||
let initialConfig = {};
|
||||
let apiLoaded = false;
|
||||
let mainModule;
|
||||
let winCounter = 0;
|
||||
|
||||
const isMac = () => os.platform() == 'darwin';
|
||||
|
||||
@@ -35,8 +34,8 @@ try {
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
const mainWindows = {};
|
||||
const mainMenus = {};
|
||||
let mainWindow;
|
||||
let mainMenu;
|
||||
let runCommandOnLoad = null;
|
||||
|
||||
log.transports.file.level = 'debug';
|
||||
@@ -135,7 +134,6 @@ ipcMain.on('update-commands', async (event, arg) => {
|
||||
}
|
||||
});
|
||||
ipcMain.on('quit-app', async (event, arg) => {
|
||||
app.quit();
|
||||
if (isMac()) {
|
||||
app.quit();
|
||||
} else {
|
||||
@@ -157,9 +155,6 @@ ipcMain.on('app-started', async (event, arg) => {
|
||||
runCommandOnLoad = null;
|
||||
}
|
||||
});
|
||||
ipcMain.on('new-window', async (event, arg) => {
|
||||
createWindow();
|
||||
});
|
||||
ipcMain.on('window-action', async (event, arg) => {
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
@@ -219,18 +214,18 @@ ipcMain.on('window-action', async (event, arg) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('showOpenDialog', async (event, { winid, options }) => {
|
||||
ipcMain.handle('showOpenDialog', async (event, options) => {
|
||||
const res = electron.dialog.showOpenDialogSync(mainWindow, options);
|
||||
return res;
|
||||
});
|
||||
ipcMain.handle('showSaveDialog', async (event, { winid, options }) => {
|
||||
ipcMain.handle('showSaveDialog', async (event, options) => {
|
||||
const res = electron.dialog.showSaveDialogSync(mainWindow, options);
|
||||
return res;
|
||||
});
|
||||
ipcMain.handle('showItemInFolder', async (event, { winid, path }) => {
|
||||
ipcMain.handle('showItemInFolder', async (event, path) => {
|
||||
electron.shell.showItemInFolder(path);
|
||||
});
|
||||
ipcMain.handle('openExternal', async (event, { winid, url }) => {
|
||||
ipcMain.handle('openExternal', async (event, url) => {
|
||||
electron.shell.openExternal(url);
|
||||
});
|
||||
|
||||
@@ -273,7 +268,6 @@ function createWindow() {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: false,
|
||||
additionalArguments: [`--winid=${++winCounter}`],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -338,7 +332,7 @@ function createWindow() {
|
||||
mainModule = main;
|
||||
apiLoaded = true;
|
||||
}
|
||||
mainModule.addElectronSender(mainWindow.webContents);
|
||||
mainModule.setElectronSender(mainWindow.webContents);
|
||||
|
||||
loadMainWindow();
|
||||
|
||||
@@ -347,8 +341,8 @@ function createWindow() {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
mainModule.removeElectronSender(mainWindow.webContents);
|
||||
mainWindow = null;
|
||||
mainModule.setElectronSender(null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ module.exports = ({ editMenu }) => [
|
||||
submenu: [
|
||||
{ command: 'new.connection', hideDisabled: true },
|
||||
{ command: 'new.sqliteDatabase', hideDisabled: true },
|
||||
{ command: 'new.window', hideDisabled: true },
|
||||
{ divider: true },
|
||||
{ command: 'new.query', hideDisabled: true },
|
||||
{ command: 'new.queryDesign', hideDisabled: true },
|
||||
{ command: 'new.diagram', hideDisabled: true },
|
||||
{ command: 'new.freetable', hideDisabled: true },
|
||||
{ command: 'new.shell', hideDisabled: true },
|
||||
{ command: 'new.jsonl', hideDisabled: true },
|
||||
|
||||
+479
-510
File diff suppressed because it is too large
Load Diff
@@ -8,4 +8,4 @@ then
|
||||
echo "$HOST_IP $HOST_DOMAIN" >> /etc/hosts
|
||||
fi
|
||||
|
||||
node bundle.js
|
||||
node bundle.js --listen-api
|
||||
|
||||
@@ -297,4 +297,33 @@ describe('Deploy database', () => {
|
||||
expect(res.rows[0].val.toString()).toEqual('5');
|
||||
})
|
||||
);
|
||||
|
||||
test.each(engines.enginesPostgre.map(engine => [engine.label, engine]))(
|
||||
'Current timestamp default value - %s',
|
||||
testWrapper(async (conn, driver, engine) => {
|
||||
await testDatabaseDeploy(conn, driver, [
|
||||
[
|
||||
{
|
||||
name: 't1.table.yaml',
|
||||
json: {
|
||||
name: 't1',
|
||||
columns: [
|
||||
{ name: 'id', type: 'int' },
|
||||
{
|
||||
name: 'val',
|
||||
type: 'timestamp',
|
||||
default: 'current_timestamp',
|
||||
},
|
||||
],
|
||||
primaryKey: ['id'],
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
await driver.query(conn, `insert into t1 (id) values (1)`);
|
||||
const res = await driver.query(conn, ` select val from t1 where id = 1`);
|
||||
expect(res.rows[0].val.toString().substring(0, 2)).toEqual('20');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: Pwd2020Db
|
||||
ports:
|
||||
- 15000:5432
|
||||
# postgres:
|
||||
# image: postgres
|
||||
# restart: always
|
||||
# environment:
|
||||
# POSTGRES_PASSWORD: Pwd2020Db
|
||||
# ports:
|
||||
# - 15000:5432
|
||||
|
||||
mariadb:
|
||||
image: mariadb
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
ports:
|
||||
- 15004:3306
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=Pwd2020Db
|
||||
# mariadb:
|
||||
# image: mariadb
|
||||
# command: --default-authentication-plugin=mysql_native_password
|
||||
# restart: always
|
||||
# ports:
|
||||
# - 15004:3306
|
||||
# environment:
|
||||
# - MYSQL_ROOT_PASSWORD=Pwd2020Db
|
||||
|
||||
# mysql:
|
||||
# image: mysql:8.0.18
|
||||
@@ -26,15 +26,15 @@ services:
|
||||
# environment:
|
||||
# - MYSQL_ROOT_PASSWORD=Pwd2020Db
|
||||
|
||||
# mssql:
|
||||
# image: mcr.microsoft.com/mssql/server
|
||||
# restart: always
|
||||
# ports:
|
||||
# - 15002:1433
|
||||
# environment:
|
||||
# - ACCEPT_EULA=Y
|
||||
# - SA_PASSWORD=Pwd2020Db
|
||||
# - MSSQL_PID=Express
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server
|
||||
restart: always
|
||||
ports:
|
||||
- 15002:1433
|
||||
environment:
|
||||
- ACCEPT_EULA=Y
|
||||
- SA_PASSWORD=Pwd2020Db
|
||||
- MSSQL_PID=Express
|
||||
|
||||
# cockroachdb:
|
||||
# image: cockroachdb/cockroach
|
||||
|
||||
@@ -135,12 +135,16 @@ const filterLocal = [
|
||||
// filter local testing
|
||||
'-MySQL',
|
||||
'-MariaDB',
|
||||
'PostgreSQL',
|
||||
'-SQL Server',
|
||||
'-PostgreSQL',
|
||||
'SQL Server',
|
||||
'-SQLite',
|
||||
'-CockroachDB',
|
||||
];
|
||||
|
||||
const enginesPostgre = engines.filter(x => x.label == 'PostgreSQL');
|
||||
|
||||
module.exports = process.env.CITEST
|
||||
? engines.filter(x => !x.skipOnCI)
|
||||
: engines.filter(x => filterLocal.find(y => x.label == y));
|
||||
|
||||
module.exports.enginesPostgre = enginesPostgre;
|
||||
|
||||
+6
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "5.0.2",
|
||||
"version": "5.1.0",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -10,6 +10,10 @@
|
||||
"scripts": {
|
||||
"start:api": "yarn workspace dbgate-api start",
|
||||
"start:app": "cd app && yarn start",
|
||||
"start:api:debug": "cross-env DEBUG=* yarn workspace dbgate-api start",
|
||||
"start:app:debug": "cd app && cross-env DEBUG=* yarn start",
|
||||
"start:api:debug:ssh": "cross-env DEBUG=ssh yarn workspace dbgate-api start",
|
||||
"start:app:debug:ssh": "cd app && cross-env DEBUG=ssh yarn start",
|
||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||
"start:api:singledb": "yarn workspace dbgate-api start:singledb",
|
||||
"start:web": "yarn workspace dbgate-web dev",
|
||||
@@ -32,6 +36,7 @@
|
||||
"start:app:local": "cd app && yarn start:local",
|
||||
"setCurrentVersion": "node setCurrentVersion",
|
||||
"generatePadFile": "node generatePadFile",
|
||||
"adjustPackageJson": "node adjustPackageJson",
|
||||
"fillNativeModules": "node fillNativeModules",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --electron",
|
||||
"fillPackagedPlugins": "node fillPackagedPlugins",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
DEVMODE=1
|
||||
SHELL_SCRIPTING=1
|
||||
|
||||
# PERMISSIONS=~widgets/app,~widgets/plugins
|
||||
# DISABLE_SHELL=1
|
||||
# HIDE_APP_EDITOR=1
|
||||
|
||||
Vendored
+11
@@ -48,4 +48,15 @@ PASSWORD_relational=relational
|
||||
ENGINE_relational=mariadb@dbgate-plugin-mysql
|
||||
READONLY_relational=1
|
||||
|
||||
# SETTINGS_dataGrid.showHintColumns=1
|
||||
|
||||
# docker run -p 3000:3000 -e CONNECTIONS=mongo -e URL_mongo=mongodb://localhost:27017 -e ENGINE_mongo=mongo@dbgate-plugin-mongo -e LABEL_mongo=mongo dbgate/dbgate:beta
|
||||
|
||||
# LOGINS=x,y
|
||||
# LOGIN_PASSWORD_x=x
|
||||
# LOGIN_PASSWORD_y=LOGIN_PASSWORD_y
|
||||
# LOGIN_PERMISSIONS_x=~*
|
||||
# LOGIN_PERMISSIONS_y=~*
|
||||
|
||||
# PERMISSIONS=~*,connections/relational
|
||||
# PERMISSIONS=~*
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"dbgate-query-splitter": "^4.9.0",
|
||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||
"dbgate-tools": "^5.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
"diff": "^5.0.0",
|
||||
"diff2html": "^3.4.13",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -45,18 +46,19 @@
|
||||
"lodash": "^4.17.21",
|
||||
"ncp": "^2.0.0",
|
||||
"node-cron": "^2.0.3",
|
||||
"node-ssh-forward": "^0.7.2",
|
||||
"on-finished": "^2.4.1",
|
||||
"portfinder": "^1.0.28",
|
||||
"simple-encryptor": "^4.0.0",
|
||||
"ssh2": "^1.11.0",
|
||||
"tar": "^6.0.5",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "env-cmd node src/index.js",
|
||||
"start:portal": "env-cmd -f env/portal/.env node src/index.js",
|
||||
"start:singledb": "env-cmd -f env/singledb/.env node src/index.js",
|
||||
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db",
|
||||
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test",
|
||||
"start": "env-cmd node src/index.js --listen-api",
|
||||
"start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api",
|
||||
"start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api",
|
||||
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api",
|
||||
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api",
|
||||
"ts": "tsc",
|
||||
"build": "webpack"
|
||||
},
|
||||
@@ -72,7 +74,7 @@
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"better-sqlite3": "7.5.0",
|
||||
"msnodesqlv8": "^2.4.4"
|
||||
"better-sqlite3": "7.6.2",
|
||||
"msnodesqlv8": "^2.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
const fs = require('fs-extra');
|
||||
const stream = require('stream');
|
||||
const readline = require('readline');
|
||||
const path = require('path');
|
||||
const { formatWithOptions } = require('util');
|
||||
const { archivedir, clearArchiveLinksCache, resolveArchiveFolder } = require('../utility/directories');
|
||||
const socket = require('../utility/socket');
|
||||
const JsonLinesDatastore = require('../utility/JsonLinesDatastore');
|
||||
const { saveFreeTableData } = require('../utility/freeTableStorage');
|
||||
const loadFilesRecursive = require('../utility/loadFilesRecursive');
|
||||
|
||||
@@ -45,29 +42,34 @@ module.exports = {
|
||||
|
||||
files_meta: true,
|
||||
async files({ folder }) {
|
||||
const dir = resolveArchiveFolder(folder);
|
||||
if (!(await fs.exists(dir))) return [];
|
||||
const files = await loadFilesRecursive(dir); // fs.readdir(dir);
|
||||
try {
|
||||
const dir = resolveArchiveFolder(folder);
|
||||
if (!(await fs.exists(dir))) return [];
|
||||
const files = await loadFilesRecursive(dir); // 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,
|
||||
}));
|
||||
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('.jsonl', 'jsonl'),
|
||||
...fileType('.table.yaml', 'table.yaml'),
|
||||
...fileType('.view.sql', 'view.sql'),
|
||||
...fileType('.proc.sql', 'proc.sql'),
|
||||
...fileType('.func.sql', 'func.sql'),
|
||||
...fileType('.trigger.sql', 'trigger.sql'),
|
||||
...fileType('.matview.sql', 'matview.sql'),
|
||||
];
|
||||
} catch (err) {
|
||||
console.log('Error reading archive files', err.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
...fileType('.jsonl', 'jsonl'),
|
||||
...fileType('.table.yaml', 'table.yaml'),
|
||||
...fileType('.view.sql', 'view.sql'),
|
||||
...fileType('.proc.sql', 'proc.sql'),
|
||||
...fileType('.func.sql', 'func.sql'),
|
||||
...fileType('.trigger.sql', 'trigger.sql'),
|
||||
...fileType('.matview.sql', 'matview.sql'),
|
||||
];
|
||||
},
|
||||
|
||||
refreshFiles_meta: true,
|
||||
|
||||
@@ -29,14 +29,14 @@ module.exports = {
|
||||
async get(_params, req) {
|
||||
const logins = getLogins();
|
||||
const login = logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null;
|
||||
const permissions = login ? login.permissions : null;
|
||||
const permissions = login ? login.permissions : process.env.PERMISSIONS;
|
||||
|
||||
return {
|
||||
runAsPortal: !!connections.portalConnections,
|
||||
singleDatabase: connections.singleDatabase,
|
||||
// hideAppEditor: !!process.env.HIDE_APP_EDITOR,
|
||||
allowShellConnection: platformInfo.allowShellConnection,
|
||||
allowShellScripting: platformInfo.allowShellConnection,
|
||||
allowShellScripting: platformInfo.allowShellScripting,
|
||||
isDocker: platformInfo.isDocker,
|
||||
permissions,
|
||||
login,
|
||||
@@ -59,13 +59,10 @@ module.exports = {
|
||||
|
||||
getSettings_meta: true,
|
||||
async getSettings() {
|
||||
try {
|
||||
return this.fillMissingSettings(
|
||||
JSON.parse(await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' }))
|
||||
);
|
||||
} catch (err) {
|
||||
return this.fillMissingSettings({});
|
||||
}
|
||||
const res = await lock.acquire('settings', async () => {
|
||||
return await this.loadSettings();
|
||||
});
|
||||
return res;
|
||||
},
|
||||
|
||||
fillMissingSettings(value) {
|
||||
@@ -76,15 +73,32 @@ module.exports = {
|
||||
// res['app.useNativeMenu'] = os.platform() == 'darwin' ? true : false;
|
||||
res['app.useNativeMenu'] = false;
|
||||
}
|
||||
for (const envVar in process.env) {
|
||||
if (envVar.startsWith('SETTINGS_')) {
|
||||
const key = envVar.substring('SETTINGS_'.length);
|
||||
if (!res[key]) {
|
||||
res[key] = process.env[envVar];
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
async loadSettings() {
|
||||
try {
|
||||
const settingsText = await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' });
|
||||
return this.fillMissingSettings(JSON.parse(settingsText));
|
||||
} catch (err) {
|
||||
return this.fillMissingSettings({});
|
||||
}
|
||||
},
|
||||
|
||||
updateSettings_meta: true,
|
||||
async updateSettings(values, req) {
|
||||
if (!hasPermission(`settings/change`, req)) return false;
|
||||
|
||||
const res = await lock.acquire('update', async () => {
|
||||
const currentValue = await this.getSettings();
|
||||
const res = await lock.acquire('settings', async () => {
|
||||
const currentValue = await this.loadSettings();
|
||||
try {
|
||||
const updated = {
|
||||
...currentValue,
|
||||
|
||||
@@ -13,6 +13,7 @@ const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { safeJsonParse } = require('dbgate-tools');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
|
||||
|
||||
function getNamedArgs() {
|
||||
const res = {};
|
||||
@@ -165,10 +166,12 @@ module.exports = {
|
||||
},
|
||||
|
||||
list_meta: true,
|
||||
async list() {
|
||||
return portalConnections && !platformInfo.allowShellConnection
|
||||
? portalConnections.map(maskConnection)
|
||||
: this.datastore.find();
|
||||
async list(_params, req) {
|
||||
if (portalConnections) {
|
||||
if (platformInfo.allowShellConnection) return portalConnections;
|
||||
return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, req));
|
||||
}
|
||||
return (await this.datastore.find()).filter(x => connectionHasPermission(x, req));
|
||||
},
|
||||
|
||||
test_meta: true,
|
||||
@@ -215,16 +218,18 @@ module.exports = {
|
||||
},
|
||||
|
||||
update_meta: true,
|
||||
async update({ _id, values }) {
|
||||
async update({ _id, values }, req) {
|
||||
if (portalConnections) return;
|
||||
testConnectionPermission(_id, req);
|
||||
const res = await this.datastore.patch(_id, values);
|
||||
socket.emitChanged('connection-list-changed');
|
||||
return res;
|
||||
},
|
||||
|
||||
updateDatabase_meta: true,
|
||||
async updateDatabase({ conid, database, values }) {
|
||||
async updateDatabase({ conid, database, values }, req) {
|
||||
if (portalConnections) return;
|
||||
testConnectionPermission(conid, req);
|
||||
const conn = await this.datastore.get(conid);
|
||||
let databases = (conn && conn.databases) || [];
|
||||
if (databases.find(x => x.name == database)) {
|
||||
@@ -240,8 +245,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
delete_meta: true,
|
||||
async delete(connection) {
|
||||
async delete(connection, req) {
|
||||
if (portalConnections) return;
|
||||
testConnectionPermission(connection, req);
|
||||
const res = await this.datastore.remove(connection._id);
|
||||
socket.emitChanged('connection-list-changed');
|
||||
return res;
|
||||
@@ -258,7 +264,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
get_meta: true,
|
||||
async get({ conid }) {
|
||||
async get({ conid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
return this.getCore({ conid, mask: true });
|
||||
},
|
||||
|
||||
|
||||
@@ -26,6 +26,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');
|
||||
|
||||
module.exports = {
|
||||
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
|
||||
@@ -130,7 +131,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
queryData_meta: true,
|
||||
async queryData({ conid, database, sql }) {
|
||||
async queryData({ conid, database, sql }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
console.log(`Processing query, conid=${conid}, database=${database}, sql=${sql}`);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
// if (opened && opened.status && opened.status.name == 'error') {
|
||||
@@ -141,14 +143,16 @@ module.exports = {
|
||||
},
|
||||
|
||||
sqlSelect_meta: true,
|
||||
async sqlSelect({ conid, database, select }) {
|
||||
async sqlSelect({ conid, database, select }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'sqlSelect', select });
|
||||
return res;
|
||||
},
|
||||
|
||||
runScript_meta: true,
|
||||
async runScript({ conid, database, sql }) {
|
||||
async runScript({ conid, database, sql }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
console.log(`Processing script, conid=${conid}, database=${database}, sql=${sql}`);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'runScript', sql });
|
||||
@@ -156,13 +160,15 @@ module.exports = {
|
||||
},
|
||||
|
||||
collectionData_meta: true,
|
||||
async collectionData({ conid, database, options }) {
|
||||
async collectionData({ conid, database, options }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'collectionData', options });
|
||||
return res.result || null;
|
||||
},
|
||||
|
||||
async loadDataCore(msgtype, { conid, database, ...args }) {
|
||||
async loadDataCore(msgtype, { conid, database, ...args }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype, ...args });
|
||||
if (res.errorMessage) {
|
||||
@@ -176,32 +182,38 @@ module.exports = {
|
||||
},
|
||||
|
||||
loadKeys_meta: true,
|
||||
async loadKeys({ conid, database, root, filter }) {
|
||||
async loadKeys({ conid, database, root, filter }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadKeys', { conid, database, root, filter });
|
||||
},
|
||||
|
||||
exportKeys_meta: true,
|
||||
async exportKeys({ conid, database, options }) {
|
||||
async exportKeys({ conid, database, options }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('exportKeys', { conid, database, options });
|
||||
},
|
||||
|
||||
loadKeyInfo_meta: true,
|
||||
async loadKeyInfo({ conid, database, key }) {
|
||||
async loadKeyInfo({ conid, database, key }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadKeyInfo', { conid, database, key });
|
||||
},
|
||||
|
||||
loadKeyTableRange_meta: true,
|
||||
async loadKeyTableRange({ conid, database, key, cursor, count }) {
|
||||
async loadKeyTableRange({ conid, database, key, cursor, count }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadKeyTableRange', { conid, database, key, cursor, count });
|
||||
},
|
||||
|
||||
loadFieldValues_meta: true,
|
||||
async loadFieldValues({ conid, database, schemaName, pureName, field, search }) {
|
||||
async loadFieldValues({ conid, database, schemaName, pureName, field, search }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('loadFieldValues', { conid, database, schemaName, pureName, field, search });
|
||||
},
|
||||
|
||||
callMethod_meta: true,
|
||||
async callMethod({ conid, database, method, args }) {
|
||||
async callMethod({ conid, database, method, args }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
return this.loadDataCore('callMethod', { conid, database, method, args });
|
||||
|
||||
// const opened = await this.ensureOpened(conid, database);
|
||||
@@ -213,7 +225,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
updateCollection_meta: true,
|
||||
async updateCollection({ conid, database, changeSet }) {
|
||||
async updateCollection({ conid, database, changeSet }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet });
|
||||
if (res.errorMessage) {
|
||||
@@ -225,7 +238,14 @@ module.exports = {
|
||||
},
|
||||
|
||||
status_meta: true,
|
||||
async status({ conid, database }) {
|
||||
async status({ conid, database }, req) {
|
||||
if (!conid) {
|
||||
return {
|
||||
name: 'error',
|
||||
message: 'No connection',
|
||||
};
|
||||
}
|
||||
testConnectionPermission(conid, req);
|
||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
if (existing) {
|
||||
return {
|
||||
@@ -247,7 +267,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
ping_meta: true,
|
||||
async ping({ conid, database }) {
|
||||
async ping({ conid, database }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
let existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||
|
||||
if (existing) {
|
||||
@@ -263,7 +284,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
refresh_meta: true,
|
||||
async refresh({ conid, database, keepOpen }) {
|
||||
async refresh({ conid, database, keepOpen }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
if (!keepOpen) this.close(conid, database);
|
||||
|
||||
await this.ensureOpened(conid, database);
|
||||
@@ -271,7 +293,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
syncModel_meta: true,
|
||||
async syncModel({ conid, database, isFullRefresh }) {
|
||||
async syncModel({ conid, database, isFullRefresh }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const conn = await this.ensureOpened(conid, database);
|
||||
conn.subprocess.send({ msgtype: 'syncModel', isFullRefresh });
|
||||
return { status: 'ok' };
|
||||
@@ -301,13 +324,15 @@ module.exports = {
|
||||
},
|
||||
|
||||
disconnect_meta: true,
|
||||
async disconnect({ conid, database }) {
|
||||
async disconnect({ conid, database }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await this.close(conid, database, true);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
structure_meta: true,
|
||||
async structure({ conid, database }) {
|
||||
async structure({ conid, database }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
if (conid == '__model') {
|
||||
const model = await importDbModel(database);
|
||||
return model;
|
||||
@@ -324,14 +349,19 @@ module.exports = {
|
||||
},
|
||||
|
||||
serverVersion_meta: true,
|
||||
async serverVersion({ conid, database }) {
|
||||
async serverVersion({ conid, database }, req) {
|
||||
if (!conid) {
|
||||
return null;
|
||||
}
|
||||
testConnectionPermission(conid, req);
|
||||
if (!conid) return null;
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
return opened.serverVersion || null;
|
||||
},
|
||||
|
||||
sqlPreview_meta: true,
|
||||
async sqlPreview({ conid, database, objects, options }) {
|
||||
async sqlPreview({ conid, database, objects, options }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
// wait for structure
|
||||
await this.structure({ conid, database });
|
||||
|
||||
@@ -341,7 +371,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
exportModel_meta: true,
|
||||
async exportModel({ conid, database }) {
|
||||
async exportModel({ conid, database }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const archiveFolder = await archive.getNewArchiveFolder({ database });
|
||||
await fs.mkdir(path.join(archivedir(), archiveFolder));
|
||||
const model = await this.structure({ conid, database });
|
||||
@@ -351,7 +382,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
generateDeploySql_meta: true,
|
||||
async generateDeploySql({ conid, database, archiveFolder }) {
|
||||
async generateDeploySql({ conid, database, archiveFolder }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid, database);
|
||||
const res = await this.sendRequest(opened, {
|
||||
msgtype: 'generateDeploySql',
|
||||
|
||||
@@ -8,6 +8,7 @@ const socket = require('../utility/socket');
|
||||
const scheduler = require('./scheduler');
|
||||
const getDiagramExport = require('../utility/getDiagramExport');
|
||||
const apps = require('./apps');
|
||||
const getMapExport = require('../utility/getMapExport');
|
||||
|
||||
function serialize(format, data) {
|
||||
if (format == 'text') return data;
|
||||
@@ -187,6 +188,12 @@ module.exports = {
|
||||
return true;
|
||||
},
|
||||
|
||||
exportMap_meta: true,
|
||||
async exportMap({ filePath, geoJson }) {
|
||||
await fs.writeFile(filePath, getMapExport(geoJson));
|
||||
return true;
|
||||
},
|
||||
|
||||
exportDiagram_meta: true,
|
||||
async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
|
||||
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
|
||||
|
||||
@@ -7,6 +7,7 @@ const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const lock = new AsyncLock();
|
||||
const config = require('./config');
|
||||
const processArgs = require('../utility/processArgs');
|
||||
const { testConnectionPermission } = require('../utility/hasPermission');
|
||||
|
||||
module.exports = {
|
||||
opened: [],
|
||||
@@ -90,19 +91,22 @@ module.exports = {
|
||||
},
|
||||
|
||||
disconnect_meta: true,
|
||||
async disconnect({ conid }) {
|
||||
async disconnect({ conid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
await this.close(conid, true);
|
||||
return { status: 'ok' };
|
||||
},
|
||||
|
||||
listDatabases_meta: true,
|
||||
async listDatabases({ conid }) {
|
||||
async listDatabases({ conid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
return opened.databases;
|
||||
},
|
||||
|
||||
version_meta: true,
|
||||
async version({ conid }) {
|
||||
async version({ conid }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
return opened.version;
|
||||
},
|
||||
@@ -132,7 +136,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
refresh_meta: true,
|
||||
async refresh({ conid, keepOpen }) {
|
||||
async refresh({ conid, keepOpen }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
if (!keepOpen) this.close(conid);
|
||||
|
||||
await this.ensureOpened(conid);
|
||||
@@ -140,7 +145,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
createDatabase_meta: true,
|
||||
async createDatabase({ conid, name }) {
|
||||
async createDatabase({ conid, name }, req) {
|
||||
testConnectionPermission(conid, req);
|
||||
const opened = await this.ensureOpened(conid);
|
||||
if (opened.connection.isReadOnly) return false;
|
||||
opened.subprocess.send({ msgtype: 'createDatabase', name });
|
||||
|
||||
@@ -8,9 +8,10 @@ if (processArgs.startProcess) {
|
||||
const proc = require('./proc');
|
||||
const module = proc[processArgs.startProcess];
|
||||
module.start();
|
||||
} else if (!processArgs.checkParent && !global['API_PACKAGE']) {
|
||||
const main = require('./main');
|
||||
}
|
||||
|
||||
if (processArgs.listenApi) {
|
||||
const main = require('./main');
|
||||
main.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ const plugins = require('./controllers/plugins');
|
||||
const files = require('./controllers/files');
|
||||
const scheduler = require('./controllers/scheduler');
|
||||
const queryHistory = require('./controllers/queryHistory');
|
||||
const onFinished = require('on-finished');
|
||||
|
||||
const { rundir } = require('./utility/directories');
|
||||
const platformInfo = require('./utility/platformInfo');
|
||||
@@ -63,7 +64,10 @@ function start() {
|
||||
|
||||
// Tell the client to retry every 10 seconds if connectivity is lost
|
||||
res.write('retry: 10000\n\n');
|
||||
socket.setSseResponse(res);
|
||||
socket.addSseResponse(res);
|
||||
onFinished(req, () => {
|
||||
socket.removeSseResponse(res);
|
||||
});
|
||||
});
|
||||
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
@@ -155,17 +159,8 @@ function useAllControllers(app, electron) {
|
||||
useController(app, electron, '/apps', apps);
|
||||
}
|
||||
|
||||
function addElectronSender(electronSender) {
|
||||
socket.addElectronSender(electronSender);
|
||||
}
|
||||
function removeElectronSender(electronSender) {
|
||||
socket.removeElectronSender(electronSender);
|
||||
function setElectronSender(electronSender) {
|
||||
socket.setElectronSender(electronSender);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
useAllControllers,
|
||||
addElectronSender,
|
||||
removeElectronSender,
|
||||
configController: config,
|
||||
};
|
||||
module.exports = { start, useAllControllers, setElectronSender, configController: config };
|
||||
|
||||
@@ -156,11 +156,11 @@ function resolveAnalysedPromises() {
|
||||
afterAnalyseCallbacks = [];
|
||||
}
|
||||
|
||||
async function handleRunScript({ msgid, sql }) {
|
||||
async function handleRunScript({ msgid, sql }, skipReadonlyCheck = false) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
ensureExecuteCustomScript(driver);
|
||||
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
|
||||
await driver.script(systemConnection, sql);
|
||||
process.send({ msgtype: 'response', msgid });
|
||||
} catch (err) {
|
||||
@@ -168,11 +168,12 @@ async function handleRunScript({ msgid, sql }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleQueryData({ msgid, sql }) {
|
||||
async function handleQueryData({ msgid, sql }, skipReadonlyCheck = false) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
try {
|
||||
ensureExecuteCustomScript(driver);
|
||||
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
|
||||
// console.log(sql);
|
||||
const res = await driver.query(systemConnection, sql);
|
||||
process.send({ msgtype: 'response', msgid, ...res });
|
||||
} catch (err) {
|
||||
@@ -184,7 +185,7 @@ async function handleSqlSelect({ msgid, select }) {
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
const dmp = driver.createDumper();
|
||||
dumpSqlSelect(dmp, select);
|
||||
return handleQueryData({ msgid, sql: dmp.s });
|
||||
return handleQueryData({ msgid, sql: dmp.s }, true);
|
||||
}
|
||||
|
||||
async function handleDriverDataCore(msgid, callMethod) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const fs = require('fs-extra');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const { SSHConnection } = require('node-ssh-forward');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const { SSHConnection } = require('../utility/SSHConnection');
|
||||
|
||||
async function getSshConnection(connection) {
|
||||
const sshConfig = {
|
||||
@@ -35,6 +35,8 @@ async function handleStart({ connection, tunnelConfig }) {
|
||||
tunnelConfig,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Error creating SSH tunnel connection:', err.message);
|
||||
|
||||
process.send({
|
||||
msgtype: 'error',
|
||||
connection,
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright 2018 Stocard GmbH.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { Client } = require('ssh2');
|
||||
const net = require('net');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const debug = require('debug');
|
||||
|
||||
// interface Options {
|
||||
// username?: string;
|
||||
// password?: string;
|
||||
// privateKey?: string | Buffer;
|
||||
// agentForward?: boolean;
|
||||
// bastionHost?: string;
|
||||
// passphrase?: string;
|
||||
// endPort?: number;
|
||||
// endHost: string;
|
||||
// agentSocket?: string;
|
||||
// skipAutoPrivateKey?: boolean;
|
||||
// noReadline?: boolean;
|
||||
// }
|
||||
|
||||
// interface ForwardingOptions {
|
||||
// fromPort: number;
|
||||
// toPort: number;
|
||||
// toHost?: string;
|
||||
// }
|
||||
|
||||
class SSHConnection {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
this.debug = debug('ssh');
|
||||
this.connections = [];
|
||||
this.isWindows = process.platform === 'win32';
|
||||
if (!options.username) {
|
||||
this.options.username = process.env['SSH_USERNAME'] || process.env['USER'];
|
||||
}
|
||||
if (!options.endPort) {
|
||||
this.options.endPort = 22;
|
||||
}
|
||||
if (!options.privateKey && !options.agentForward && !options.skipAutoPrivateKey) {
|
||||
const defaultFilePath = path.join(os.homedir(), '.ssh', 'id_rsa');
|
||||
if (fs.existsSync(defaultFilePath)) {
|
||||
this.options.privateKey = fs.readFileSync(defaultFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
this.debug('Shutdown connections');
|
||||
for (const connection of this.connections) {
|
||||
connection.removeAllListeners();
|
||||
connection.end();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
if (this.server) {
|
||||
this.server.close(resolve);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async tty() {
|
||||
const connection = await this.establish();
|
||||
this.debug('Opening tty');
|
||||
await this.shell(connection);
|
||||
}
|
||||
|
||||
async executeCommand(command) {
|
||||
const connection = await this.establish();
|
||||
this.debug('Executing command "%s"', command);
|
||||
await this.shell(connection, command);
|
||||
}
|
||||
|
||||
async shell(connection, command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.shell((err, stream) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
stream
|
||||
.on('close', async () => {
|
||||
stream.end();
|
||||
process.stdin.unpipe(stream);
|
||||
process.stdin.destroy();
|
||||
connection.end();
|
||||
await this.shutdown();
|
||||
return resolve();
|
||||
})
|
||||
.stderr.on('data', data => {
|
||||
return reject(data);
|
||||
});
|
||||
stream.pipe(process.stdout);
|
||||
|
||||
if (command) {
|
||||
stream.end(`${command}\nexit\n`);
|
||||
} else {
|
||||
process.stdin.pipe(stream);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async establish() {
|
||||
let connection;
|
||||
if (this.options.bastionHost) {
|
||||
connection = await this.connectViaBastion(this.options.bastionHost);
|
||||
} else {
|
||||
connection = await this.connect(this.options.endHost);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
async connectViaBastion(bastionHost) {
|
||||
this.debug('Connecting to bastion host "%s"', bastionHost);
|
||||
const connectionToBastion = await this.connect(bastionHost);
|
||||
return new Promise((resolve, reject) => {
|
||||
connectionToBastion.forwardOut(
|
||||
'127.0.0.1',
|
||||
22,
|
||||
this.options.endHost,
|
||||
this.options.endPort || 22,
|
||||
async (err, stream) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const connection = await this.connect(this.options.endHost, stream);
|
||||
return resolve(connection);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async connect(host, stream) {
|
||||
this.debug('Connecting to "%s"', host);
|
||||
const connection = new Client();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const options = {
|
||||
host,
|
||||
port: this.options.endPort,
|
||||
username: this.options.username,
|
||||
password: this.options.password,
|
||||
privateKey: this.options.privateKey,
|
||||
};
|
||||
if (this.options.agentForward) {
|
||||
options['agentForward'] = true;
|
||||
|
||||
// see https://github.com/mscdex/ssh2#client for agents on Windows
|
||||
// guaranteed to give the ssh agent sock if the agent is running (posix)
|
||||
let agentDefault = process.env['SSH_AUTH_SOCK'];
|
||||
if (this.isWindows) {
|
||||
// null or undefined
|
||||
if (agentDefault == null) {
|
||||
agentDefault = 'pageant';
|
||||
}
|
||||
}
|
||||
|
||||
const agentSock = this.options.agentSocket ? this.options.agentSocket : agentDefault;
|
||||
if (agentSock == null) {
|
||||
throw new Error('SSH Agent Socket is not provided, or is not set in the SSH_AUTH_SOCK env variable');
|
||||
}
|
||||
options['agent'] = agentSock;
|
||||
}
|
||||
if (stream) {
|
||||
options['sock'] = stream;
|
||||
}
|
||||
// PPK private keys can be encrypted, but won't contain the word 'encrypted'
|
||||
// in fact they always contain a `encryption` header, so we can't do a simple check
|
||||
options['passphrase'] = this.options.passphrase;
|
||||
const looksEncrypted = this.options.privateKey
|
||||
? this.options.privateKey.toString().toLowerCase().includes('encrypted')
|
||||
: false;
|
||||
if (looksEncrypted && !options['passphrase'] && !this.options.noReadline) {
|
||||
// options['passphrase'] = await this.getPassphrase();
|
||||
}
|
||||
connection.on('ready', () => {
|
||||
this.connections.push(connection);
|
||||
return resolve(connection);
|
||||
});
|
||||
|
||||
connection.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
try {
|
||||
connection.connect(options);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// private async getPassphrase() {
|
||||
// return new Promise(resolve => {
|
||||
// const rl = readline.createInterface({
|
||||
// input: process.stdin,
|
||||
// output: process.stdout,
|
||||
// });
|
||||
// rl.question('Please type in the passphrase for your private key: ', answer => {
|
||||
// return resolve(answer);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
async forward(options) {
|
||||
const connection = await this.establish();
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = net
|
||||
.createServer(socket => {
|
||||
this.debug(
|
||||
'Forwarding connection from "localhost:%d" to "%s:%d"',
|
||||
options.fromPort,
|
||||
options.toHost,
|
||||
options.toPort
|
||||
);
|
||||
connection.forwardOut(
|
||||
'localhost',
|
||||
options.fromPort,
|
||||
options.toHost || 'localhost',
|
||||
options.toPort,
|
||||
(error, stream) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
socket.pipe(stream);
|
||||
stream.pipe(socket);
|
||||
}
|
||||
);
|
||||
})
|
||||
.listen(options.fromPort, 'localhost', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SSHConnection };
|
||||
@@ -1,8 +1,5 @@
|
||||
const { SSHConnection } = require('node-ssh-forward');
|
||||
const portfinder = require('portfinder');
|
||||
const fs = require('fs-extra');
|
||||
const { decryptConnection } = require('./crypting');
|
||||
const { getSshTunnel } = require('./sshTunnel');
|
||||
const { getSshTunnelProxy } = require('./sshTunnelProxy');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const connections = require('../controllers/connections');
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
const getMapExport = (geoJson) => {
|
||||
return `<html>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css"
|
||||
integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
|
||||
crossorigin=""/>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"
|
||||
integrity="sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ=="
|
||||
crossorigin=""></script>
|
||||
|
||||
<script>
|
||||
function createMap() {
|
||||
map = leaflet.map('map').setView([50, 15], 13);
|
||||
|
||||
leaflet
|
||||
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '<a href="https://dbgate.org" title="Exported from DbGate">DbGate</a> | © OpenStreetMap',
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
const geoJsonObj = leaflet
|
||||
.geoJSON(${JSON.stringify(geoJson)}, {
|
||||
style: function () {
|
||||
return {
|
||||
weight: 2,
|
||||
fillColor: '#ff7800',
|
||||
color: '#ff7800',
|
||||
opacity: 0.8,
|
||||
fillOpacity: 0.4,
|
||||
};
|
||||
},
|
||||
pointToLayer: (feature, latlng) => {
|
||||
return leaflet.circleMarker(latlng, {
|
||||
radius: 7,
|
||||
weight: 2,
|
||||
fillColor: '#ff0000',
|
||||
color: '#ff0000',
|
||||
opacity: 0.9,
|
||||
fillOpacity: 0.9,
|
||||
});
|
||||
},
|
||||
onEachFeature: (feature, layer) => {
|
||||
// does this feature have a property named popupContent?
|
||||
if (feature.properties && feature.properties.popupContent) {
|
||||
layer.bindPopup(feature.properties.popupContent);
|
||||
layer.bindTooltip(feature.properties.popupContent);
|
||||
}
|
||||
},
|
||||
})
|
||||
.addTo(map);
|
||||
map.fitBounds(geoJsonObj.getBounds());
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#map {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body onload='createMap()'>
|
||||
<div id='map'></div>
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
};
|
||||
|
||||
module.exports = getMapExport;
|
||||
@@ -4,12 +4,21 @@ const _ = require('lodash');
|
||||
const userPermissions = {};
|
||||
|
||||
function hasPermission(tested, req) {
|
||||
if (!req) {
|
||||
// request object not available, allow all
|
||||
return true;
|
||||
}
|
||||
const { user } = (req && req.auth) || {};
|
||||
const key = user || '';
|
||||
const logins = getLogins();
|
||||
if (!userPermissions[key] && logins) {
|
||||
const login = logins.find(x => x.login == user);
|
||||
userPermissions[key] = compilePermissions(login ? login.permissions : null);
|
||||
|
||||
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]);
|
||||
}
|
||||
@@ -50,7 +59,26 @@ function getLogins() {
|
||||
return loginsCache;
|
||||
}
|
||||
|
||||
function connectionHasPermission(connection, req) {
|
||||
if (!connection) {
|
||||
return true;
|
||||
}
|
||||
if (_.isString(connection)) {
|
||||
return hasPermission(`connections/${connection}`, req);
|
||||
} else {
|
||||
return hasPermission(`connections/${connection._id}`, req);
|
||||
}
|
||||
}
|
||||
|
||||
function testConnectionPermission(connection, req) {
|
||||
if (!connectionHasPermission(connection, req)) {
|
||||
throw new Error('Connection permission not granted');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hasPermission,
|
||||
getLogins,
|
||||
connectionHasPermission,
|
||||
testConnectionPermission,
|
||||
};
|
||||
|
||||
@@ -39,8 +39,8 @@ const platformInfo = {
|
||||
environment: process.env.NODE_ENV,
|
||||
platform,
|
||||
runningInWebpack: !!process.env.WEBPACK_DEV_SERVER_URL,
|
||||
allowShellConnection: !!process.env.SHELL_CONNECTION || !!isElectron(),
|
||||
allowShellScripting: !!process.env.SHELL_SCRIPTING || !!isElectron(),
|
||||
allowShellConnection: !processArgs.listenApiChild || !!process.env.SHELL_CONNECTION || !!isElectron(),
|
||||
allowShellScripting: !processArgs.listenApiChild || !!process.env.SHELL_SCRIPTING || !!isElectron(),
|
||||
defaultKeyfile: path.join(os.homedir(), '.ssh/id_rsa'),
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ const startProcess = getNamedArg('--start-process');
|
||||
const isForkedApi = process.argv.includes('--is-forked-api');
|
||||
const pluginsDir = getNamedArg('--plugins-dir');
|
||||
const workspaceDir = getNamedArg('--workspace-dir');
|
||||
const listenApi = process.argv.includes('--listen-api');
|
||||
const listenApiChild = process.argv.includes('--listen-api-child') || listenApi;
|
||||
|
||||
function getPassArgs() {
|
||||
const res = [];
|
||||
@@ -20,6 +22,9 @@ function getPassArgs() {
|
||||
if (global['PLUGINS_DIR']) {
|
||||
res.push('--plugins-dir', global['PLUGINS_DIR']);
|
||||
}
|
||||
if (listenApiChild) {
|
||||
res.push('listen-api-child');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -30,4 +35,6 @@ module.exports = {
|
||||
getPassArgs,
|
||||
pluginsDir,
|
||||
workspaceDir,
|
||||
listenApi,
|
||||
listenApiChild,
|
||||
};
|
||||
|
||||
@@ -16,7 +16,9 @@ function requireEngineDriver(connection) {
|
||||
if (engine.includes('@')) {
|
||||
const [shortName, packageName] = engine.split('@');
|
||||
const plugin = requirePlugin(packageName);
|
||||
return plugin.drivers.find(x => x.engine == engine);
|
||||
if (plugin.drivers) {
|
||||
return plugin.drivers.find(x => x.engine == engine);
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find engine driver ${engine}`);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,33 @@
|
||||
let sseResponse = null;
|
||||
let electronSenders = [];
|
||||
let init = [];
|
||||
const _ = require('lodash');
|
||||
|
||||
const sseResponses = [];
|
||||
let electronSender = null;
|
||||
let pingConfigured = false;
|
||||
|
||||
module.exports = {
|
||||
setSseResponse(value) {
|
||||
sseResponse = value;
|
||||
setInterval(() => this.emit('ping'), 29 * 1000);
|
||||
ensurePing() {
|
||||
if (!pingConfigured) {
|
||||
setInterval(() => this.emit('ping'), 29 * 1000);
|
||||
pingConfigured = true;
|
||||
}
|
||||
},
|
||||
addElectronSender(value) {
|
||||
electronSenders = [...electronSenders, value];
|
||||
addSseResponse(value) {
|
||||
sseResponses.push(value);
|
||||
this.ensurePing();
|
||||
},
|
||||
removeElectronSender(value) {
|
||||
electronSenders = electronSenders.filter(x => x != value);
|
||||
removeSseResponse(value) {
|
||||
_.remove(sseResponses, x => x == value);
|
||||
},
|
||||
setElectronSender(value) {
|
||||
electronSender = value;
|
||||
this.ensurePing();
|
||||
},
|
||||
emit(message, data) {
|
||||
if (electronSenders.length > 0) {
|
||||
if (init.length > 0) {
|
||||
for (const item of init) {
|
||||
for (const sender of electronSenders) {
|
||||
sender.send(item.message, item.data == null ? null : item.data);
|
||||
}
|
||||
}
|
||||
init = [];
|
||||
}
|
||||
for (const sender of electronSenders) {
|
||||
sender.send(message, data == null ? null : data);
|
||||
}
|
||||
} else if (sseResponse) {
|
||||
if (init.length > 0) {
|
||||
for (const item of init) {
|
||||
sseResponse.write(
|
||||
`event: ${item.message}\ndata: ${JSON.stringify(item.data == null ? null : item.data)}\n\n`
|
||||
);
|
||||
}
|
||||
init = [];
|
||||
}
|
||||
sseResponse.write(`event: ${message}\ndata: ${JSON.stringify(data == null ? null : data)}\n\n`);
|
||||
} else {
|
||||
init.push([{ message, data }]);
|
||||
if (electronSender) {
|
||||
electronSender.send(message, data == null ? null : data);
|
||||
}
|
||||
for (const res of sseResponses) {
|
||||
res.write(`event: ${message}\ndata: ${JSON.stringify(data == null ? null : data)}\n\n`);
|
||||
}
|
||||
},
|
||||
emitChanged(key) {
|
||||
|
||||
@@ -47,7 +47,6 @@ module.exports = function useController(app, electron, route, controller) {
|
||||
|
||||
let method = 'post';
|
||||
let raw = false;
|
||||
let rawParams = false;
|
||||
|
||||
// if (_.isString(meta)) {
|
||||
// method = meta;
|
||||
@@ -55,7 +54,6 @@ module.exports = function useController(app, electron, route, controller) {
|
||||
if (_.isPlainObject(meta)) {
|
||||
method = meta.method;
|
||||
raw = meta.raw;
|
||||
rawParams = meta.rawParams;
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
@@ -67,9 +65,7 @@ module.exports = function useController(app, electron, route, controller) {
|
||||
// controller._init_called = true;
|
||||
// }
|
||||
try {
|
||||
let params = [{ ...req.body, ...req.query }, req];
|
||||
if (rawParams) params = [req, res];
|
||||
const data = await controller[key](...params);
|
||||
const data = await controller[key]({ ...req.body, ...req.query }, req);
|
||||
res.json(data);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
moduleFileExtensions: ['js'],
|
||||
};
|
||||
@@ -5,6 +5,8 @@
|
||||
"typings": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
"test:ci": "jest --json --outputFile=result.json --testLocationInResults",
|
||||
"start": "tsc --watch"
|
||||
},
|
||||
"files": [
|
||||
@@ -12,11 +14,14 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||
"dbgate-tools": "^5.0.0-alpha.1",
|
||||
"dbgate-filterparser": "^5.0.0-alpha.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^5.0.0-alpha.1",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^28.1.3",
|
||||
"ts-jest": "^28.0.7",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
import _ from 'lodash';
|
||||
import { Command, Insert, Update, Delete, UpdateField, Condition, AllowIdentityInsert } from 'dbgate-sqltree';
|
||||
import {
|
||||
Command,
|
||||
Insert,
|
||||
Update,
|
||||
Delete,
|
||||
UpdateField,
|
||||
Condition,
|
||||
AllowIdentityInsert,
|
||||
Expression,
|
||||
} from 'dbgate-sqltree';
|
||||
import { NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
|
||||
export interface ChangeSetItem {
|
||||
@@ -262,27 +271,39 @@ function changeSetInsertToSql(
|
||||
}
|
||||
|
||||
export function extractChangeSetCondition(item: ChangeSetItem, alias?: string): Condition {
|
||||
function getColumnCondition(columnName: string): Condition {
|
||||
const value = item.condition[columnName];
|
||||
const expr: Expression = {
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source: {
|
||||
name: {
|
||||
pureName: item.pureName,
|
||||
schemaName: item.schemaName,
|
||||
},
|
||||
alias,
|
||||
},
|
||||
};
|
||||
if (value == null) {
|
||||
return {
|
||||
conditionType: 'isNull',
|
||||
expr,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: expr,
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: _.keys(item.condition).map(columnName => ({
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source: {
|
||||
name: {
|
||||
pureName: item.pureName,
|
||||
schemaName: item.schemaName,
|
||||
},
|
||||
alias,
|
||||
},
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: item.condition[columnName],
|
||||
},
|
||||
})),
|
||||
conditions: _.keys(item.condition).map(columnName => getColumnCondition(columnName)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig';
|
||||
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc, createGridConfig } from './GridConfig';
|
||||
import {
|
||||
ForeignKeyInfo,
|
||||
TableInfo,
|
||||
@@ -24,7 +24,7 @@ export interface DisplayColumn {
|
||||
headerText: string;
|
||||
uniqueName: string;
|
||||
uniquePath: string[];
|
||||
notNull: boolean;
|
||||
notNull?: boolean;
|
||||
autoIncrement?: boolean;
|
||||
isPrimaryKey?: boolean;
|
||||
foreignKey?: ForeignKeyInfo;
|
||||
@@ -194,12 +194,14 @@ export abstract class GridDisplay {
|
||||
if (condition) {
|
||||
conditions.push(
|
||||
_.cloneDeepWith(condition, (expr: Expression) => {
|
||||
if (expr.exprType == 'placeholder')
|
||||
return {
|
||||
exprType: 'column',
|
||||
columnName: column.columnName,
|
||||
source: { alias: column.sourceAlias },
|
||||
};
|
||||
if (expr.exprType == 'placeholder') {
|
||||
return this.createColumnExpression(column, { alias: column.sourceAlias });
|
||||
}
|
||||
// return {
|
||||
// exprType: 'column',
|
||||
// columnName: column.columnName,
|
||||
// source: { alias: column.sourceAlias },
|
||||
// };
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -372,6 +374,22 @@ export abstract class GridDisplay {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
addToSort(uniqueName, order) {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
sort: [...(cfg.sort || []), { uniqueName, order }],
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
|
||||
clearSort() {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
sort: [],
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
|
||||
setGrouping(uniqueName, groupFunc: GroupFunc) {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
@@ -408,6 +426,15 @@ export abstract class GridDisplay {
|
||||
return this.config.sort.find(x => x.uniqueName == uniqueName)?.order;
|
||||
}
|
||||
|
||||
getSortOrderIndex(uniqueName) {
|
||||
if (this.config.sort.length <= 1) return -1;
|
||||
return _.findIndex(this.config.sort, x => x.uniqueName == uniqueName);
|
||||
}
|
||||
|
||||
isSortDefined() {
|
||||
return (this.config.sort || []).length > 0;
|
||||
}
|
||||
|
||||
get filterCount() {
|
||||
return _.compact(_.values(this.config.filters)).length;
|
||||
}
|
||||
@@ -420,6 +447,11 @@ export abstract class GridDisplay {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
resetConfig() {
|
||||
this.setConfig(cfg => createGridConfig());
|
||||
this.reload();
|
||||
}
|
||||
|
||||
getChangeSetCondition(row) {
|
||||
if (!this.changeSetKeyFields) return null;
|
||||
return _.pick(row, this.changeSetKeyFields);
|
||||
@@ -458,6 +490,22 @@ export abstract class GridDisplay {
|
||||
|
||||
processReferences(select: Select, displayedColumnInfo: DisplayedColumnInfo, options) {}
|
||||
|
||||
createColumnExpression(col, source, alias?) {
|
||||
let expr = null;
|
||||
if (this.dialect.createColumnViewExpression) {
|
||||
expr = this.dialect.createColumnViewExpression(col.columnName, col.dataType, source, alias);
|
||||
if (expr) {
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
return {
|
||||
exprType: 'column',
|
||||
alias: alias || col.columnName,
|
||||
source,
|
||||
...col,
|
||||
};
|
||||
}
|
||||
|
||||
createSelectBase(name: NamedObjectInfo, columns: ColumnInfo[], options) {
|
||||
if (!columns) return null;
|
||||
const orderColumnName = columns[0].columnName;
|
||||
@@ -467,12 +515,7 @@ export abstract class GridDisplay {
|
||||
name: _.pick(name, ['schemaName', 'pureName']),
|
||||
alias: 'basetbl',
|
||||
},
|
||||
columns: columns.map(col => ({
|
||||
exprType: 'column',
|
||||
alias: col.columnName,
|
||||
source: { alias: 'basetbl' },
|
||||
...col,
|
||||
})),
|
||||
columns: columns.map(col => this.createColumnExpression(col, { alias: 'basetbl' })),
|
||||
orderBy: [
|
||||
{
|
||||
exprType: 'column',
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { RangeDefinition } from 'dbgate-types';
|
||||
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
|
||||
import _pick from 'lodash/pick';
|
||||
import _zip from 'lodash/zip';
|
||||
import _difference from 'lodash/difference';
|
||||
import debug from 'debug';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
|
||||
const dbg = debug('dbgate:PerspectiveCache');
|
||||
|
||||
export class PerspectiveBindingGroup {
|
||||
constructor(public table: PerspectiveCacheTable) {}
|
||||
|
||||
groupSize?: number;
|
||||
loadedAll: boolean;
|
||||
loadedRows: any[] = [];
|
||||
bindingValues: any[];
|
||||
|
||||
matchRow(row) {
|
||||
return this.table.bindingColumns.every((column, index) => row[column] == this.bindingValues[index]);
|
||||
}
|
||||
}
|
||||
|
||||
export class PerspectiveCacheTable {
|
||||
constructor(props: PerspectiveDataLoadProps, public cache: PerspectiveCache) {
|
||||
this.schemaName = props.schemaName;
|
||||
this.pureName = props.pureName;
|
||||
this.bindingColumns = props.bindingColumns;
|
||||
this.dataColumns = props.dataColumns;
|
||||
this.loadedAll = false;
|
||||
}
|
||||
|
||||
schemaName: string;
|
||||
pureName: string;
|
||||
bindingColumns?: string[];
|
||||
dataColumns: string[];
|
||||
loadedAll: boolean;
|
||||
loadedRows: any[] = [];
|
||||
bindingGroups: { [bindingKey: string]: PerspectiveBindingGroup } = {};
|
||||
|
||||
get loadedCount() {
|
||||
return this.loadedRows.length;
|
||||
}
|
||||
|
||||
getRowsResult(props: PerspectiveDataLoadProps): { rows: any[]; incomplete: boolean } {
|
||||
return {
|
||||
rows: this.loadedRows.slice(0, props.topCount),
|
||||
incomplete: props.topCount < this.loadedCount || !this.loadedAll,
|
||||
};
|
||||
}
|
||||
|
||||
getBindingGroup(groupValues: any[]) {
|
||||
const key = stableStringify(groupValues);
|
||||
return this.bindingGroups[key];
|
||||
}
|
||||
|
||||
getUncachedBindingGroups(props: PerspectiveDataLoadProps): any[][] {
|
||||
const uncached = [];
|
||||
for (const group of props.bindingValues) {
|
||||
const key = stableStringify(group);
|
||||
const item = this.bindingGroups[key];
|
||||
if (!item) {
|
||||
uncached.push(group);
|
||||
}
|
||||
}
|
||||
return uncached;
|
||||
}
|
||||
|
||||
storeGroupSize(props: PerspectiveDataLoadProps, bindingValues: any[], count: number) {
|
||||
const originalBindingValue = props.bindingValues.find(v => _zip(v, bindingValues).every(([x, y]) => x == y));
|
||||
if (originalBindingValue) {
|
||||
const key = stableStringify(originalBindingValue);
|
||||
// console.log('SET SIZE', originalBindingValue, bindingValues, key, count);
|
||||
const group = new PerspectiveBindingGroup(this);
|
||||
group.bindingValues = bindingValues;
|
||||
group.groupSize = count;
|
||||
this.bindingGroups[key] = group;
|
||||
} else {
|
||||
dbg('Group not found', bindingValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PerspectiveCache {
|
||||
constructor() {}
|
||||
|
||||
tables: { [tableKey: string]: PerspectiveCacheTable } = {};
|
||||
|
||||
getTableCache(props: PerspectiveDataLoadProps) {
|
||||
const tableKey = stableStringify(
|
||||
_pick(props, ['schemaName', 'pureName', 'bindingColumns', 'databaseConfig', 'orderBy', 'condition'])
|
||||
);
|
||||
let res = this.tables[tableKey];
|
||||
|
||||
if (res && _difference(props.dataColumns, res.dataColumns).length > 0) {
|
||||
dbg('Delete cache because incomplete columns', props.pureName, res.dataColumns);
|
||||
|
||||
// we have incomplete cache
|
||||
delete this.tables[tableKey];
|
||||
res = null;
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
res = new PerspectiveCacheTable(props, this);
|
||||
this.tables[tableKey] = res;
|
||||
return res;
|
||||
}
|
||||
|
||||
// cache could be used
|
||||
return res;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.tables = {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { DatabaseInfo, ForeignKeyInfo, NamedObjectInfo } from 'dbgate-types';
|
||||
|
||||
export interface PerspectiveConfigColumns {
|
||||
expandedColumns: string[];
|
||||
checkedColumns: string[];
|
||||
uncheckedColumns: string[];
|
||||
}
|
||||
|
||||
export interface PerspectiveCustomJoinConfig {
|
||||
joinid: string;
|
||||
joinName: string;
|
||||
baseUniqueName: string;
|
||||
conid?: string;
|
||||
database?: string;
|
||||
refSchemaName?: string;
|
||||
refTableName: string;
|
||||
columns: {
|
||||
baseColumnName: string;
|
||||
refColumnName: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface PerspectiveFilterColumnInfo {
|
||||
columnName: string;
|
||||
filterType: string;
|
||||
pureName: string;
|
||||
schemaName: string;
|
||||
foreignKey: ForeignKeyInfo;
|
||||
}
|
||||
|
||||
export interface PerspectiveParentFilterConfig {
|
||||
uniqueName: string;
|
||||
}
|
||||
export interface PerspectiveConfig extends PerspectiveConfigColumns {
|
||||
rootObject: { schemaName?: string; pureName: string };
|
||||
filters: { [uniqueName: string]: string };
|
||||
sort: {
|
||||
[parentUniqueName: string]: {
|
||||
uniqueName: string;
|
||||
order: 'ASC' | 'DESC';
|
||||
}[];
|
||||
};
|
||||
customJoins: PerspectiveCustomJoinConfig[];
|
||||
parentFilters: PerspectiveParentFilterConfig[];
|
||||
}
|
||||
|
||||
export function createPerspectiveConfig(rootObject: { schemaName?: string; pureName: string }): PerspectiveConfig {
|
||||
return {
|
||||
expandedColumns: [],
|
||||
checkedColumns: [],
|
||||
uncheckedColumns: [],
|
||||
customJoins: [],
|
||||
filters: {},
|
||||
sort: {},
|
||||
rootObject,
|
||||
parentFilters: [],
|
||||
};
|
||||
}
|
||||
|
||||
export type ChangePerspectiveConfigFunc = (
|
||||
changeFunc: (config: PerspectiveConfig) => PerspectiveConfig,
|
||||
reload?: boolean
|
||||
) => void;
|
||||
|
||||
export function extractPerspectiveDatabases(
|
||||
{ conid, database },
|
||||
cfg: PerspectiveConfig
|
||||
): { conid: string; database: string }[] {
|
||||
const res: { conid: string; database: string }[] = [];
|
||||
res.push({ conid, database });
|
||||
|
||||
function add(conid, database) {
|
||||
if (res.find(x => x.conid == conid && x.database == database)) return;
|
||||
res.push({ conid, database });
|
||||
}
|
||||
|
||||
for (const custom of cfg.customJoins) {
|
||||
add(custom.conid || conid, custom.database || database);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export interface MultipleDatabaseInfo {
|
||||
[conid: string]: {
|
||||
[database: string]: DatabaseInfo;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Condition, Expression, Select } from 'dbgate-sqltree';
|
||||
import { PerspectiveDataLoadProps } from './PerspectiveDataProvider';
|
||||
import debug from 'debug';
|
||||
|
||||
const dbg = debug('dbgate:PerspectiveDataLoader');
|
||||
|
||||
export class PerspectiveDataLoader {
|
||||
constructor(public apiCall) {}
|
||||
|
||||
buildCondition(props: PerspectiveDataLoadProps): Condition {
|
||||
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props;
|
||||
|
||||
const conditions = [];
|
||||
|
||||
if (condition) {
|
||||
conditions.push(condition);
|
||||
}
|
||||
|
||||
if (bindingColumns?.length == 1) {
|
||||
conditions.push({
|
||||
conditionType: 'in',
|
||||
expr: {
|
||||
exprType: 'column',
|
||||
columnName: bindingColumns[0],
|
||||
source: {
|
||||
name: { schemaName, pureName },
|
||||
},
|
||||
},
|
||||
values: bindingValues.map(x => x[0]),
|
||||
});
|
||||
}
|
||||
|
||||
return conditions.length > 0
|
||||
? {
|
||||
conditionType: 'and',
|
||||
conditions,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
async loadGrouping(props: PerspectiveDataLoadProps) {
|
||||
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns } = props;
|
||||
|
||||
const bindingColumnExpressions = bindingColumns.map(
|
||||
columnName =>
|
||||
({
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source: {
|
||||
name: { schemaName, pureName },
|
||||
},
|
||||
} as Expression)
|
||||
);
|
||||
|
||||
const select: Select = {
|
||||
commandType: 'select',
|
||||
from: {
|
||||
name: { schemaName, pureName },
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
exprType: 'call',
|
||||
func: 'COUNT',
|
||||
args: [
|
||||
{
|
||||
exprType: 'raw',
|
||||
sql: '*',
|
||||
},
|
||||
],
|
||||
alias: '_perspective_group_size_',
|
||||
},
|
||||
...bindingColumnExpressions,
|
||||
],
|
||||
where: this.buildCondition(props),
|
||||
};
|
||||
|
||||
select.groupBy = bindingColumnExpressions;
|
||||
|
||||
if (dbg?.enabled) {
|
||||
dbg(`LOAD COUNTS, table=${props.pureName}, columns=${props.dataColumns?.join(',')}`);
|
||||
}
|
||||
|
||||
const response = await this.apiCall('database-connections/sql-select', {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
select,
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows.map(row => ({
|
||||
...row,
|
||||
_perspective_group_size_: parseInt(row._perspective_group_size_),
|
||||
}));
|
||||
}
|
||||
|
||||
async loadData(props: PerspectiveDataLoadProps) {
|
||||
const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props;
|
||||
|
||||
const select: Select = {
|
||||
commandType: 'select',
|
||||
from: {
|
||||
name: { schemaName, pureName },
|
||||
},
|
||||
columns: dataColumns?.map(columnName => ({
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source: {
|
||||
name: { schemaName, pureName },
|
||||
},
|
||||
})),
|
||||
selectAll: !dataColumns,
|
||||
orderBy: orderBy?.map(({ columnName, order }) => ({
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
direction: order,
|
||||
source: {
|
||||
name: { schemaName, pureName },
|
||||
},
|
||||
})),
|
||||
range: props.range,
|
||||
where: this.buildCondition(props),
|
||||
};
|
||||
|
||||
if (dbg?.enabled) {
|
||||
dbg(
|
||||
`LOAD DATA, table=${props.pureName}, columns=${props.dataColumns?.join(',')}, range=${props.range?.offset},${
|
||||
props.range?.limit
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const response = await this.apiCall('database-connections/sql-select', {
|
||||
conid: props.databaseConfig.conid,
|
||||
database: props.databaseConfig.database,
|
||||
select,
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import debug from 'debug';
|
||||
import { Condition } from 'dbgate-sqltree';
|
||||
import { RangeDefinition } from 'dbgate-types';
|
||||
import { format } from 'path';
|
||||
import { PerspectiveBindingGroup, PerspectiveCache } from './PerspectiveCache';
|
||||
import { PerspectiveDataLoader } from './PerspectiveDataLoader';
|
||||
|
||||
export const PERSPECTIVE_PAGE_SIZE = 100;
|
||||
|
||||
const dbg = debug('dbgate:PerspectiveDataProvider');
|
||||
|
||||
export interface PerspectiveDatabaseConfig {
|
||||
conid: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
export interface PerspectiveDataLoadProps {
|
||||
databaseConfig: PerspectiveDatabaseConfig;
|
||||
schemaName: string;
|
||||
pureName: string;
|
||||
dataColumns: string[];
|
||||
orderBy: {
|
||||
columnName: string;
|
||||
order: 'ASC' | 'DESC';
|
||||
}[];
|
||||
bindingColumns?: string[];
|
||||
bindingValues?: any[][];
|
||||
range?: RangeDefinition;
|
||||
topCount?: number;
|
||||
condition?: Condition;
|
||||
}
|
||||
|
||||
export class PerspectiveDataProvider {
|
||||
constructor(public cache: PerspectiveCache, public loader: PerspectiveDataLoader) {}
|
||||
async loadData(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
|
||||
dbg('load data', props);
|
||||
// console.log('LOAD DATA', props);
|
||||
if (props.bindingColumns) {
|
||||
return this.loadDataNested(props);
|
||||
} else {
|
||||
return this.loadDataFlat(props);
|
||||
}
|
||||
}
|
||||
|
||||
async loadDataNested(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
|
||||
const tableCache = this.cache.getTableCache(props);
|
||||
|
||||
const uncached = tableCache.getUncachedBindingGroups(props);
|
||||
if (uncached.length > 0) {
|
||||
const counts = await this.loader.loadGrouping({
|
||||
...props,
|
||||
bindingValues: uncached,
|
||||
});
|
||||
// console.log('COUNTS', counts);
|
||||
for (const resetItem of uncached) {
|
||||
tableCache.storeGroupSize(props, resetItem, 0);
|
||||
}
|
||||
for (const countItem of counts) {
|
||||
const { _perspective_group_size_, ...fields } = countItem;
|
||||
tableCache.storeGroupSize(
|
||||
props,
|
||||
props.bindingColumns.map(col => fields[col]),
|
||||
_perspective_group_size_
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
|
||||
// console.log('CACHE', tableCache.bindingGroups);
|
||||
|
||||
let groupIndex = 0;
|
||||
let loadCalled = false;
|
||||
let shouldReturn = false;
|
||||
for (; groupIndex < props.bindingValues.length; groupIndex++) {
|
||||
const groupValues = props.bindingValues[groupIndex];
|
||||
const group = tableCache.getBindingGroup(groupValues);
|
||||
|
||||
if (!group.loadedAll) {
|
||||
if (loadCalled) {
|
||||
shouldReturn = true;
|
||||
} else {
|
||||
// we need to load next data
|
||||
await this.loadNextGroup(props, groupIndex);
|
||||
loadCalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('GRP', groupValues, group);
|
||||
rows.push(...group.loadedRows);
|
||||
if (rows.length >= props.topCount || shouldReturn) {
|
||||
return {
|
||||
rows: rows.slice(0, props.topCount),
|
||||
incomplete: props.topCount < rows.length || !group.loadedAll || groupIndex < props.bindingValues.length - 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (groupIndex >= props.bindingValues.length) {
|
||||
// all groups are fully loaded
|
||||
return { rows, incomplete: false };
|
||||
}
|
||||
}
|
||||
|
||||
async loadNextGroup(props: PerspectiveDataLoadProps, groupIndex: number) {
|
||||
const tableCache = this.cache.getTableCache(props);
|
||||
|
||||
const planLoadingGroupIndexes: number[] = [];
|
||||
const planLoadingGroups: PerspectiveBindingGroup[] = [];
|
||||
let planLoadRowCount = 0;
|
||||
|
||||
const loadPlanned = async () => {
|
||||
// console.log(
|
||||
// 'LOAD PLANNED',
|
||||
// planLoadingGroupIndexes,
|
||||
// planLoadingGroupIndexes.map(idx => props.bindingValues[idx])
|
||||
// );
|
||||
const rows = await this.loader.loadData({
|
||||
...props,
|
||||
bindingValues: planLoadingGroupIndexes.map(idx => props.bindingValues[idx]),
|
||||
});
|
||||
// console.log('LOADED PLANNED', rows);
|
||||
// distribute rows into groups
|
||||
for (const row of rows) {
|
||||
const group = planLoadingGroups.find(x => x.matchRow(row));
|
||||
if (group) {
|
||||
group.loadedRows.push(row);
|
||||
}
|
||||
}
|
||||
for (const group of planLoadingGroups) {
|
||||
group.loadedAll = true;
|
||||
}
|
||||
};
|
||||
|
||||
for (; groupIndex < props.bindingValues.length; groupIndex++) {
|
||||
const groupValues = props.bindingValues[groupIndex];
|
||||
const group = tableCache.getBindingGroup(groupValues);
|
||||
if (group.loadedAll) continue;
|
||||
if (group.groupSize == 0) {
|
||||
group.loadedAll = true;
|
||||
continue;
|
||||
}
|
||||
if (group.groupSize >= PERSPECTIVE_PAGE_SIZE) {
|
||||
if (planLoadingGroupIndexes.length > 0) {
|
||||
await loadPlanned();
|
||||
return;
|
||||
}
|
||||
const nextRows = await this.loader.loadData({
|
||||
...props,
|
||||
topCount: null,
|
||||
range: {
|
||||
offset: group.loadedRows.length,
|
||||
limit: PERSPECTIVE_PAGE_SIZE,
|
||||
},
|
||||
bindingValues: [group.bindingValues],
|
||||
});
|
||||
group.loadedRows = [...group.loadedRows, ...nextRows];
|
||||
group.loadedAll = nextRows.length < PERSPECTIVE_PAGE_SIZE;
|
||||
return;
|
||||
} else {
|
||||
if (planLoadRowCount + group.groupSize > PERSPECTIVE_PAGE_SIZE) {
|
||||
await loadPlanned();
|
||||
return;
|
||||
}
|
||||
planLoadingGroupIndexes.push(groupIndex);
|
||||
planLoadingGroups.push(group);
|
||||
planLoadRowCount += group.groupSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (planLoadingGroupIndexes.length > 0) {
|
||||
await loadPlanned();
|
||||
}
|
||||
}
|
||||
|
||||
async loadDataFlat(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> {
|
||||
const tableCache = this.cache.getTableCache(props);
|
||||
|
||||
if (props.topCount <= tableCache.loadedCount) {
|
||||
return tableCache.getRowsResult(props);
|
||||
}
|
||||
|
||||
// load missing rows
|
||||
tableCache.dataColumns = props.dataColumns;
|
||||
|
||||
const nextRows = await this.loader.loadData({
|
||||
...props,
|
||||
topCount: null,
|
||||
range: {
|
||||
offset: tableCache.loadedCount,
|
||||
limit: props.topCount - tableCache.loadedCount,
|
||||
},
|
||||
});
|
||||
|
||||
if (nextRows.errorMessage) {
|
||||
throw new Error(nextRows.errorMessage);
|
||||
}
|
||||
|
||||
tableCache.loadedRows = [...tableCache.loadedRows, ...nextRows];
|
||||
tableCache.loadedAll = nextRows.length < props.topCount - tableCache.loadedCount;
|
||||
|
||||
// const rows=tableCache.getRows(props);
|
||||
|
||||
return tableCache.getRowsResult(props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import { getTableChildPerspectiveNodes, PerspectiveTableNode, PerspectiveTreeNode } from './PerspectiveTreeNode';
|
||||
import _max from 'lodash/max';
|
||||
import _range from 'lodash/max';
|
||||
import _fill from 'lodash/fill';
|
||||
import _findIndex from 'lodash/findIndex';
|
||||
import debug from 'debug';
|
||||
|
||||
const dbg = debug('dbgate:PerspectiveDisplay');
|
||||
|
||||
let lastJoinId = 0;
|
||||
function getJoinId(): number {
|
||||
lastJoinId += 1;
|
||||
return lastJoinId;
|
||||
}
|
||||
|
||||
export class PerspectiveDisplayColumn {
|
||||
title: string;
|
||||
dataField: string;
|
||||
parentNodes: PerspectiveTreeNode[] = [];
|
||||
colSpanAtLevel = {};
|
||||
columnIndex = 0;
|
||||
dataNode: PerspectiveTreeNode = null;
|
||||
|
||||
constructor(public display: PerspectiveDisplay) {}
|
||||
|
||||
get rowSpan() {
|
||||
return this.display.columnLevelCount - this.parentNodes.length;
|
||||
}
|
||||
|
||||
showParent(level: number) {
|
||||
return !!this.colSpanAtLevel[level];
|
||||
}
|
||||
|
||||
getColSpan(level: number) {
|
||||
return this.colSpanAtLevel[level];
|
||||
}
|
||||
|
||||
isVisible(level: number) {
|
||||
return level == this.columnLevel;
|
||||
}
|
||||
|
||||
get columnLevel() {
|
||||
return this.parentNodes.length;
|
||||
}
|
||||
|
||||
getParentName(level) {
|
||||
return this.parentNodes[level]?.title;
|
||||
}
|
||||
|
||||
getParentNode(level) {
|
||||
return this.parentNodes[level];
|
||||
}
|
||||
|
||||
getParentTableUniqueName(level) {
|
||||
return this.parentNodes[level]?.headerTableAttributes ? this.parentNodes[level]?.uniqueName : '';
|
||||
}
|
||||
|
||||
// hasParentNode(node: PerspectiveTreeNode) {
|
||||
// return this.parentNodes.includes(node);
|
||||
// }
|
||||
}
|
||||
|
||||
interface PerspectiveSubRowCollection {
|
||||
rows: CollectedPerspectiveDisplayRow[];
|
||||
}
|
||||
|
||||
interface CollectedPerspectiveDisplayRow {
|
||||
columnIndexes: number[];
|
||||
rowData: any[];
|
||||
subRowCollections: PerspectiveSubRowCollection[];
|
||||
incompleteRowsIndicator?: string[];
|
||||
}
|
||||
|
||||
export class PerspectiveDisplayRow {
|
||||
constructor(public display: PerspectiveDisplay) {
|
||||
this.rowData = _fill(Array(display.columns.length), undefined);
|
||||
this.rowSpans = _fill(Array(display.columns.length), 1);
|
||||
this.rowJoinIds = _fill(Array(display.columns.length), 0);
|
||||
this.rowCellSkips = _fill(Array(display.columns.length), false);
|
||||
}
|
||||
|
||||
rowData: any[] = [];
|
||||
rowSpans: number[] = null;
|
||||
rowCellSkips: boolean[] = null;
|
||||
|
||||
rowJoinIds: number[] = [];
|
||||
}
|
||||
|
||||
export class PerspectiveDisplay {
|
||||
columns: PerspectiveDisplayColumn[] = [];
|
||||
rows: PerspectiveDisplayRow[] = [];
|
||||
readonly columnLevelCount: number;
|
||||
loadIndicatorsCounts: { [uniqueName: string]: number } = {};
|
||||
|
||||
constructor(public root: PerspectiveTreeNode, rows: any[]) {
|
||||
// dbg('source rows', rows);
|
||||
this.fillColumns(root.childNodes, [root]);
|
||||
if (this.columns.length > 0) {
|
||||
this.columns[0].colSpanAtLevel[0] = this.columns.length;
|
||||
}
|
||||
this.columnLevelCount = _max(this.columns.map(x => x.parentNodes.length)) + 1;
|
||||
const collectedRows = this.collectRows(rows, root.childNodes);
|
||||
dbg('collected rows', collectedRows);
|
||||
// console.log('COLLECTED', JSON.stringify(collectedRows, null, 2));
|
||||
// this.mergeRows(collectedRows);
|
||||
this.mergeRows(collectedRows);
|
||||
// dbg('merged rows', this.rows);
|
||||
|
||||
// console.log(
|
||||
// 'MERGED',
|
||||
// this.rows.map(r =>
|
||||
// r.incompleteRowsIndicator
|
||||
// ? `************************************ ${r.incompleteRowsIndicator.join('|')}`
|
||||
// : r.rowData.join('|')
|
||||
// )
|
||||
// );
|
||||
}
|
||||
|
||||
private getRowAt(rowIndex) {
|
||||
while (this.rows.length <= rowIndex) {
|
||||
this.rows.push(new PerspectiveDisplayRow(this));
|
||||
}
|
||||
return this.rows[rowIndex];
|
||||
}
|
||||
|
||||
fillColumns(children: PerspectiveTreeNode[], parentNodes: PerspectiveTreeNode[]) {
|
||||
for (const child of children) {
|
||||
if (child.isChecked) {
|
||||
this.processColumn(child, parentNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processColumn(node: PerspectiveTreeNode, parentNodes: PerspectiveTreeNode[]) {
|
||||
if (node.isExpandable) {
|
||||
const countBefore = this.columns.length;
|
||||
this.fillColumns(node.childNodes, [...parentNodes, node]);
|
||||
|
||||
if (this.columns.length > countBefore) {
|
||||
this.columns[countBefore].colSpanAtLevel[parentNodes.length] = this.columns.length - countBefore;
|
||||
}
|
||||
} else {
|
||||
const column = new PerspectiveDisplayColumn(this);
|
||||
column.title = node.columnTitle;
|
||||
column.dataField = node.dataField;
|
||||
column.parentNodes = parentNodes;
|
||||
column.display = this;
|
||||
column.columnIndex = this.columns.length;
|
||||
column.dataNode = node;
|
||||
this.columns.push(column);
|
||||
}
|
||||
}
|
||||
|
||||
findColumnIndexFromNode(node: PerspectiveTreeNode) {
|
||||
return _findIndex(this.columns, x => x.dataNode.uniqueName == node.uniqueName);
|
||||
}
|
||||
|
||||
collectRows(sourceRows: any[], nodes: PerspectiveTreeNode[]): CollectedPerspectiveDisplayRow[] {
|
||||
const columnNodes = nodes.filter(x => x.isChecked && !x.isExpandable);
|
||||
const treeNodes = nodes.filter(x => x.isChecked && x.isExpandable);
|
||||
|
||||
const columnIndexes = columnNodes.map(node => this.findColumnIndexFromNode(node));
|
||||
|
||||
const res: CollectedPerspectiveDisplayRow[] = [];
|
||||
for (const sourceRow of sourceRows) {
|
||||
// console.log('PROCESS SOURCE', sourceRow);
|
||||
// row.startIndex = startIndex;
|
||||
const rowData = columnNodes.map(node => sourceRow[node.codeName]);
|
||||
const subRowCollections = [];
|
||||
|
||||
for (const node of treeNodes) {
|
||||
if (sourceRow[node.fieldName]) {
|
||||
const subrows = {
|
||||
rows: this.collectRows(sourceRow[node.fieldName], node.childNodes),
|
||||
};
|
||||
subRowCollections.push(subrows);
|
||||
}
|
||||
}
|
||||
|
||||
res.push({
|
||||
rowData,
|
||||
columnIndexes,
|
||||
subRowCollections,
|
||||
incompleteRowsIndicator: sourceRow.incompleteRowsIndicator,
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
fillRowSpans() {
|
||||
for (let col = 0; col < this.columns.length; col++) {
|
||||
// let lastFilledJoinId = null;
|
||||
let lastFilledRow = 0;
|
||||
let rowIndex = 0;
|
||||
|
||||
for (const row of this.rows) {
|
||||
if (
|
||||
row.rowData[col] === undefined &&
|
||||
row.rowJoinIds[col] == this.rows[lastFilledRow].rowJoinIds[col] &&
|
||||
row.rowJoinIds[col]
|
||||
) {
|
||||
row.rowCellSkips[col] = true;
|
||||
this.rows[lastFilledRow].rowSpans[col] = rowIndex - lastFilledRow + 1;
|
||||
} else {
|
||||
lastFilledRow = rowIndex;
|
||||
}
|
||||
rowIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mergeRows(collectedRows: CollectedPerspectiveDisplayRow[]) {
|
||||
let rowIndex = 0;
|
||||
for (const collectedRow of collectedRows) {
|
||||
const count = this.mergeRow(collectedRow, rowIndex);
|
||||
rowIndex += count;
|
||||
}
|
||||
this.fillRowSpans();
|
||||
}
|
||||
|
||||
mergeRow(collectedRow: CollectedPerspectiveDisplayRow, rowIndex: number): number {
|
||||
if (collectedRow.incompleteRowsIndicator?.length > 0) {
|
||||
for (const indicator of collectedRow.incompleteRowsIndicator) {
|
||||
if (!this.loadIndicatorsCounts[indicator]) {
|
||||
this.loadIndicatorsCounts[indicator] = rowIndex;
|
||||
}
|
||||
if (rowIndex < this.loadIndicatorsCounts[indicator]) {
|
||||
this.loadIndicatorsCounts[indicator] = rowIndex;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const mainRow = this.getRowAt(rowIndex);
|
||||
for (let i = 0; i < collectedRow.columnIndexes.length; i++) {
|
||||
mainRow.rowData[collectedRow.columnIndexes[i]] = collectedRow.rowData[i];
|
||||
}
|
||||
|
||||
let rowCount = 1;
|
||||
for (const subrows of collectedRow.subRowCollections) {
|
||||
let additionalRowCount = 0;
|
||||
let currentRowIndex = rowIndex;
|
||||
for (const subrow of subrows.rows) {
|
||||
const count = this.mergeRow(subrow, currentRowIndex);
|
||||
additionalRowCount += count;
|
||||
currentRowIndex += count;
|
||||
}
|
||||
if (additionalRowCount > rowCount) {
|
||||
rowCount = additionalRowCount;
|
||||
}
|
||||
}
|
||||
|
||||
const joinId = getJoinId();
|
||||
for (let radd = 0; radd < rowCount; radd++) {
|
||||
const row = this.getRowAt(rowIndex + radd);
|
||||
for (let i = 0; i < collectedRow.columnIndexes.length; i++) {
|
||||
row.rowJoinIds[collectedRow.columnIndexes[i]] = joinId;
|
||||
}
|
||||
}
|
||||
|
||||
return rowCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,916 @@
|
||||
import {
|
||||
ColumnInfo,
|
||||
DatabaseInfo,
|
||||
ForeignKeyInfo,
|
||||
NamedObjectInfo,
|
||||
RangeDefinition,
|
||||
TableInfo,
|
||||
ViewInfo,
|
||||
} from 'dbgate-types';
|
||||
import {
|
||||
ChangePerspectiveConfigFunc,
|
||||
MultipleDatabaseInfo,
|
||||
PerspectiveConfig,
|
||||
PerspectiveConfigColumns,
|
||||
PerspectiveCustomJoinConfig,
|
||||
PerspectiveFilterColumnInfo,
|
||||
} from './PerspectiveConfig';
|
||||
import _isEqual from 'lodash/isEqual';
|
||||
import _cloneDeep from 'lodash/cloneDeep';
|
||||
import _compact from 'lodash/compact';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _flatten from 'lodash/flatten';
|
||||
import _uniqBy from 'lodash/uniqBy';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _cloneDeepWith from 'lodash/cloneDeepWith';
|
||||
import {
|
||||
PerspectiveDatabaseConfig,
|
||||
PerspectiveDataLoadProps,
|
||||
PerspectiveDataProvider,
|
||||
} from './PerspectiveDataProvider';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { getFilterType, parseFilter } from 'dbgate-filterparser';
|
||||
import { FilterType } from 'dbgate-filterparser/lib/types';
|
||||
import { Condition, Expression, Select } from 'dbgate-sqltree';
|
||||
import { getPerspectiveDefaultColumns } from './getPerspectiveDefaultColumns';
|
||||
|
||||
export interface PerspectiveDataLoadPropsWithNode {
|
||||
props: PerspectiveDataLoadProps;
|
||||
node: PerspectiveTreeNode;
|
||||
}
|
||||
|
||||
// export function groupPerspectiveLoadProps(
|
||||
// ...list: PerspectiveDataLoadPropsWithNode[]
|
||||
// ): PerspectiveDataLoadPropsWithNode[] {
|
||||
// const res: PerspectiveDataLoadPropsWithNode[] = [];
|
||||
// for (const item of list) {
|
||||
// const existing = res.find(
|
||||
// x =>
|
||||
// x.node == item.node &&
|
||||
// x.props.schemaName == item.props.schemaName &&
|
||||
// x.props.pureName == item.props.pureName &&
|
||||
// _isEqual(x.props.bindingColumns, item.props.bindingColumns)
|
||||
// );
|
||||
// if (existing) {
|
||||
// existing.props.bindingValues.push(...item.props.bindingValues);
|
||||
// } else {
|
||||
// res.push(_cloneDeep(item));
|
||||
// }
|
||||
// }
|
||||
// return res;
|
||||
// }
|
||||
|
||||
export abstract class PerspectiveTreeNode {
|
||||
constructor(
|
||||
public dbs: MultipleDatabaseInfo,
|
||||
public config: PerspectiveConfig,
|
||||
public setConfig: ChangePerspectiveConfigFunc,
|
||||
public parentNode: PerspectiveTreeNode,
|
||||
public dataProvider: PerspectiveDataProvider,
|
||||
public databaseConfig: PerspectiveDatabaseConfig
|
||||
) {}
|
||||
defaultChecked: boolean;
|
||||
abstract get title();
|
||||
abstract get codeName();
|
||||
abstract get isExpandable();
|
||||
abstract get childNodes(): PerspectiveTreeNode[];
|
||||
abstract get icon(): string;
|
||||
get fieldName() {
|
||||
return this.codeName;
|
||||
}
|
||||
get headerTableAttributes() {
|
||||
return null;
|
||||
}
|
||||
get dataField() {
|
||||
return this.codeName;
|
||||
}
|
||||
get tableCode() {
|
||||
return null;
|
||||
}
|
||||
get namedObject(): NamedObjectInfo {
|
||||
return null;
|
||||
}
|
||||
abstract getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps;
|
||||
get isRoot() {
|
||||
return this.parentNode == null;
|
||||
}
|
||||
get rootNode(): PerspectiveTreeNode {
|
||||
if (this.isRoot) return this;
|
||||
return this.parentNode?.rootNode;
|
||||
}
|
||||
matchChildRow(parentRow: any, childRow: any): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
hasTableCode(code: string) {
|
||||
return code == this.tableCode || this.parentNode?.hasTableCode(code);
|
||||
}
|
||||
|
||||
get uniqueName() {
|
||||
if (this.parentNode) return `${this.parentNode.uniqueName}::${this.codeName}`;
|
||||
return this.codeName;
|
||||
}
|
||||
get level() {
|
||||
if (this.parentNode) return this.parentNode.level + 1;
|
||||
return 0;
|
||||
}
|
||||
get isExpanded() {
|
||||
return this.config.expandedColumns.includes(this.uniqueName);
|
||||
}
|
||||
get isChecked() {
|
||||
if (this.config.checkedColumns.includes(this.uniqueName)) return true;
|
||||
if (this.config.uncheckedColumns.includes(this.uniqueName)) return false;
|
||||
return this.defaultChecked;
|
||||
}
|
||||
get columnTitle() {
|
||||
return this.title;
|
||||
}
|
||||
get filterType(): FilterType {
|
||||
return 'string';
|
||||
}
|
||||
get columnName() {
|
||||
return null;
|
||||
}
|
||||
get customJoinConfig(): PerspectiveCustomJoinConfig {
|
||||
return null;
|
||||
}
|
||||
get db(): DatabaseInfo {
|
||||
return this.dbs?.[this.databaseConfig.conid]?.[this.databaseConfig.database];
|
||||
}
|
||||
|
||||
getChildMatchColumns() {
|
||||
return [];
|
||||
}
|
||||
|
||||
getParentMatchColumns() {
|
||||
return [];
|
||||
}
|
||||
|
||||
parseFilterCondition(source = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
get childDataColumn() {
|
||||
if (!this.isExpandable && this.isChecked) {
|
||||
return this.codeName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
toggleExpanded(value?: boolean) {
|
||||
this.includeInColumnSet('expandedColumns', this.uniqueName, value == null ? !this.isExpanded : value);
|
||||
}
|
||||
|
||||
toggleChecked(value?: boolean) {
|
||||
if (this.defaultChecked) {
|
||||
this.includeInColumnSet('uncheckedColumns', this.uniqueName, value == null ? this.isChecked : value);
|
||||
} else {
|
||||
this.includeInColumnSet('checkedColumns', this.uniqueName, value == null ? !this.isChecked : value);
|
||||
}
|
||||
}
|
||||
|
||||
includeInColumnSet(field: keyof PerspectiveConfigColumns, uniqueName: string, isIncluded: boolean) {
|
||||
if (isIncluded) {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
[field]: [...(cfg[field] || []), uniqueName],
|
||||
}));
|
||||
} else {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
[field]: (cfg[field] || []).filter(x => x != uniqueName),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
getFilter() {
|
||||
return this.config.filters[this.uniqueName];
|
||||
}
|
||||
|
||||
getDataLoadColumns() {
|
||||
return _compact(
|
||||
_uniq([
|
||||
...this.childNodes.map(x => x.childDataColumn),
|
||||
..._flatten(this.childNodes.filter(x => x.isExpandable && x.isChecked).map(x => x.getChildMatchColumns())),
|
||||
...this.getParentMatchColumns(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
getChildrenCondition(source = null): Condition {
|
||||
const conditions = _compact([
|
||||
...this.childNodes.map(x => x.parseFilterCondition(source)),
|
||||
...this.buildParentFilterConditions(),
|
||||
]);
|
||||
if (conditions.length == 0) {
|
||||
return null;
|
||||
}
|
||||
if (conditions.length == 1) {
|
||||
return conditions[0];
|
||||
}
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions,
|
||||
};
|
||||
}
|
||||
|
||||
getOrderBy(table: TableInfo | ViewInfo): PerspectiveDataLoadProps['orderBy'] {
|
||||
const res = _compact(
|
||||
this.childNodes.map(node => {
|
||||
const sort = this.config?.sort?.[node?.parentNode?.uniqueName]?.find(x => x.uniqueName == node.uniqueName);
|
||||
if (sort) {
|
||||
return {
|
||||
columnName: node.columnName,
|
||||
order: sort.order,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
return res.length > 0
|
||||
? res
|
||||
: (table as TableInfo)?.primaryKey?.columns.map(x => ({ columnName: x.columnName, order: 'ASC' })) || [
|
||||
{ columnName: table?.columns[0].columnName, order: 'ASC' },
|
||||
];
|
||||
}
|
||||
|
||||
getBaseTables() {
|
||||
const res = [];
|
||||
const table = this.getBaseTableFromThis();
|
||||
if (table) res.push({ table, node: this });
|
||||
for (const child of this.childNodes) {
|
||||
if (!child.isChecked) continue;
|
||||
res.push(...child.getBaseTables());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
getBaseTableFromThis() {
|
||||
return null;
|
||||
}
|
||||
|
||||
get filterInfo(): PerspectiveFilterColumnInfo {
|
||||
return null;
|
||||
}
|
||||
|
||||
findChildNodeByUniquePath(uniquePath: string[]) {
|
||||
if (uniquePath.length == 0) {
|
||||
return this;
|
||||
}
|
||||
const child = this.childNodes.find(x => x.codeName == uniquePath[0]);
|
||||
return child?.findChildNodeByUniquePath(uniquePath.slice(1));
|
||||
}
|
||||
|
||||
findNodeByUniqueName(uniqueName: string): PerspectiveTreeNode {
|
||||
if (!uniqueName) return null;
|
||||
const uniquePath = uniqueName.split('::');
|
||||
if (uniquePath[0] != this.codeName) return null;
|
||||
return this.findChildNodeByUniquePath(uniquePath.slice(1));
|
||||
}
|
||||
|
||||
get supportsParentFilter() {
|
||||
return (
|
||||
(this.parentNode?.isRoot || this.parentNode?.supportsParentFilter) &&
|
||||
this.parentNode?.databaseConfig?.conid == this.databaseConfig?.conid &&
|
||||
this.parentNode?.databaseConfig?.database == this.databaseConfig?.database
|
||||
);
|
||||
}
|
||||
|
||||
get isParentFilter() {
|
||||
return !!(this.config.parentFilters || []).find(x => x.uniqueName == this.uniqueName);
|
||||
}
|
||||
|
||||
buildParentFilterConditions(): Condition[] {
|
||||
const leafNodes = _compact(
|
||||
(this.config?.parentFilters || []).map(x => this.rootNode.findNodeByUniqueName(x.uniqueName))
|
||||
);
|
||||
const conditions: Condition[] = _compact(
|
||||
leafNodes.map(leafNode => {
|
||||
if (leafNode == this) return null;
|
||||
const select: Select = {
|
||||
commandType: 'select',
|
||||
from: {
|
||||
name: leafNode.namedObject,
|
||||
alias: 'pert_0',
|
||||
relations: [],
|
||||
},
|
||||
selectAll: true,
|
||||
};
|
||||
let lastNode = leafNode;
|
||||
let node = leafNode;
|
||||
let index = 1;
|
||||
let lastAlias = 'pert_0';
|
||||
while (node?.parentNode && node?.parentNode?.uniqueName != this?.uniqueName) {
|
||||
node = node.parentNode;
|
||||
let alias = `pert_${index}`;
|
||||
select.from.relations.push({
|
||||
joinType: 'INNER JOIN',
|
||||
alias,
|
||||
name: node.namedObject,
|
||||
conditions: lastNode.getParentJoinCondition(lastAlias, alias),
|
||||
});
|
||||
lastAlias = alias;
|
||||
lastNode = node;
|
||||
}
|
||||
if (node?.parentNode?.uniqueName != this?.uniqueName) return null;
|
||||
select.where = {
|
||||
conditionType: 'and',
|
||||
conditions: _compact([
|
||||
...lastNode.getParentJoinCondition(lastAlias, this.namedObject.pureName),
|
||||
leafNode.getChildrenCondition({ alias: 'pert_0' }),
|
||||
]),
|
||||
};
|
||||
|
||||
return {
|
||||
conditionType: 'exists',
|
||||
subQuery: select,
|
||||
};
|
||||
})
|
||||
);
|
||||
return conditions;
|
||||
}
|
||||
|
||||
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export class PerspectiveTableColumnNode extends PerspectiveTreeNode {
|
||||
foreignKey: ForeignKeyInfo;
|
||||
refTable: TableInfo;
|
||||
isView: boolean;
|
||||
isTable: boolean;
|
||||
constructor(
|
||||
public column: ColumnInfo,
|
||||
public table: TableInfo | ViewInfo,
|
||||
dbs: MultipleDatabaseInfo,
|
||||
config: PerspectiveConfig,
|
||||
setConfig: ChangePerspectiveConfigFunc,
|
||||
dataProvider: PerspectiveDataProvider,
|
||||
databaseConfig: PerspectiveDatabaseConfig,
|
||||
parentNode: PerspectiveTreeNode
|
||||
) {
|
||||
super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
|
||||
|
||||
this.isTable = !!this.db?.tables?.find(x => x.schemaName == table.schemaName && x.pureName == table.pureName);
|
||||
this.isView = !!this.db?.views?.find(x => x.schemaName == table.schemaName && x.pureName == table.pureName);
|
||||
|
||||
this.foreignKey = (table as TableInfo)?.foreignKeys?.find(
|
||||
fk => fk.columns.length == 1 && fk.columns[0].columnName == column.columnName
|
||||
);
|
||||
|
||||
this.refTable = this.db.tables.find(
|
||||
x => x.pureName == this.foreignKey?.refTableName && x.schemaName == this.foreignKey?.refSchemaName
|
||||
);
|
||||
}
|
||||
|
||||
matchChildRow(parentRow: any, childRow: any): boolean {
|
||||
if (!this.foreignKey) return false;
|
||||
return parentRow[this.foreignKey.columns[0].columnName] == childRow[this.foreignKey.columns[0].refColumnName];
|
||||
}
|
||||
|
||||
getChildMatchColumns() {
|
||||
if (!this.foreignKey) return [];
|
||||
return [this.foreignKey.columns[0].columnName];
|
||||
}
|
||||
|
||||
getParentMatchColumns() {
|
||||
if (!this.foreignKey) return [];
|
||||
return [this.foreignKey.columns[0].refColumnName];
|
||||
}
|
||||
|
||||
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
|
||||
if (!this.foreignKey) return [];
|
||||
return this.foreignKey.columns.map(column => {
|
||||
const res: Condition = {
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'column',
|
||||
columnName: column.columnName,
|
||||
source: { alias: parentAlias },
|
||||
},
|
||||
right: {
|
||||
exprType: 'column',
|
||||
columnName: column.refColumnName,
|
||||
source: { alias },
|
||||
},
|
||||
};
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||
if (!this.foreignKey) return null;
|
||||
return {
|
||||
schemaName: this.foreignKey.refSchemaName,
|
||||
pureName: this.foreignKey.refTableName,
|
||||
bindingColumns: [this.foreignKey.columns[0].refColumnName],
|
||||
bindingValues: _uniqBy(
|
||||
parentRows.map(row => [row[this.foreignKey.columns[0].columnName]]),
|
||||
stableStringify
|
||||
),
|
||||
dataColumns: this.getDataLoadColumns(),
|
||||
databaseConfig: this.databaseConfig,
|
||||
orderBy: this.getOrderBy(this.refTable),
|
||||
condition: this.getChildrenCondition(),
|
||||
};
|
||||
}
|
||||
|
||||
get icon() {
|
||||
if (this.isCircular) return 'img circular';
|
||||
if (this.column.autoIncrement) return 'img autoincrement';
|
||||
if (this.foreignKey) return 'img foreign-key';
|
||||
return 'img column';
|
||||
}
|
||||
|
||||
get codeName() {
|
||||
return this.column.columnName;
|
||||
}
|
||||
|
||||
get columnName() {
|
||||
return this.column.columnName;
|
||||
}
|
||||
|
||||
get fieldName() {
|
||||
return this.codeName + 'Ref';
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.column.columnName;
|
||||
}
|
||||
|
||||
get isExpandable() {
|
||||
return !!this.foreignKey;
|
||||
}
|
||||
|
||||
get filterType(): FilterType {
|
||||
return getFilterType(this.column.dataType);
|
||||
}
|
||||
|
||||
get isCircular() {
|
||||
return !!this.parentNode?.parentNode?.hasTableCode(this.tableCode);
|
||||
}
|
||||
|
||||
get childNodes(): PerspectiveTreeNode[] {
|
||||
if (!this.foreignKey) return [];
|
||||
const tbl = this?.db?.tables?.find(
|
||||
x => x.pureName == this.foreignKey?.refTableName && x.schemaName == this.foreignKey?.refSchemaName
|
||||
);
|
||||
|
||||
return getTableChildPerspectiveNodes(
|
||||
tbl,
|
||||
this.dbs,
|
||||
this.config,
|
||||
this.setConfig,
|
||||
this.dataProvider,
|
||||
this.databaseConfig,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
getBaseTableFromThis() {
|
||||
return this.refTable;
|
||||
}
|
||||
|
||||
get filterInfo(): PerspectiveFilterColumnInfo {
|
||||
return {
|
||||
columnName: this.columnName,
|
||||
filterType: this.filterType,
|
||||
pureName: this.column.pureName,
|
||||
schemaName: this.column.schemaName,
|
||||
foreignKey: this.foreignKey,
|
||||
};
|
||||
}
|
||||
|
||||
parseFilterCondition(source = null): Condition {
|
||||
const filter = this.getFilter();
|
||||
if (!filter) return null;
|
||||
const condition = parseFilter(filter, this.filterType);
|
||||
if (!condition) return null;
|
||||
return _cloneDeepWith(condition, (expr: Expression) => {
|
||||
if (expr.exprType == 'placeholder') {
|
||||
return {
|
||||
exprType: 'column',
|
||||
columnName: this.column.columnName,
|
||||
source,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get headerTableAttributes() {
|
||||
if (this.foreignKey) {
|
||||
return {
|
||||
schemaName: this.foreignKey.refSchemaName,
|
||||
pureName: this.foreignKey.refTableName,
|
||||
conid: this.databaseConfig.conid,
|
||||
database: this.databaseConfig.database,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get tableCode() {
|
||||
if (this.foreignKey) {
|
||||
return `${this.foreignKey.refSchemaName}|${this.foreignKey.refTableName}`;
|
||||
}
|
||||
return `${this.table.schemaName}|${this.table.pureName}`;
|
||||
}
|
||||
|
||||
get namedObject(): NamedObjectInfo {
|
||||
if (this.foreignKey) {
|
||||
return {
|
||||
schemaName: this.foreignKey.refSchemaName,
|
||||
pureName: this.foreignKey.refTableName,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class PerspectiveTableNode extends PerspectiveTreeNode {
|
||||
constructor(
|
||||
public table: TableInfo | ViewInfo,
|
||||
dbs: MultipleDatabaseInfo,
|
||||
config: PerspectiveConfig,
|
||||
setConfig: ChangePerspectiveConfigFunc,
|
||||
public dataProvider: PerspectiveDataProvider,
|
||||
databaseConfig: PerspectiveDatabaseConfig,
|
||||
parentNode: PerspectiveTreeNode
|
||||
) {
|
||||
super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
|
||||
}
|
||||
|
||||
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||
return {
|
||||
schemaName: this.table.schemaName,
|
||||
pureName: this.table.pureName,
|
||||
dataColumns: this.getDataLoadColumns(),
|
||||
databaseConfig: this.databaseConfig,
|
||||
orderBy: this.getOrderBy(this.table),
|
||||
condition: this.getChildrenCondition(),
|
||||
};
|
||||
}
|
||||
|
||||
get codeName() {
|
||||
return this.table.schemaName ? `${this.table.schemaName}:${this.table.pureName}` : this.table.pureName;
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.table.pureName;
|
||||
}
|
||||
|
||||
get isExpandable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get childNodes(): PerspectiveTreeNode[] {
|
||||
return getTableChildPerspectiveNodes(
|
||||
this.table,
|
||||
this.dbs,
|
||||
this.config,
|
||||
this.setConfig,
|
||||
this.dataProvider,
|
||||
this.databaseConfig,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return 'img table';
|
||||
}
|
||||
|
||||
getBaseTableFromThis() {
|
||||
return this.table;
|
||||
}
|
||||
|
||||
get headerTableAttributes() {
|
||||
return {
|
||||
schemaName: this.table.schemaName,
|
||||
pureName: this.table.pureName,
|
||||
conid: this.databaseConfig.conid,
|
||||
database: this.databaseConfig.database,
|
||||
};
|
||||
}
|
||||
|
||||
get tableCode() {
|
||||
return `${this.table.schemaName}|${this.table.pureName}`;
|
||||
}
|
||||
|
||||
get namedObject(): NamedObjectInfo {
|
||||
return {
|
||||
schemaName: this.table.schemaName,
|
||||
pureName: this.table.pureName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// export class PerspectiveViewNode extends PerspectiveTreeNode {
|
||||
// constructor(
|
||||
// public view: ViewInfo,
|
||||
// dbs: MultipleDatabaseInfo,
|
||||
// config: PerspectiveConfig,
|
||||
// setConfig: ChangePerspectiveConfigFunc,
|
||||
// public dataProvider: PerspectiveDataProvider,
|
||||
// databaseConfig: PerspectiveDatabaseConfig,
|
||||
// parentNode: PerspectiveTreeNode
|
||||
// ) {
|
||||
// super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig);
|
||||
// }
|
||||
|
||||
// getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||
// return {
|
||||
// schemaName: this.view.schemaName,
|
||||
// pureName: this.view.pureName,
|
||||
// dataColumns: this.getDataLoadColumns(),
|
||||
// databaseConfig: this.databaseConfig,
|
||||
// orderBy: this.getOrderBy(this.view),
|
||||
// condition: this.getChildrenCondition(),
|
||||
// };
|
||||
// }
|
||||
|
||||
// get codeName() {
|
||||
// return this.view.schemaName ? `${this.view.schemaName}:${this.view.pureName}` : this.view.pureName;
|
||||
// }
|
||||
|
||||
// get title() {
|
||||
// return this.view.pureName;
|
||||
// }
|
||||
|
||||
// get isExpandable() {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// get childNodes(): PerspectiveTreeNode[] {
|
||||
// return getTableChildPerspectiveNodes(
|
||||
// this.view,
|
||||
// this.dbs,
|
||||
// this.config,
|
||||
// this.setConfig,
|
||||
// this.dataProvider,
|
||||
// this.databaseConfig,
|
||||
// this
|
||||
// );
|
||||
// }
|
||||
|
||||
// get icon() {
|
||||
// return 'img table';
|
||||
// }
|
||||
|
||||
// getBaseTableFromThis() {
|
||||
// return this.view;
|
||||
// }
|
||||
// }
|
||||
|
||||
export class PerspectiveTableReferenceNode extends PerspectiveTableNode {
|
||||
constructor(
|
||||
public foreignKey: ForeignKeyInfo,
|
||||
table: TableInfo,
|
||||
dbs: MultipleDatabaseInfo,
|
||||
config: PerspectiveConfig,
|
||||
setConfig: ChangePerspectiveConfigFunc,
|
||||
public dataProvider: PerspectiveDataProvider,
|
||||
databaseConfig: PerspectiveDatabaseConfig,
|
||||
public isMultiple: boolean,
|
||||
parentNode: PerspectiveTreeNode
|
||||
) {
|
||||
super(table, dbs, config, setConfig, dataProvider, databaseConfig, parentNode);
|
||||
}
|
||||
|
||||
matchChildRow(parentRow: any, childRow: any): boolean {
|
||||
if (!this.foreignKey) return false;
|
||||
return parentRow[this.foreignKey.columns[0].refColumnName] == childRow[this.foreignKey.columns[0].columnName];
|
||||
}
|
||||
|
||||
getChildMatchColumns() {
|
||||
if (!this.foreignKey) return [];
|
||||
return [this.foreignKey.columns[0].refColumnName];
|
||||
}
|
||||
|
||||
getParentMatchColumns() {
|
||||
if (!this.foreignKey) return [];
|
||||
return [this.foreignKey.columns[0].columnName];
|
||||
}
|
||||
|
||||
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||
if (!this.foreignKey) return null;
|
||||
return {
|
||||
schemaName: this.table.schemaName,
|
||||
pureName: this.table.pureName,
|
||||
bindingColumns: [this.foreignKey.columns[0].columnName],
|
||||
bindingValues: _uniqBy(
|
||||
parentRows.map(row => [row[this.foreignKey.columns[0].refColumnName]]),
|
||||
stableStringify
|
||||
),
|
||||
dataColumns: this.getDataLoadColumns(),
|
||||
databaseConfig: this.databaseConfig,
|
||||
orderBy: this.getOrderBy(this.table),
|
||||
condition: this.getChildrenCondition(),
|
||||
};
|
||||
}
|
||||
|
||||
get columnTitle() {
|
||||
return this.table.pureName;
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (this.isMultiple) {
|
||||
return `${super.title} (${this.foreignKey.columns.map(x => x.columnName).join(', ')})`;
|
||||
}
|
||||
return super.title;
|
||||
}
|
||||
|
||||
get codeName() {
|
||||
if (this.isMultiple) {
|
||||
return `${super.codeName}-${this.foreignKey.columns.map(x => x.columnName).join('_')}`;
|
||||
}
|
||||
return super.codeName;
|
||||
}
|
||||
|
||||
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
|
||||
if (!this.foreignKey) return [];
|
||||
return this.foreignKey.columns.map(column => {
|
||||
const res: Condition = {
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'column',
|
||||
columnName: column.refColumnName,
|
||||
source: { alias: parentAlias },
|
||||
},
|
||||
right: {
|
||||
exprType: 'column',
|
||||
columnName: column.columnName,
|
||||
source: { alias },
|
||||
},
|
||||
};
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode {
|
||||
constructor(
|
||||
public customJoin: PerspectiveCustomJoinConfig,
|
||||
table: TableInfo | ViewInfo,
|
||||
dbs: MultipleDatabaseInfo,
|
||||
config: PerspectiveConfig,
|
||||
setConfig: ChangePerspectiveConfigFunc,
|
||||
public dataProvider: PerspectiveDataProvider,
|
||||
databaseConfig: PerspectiveDatabaseConfig,
|
||||
parentNode: PerspectiveTreeNode
|
||||
) {
|
||||
super(table, dbs, config, setConfig, dataProvider, databaseConfig, parentNode);
|
||||
}
|
||||
|
||||
matchChildRow(parentRow: any, childRow: any): boolean {
|
||||
for (const column of this.customJoin.columns) {
|
||||
if (parentRow[column.baseColumnName] != childRow[column.refColumnName]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getChildMatchColumns() {
|
||||
return this.customJoin.columns.map(x => x.baseColumnName);
|
||||
}
|
||||
|
||||
getParentMatchColumns() {
|
||||
return this.customJoin.columns.map(x => x.refColumnName);
|
||||
}
|
||||
|
||||
getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps {
|
||||
// console.log('CUSTOM JOIN', this.customJoin);
|
||||
// console.log('this.getDataLoadColumns()', this.getDataLoadColumns());
|
||||
return {
|
||||
schemaName: this.table.schemaName,
|
||||
pureName: this.table.pureName,
|
||||
bindingColumns: this.getParentMatchColumns(),
|
||||
bindingValues: _uniqBy(
|
||||
parentRows.map(row => this.customJoin.columns.map(x => row[x.baseColumnName])),
|
||||
stableStringify
|
||||
),
|
||||
dataColumns: this.getDataLoadColumns(),
|
||||
databaseConfig: this.databaseConfig,
|
||||
orderBy: this.getOrderBy(this.table),
|
||||
condition: this.getChildrenCondition(),
|
||||
};
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.customJoin.joinName;
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return 'icon custom-join';
|
||||
}
|
||||
|
||||
get codeName() {
|
||||
return this.customJoin.joinid;
|
||||
}
|
||||
|
||||
get customJoinConfig(): PerspectiveCustomJoinConfig {
|
||||
return this.customJoin;
|
||||
}
|
||||
|
||||
getParentJoinCondition(alias: string, parentAlias: string): Condition[] {
|
||||
return this.customJoin.columns.map(column => {
|
||||
const res: Condition = {
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'column',
|
||||
columnName: column.baseColumnName,
|
||||
source: { alias: parentAlias },
|
||||
},
|
||||
right: {
|
||||
exprType: 'column',
|
||||
columnName: column.refColumnName,
|
||||
source: { alias },
|
||||
},
|
||||
};
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getTableChildPerspectiveNodes(
|
||||
table: TableInfo | ViewInfo,
|
||||
dbs: MultipleDatabaseInfo,
|
||||
config: PerspectiveConfig,
|
||||
setConfig: ChangePerspectiveConfigFunc,
|
||||
dataProvider: PerspectiveDataProvider,
|
||||
databaseConfig: PerspectiveDatabaseConfig,
|
||||
parentColumn: PerspectiveTreeNode
|
||||
) {
|
||||
if (!table) return [];
|
||||
const db = parentColumn.db;
|
||||
|
||||
const columnNodes = table.columns.map(
|
||||
col =>
|
||||
new PerspectiveTableColumnNode(col, table, dbs, config, setConfig, dataProvider, databaseConfig, parentColumn)
|
||||
);
|
||||
const circularColumns = columnNodes.filter(x => x.isCircular).map(x => x.columnName);
|
||||
const defaultColumns = getPerspectiveDefaultColumns(table, db, circularColumns);
|
||||
for (const node of columnNodes) {
|
||||
node.defaultChecked = defaultColumns.includes(node.columnName);
|
||||
}
|
||||
|
||||
const res = [];
|
||||
res.push(...columnNodes);
|
||||
const dependencies = [];
|
||||
if (db && (table as TableInfo)?.dependencies) {
|
||||
for (const fk of (table as TableInfo)?.dependencies) {
|
||||
const tbl = db.tables.find(x => x.pureName == fk.pureName && x.schemaName == fk.schemaName);
|
||||
if (tbl) {
|
||||
const isMultiple =
|
||||
(table as TableInfo)?.dependencies.filter(x => x.pureName == fk.pureName && x.schemaName == fk.schemaName)
|
||||
.length >= 2;
|
||||
dependencies.push(
|
||||
new PerspectiveTableReferenceNode(
|
||||
fk,
|
||||
tbl,
|
||||
dbs,
|
||||
config,
|
||||
setConfig,
|
||||
dataProvider,
|
||||
databaseConfig,
|
||||
isMultiple,
|
||||
parentColumn
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.push(..._sortBy(dependencies, 'title'));
|
||||
|
||||
const customs = [];
|
||||
for (const join of config.customJoins || []) {
|
||||
if (join.baseUniqueName == parentColumn.uniqueName) {
|
||||
const newConfig = { ...databaseConfig };
|
||||
if (join.conid) newConfig.conid = join.conid;
|
||||
if (join.database) newConfig.database = join.database;
|
||||
const db = dbs?.[newConfig.conid]?.[newConfig.database];
|
||||
const table = db?.tables?.find(x => x.pureName == join.refTableName && x.schemaName == join.refSchemaName);
|
||||
const view = db?.views?.find(x => x.pureName == join.refTableName && x.schemaName == join.refSchemaName);
|
||||
|
||||
if (table || view) {
|
||||
customs.push(
|
||||
new PerspectiveCustomJoinTreeNode(
|
||||
join,
|
||||
table || view,
|
||||
dbs,
|
||||
config,
|
||||
setConfig,
|
||||
dataProvider,
|
||||
newConfig,
|
||||
parentColumn
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.push(..._sortBy(customs, 'title'));
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -267,12 +267,9 @@ export class TableGridDisplay extends GridDisplay {
|
||||
) {
|
||||
for (const column of columns) {
|
||||
if (this.addAllExpandedColumnsToSelected || this.config.addedColumns.includes(column.uniqueName)) {
|
||||
select.columns.push({
|
||||
exprType: 'column',
|
||||
columnName: column.columnName,
|
||||
alias: column.uniqueName,
|
||||
source: { name: column, alias: parentAlias },
|
||||
});
|
||||
select.columns.push(
|
||||
this.createColumnExpression(column, { name: column, alias: parentAlias }, column.uniqueName)
|
||||
);
|
||||
displayedColumnInfo[column.uniqueName] = {
|
||||
...column,
|
||||
sourceAlias: parentAlias,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { findForeignKeyForColumn } from 'dbgate-tools';
|
||||
import { DatabaseInfo, TableInfo, ViewInfo } from 'dbgate-types';
|
||||
|
||||
export function getPerspectiveDefaultColumns(
|
||||
table: TableInfo | ViewInfo,
|
||||
db: DatabaseInfo,
|
||||
circularColumns: string[]
|
||||
): string[] {
|
||||
const columns = table.columns.map(x => x.columnName);
|
||||
const predicates = [
|
||||
x => x.toLowerCase() == 'name',
|
||||
x => x.toLowerCase() == 'title',
|
||||
x => x.toLowerCase().includes('name'),
|
||||
x => x.toLowerCase().includes('title'),
|
||||
x => x.toLowerCase().includes('subject'),
|
||||
// x => x.toLowerCase().includes('text'),
|
||||
// x => x.toLowerCase().includes('desc'),
|
||||
x =>
|
||||
table.columns
|
||||
.find(y => y.columnName == x)
|
||||
?.dataType?.toLowerCase()
|
||||
?.includes('char'),
|
||||
x => findForeignKeyForColumn(table as TableInfo, x)?.columns?.length == 1 && !circularColumns.includes(x),
|
||||
x => findForeignKeyForColumn(table as TableInfo, x)?.columns?.length == 1,
|
||||
];
|
||||
|
||||
for (const predicate of predicates) {
|
||||
const col = columns.find(predicate);
|
||||
if (col) return [col];
|
||||
}
|
||||
|
||||
return [columns[0]];
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from './GridDisplay';
|
||||
export * from './GridConfig';
|
||||
export * from './PerspectiveConfig';
|
||||
export * from './PerspectiveTreeNode';
|
||||
export * from './TableGridDisplay';
|
||||
export * from './ViewGridDisplay';
|
||||
export * from './JslGridDisplay';
|
||||
@@ -12,3 +14,7 @@ export * from './FormViewDisplay';
|
||||
export * from './TableFormViewDisplay';
|
||||
export * from './CollectionGridDisplay';
|
||||
export * from './deleteCascade';
|
||||
export * from './PerspectiveDisplay';
|
||||
export * from './PerspectiveDataProvider';
|
||||
export * from './PerspectiveCache';
|
||||
export * from './PerspectiveConfig';
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { TableInfo } from 'dbgate-types';
|
||||
import { PerspectiveDisplay } from '../PerspectiveDisplay';
|
||||
import { PerspectiveTableNode } from '../PerspectiveTreeNode';
|
||||
import { chinookDbInfo } from './chinookDbInfo';
|
||||
import { createPerspectiveConfig } from '../PerspectiveConfig';
|
||||
import artistDataFlat from './artistDataFlat';
|
||||
import artistDataAlbum from './artistDataAlbum';
|
||||
import artistDataAlbumTrack from './artistDataAlbumTrack';
|
||||
|
||||
test('test flat view', () => {
|
||||
const artistTable = chinookDbInfo.tables.find(x => x.pureName == 'Artist');
|
||||
const root = new PerspectiveTableNode(
|
||||
artistTable,
|
||||
{ conid: { db: chinookDbInfo } },
|
||||
createPerspectiveConfig({ pureName: 'Artist' }),
|
||||
null,
|
||||
null,
|
||||
{ conid: 'conid', database: 'db' },
|
||||
null
|
||||
);
|
||||
const display = new PerspectiveDisplay(root, artistDataFlat);
|
||||
|
||||
// console.log(display.loadIndicatorsCounts);
|
||||
// console.log(display.rows);
|
||||
expect(display.rows.length).toEqual(4);
|
||||
expect(display.rows[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
rowData: ['AC/DC'],
|
||||
})
|
||||
);
|
||||
expect(display.loadIndicatorsCounts).toEqual({
|
||||
Artist: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test('test one level nesting', () => {
|
||||
const artistTable = chinookDbInfo.tables.find(x => x.pureName == 'Artist');
|
||||
const root = new PerspectiveTableNode(
|
||||
artistTable,
|
||||
{ conid: { db: chinookDbInfo } },
|
||||
{ ...createPerspectiveConfig({ pureName: 'Artist' }), checkedColumns: ['Artist::Album'] },
|
||||
null,
|
||||
null,
|
||||
{ conid: 'conid', database: 'db' },
|
||||
null
|
||||
);
|
||||
const display = new PerspectiveDisplay(root, artistDataAlbum);
|
||||
|
||||
console.log(display.loadIndicatorsCounts);
|
||||
// console.log(display.rows);
|
||||
expect(display.rows.length).toEqual(6);
|
||||
expect(display.rows[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
rowData: ['AC/DC', 'For Those About To Rock We Salute You'],
|
||||
rowSpans: [2, 1],
|
||||
rowCellSkips: [false, false],
|
||||
})
|
||||
);
|
||||
expect(display.rows[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
rowData: [undefined, 'Let There Be Rock'],
|
||||
rowSpans: [1, 1],
|
||||
rowCellSkips: [true, false],
|
||||
})
|
||||
);
|
||||
expect(display.rows[2]).toEqual(
|
||||
expect.objectContaining({
|
||||
rowData: ['Accept', 'Balls to the Wall'],
|
||||
rowSpans: [2, 1],
|
||||
rowCellSkips: [false, false],
|
||||
})
|
||||
);
|
||||
expect(display.rows[5]).toEqual(
|
||||
expect.objectContaining({
|
||||
rowData: ['Alanis Morissette', 'Jagged Little Pill'],
|
||||
rowSpans: [1, 1],
|
||||
})
|
||||
);
|
||||
|
||||
expect(display.loadIndicatorsCounts).toEqual({
|
||||
Artist: 6,
|
||||
'Artist.Album': 6,
|
||||
});
|
||||
});
|
||||
|
||||
test('test two level nesting', () => {
|
||||
const artistTable = chinookDbInfo.tables.find(x => x.pureName == 'Artist');
|
||||
const root = new PerspectiveTableNode(
|
||||
artistTable,
|
||||
{ conid: { db: chinookDbInfo } },
|
||||
{ ...createPerspectiveConfig({ pureName: 'Artist' }), checkedColumns: ['Artist::Album', 'Artist::Album::Track'] },
|
||||
null,
|
||||
null,
|
||||
{ conid: 'conid', database: 'db' },
|
||||
null
|
||||
);
|
||||
const display = new PerspectiveDisplay(root, artistDataAlbumTrack);
|
||||
|
||||
console.log(display.rows);
|
||||
expect(display.rows.length).toEqual(8);
|
||||
expect(display.rows[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
rowData: ['AC/DC', 'For Those About To Rock We Salute You', 'For Those About To Rock (We Salute You)'],
|
||||
rowSpans: [4, 2, 1],
|
||||
rowCellSkips: [false, false, false],
|
||||
})
|
||||
);
|
||||
expect(display.rows[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
rowData: [undefined, undefined, 'Put The Finger On You'],
|
||||
rowSpans: [1, 1, 1],
|
||||
rowCellSkips: [true, true, false],
|
||||
})
|
||||
);
|
||||
expect(display.rows[2]).toEqual(
|
||||
expect.objectContaining({
|
||||
rowData: [undefined, 'Let There Be Rock', 'Go Down'],
|
||||
rowSpans: [1, 2, 1],
|
||||
rowCellSkips: [true, false, false],
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
export default [
|
||||
{
|
||||
ArtistId: 1,
|
||||
Name: 'AC/DC',
|
||||
Album: [
|
||||
{
|
||||
Title: 'For Those About To Rock We Salute You',
|
||||
ArtistId: 1,
|
||||
},
|
||||
{
|
||||
Title: 'Let There Be Rock',
|
||||
ArtistId: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ArtistId: 2,
|
||||
Name: 'Accept',
|
||||
Album: [
|
||||
{
|
||||
Title: 'Balls to the Wall',
|
||||
ArtistId: 2,
|
||||
},
|
||||
{
|
||||
Title: 'Restless and Wild',
|
||||
ArtistId: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ArtistId: 3,
|
||||
Name: 'Aerosmith',
|
||||
Album: [
|
||||
{
|
||||
Title: 'Big Ones',
|
||||
ArtistId: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ArtistId: 4,
|
||||
Name: 'Alanis Morissette',
|
||||
Album: [
|
||||
{
|
||||
Title: 'Jagged Little Pill',
|
||||
ArtistId: 4,
|
||||
},
|
||||
{
|
||||
incompleteRowsIndicator: ['Artist.Album'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
incompleteRowsIndicator: ['Artist'],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,78 @@
|
||||
export default [
|
||||
{
|
||||
ArtistId: 1,
|
||||
Name: 'AC/DC',
|
||||
Album: [
|
||||
{
|
||||
Title: 'For Those About To Rock We Salute You',
|
||||
AlbumId: 1,
|
||||
ArtistId: 1,
|
||||
Track: [
|
||||
{
|
||||
Name: 'For Those About To Rock (We Salute You)',
|
||||
AlbumId: 1,
|
||||
},
|
||||
{
|
||||
Name: 'Put The Finger On You',
|
||||
AlbumId: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Title: 'Let There Be Rock',
|
||||
AlbumId: 4,
|
||||
ArtistId: 1,
|
||||
Track: [
|
||||
{
|
||||
Name: 'Go Down',
|
||||
AlbumId: 4,
|
||||
},
|
||||
{
|
||||
Name: 'Dog Eat Dog',
|
||||
AlbumId: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
ArtistId: 2,
|
||||
Name: 'Accept',
|
||||
Album: [
|
||||
{
|
||||
Title: 'Balls to the Wall',
|
||||
AlbumId: 2,
|
||||
ArtistId: 2,
|
||||
Track: [
|
||||
{
|
||||
Name: 'Balls to the Wall',
|
||||
AlbumId: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Title: 'Restless and Wild',
|
||||
AlbumId: 3,
|
||||
ArtistId: 2,
|
||||
Track: [
|
||||
{
|
||||
Name: 'Fast As a Shark',
|
||||
AlbumId: 3,
|
||||
},
|
||||
{
|
||||
Name: 'Restless and Wild',
|
||||
AlbumId: 3,
|
||||
},
|
||||
{
|
||||
Name: 'Princess of the Dawn',
|
||||
AlbumId: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
incompleteRowsIndicator: ['Artist'],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
export default [
|
||||
{
|
||||
ArtistId: 1,
|
||||
Name: 'AC/DC',
|
||||
},
|
||||
{
|
||||
ArtistId: 2,
|
||||
Name: 'Accept',
|
||||
},
|
||||
{
|
||||
ArtistId: 3,
|
||||
Name: 'Aerosmith',
|
||||
},
|
||||
{
|
||||
ArtistId: 4,
|
||||
Name: 'Alanis Morissette',
|
||||
},
|
||||
{
|
||||
incompleteRowsIndicator: ['Artist'],
|
||||
},
|
||||
];
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,8 @@
|
||||
"dbgate-types": "^5.0.0-alpha.1",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^24.9.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"jest": "^28.1.3",
|
||||
"ts-jest": "^28.0.7",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import P from 'parsimmon';
|
||||
import moment from 'moment';
|
||||
import { FilterType } from './types';
|
||||
import { Condition } from 'dbgate-sqltree';
|
||||
import { TransformType } from 'dbgate-types';
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
|
||||
const compoudCondition = conditionType => conditions => {
|
||||
if (conditions.length == 1) return conditions[0];
|
||||
return {
|
||||
[conditionType]: conditions,
|
||||
};
|
||||
};
|
||||
|
||||
function getTransformCondition(transform: TransformType, value) {
|
||||
return {
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'transform',
|
||||
transform,
|
||||
expr: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const yearCondition = () => value => {
|
||||
return getTransformCondition('YEAR', value);
|
||||
};
|
||||
|
||||
const yearMonthCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)/);
|
||||
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [getTransformCondition('YEAR', m[1]), getTransformCondition('MONTH', m[2])],
|
||||
};
|
||||
};
|
||||
|
||||
const yearMonthDayCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)/);
|
||||
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
getTransformCondition('YEAR', m[1]),
|
||||
getTransformCondition('MONTH', m[2]),
|
||||
getTransformCondition('DAY', m[3]),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const yearEdge = edgeFunction => value => {
|
||||
return moment(new Date(parseInt(value), 0, 1))
|
||||
[edgeFunction]('year')
|
||||
.format('YYYY-MM-DDTHH:mm:ss.SSS');
|
||||
};
|
||||
|
||||
const yearMonthEdge = edgeFunction => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)/);
|
||||
|
||||
return moment(new Date(parseInt(m[1]), parseInt(m[2]) - 1, 1))
|
||||
[edgeFunction]('month')
|
||||
.format('YYYY-MM-DDTHH:mm:ss.SSS');
|
||||
};
|
||||
|
||||
const yearMonthDayEdge = edgeFunction => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)/);
|
||||
|
||||
return moment(new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3])))
|
||||
[edgeFunction]('day')
|
||||
.format('YYYY-MM-DDTHH:mm:ss.SSS');
|
||||
};
|
||||
|
||||
const yearMonthDayMinuteEdge = edgeFunction => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)\s+(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[4];
|
||||
const minute = m[5];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute);
|
||||
|
||||
return moment(dateObject)[edgeFunction]('minute').format('YYYY-MM-DDTHH:mm:ss.SSS');
|
||||
};
|
||||
|
||||
const yearMonthDayMinuteSecondEdge = edgeFunction => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)(T|\s+)(\d\d?):(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[5];
|
||||
const minute = m[6];
|
||||
const second = m[7];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute, second);
|
||||
|
||||
return moment(dateObject)[edgeFunction]('second').format('YYYY-MM-DDTHH:mm:ss.SSS');
|
||||
};
|
||||
|
||||
const createIntervalCondition = (start, end) => {
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
{
|
||||
conditionType: 'binary',
|
||||
operator: '>=',
|
||||
left: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: start,
|
||||
},
|
||||
},
|
||||
{
|
||||
conditionType: 'binary',
|
||||
operator: '<=',
|
||||
left: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: end,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const createDateIntervalCondition = (start, end) => {
|
||||
return createIntervalCondition(start.format('YYYY-MM-DDTHH:mm:ss.SSS'), end.format('YYYY-MM-DDTHH:mm:ss.SSS'));
|
||||
};
|
||||
|
||||
const fixedMomentIntervalCondition = (intervalType, diff) => () => {
|
||||
return createDateIntervalCondition(
|
||||
moment().add(intervalType, diff).startOf(intervalType),
|
||||
moment().add(intervalType, diff).endOf(intervalType)
|
||||
);
|
||||
};
|
||||
|
||||
const yearMonthDayMinuteCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)\s+(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[4];
|
||||
const minute = m[5];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute);
|
||||
|
||||
return createDateIntervalCondition(moment(dateObject).startOf('minute'), moment(dateObject).endOf('minute'));
|
||||
};
|
||||
|
||||
const yearMonthDaySecondCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)(T|\s+)(\d\d?):(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[5];
|
||||
const minute = m[6];
|
||||
const second = m[7];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute, second);
|
||||
|
||||
return createDateIntervalCondition(moment(dateObject).startOf('second'), moment(dateObject).endOf('second'));
|
||||
};
|
||||
|
||||
const binaryCondition = operator => value => ({
|
||||
conditionType: 'binary',
|
||||
operator,
|
||||
left: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
const createParser = () => {
|
||||
const langDef = {
|
||||
comma: () => word(','),
|
||||
|
||||
yearNum: () => P.regexp(/\d\d\d\d/).map(yearCondition()),
|
||||
yearMonthNum: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthCondition()),
|
||||
yearMonthDayNum: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayCondition()),
|
||||
yearMonthDayMinute: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteCondition()),
|
||||
yearMonthDaySecond: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
|
||||
|
||||
yearNumStart: () => P.regexp(/\d\d\d\d/).map(yearEdge('startOf')),
|
||||
yearNumEnd: () => P.regexp(/\d\d\d\d/).map(yearEdge('endOf')),
|
||||
yearMonthStart: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthEdge('startOf')),
|
||||
yearMonthEnd: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthEdge('endOf')),
|
||||
yearMonthDayStart: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayEdge('startOf')),
|
||||
yearMonthDayEnd: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayEdge('endOf')),
|
||||
yearMonthDayMinuteStart: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteEdge('startOf')),
|
||||
yearMonthDayMinuteEnd: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteEdge('endOf')),
|
||||
yearMonthDayMinuteSecondStart: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDayMinuteSecondEdge('startOf')),
|
||||
yearMonthDayMinuteSecondEnd: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDayMinuteSecondEdge('endOf')),
|
||||
|
||||
this: () => word('THIS'),
|
||||
last: () => word('LAST'),
|
||||
next: () => word('NEXT'),
|
||||
week: () => word('WEEK'),
|
||||
month: () => word('MONTH'),
|
||||
year: () => word('YEAR'),
|
||||
|
||||
yesterday: () => word('YESTERDAY').map(fixedMomentIntervalCondition('day', -1)),
|
||||
today: () => word('TODAY').map(fixedMomentIntervalCondition('day', 0)),
|
||||
tomorrow: () => word('TOMORROW').map(fixedMomentIntervalCondition('day', 1)),
|
||||
|
||||
lastWeek: r => r.last.then(r.week).map(fixedMomentIntervalCondition('week', -1)),
|
||||
thisWeek: r => r.this.then(r.week).map(fixedMomentIntervalCondition('week', 0)),
|
||||
nextWeek: r => r.next.then(r.week).map(fixedMomentIntervalCondition('week', 1)),
|
||||
|
||||
lastMonth: r => r.last.then(r.month).map(fixedMomentIntervalCondition('month', -1)),
|
||||
thisMonth: r => r.this.then(r.month).map(fixedMomentIntervalCondition('month', 0)),
|
||||
nextMonth: r => r.next.then(r.month).map(fixedMomentIntervalCondition('month', 1)),
|
||||
|
||||
lastYear: r => r.last.then(r.year).map(fixedMomentIntervalCondition('year', -1)),
|
||||
thisYear: r => r.this.then(r.year).map(fixedMomentIntervalCondition('year', 0)),
|
||||
nextYear: r => r.next.then(r.year).map(fixedMomentIntervalCondition('year', 1)),
|
||||
|
||||
valueStart: r =>
|
||||
P.alt(
|
||||
r.yearMonthDayMinuteSecondStart,
|
||||
r.yearMonthDayMinuteStart,
|
||||
r.yearMonthDayStart,
|
||||
r.yearMonthStart,
|
||||
r.yearNumStart
|
||||
),
|
||||
valueEnd: r =>
|
||||
P.alt(r.yearMonthDayMinuteSecondEnd, r.yearMonthDayMinuteEnd, r.yearMonthDayEnd, r.yearMonthEnd, r.yearNumEnd),
|
||||
|
||||
le: r => word('<=').then(r.valueEnd).map(binaryCondition('<=')),
|
||||
ge: r => word('>=').then(r.valueStart).map(binaryCondition('>=')),
|
||||
lt: r => word('<').then(r.valueStart).map(binaryCondition('<')),
|
||||
gt: r => word('>').then(r.valueEnd).map(binaryCondition('>')),
|
||||
|
||||
element: r =>
|
||||
P.alt(
|
||||
r.yearMonthDaySecond,
|
||||
r.yearMonthDayMinute,
|
||||
r.yearMonthDayNum,
|
||||
r.yearMonthNum,
|
||||
r.yearNum,
|
||||
r.yesterday,
|
||||
r.today,
|
||||
r.tomorrow,
|
||||
r.lastWeek,
|
||||
r.thisWeek,
|
||||
r.nextWeek,
|
||||
r.lastMonth,
|
||||
r.thisMonth,
|
||||
r.nextMonth,
|
||||
r.lastYear,
|
||||
r.thisYear,
|
||||
r.nextYear,
|
||||
r.le,
|
||||
r.lt,
|
||||
r.ge,
|
||||
r.gt
|
||||
).trim(whitespace),
|
||||
factor: r => r.element.sepBy(whitespace).map(compoudCondition('$and')),
|
||||
list: r => r.factor.sepBy(r.comma).map(compoudCondition('$or')),
|
||||
};
|
||||
|
||||
return P.createLanguage(langDef);
|
||||
};
|
||||
|
||||
export const datetimeParser = createParser();
|
||||
@@ -3,7 +3,7 @@ import moment from 'moment';
|
||||
|
||||
export type FilterMultipleValuesMode = 'is' | 'is_not' | 'contains' | 'begins' | 'ends';
|
||||
|
||||
export function getFilterValueExpression(value, dataType) {
|
||||
export function getFilterValueExpression(value, dataType?) {
|
||||
if (value == null) return 'NULL';
|
||||
if (isTypeDateTime(dataType)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
if (value === true) return 'TRUE';
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Condition } from 'dbgate-sqltree';
|
||||
import { TransformType } from 'dbgate-types';
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
import { mongoParser } from './mongoParser';
|
||||
import { datetimeParser } from './datetimeParser';
|
||||
|
||||
const binaryCondition = operator => value => ({
|
||||
conditionType: 'binary',
|
||||
@@ -67,116 +68,6 @@ const negateCondition = condition => {
|
||||
};
|
||||
};
|
||||
|
||||
function getTransformCondition(transform: TransformType, value) {
|
||||
return {
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'transform',
|
||||
transform,
|
||||
expr: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const yearCondition = () => value => {
|
||||
return getTransformCondition('YEAR', value);
|
||||
};
|
||||
|
||||
const yearMonthCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)/);
|
||||
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [getTransformCondition('YEAR', m[1]), getTransformCondition('MONTH', m[2])],
|
||||
};
|
||||
};
|
||||
|
||||
const yearMonthDayCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)/);
|
||||
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
getTransformCondition('YEAR', m[1]),
|
||||
getTransformCondition('MONTH', m[2]),
|
||||
getTransformCondition('DAY', m[3]),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const createIntervalCondition = (start, end) => {
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
{
|
||||
conditionType: 'binary',
|
||||
operator: '>=',
|
||||
left: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: start,
|
||||
},
|
||||
},
|
||||
{
|
||||
conditionType: 'binary',
|
||||
operator: '<=',
|
||||
left: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: end,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const createDateIntervalCondition = (start, end) => {
|
||||
return createIntervalCondition(start.format('YYYY-MM-DDTHH:mm:ss.SSS'), end.format('YYYY-MM-DDTHH:mm:ss.SSS'));
|
||||
};
|
||||
|
||||
const fixedMomentIntervalCondition = (intervalType, diff) => () => {
|
||||
return createDateIntervalCondition(
|
||||
moment().add(intervalType, diff).startOf(intervalType),
|
||||
moment().add(intervalType, diff).endOf(intervalType)
|
||||
);
|
||||
};
|
||||
|
||||
const yearMonthDayMinuteCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)\s+(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[4];
|
||||
const minute = m[5];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute);
|
||||
|
||||
return createDateIntervalCondition(moment(dateObject).startOf('minute'), moment(dateObject).endOf('minute'));
|
||||
};
|
||||
|
||||
const yearMonthDaySecondCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)(T|\s+)(\d\d?):(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[5];
|
||||
const minute = m[6];
|
||||
const second = m[7];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute, second);
|
||||
|
||||
return createDateIntervalCondition(moment(dateObject).startOf('second'), moment(dateObject).endOf('second'));
|
||||
};
|
||||
|
||||
const createParser = (filterType: FilterType) => {
|
||||
const langDef = {
|
||||
string1: () =>
|
||||
@@ -206,13 +97,6 @@ const createParser = (filterType: FilterType) => {
|
||||
|
||||
noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'),
|
||||
|
||||
yearNum: () => P.regexp(/\d\d\d\d/).map(yearCondition()),
|
||||
yearMonthNum: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthCondition()),
|
||||
yearMonthDayNum: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayCondition()),
|
||||
yearMonthDayMinute: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteCondition()),
|
||||
yearMonthDaySecond: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
|
||||
|
||||
value: r => P.alt(...allowedValues.map(x => r[x])),
|
||||
valueTestEq: r => r.value.map(binaryCondition('=')),
|
||||
valueTestStr: r => r.value.map(likeCondition('like', '%#VALUE#%')),
|
||||
@@ -223,33 +107,10 @@ const createParser = (filterType: FilterType) => {
|
||||
null: () => word('NULL').map(unaryCondition('isNull')),
|
||||
empty: () => word('EMPTY').map(unaryCondition('isEmpty')),
|
||||
notEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')),
|
||||
true: () => P.regexp(/true/i).map(binaryFixedValueCondition(1)),
|
||||
false: () => P.regexp(/false/i).map(binaryFixedValueCondition(0)),
|
||||
trueNum: () => word('1').map(binaryFixedValueCondition(1)),
|
||||
falseNum: () => word('0').map(binaryFixedValueCondition(0)),
|
||||
|
||||
this: () => word('THIS'),
|
||||
last: () => word('LAST'),
|
||||
next: () => word('NEXT'),
|
||||
week: () => word('WEEK'),
|
||||
month: () => word('MONTH'),
|
||||
year: () => word('YEAR'),
|
||||
|
||||
yesterday: () => word('YESTERDAY').map(fixedMomentIntervalCondition('day', -1)),
|
||||
today: () => word('TODAY').map(fixedMomentIntervalCondition('day', 0)),
|
||||
tomorrow: () => word('TOMORROW').map(fixedMomentIntervalCondition('day', 1)),
|
||||
|
||||
lastWeek: r => r.last.then(r.week).map(fixedMomentIntervalCondition('week', -1)),
|
||||
thisWeek: r => r.this.then(r.week).map(fixedMomentIntervalCondition('week', 0)),
|
||||
nextWeek: r => r.next.then(r.week).map(fixedMomentIntervalCondition('week', 1)),
|
||||
|
||||
lastMonth: r => r.last.then(r.month).map(fixedMomentIntervalCondition('month', -1)),
|
||||
thisMonth: r => r.this.then(r.month).map(fixedMomentIntervalCondition('month', 0)),
|
||||
nextMonth: r => r.next.then(r.month).map(fixedMomentIntervalCondition('month', 1)),
|
||||
|
||||
lastYear: r => r.last.then(r.year).map(fixedMomentIntervalCondition('year', -1)),
|
||||
thisYear: r => r.this.then(r.year).map(fixedMomentIntervalCondition('year', 0)),
|
||||
nextYear: r => r.next.then(r.year).map(fixedMomentIntervalCondition('year', 1)),
|
||||
true: () => P.regexp(/true/i).map(binaryFixedValueCondition('1')),
|
||||
false: () => P.regexp(/false/i).map(binaryFixedValueCondition('0')),
|
||||
trueNum: () => word('1').map(binaryFixedValueCondition('1')),
|
||||
falseNum: () => word('0').map(binaryFixedValueCondition('0')),
|
||||
|
||||
eq: r => word('=').then(r.value).map(binaryCondition('=')),
|
||||
ne: r => word('!=').then(r.value).map(binaryCondition('<>')),
|
||||
@@ -294,27 +155,7 @@ const createParser = (filterType: FilterType) => {
|
||||
if (filterType == 'eval') {
|
||||
allowedElements.push('true', 'false');
|
||||
}
|
||||
if (filterType == 'datetime') {
|
||||
allowedElements.push(
|
||||
'yearMonthDaySecond',
|
||||
'yearMonthDayMinute',
|
||||
'yearMonthDayNum',
|
||||
'yearMonthNum',
|
||||
'yearNum',
|
||||
'yesterday',
|
||||
'today',
|
||||
'tomorrow',
|
||||
'lastWeek',
|
||||
'thisWeek',
|
||||
'nextWeek',
|
||||
'lastMonth',
|
||||
'thisMonth',
|
||||
'nextMonth',
|
||||
'lastYear',
|
||||
'thisYear',
|
||||
'nextYear'
|
||||
);
|
||||
}
|
||||
|
||||
// must be last
|
||||
if (filterType == 'string' || filterType == 'eval') {
|
||||
allowedElements.push('valueTestStr');
|
||||
@@ -328,10 +169,10 @@ const createParser = (filterType: FilterType) => {
|
||||
const parsers = {
|
||||
number: createParser('number'),
|
||||
string: createParser('string'),
|
||||
datetime: createParser('datetime'),
|
||||
logical: createParser('logical'),
|
||||
eval: createParser('eval'),
|
||||
mongo: mongoParser,
|
||||
datetime: datetimeParser,
|
||||
};
|
||||
|
||||
export function parseFilter(value: string, filterType: FilterType): Condition {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseFilter } from './parseFilter';
|
||||
const { parseFilter } = require('./parseFilter');
|
||||
|
||||
test('parse string', () => {
|
||||
const ast = parseFilter('"123"', 'string');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { SqlDumper } from 'dbgate-types';
|
||||
import { Condition, BinaryCondition } from './types';
|
||||
import { dumpSqlExpression } from './dumpSqlExpression';
|
||||
import { link } from 'fs';
|
||||
import { dumpSqlSelect } from './dumpSqlCommand';
|
||||
|
||||
export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
@@ -69,5 +68,9 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
dmp.put(' ^and ');
|
||||
dumpSqlExpression(dmp, condition.right);
|
||||
break;
|
||||
case 'in':
|
||||
dumpSqlExpression(dmp, condition.expr);
|
||||
dmp.put(' ^in (%,v)', condition.values);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,17 +35,24 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||
dmp.put(')');
|
||||
break;
|
||||
|
||||
case 'methodCall':
|
||||
dumpSqlExpression(dmp, expr.thisObject)
|
||||
dmp.put('.%s(', expr.method);
|
||||
dmp.putCollection(',', expr.args, x => dumpSqlExpression(dmp, x));
|
||||
dmp.put(')');
|
||||
break;
|
||||
|
||||
case 'transform':
|
||||
dmp.transform(expr.transform, () => dumpSqlExpression(dmp, expr.expr));
|
||||
break;
|
||||
|
||||
case 'rowNumber':
|
||||
dmp.put(" ^row_number() ^over (^order ^by ");
|
||||
dmp.put(' ^row_number() ^over (^order ^by ');
|
||||
dmp.putCollection(', ', expr.orderBy, x => {
|
||||
dumpSqlExpression(dmp, x);
|
||||
dmp.put(' %k', x.direction);
|
||||
});
|
||||
dmp.put(")");
|
||||
dmp.put(')');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ export function evaluateExpression(expr: Expression, values) {
|
||||
case 'call':
|
||||
return null;
|
||||
|
||||
case 'methodCall':
|
||||
return null;
|
||||
|
||||
case 'transform':
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -99,6 +99,12 @@ export interface BetweenCondition {
|
||||
right: Expression;
|
||||
}
|
||||
|
||||
export interface InCondition {
|
||||
conditionType: 'in';
|
||||
expr: Expression;
|
||||
values: any[];
|
||||
}
|
||||
|
||||
export type Condition =
|
||||
| BinaryCondition
|
||||
| NotCondition
|
||||
@@ -107,7 +113,8 @@ export type Condition =
|
||||
| LikeCondition
|
||||
| ExistsCondition
|
||||
| NotExistsCondition
|
||||
| BetweenCondition;
|
||||
| BetweenCondition
|
||||
| InCondition;
|
||||
|
||||
export interface Source {
|
||||
name?: NamedObjectInfo;
|
||||
@@ -155,6 +162,13 @@ export interface CallExpression {
|
||||
argsPrefix?: string; // DISTINCT in case of COUNT DISTINCT
|
||||
}
|
||||
|
||||
export interface MethodCallExpression {
|
||||
exprType: 'methodCall';
|
||||
method: string;
|
||||
args: Expression[];
|
||||
thisObject: Expression;
|
||||
}
|
||||
|
||||
export interface TranformExpression {
|
||||
exprType: 'transform';
|
||||
expr: Expression;
|
||||
@@ -172,6 +186,7 @@ export type Expression =
|
||||
| PlaceholderExpression
|
||||
| RawExpression
|
||||
| CallExpression
|
||||
| MethodCallExpression
|
||||
| TranformExpression
|
||||
| RowNumberExpression;
|
||||
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
|
||||
|
||||
@@ -31,9 +31,11 @@
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"dbgate-query-splitter": "^4.9.0",
|
||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"uuid": "^3.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import _compact from 'lodash/compact';
|
||||
import _isString from 'lodash/isString';
|
||||
|
||||
export interface FilterNameDefinition {
|
||||
childName: string;
|
||||
}
|
||||
|
||||
// original C# variant
|
||||
// public bool Match(string value)
|
||||
@@ -54,17 +59,29 @@ function fuzzysearch(needle, haystack) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function filterName(filter: string, ...names: string[]) {
|
||||
export function filterName(filter: string, ...names: (string | FilterNameDefinition)[]) {
|
||||
if (!filter) return true;
|
||||
|
||||
// const camelVariants = [name.replace(/[^A-Z]/g, '')]
|
||||
const tokens = filter.split(' ').map(x => x.trim());
|
||||
|
||||
const namesCompacted = _compact(names);
|
||||
|
||||
// @ts-ignore
|
||||
const namesOwn: string[] = namesCompacted.filter(x => _isString(x));
|
||||
// @ts-ignore
|
||||
const namesChild: string[] = namesCompacted.filter(x => x.childName).map(x => x.childName);
|
||||
|
||||
for (const token of tokens) {
|
||||
const tokenUpper = token.toUpperCase();
|
||||
const found = namesCompacted.find(name => fuzzysearch(tokenUpper, name.toUpperCase()));
|
||||
if (!found) return false;
|
||||
if (tokenUpper.startsWith('#')) {
|
||||
const tokenUpperSub = tokenUpper.substring(1);
|
||||
const found = namesChild.find(name => fuzzysearch(tokenUpperSub, name.toUpperCase()));
|
||||
if (!found) return false;
|
||||
} else {
|
||||
const found = namesOwn.find(name => fuzzysearch(tokenUpper, name.toUpperCase()));
|
||||
if (!found) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import _cloneDeep from 'lodash/cloneDeep';
|
||||
import _isString from 'lodash/isString';
|
||||
import { ColumnInfo, ColumnReference, DatabaseInfo, DatabaseInfoObjects, SqlDialect, TableInfo } from 'dbgate-types';
|
||||
|
||||
export function fullNameFromString(name) {
|
||||
@@ -54,7 +55,10 @@ export function findObjectLike(
|
||||
return dbinfo[objectTypeField]?.find(x => equalStringLike(x.pureName, pureName));
|
||||
}
|
||||
|
||||
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) {
|
||||
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo | string) {
|
||||
if (_isString(column)) {
|
||||
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column));
|
||||
}
|
||||
return (table.foreignKeys || []).find(fk => fk.columns.find(col => col.columnName == column.columnName));
|
||||
}
|
||||
|
||||
@@ -76,7 +80,7 @@ function columnsConstraintName(prefix: string, table: TableInfo, columns: Column
|
||||
|
||||
export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
|
||||
if (!table) return table;
|
||||
const res = _.cloneDeep(table);
|
||||
const res = _cloneDeep(table);
|
||||
if (res.primaryKey && !res.primaryKey.constraintName && !dialect.anonymousPrimaryKey) {
|
||||
res.primaryKey.constraintName = `PK_${res.pureName}`;
|
||||
}
|
||||
|
||||
@@ -84,3 +84,12 @@ export function getIconForRedisType(type) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isWktGeometry(s) {
|
||||
if (!_isString(s)) return false;
|
||||
|
||||
// return !!s.match(/^POINT\s*\(|/)
|
||||
return !!s.match(
|
||||
/^POINT\s*\(|^LINESTRING\s*\(|^POLYGON\s*\(|^MULTIPOINT\s*\(|^MULTILINESTRING\s*\(|^MULTIPOLYGON\s*\(|^GEOMCOLLECTION\s*\(|^GEOMETRYCOLLECTION\s*\(/
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ColumnInfo, TableInfo, ForeignKeyInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import { StringNullableChain } from 'lodash';
|
||||
import _cloneDeep from 'lodash/cloneDeep';
|
||||
import _compact from 'lodash/compact';
|
||||
import { DatabaseAnalyser } from './DatabaseAnalyser';
|
||||
@@ -11,6 +12,7 @@ export interface ColumnInfoYaml {
|
||||
autoIncrement?: boolean;
|
||||
references?: string;
|
||||
primaryKey?: boolean;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export interface DatabaseModelFile {
|
||||
@@ -39,6 +41,7 @@ function columnInfoToYaml(column: ColumnInfo, table: TableInfo): ColumnInfoYaml
|
||||
const res: ColumnInfoYaml = {
|
||||
name: column.columnName,
|
||||
type: column.dataType,
|
||||
default: column.defaultValue,
|
||||
};
|
||||
if (column.autoIncrement) res.autoIncrement = true;
|
||||
if (column.notNull) res.notNull = true;
|
||||
@@ -71,6 +74,7 @@ function columnInfoFromYaml(column: ColumnInfoYaml, table: TableInfoYaml): Colum
|
||||
dataType: column.length ? `${column.type}(${column.length})` : column.type,
|
||||
autoIncrement: column.autoIncrement,
|
||||
notNull: column.notNull || (table.primaryKey && table.primaryKey.includes(column.name)),
|
||||
defaultValue: column.default,
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
Vendored
+7
-4
@@ -1,6 +1,8 @@
|
||||
export interface NamedObjectInfo {
|
||||
pureName: string;
|
||||
schemaName?: string;
|
||||
contentHash?: string;
|
||||
engine?: string;
|
||||
}
|
||||
|
||||
export interface ColumnReference {
|
||||
@@ -31,7 +33,8 @@ export interface ForeignKeyInfo extends ColumnsConstraintInfo {
|
||||
|
||||
export interface IndexInfo extends ColumnsConstraintInfo {
|
||||
isUnique: boolean;
|
||||
indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext';
|
||||
// indexType: 'normal' | 'clustered' | 'xml' | 'spatial' | 'fulltext';
|
||||
indexType: string;
|
||||
}
|
||||
|
||||
export interface UniqueInfo extends ColumnsConstraintInfo {}
|
||||
@@ -43,8 +46,8 @@ export interface CheckInfo extends ConstraintInfo {
|
||||
export interface ColumnInfo extends NamedObjectInfo {
|
||||
pairingId?: string;
|
||||
columnName: string;
|
||||
notNull: boolean;
|
||||
autoIncrement: boolean;
|
||||
notNull?: boolean;
|
||||
autoIncrement?: boolean;
|
||||
dataType: string;
|
||||
precision?: number;
|
||||
scale?: number;
|
||||
@@ -119,7 +122,7 @@ export interface DatabaseInfoObjects {
|
||||
}
|
||||
|
||||
export interface DatabaseInfo extends DatabaseInfoObjects {
|
||||
schemas: SchemaInfo[];
|
||||
schemas?: SchemaInfo[];
|
||||
engine?: string;
|
||||
defaultSchema?: string;
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -34,4 +34,7 @@ export interface SqlDialect {
|
||||
disableExplicitTransaction?: boolean;
|
||||
|
||||
predefinedDataTypes: string[];
|
||||
|
||||
// create sql-tree expression
|
||||
createColumnViewExpression(columnName: string, dataType: string, source: { alias: string }, alias?: string): any;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
"dependencies": {
|
||||
"chartjs-plugin-zoom": "^1.2.0",
|
||||
"date-fns": "^2.28.0",
|
||||
"interval-operations": "^1.0.7"
|
||||
"debug": "^4.3.4",
|
||||
"interval-operations": "^1.0.7",
|
||||
"leaflet": "^1.8.0",
|
||||
"wellknown": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import _ from 'lodash';
|
||||
import FontIcon from './icons/FontIcon.svelte';
|
||||
|
||||
import { extensions } from './stores';
|
||||
import { extensions, isFileDragActive } from './stores';
|
||||
|
||||
import getElectron from './utility/getElectron';
|
||||
|
||||
@@ -21,6 +21,15 @@
|
||||
</div>
|
||||
<div class="title">Drop the files to upload to DbGate</div>
|
||||
<div class="info">Supported file types: {fileTypeNames.join(', ')}</div>
|
||||
<div
|
||||
class="class-button"
|
||||
on:click={() => {
|
||||
$isFileDragActive = false;
|
||||
}}
|
||||
>
|
||||
<FontIcon icon="icon close" padRight />
|
||||
Close
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,4 +62,12 @@
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.class-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
font-size: 14pt;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -76,6 +76,10 @@
|
||||
on:dragstart={e => {
|
||||
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
||||
}}
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragend
|
||||
on:drop
|
||||
>
|
||||
{#if checkedObjectsStore}
|
||||
<CheckboxField
|
||||
|
||||
@@ -27,11 +27,51 @@
|
||||
conid: connection._id,
|
||||
keepOpen: true,
|
||||
});
|
||||
if (!config.runAsPortal) {
|
||||
expandedConnections.update(x => _.uniq([...x, connection._id]));
|
||||
expandedConnections.update(x => _.uniq([...x, connection._id]));
|
||||
|
||||
// if (!config.runAsPortal && getCurrentSettings()['defaultAction.connectionClick'] != 'connect') {
|
||||
// expandedConnections.update(x => _.uniq([...x, connection._id]));
|
||||
// }
|
||||
}
|
||||
// closeMultipleTabs(x => x.tabComponent == 'ConnectionTab' && x.props?.conid == connection._id, true);
|
||||
}
|
||||
export function disconnectServerConnection(conid, showConfirmation = true) {
|
||||
const closeCondition = x => x.props?.conid == conid && x.tabComponent != 'ConnectionTab' && x.closedTime == null;
|
||||
|
||||
if (showConfirmation) {
|
||||
const count = getOpenedTabs().filter(closeCondition).length;
|
||||
if (count > 0) {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Closing connection will close ${count} opened tabs, continue?`,
|
||||
onConfirm: () => disconnectServerConnection(conid, false),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
closeMultipleTabs(x => x.tabComponent == 'ConnectionTab' && x.props?.conid == connection._id, true);
|
||||
|
||||
const electron = getElectron();
|
||||
const currentDb = getCurrentDatabase();
|
||||
openedConnections.update(list => list.filter(x => x != conid));
|
||||
if (electron) {
|
||||
apiCall('server-connections/disconnect', { conid });
|
||||
}
|
||||
if (currentDb?.connection?._id == conid) {
|
||||
if (electron) {
|
||||
apiCall('database-connections/disconnect', { conid, database: currentDb.name });
|
||||
}
|
||||
currentDatabase.set(null);
|
||||
}
|
||||
closeMultipleTabs(closeCondition);
|
||||
// if (data.unsaved) {
|
||||
// openNewTab({
|
||||
// title: 'New Connection',
|
||||
// icon: 'img connection',
|
||||
// tabComponent: 'ConnectionTab',
|
||||
// props: {
|
||||
// conid: data._id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -43,7 +83,10 @@
|
||||
expandedConnections,
|
||||
extensions,
|
||||
getCurrentConfig,
|
||||
getCurrentDatabase,
|
||||
getCurrentSettings,
|
||||
getOpenedConnections,
|
||||
getOpenedTabs,
|
||||
openedConnections,
|
||||
openedSingleDatabaseConnections,
|
||||
} from '../stores';
|
||||
@@ -61,6 +104,7 @@
|
||||
import ImportDatabaseDumpModal from '../modals/ImportDatabaseDumpModal.svelte';
|
||||
import { closeMultipleTabs } from '../widgets/TabsPanel.svelte';
|
||||
import AboutModal from '../modals/AboutModal.svelte';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -88,9 +132,10 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
const handleClick = async () => {
|
||||
const config = getCurrentConfig();
|
||||
if (config.runAsPortal) {
|
||||
await tick();
|
||||
handleConnect();
|
||||
return;
|
||||
}
|
||||
@@ -102,7 +147,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
handleOpenConnectionTab();
|
||||
if (getCurrentSettings()['defaultAction.connectionClick'] == 'openDetails') {
|
||||
handleOpenConnectionTab();
|
||||
} else {
|
||||
await tick();
|
||||
handleConnect();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSqlRestore = () => {
|
||||
@@ -118,27 +168,7 @@
|
||||
apiCall('server-connections/refresh', { conid: data._id });
|
||||
};
|
||||
const handleDisconnect = () => {
|
||||
openedConnections.update(list => list.filter(x => x != data._id));
|
||||
if (electron) {
|
||||
apiCall('server-connections/disconnect', { conid: data._id });
|
||||
}
|
||||
if (_.get($currentDatabase, 'connection._id') == data._id) {
|
||||
if (electron) {
|
||||
apiCall('database-connections/disconnect', { conid: data._id, database: $currentDatabase.name });
|
||||
}
|
||||
currentDatabase.set(null);
|
||||
}
|
||||
closeMultipleTabs(x => x.props.conid == data._id);
|
||||
if (data.unsaved) {
|
||||
openNewTab({
|
||||
title: 'New Connection',
|
||||
icon: 'img connection',
|
||||
tabComponent: 'ConnectionTab',
|
||||
props: {
|
||||
conid: data._id,
|
||||
},
|
||||
});
|
||||
}
|
||||
disconnectServerConnection(data._id);
|
||||
};
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
@@ -180,8 +210,8 @@
|
||||
|
||||
return [
|
||||
config.runAsPortal == false && [
|
||||
!$openedConnections.includes(data._id) && {
|
||||
text: 'Edit',
|
||||
{
|
||||
text: $openedConnections.includes(data._id) ? 'View details' : 'Edit',
|
||||
onClick: handleOpenConnectionTab,
|
||||
},
|
||||
!$openedConnections.includes(data._id) && {
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = props => props.name;
|
||||
|
||||
export function disconnectDatabaseConnection(conid, database, showConfirmation = true) {
|
||||
const closeCondition = x =>
|
||||
x.props?.conid == conid &&
|
||||
x.props?.database == database &&
|
||||
x.tabComponent != 'ConnectionTab' &&
|
||||
x.closedTime == null;
|
||||
|
||||
if (showConfirmation) {
|
||||
const count = getOpenedTabs().filter(closeCondition).length;
|
||||
if (count > 0) {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Closing connection will close ${count} opened tabs, continue?`,
|
||||
onConfirm: () => disconnectDatabaseConnection(conid, database, false),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const electron = getElectron();
|
||||
if (electron) {
|
||||
apiCall('database-connections/disconnect', { conid, database });
|
||||
}
|
||||
if (getCurrentDatabase()?.connection?._id == conid && getCurrentDatabase()?.name == database) {
|
||||
currentDatabase.set(null);
|
||||
}
|
||||
openedSingleDatabaseConnections.update(list => list.filter(x => x != conid));
|
||||
closeMultipleTabs(closeCondition);
|
||||
}
|
||||
|
||||
export function getDatabaseMenuItems(
|
||||
connection,
|
||||
name,
|
||||
@@ -54,12 +83,7 @@
|
||||
label: 'New collection name',
|
||||
header: 'Create collection',
|
||||
onConfirm: async newCollection => {
|
||||
const dbid = { conid: connection._id, database: name };
|
||||
await apiCall('database-connections/run-script', {
|
||||
...dbid,
|
||||
sql: `db.createCollection('${newCollection}')`,
|
||||
});
|
||||
await apiCall('database-connections/sync-model', dbid);
|
||||
saveScriptToDatabase({ conid: connection._id, database: name }, `db.createCollection('${newCollection}')`);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -135,14 +159,7 @@
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
const electron = getElectron();
|
||||
if (electron) {
|
||||
apiCall('database-connections/disconnect', { conid: connection._id, database: name });
|
||||
}
|
||||
if (getCurrentDatabase()?.connection?._id == connection._id && getCurrentDatabase()?.name == name) {
|
||||
currentDatabase.set(null);
|
||||
}
|
||||
openedSingleDatabaseConnections.update(list => list.filter(x => x != connection._id));
|
||||
disconnectDatabaseConnection(connection._id, name);
|
||||
};
|
||||
|
||||
const handleExportModel = async () => {
|
||||
@@ -202,13 +219,7 @@
|
||||
};
|
||||
|
||||
async function handleConfirmSql(sql) {
|
||||
const resp = await apiCall('database-connections/run-script', { conid: connection._id, database: name, sql });
|
||||
const { errorMessage } = resp || {};
|
||||
if (errorMessage) {
|
||||
showModal(ErrorMessageModal, { title: 'Error when executing script', message: errorMessage });
|
||||
} else {
|
||||
showSnackbarSuccess('Saved to database');
|
||||
}
|
||||
saveScriptToDatabase({ conid: connection._id, database: name }, sql, false);
|
||||
}
|
||||
|
||||
const driver = findEngineDriver(connection, getExtensions());
|
||||
@@ -279,6 +290,7 @@
|
||||
extensions,
|
||||
getCurrentDatabase,
|
||||
getExtensions,
|
||||
getOpenedTabs,
|
||||
openedConnections,
|
||||
openedSingleDatabaseConnections,
|
||||
pinnedDatabases,
|
||||
@@ -294,12 +306,14 @@
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
|
||||
import ConfirmSqlModal, { saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';
|
||||
import { filterAppsForDatabase } from '../utility/appTools';
|
||||
import newQuery from '../query/newQuery';
|
||||
import { exportSqlDump } from '../utility/exportFileTools';
|
||||
import ImportDatabaseDumpModal from '../modals/ImportDatabaseDumpModal.svelte';
|
||||
import ExportDatabaseDumpModal from '../modals/ExportDatabaseDumpModal.svelte';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { closeMultipleTabs } from '../widgets/TabsPanel.svelte';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -315,7 +329,7 @@
|
||||
);
|
||||
}
|
||||
|
||||
$: isPinned = !!$pinnedDatabases.find(x => x.name == data.name && x.connection?._id == data.connection?._id);
|
||||
$: isPinned = !!$pinnedDatabases.find(x => x?.name == data.name && x?.connection?._id == data.connection?._id);
|
||||
$: apps = useUsedApps();
|
||||
</script>
|
||||
|
||||
@@ -330,6 +344,10 @@
|
||||
isBold={_.get($currentDatabase, 'connection._id') == _.get(data.connection, '_id') &&
|
||||
_.get($currentDatabase, 'name') == data.name}
|
||||
on:click={() => ($currentDatabase = data)}
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragend
|
||||
on:drop
|
||||
on:middleclick={() => {
|
||||
createMenu()
|
||||
.find(x => x.isNewQuery)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts" context="module">
|
||||
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
|
||||
export const createMatcher = ({ schemaName, pureName }) => filter => filterName(filter, pureName, schemaName);
|
||||
const electron = getElectron();
|
||||
export const createMatcher = ({ schemaName, pureName, columns }) => filter =>
|
||||
filterName(filter, pureName, schemaName, ...(columns?.map(({ columnName }) => ({ childName: columnName })) || []));
|
||||
export const createTitle = ({ pureName }) => pureName;
|
||||
|
||||
export const databaseObjectIcons = {
|
||||
tables: 'img table',
|
||||
@@ -43,21 +44,34 @@
|
||||
tab: 'TableStructureTab',
|
||||
icon: 'img table-structure',
|
||||
},
|
||||
{
|
||||
label: 'Open perspective',
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Drop table',
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
label: 'Rename table',
|
||||
isRename: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
label: 'Create table backup',
|
||||
isDuplicateTable: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
label: 'Query designer',
|
||||
isQueryDesigner: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
label: 'Show diagram',
|
||||
@@ -74,6 +88,7 @@
|
||||
{
|
||||
label: 'Import',
|
||||
isImport: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
label: 'Open as data sheet',
|
||||
@@ -127,6 +142,12 @@
|
||||
tab: 'TableStructureTab',
|
||||
icon: 'img view-structure',
|
||||
},
|
||||
{
|
||||
label: 'Open perspective',
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
{
|
||||
label: 'Drop view',
|
||||
isDrop: true,
|
||||
@@ -339,13 +360,213 @@
|
||||
],
|
||||
};
|
||||
|
||||
async function databaseObjectMenuClickHandler(data, menu) {
|
||||
const getDriver = async () => {
|
||||
const conn = await getConnectionInfo(data);
|
||||
if (!conn) return;
|
||||
const driver = findEngineDriver(conn, getExtensions());
|
||||
return driver;
|
||||
};
|
||||
|
||||
if (menu.isOpenFreeTable) {
|
||||
const coninfo = await getConnectionInfo(data);
|
||||
openNewTab({
|
||||
title: data.pureName,
|
||||
icon: 'img free-table',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props: {
|
||||
initialArgs: {
|
||||
functionName: 'tableReader',
|
||||
props: {
|
||||
connection: {
|
||||
...coninfo,
|
||||
database: data.database,
|
||||
},
|
||||
schemaName: data.schemaName,
|
||||
pureName: data.pureName,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (menu.isActiveChart) {
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
dmp.put('^select * from %f', data);
|
||||
openNewTab(
|
||||
{
|
||||
title: data.pureName,
|
||||
icon: 'img chart',
|
||||
tabComponent: 'ChartTab',
|
||||
props: {
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
config: { chartType: 'bar' },
|
||||
sql: dmp.s,
|
||||
},
|
||||
}
|
||||
);
|
||||
} else if (menu.isQueryDesigner) {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Query #',
|
||||
icon: 'img query-design',
|
||||
tabComponent: 'QueryDesignTab',
|
||||
props: {
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
tables: [
|
||||
{
|
||||
...data,
|
||||
designerId: uuidv1(),
|
||||
left: 50,
|
||||
top: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
} else if (menu.isDiagram) {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Diagram #',
|
||||
icon: 'img diagram',
|
||||
tabComponent: 'DiagramTab',
|
||||
props: {
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
tables: [
|
||||
{
|
||||
...data,
|
||||
designerId: `${data.pureName}-${uuidv1()}`,
|
||||
autoAddReferences: true,
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
autoLayout: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
} else if (menu.sqlGeneratorProps) {
|
||||
showModal(SqlGeneratorModal, {
|
||||
initialObjects: [data],
|
||||
initialConfig: menu.sqlGeneratorProps,
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
});
|
||||
} else if (menu.isDrop) {
|
||||
const { conid, database } = data;
|
||||
alterDatabaseDialog(conid, database, db => {
|
||||
_.remove(
|
||||
db[data.objectTypeField] as any[],
|
||||
x => x.schemaName == data.schemaName && x.pureName == data.pureName
|
||||
);
|
||||
});
|
||||
} else if (menu.isRename) {
|
||||
const { conid, database } = data;
|
||||
renameDatabaseObjectDialog(conid, database, data.pureName, (db, newName) => {
|
||||
const obj = db[data.objectTypeField].find(x => x.schemaName == data.schemaName && x.pureName == data.pureName);
|
||||
obj.pureName = newName;
|
||||
});
|
||||
} else if (menu.isDropCollection) {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really drop collection ${data.pureName}?`,
|
||||
onConfirm: async () => {
|
||||
saveScriptToDatabase(_.pick(data, ['conid', 'database']), `db.dropCollection('${data.pureName}')`);
|
||||
const dbid = _.pick(data, ['conid', 'database']);
|
||||
},
|
||||
});
|
||||
} else if (menu.isRenameCollection) {
|
||||
showModal(InputTextModal, {
|
||||
label: 'New collection name',
|
||||
header: 'Rename collection',
|
||||
value: data.pureName,
|
||||
onConfirm: async newName => {
|
||||
const dbid = _.pick(data, ['conid', 'database']);
|
||||
await apiCall('database-connections/run-script', {
|
||||
...dbid,
|
||||
sql: `db.renameCollection('${data.pureName}', '${newName}')`,
|
||||
});
|
||||
apiCall('database-connections/sync-model', dbid);
|
||||
},
|
||||
});
|
||||
} else if (menu.isDuplicateTable) {
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
const newTable = _.cloneDeep(data);
|
||||
const { conid, database } = data;
|
||||
|
||||
newTable.pureName = `_${newTable.pureName}_${dateFormat(new Date(), 'yyyy-MM-dd-hh-mm-ss')}`;
|
||||
newTable.columns.forEach(x => {
|
||||
x.autoIncrement = false;
|
||||
x.defaultConstraint = null;
|
||||
});
|
||||
newTable.foreignKeys = [];
|
||||
newTable.checks = [];
|
||||
newTable.uniques = [];
|
||||
newTable.indexes = [];
|
||||
if (newTable.primaryKey) {
|
||||
newTable.primaryKey.constraintName = null;
|
||||
}
|
||||
dmp.createTable(newTable);
|
||||
dmp.putCmd(
|
||||
'^insert ^into %f(%,i) ^select %,i from %f',
|
||||
newTable,
|
||||
newTable.columns.map(x => x.columnName),
|
||||
data.columns.map(x => x.columnName),
|
||||
data
|
||||
);
|
||||
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql: dmp.s,
|
||||
onConfirm: async () => {
|
||||
saveScriptToDatabase({ conid, database }, dmp.s);
|
||||
},
|
||||
engine: driver.engine,
|
||||
});
|
||||
} else if (menu.isImport) {
|
||||
const { conid, database } = data;
|
||||
showModal(ImportExportModal, {
|
||||
initialValues: {
|
||||
sourceStorageType: getDefaultFileFormat(getExtensions()).storageType,
|
||||
targetStorageType: 'database',
|
||||
targetConnectionId: conid,
|
||||
targetDatabaseName: database,
|
||||
fixedTargetPureName: data.pureName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
openDatabaseObjectDetail(
|
||||
menu.tab,
|
||||
menu.scriptTemplate,
|
||||
data,
|
||||
menu.forceNewTab,
|
||||
menu.initialData,
|
||||
menu.icon,
|
||||
data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function openDatabaseObjectDetail(
|
||||
tabComponent,
|
||||
scriptTemplate,
|
||||
{ schemaName, pureName, conid, database, objectTypeField },
|
||||
forceNewTab?,
|
||||
initialData?,
|
||||
icon?
|
||||
icon?,
|
||||
appObjectData?
|
||||
) {
|
||||
const connection = await getConnectionInfo({ conid });
|
||||
const tooltip = `${getConnectionLabel(connection)}\n${database}\n${fullDisplayName({
|
||||
@@ -359,6 +580,8 @@
|
||||
tooltip,
|
||||
icon: icon || (scriptTemplate ? 'img sql-file' : databaseObjectIcons[objectTypeField]),
|
||||
tabComponent: scriptTemplate ? 'QueryTab' : tabComponent,
|
||||
appObject: 'DatabaseObjectAppObject',
|
||||
appObjectData,
|
||||
props: {
|
||||
schemaName,
|
||||
pureName,
|
||||
@@ -376,6 +599,13 @@
|
||||
export function handleDatabaseObjectClick(data, forceNewTab = false) {
|
||||
const { schemaName, pureName, conid, database, objectTypeField } = data;
|
||||
|
||||
const configuredAction = getCurrentSettings()[`defaultAction.dbObjectClick.${objectTypeField}`];
|
||||
const overrideMenu = menus[objectTypeField].find(x => x.label && x.label == configuredAction);
|
||||
if (overrideMenu) {
|
||||
databaseObjectMenuClickHandler(data, overrideMenu);
|
||||
return;
|
||||
}
|
||||
|
||||
openDatabaseObjectDetail(
|
||||
defaultTabs[objectTypeField],
|
||||
defaultTabs[objectTypeField] ? null : 'CREATE OBJECT',
|
||||
@@ -388,7 +618,8 @@
|
||||
},
|
||||
forceNewTab,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
@@ -402,14 +633,7 @@
|
||||
);
|
||||
}
|
||||
|
||||
export function createDatabaseObjectMenu(data) {
|
||||
const getDriver = async () => {
|
||||
const conn = await getConnectionInfo(data);
|
||||
if (!conn) return;
|
||||
const driver = findEngineDriver(conn, getExtensions());
|
||||
return driver;
|
||||
};
|
||||
|
||||
export function createDatabaseObjectMenu(data, connection = null) {
|
||||
const { objectTypeField } = data;
|
||||
return menus[objectTypeField]
|
||||
.filter(x => x)
|
||||
@@ -448,198 +672,13 @@
|
||||
);
|
||||
}
|
||||
|
||||
if (connection?.isReadOnly && menu.requiresWriteAccess) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
text: menu.label,
|
||||
onClick: async () => {
|
||||
if (menu.isOpenFreeTable) {
|
||||
const coninfo = await getConnectionInfo(data);
|
||||
openNewTab({
|
||||
title: data.pureName,
|
||||
icon: 'img free-table',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props: {
|
||||
initialArgs: {
|
||||
functionName: 'tableReader',
|
||||
props: {
|
||||
connection: {
|
||||
...coninfo,
|
||||
database: data.database,
|
||||
},
|
||||
schemaName: data.schemaName,
|
||||
pureName: data.pureName,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (menu.isActiveChart) {
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
dmp.put('^select * from %f', data);
|
||||
openNewTab(
|
||||
{
|
||||
title: data.pureName,
|
||||
icon: 'img chart',
|
||||
tabComponent: 'ChartTab',
|
||||
props: {
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
config: { chartType: 'bar' },
|
||||
sql: dmp.s,
|
||||
},
|
||||
}
|
||||
);
|
||||
} else if (menu.isQueryDesigner) {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Query #',
|
||||
icon: 'img query-design',
|
||||
tabComponent: 'QueryDesignTab',
|
||||
props: {
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
tables: [
|
||||
{
|
||||
...data,
|
||||
designerId: uuidv1(),
|
||||
left: 50,
|
||||
top: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
} else if (menu.isDiagram) {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Diagram #',
|
||||
icon: 'img diagram',
|
||||
tabComponent: 'DiagramTab',
|
||||
props: {
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
tables: [
|
||||
{
|
||||
...data,
|
||||
designerId: `${data.pureName}-${uuidv1()}`,
|
||||
autoAddReferences: true,
|
||||
},
|
||||
],
|
||||
references: [],
|
||||
autoLayout: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
} else if (menu.sqlGeneratorProps) {
|
||||
showModal(SqlGeneratorModal, {
|
||||
initialObjects: [data],
|
||||
initialConfig: menu.sqlGeneratorProps,
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
});
|
||||
} else if (menu.isDrop) {
|
||||
const { conid, database } = data;
|
||||
alterDatabaseDialog(conid, database, db => {
|
||||
_.remove(
|
||||
db[data.objectTypeField] as any[],
|
||||
x => x.schemaName == data.schemaName && x.pureName == data.pureName
|
||||
);
|
||||
});
|
||||
} else if (menu.isRename) {
|
||||
const { conid, database } = data;
|
||||
renameDatabaseObjectDialog(conid, database, data.pureName, (db, newName) => {
|
||||
const obj = db[data.objectTypeField].find(
|
||||
x => x.schemaName == data.schemaName && x.pureName == data.pureName
|
||||
);
|
||||
obj.pureName = newName;
|
||||
});
|
||||
} else if (menu.isDropCollection) {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really drop collection ${data.pureName}?`,
|
||||
onConfirm: async () => {
|
||||
const dbid = _.pick(data, ['conid', 'database']);
|
||||
await apiCall('database-connections/run-script', {
|
||||
...dbid,
|
||||
sql: `db.dropCollection('${data.pureName}')`,
|
||||
});
|
||||
apiCall('database-connections/sync-model', dbid);
|
||||
},
|
||||
});
|
||||
} else if (menu.isRenameCollection) {
|
||||
showModal(InputTextModal, {
|
||||
label: 'New collection name',
|
||||
header: 'Rename collection',
|
||||
value: data.pureName,
|
||||
onConfirm: async newName => {
|
||||
const dbid = _.pick(data, ['conid', 'database']);
|
||||
await apiCall('database-connections/run-script', {
|
||||
...dbid,
|
||||
sql: `db.renameCollection('${data.pureName}', '${newName}')`,
|
||||
});
|
||||
apiCall('database-connections/sync-model', dbid);
|
||||
},
|
||||
});
|
||||
} else if (menu.isDuplicateTable) {
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
const newTable = _.cloneDeep(data);
|
||||
const { conid, database } = data;
|
||||
|
||||
newTable.pureName = `_${newTable.pureName}_${dateFormat(new Date(), 'yyyy-MM-dd-hh-mm-ss')}`;
|
||||
newTable.columns.forEach(x => {
|
||||
x.autoIncrement = false;
|
||||
});
|
||||
newTable.foreignKeys = [];
|
||||
newTable.indexes = [];
|
||||
dmp.createTable(newTable);
|
||||
dmp.putCmd(
|
||||
'^insert ^into %f(%,i) ^select %,i from %f',
|
||||
newTable,
|
||||
newTable.columns.map(x => x.columnName),
|
||||
data.columns.map(x => x.columnName),
|
||||
data
|
||||
);
|
||||
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql: dmp.s,
|
||||
onConfirm: async () => {
|
||||
const resp = await apiCall('database-connections/run-script', { conid, database, sql: dmp.s });
|
||||
await apiCall('database-connections/sync-model', { conid, database });
|
||||
},
|
||||
engine: driver.engine,
|
||||
});
|
||||
} else if (menu.isImport) {
|
||||
const { conid, database } = data;
|
||||
showModal(ImportExportModal, {
|
||||
initialValues: {
|
||||
sourceStorageType: getDefaultFileFormat(getExtensions()).storageType,
|
||||
targetStorageType: 'database',
|
||||
targetConnectionId: conid,
|
||||
targetDatabaseName: database,
|
||||
fixedTargetPureName: data.pureName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
openDatabaseObjectDetail(
|
||||
menu.tab,
|
||||
menu.scriptTemplate,
|
||||
data,
|
||||
menu.forceNewTab,
|
||||
menu.initialData,
|
||||
menu.icon
|
||||
);
|
||||
}
|
||||
onClick: () => {
|
||||
databaseObjectMenuClickHandler(data, menu);
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -650,12 +689,23 @@
|
||||
if (_.isNaN(num)) return value;
|
||||
return num.toLocaleString();
|
||||
}
|
||||
|
||||
export function createAppObjectMenu(data) {
|
||||
return createDatabaseObjectMenu(data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
import { currentDatabase, extensions, getExtensions, openedConnections, pinnedTables } from '../stores';
|
||||
import {
|
||||
currentDatabase,
|
||||
extensions,
|
||||
getCurrentSettings,
|
||||
getExtensions,
|
||||
openedConnections,
|
||||
pinnedTables,
|
||||
} from '../stores';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { filterName, generateDbPairingId, getAlterDatabaseScript } from 'dbgate-tools';
|
||||
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
|
||||
@@ -666,10 +716,9 @@
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
|
||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { exportQuickExportFile } from '../utility/exportFileTools';
|
||||
import createQuickExportMenu from '../utility/createQuickExportMenu';
|
||||
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
|
||||
import ConfirmSqlModal, { saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';
|
||||
import { alterDatabaseDialog, renameDatabaseObjectDialog } from '../utility/alterDatabaseTools';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
@@ -686,7 +735,7 @@
|
||||
}
|
||||
|
||||
function createMenu() {
|
||||
return createDatabaseObjectMenu(data);
|
||||
return createDatabaseObjectMenu(data, passProps?.connection);
|
||||
}
|
||||
|
||||
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
|
||||
@@ -706,4 +755,8 @@
|
||||
on:click={() => handleClick()}
|
||||
on:middleclick={() => handleClick(true)}
|
||||
on:expand
|
||||
on:dragstart
|
||||
on:dragenter
|
||||
on:dragend
|
||||
on:drop
|
||||
/>
|
||||
|
||||
@@ -8,14 +8,67 @@
|
||||
}
|
||||
return data.connection._id;
|
||||
};
|
||||
|
||||
function dragExchange(dragged, data, pinned, setPinned, compare) {
|
||||
if (!compare(dragged, data)) {
|
||||
const i1 = _.findIndex(pinned, x => compare(x, dragged));
|
||||
const i2 = _.findIndex(pinned, x => compare(x, data));
|
||||
if (i1 >= 0 && i2 >= 0 && i1 != i2) {
|
||||
const newPinned = [...pinned];
|
||||
const tmp = newPinned[i1];
|
||||
newPinned[i1] = newPinned[i2];
|
||||
newPinned[i2] = tmp;
|
||||
setPinned(newPinned);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _, { values } from 'lodash';
|
||||
import { draggedPinnedObject, pinnedDatabases, pinnedTables } from '../stores';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
{#if data.objectTypeField}
|
||||
<DatabaseObjectAppObject {...$$props} />
|
||||
{:else}
|
||||
<DatabaseAppObject {...$$props} />
|
||||
{#if data}
|
||||
{#if data.objectTypeField}
|
||||
<DatabaseObjectAppObject
|
||||
{...$$props}
|
||||
on:dragstart={() => {
|
||||
$draggedPinnedObject = data;
|
||||
}}
|
||||
on:dragenter={e => {
|
||||
dragExchange(
|
||||
$draggedPinnedObject,
|
||||
data,
|
||||
$pinnedTables,
|
||||
value => ($pinnedTables = value),
|
||||
(a, b) => a?.pureName == b?.pureName && a?.schemaName == b?.schemaName
|
||||
);
|
||||
}}
|
||||
on:dragend={() => {
|
||||
$draggedPinnedObject = null;
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<DatabaseAppObject
|
||||
{...$$props}
|
||||
on:dragstart={() => {
|
||||
$draggedPinnedObject = data;
|
||||
}}
|
||||
on:dragenter={e => {
|
||||
dragExchange(
|
||||
$draggedPinnedObject,
|
||||
data,
|
||||
$pinnedDatabases,
|
||||
value => ($pinnedDatabases = value),
|
||||
(a, b) => a?.name == b?.name && a?.connection?._id == b?.connection?._id
|
||||
);
|
||||
}}
|
||||
on:dragend={() => {
|
||||
$draggedPinnedObject = null;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -65,6 +65,14 @@
|
||||
currentConnection: true,
|
||||
};
|
||||
|
||||
const perspectives: FileTypeHandler = {
|
||||
icon: 'img perspective',
|
||||
format: 'json',
|
||||
tabComponent: 'PerspectiveTab',
|
||||
folder: 'pesrpectives',
|
||||
currentConnection: true,
|
||||
};
|
||||
|
||||
export const SAVED_FILE_HANDLERS = {
|
||||
sql,
|
||||
shell,
|
||||
@@ -73,10 +81,14 @@
|
||||
query,
|
||||
sqlite,
|
||||
diagrams,
|
||||
perspectives,
|
||||
};
|
||||
|
||||
export const extractKey = data => data.file;
|
||||
export const createMatcher = ({ file }) => filter => filterName(filter, file);
|
||||
export const createMatcher =
|
||||
({ file }) =>
|
||||
filter =>
|
||||
filterName(filter, file);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as DatabaseObjectAppObject from './DatabaseObjectAppObject.svelte';
|
||||
|
||||
export default {
|
||||
DatabaseObjectAppObject,
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<script context="module">
|
||||
function getCommandTitle(command) {
|
||||
let res = command.text;
|
||||
if (command.keyText || command.keyTextFromGroup) {
|
||||
res += ` (${formatKeyText(command.keyText || command.keyTextFromGroup)})`;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { commandsCustomized } from '../stores';
|
||||
import { formatKeyText } from '../utility/common';
|
||||
import FormStyledButton from './FormStyledButton.svelte';
|
||||
|
||||
export let command;
|
||||
export let component = FormStyledButton;
|
||||
export let hideDisabled = false;
|
||||
|
||||
$: cmd = Object.values($commandsCustomized).find((x: any) => x.id == command) as any;
|
||||
</script>
|
||||
|
||||
{#if cmd && (!hideDisabled || cmd.enabled)}
|
||||
<svelte:component
|
||||
this={component}
|
||||
title={getCommandTitle(cmd)}
|
||||
icon={cmd.icon}
|
||||
on:click={cmd.onClick}
|
||||
disabled={!cmd.enabled}
|
||||
value={cmd.toolbarName || cmd.name}
|
||||
{...$$restProps}
|
||||
/>
|
||||
{/if}
|
||||
@@ -16,6 +16,7 @@
|
||||
export let command;
|
||||
export let component = ToolStripButton;
|
||||
export let hideDisabled = false;
|
||||
export let buttonLabel = null;
|
||||
|
||||
$: cmd = Object.values($commandsCustomized).find((x: any) => x.id == command) as any;
|
||||
</script>
|
||||
@@ -29,6 +30,6 @@
|
||||
disabled={!cmd.enabled}
|
||||
{...$$restProps}
|
||||
>
|
||||
{cmd.toolbarName || cmd.name}
|
||||
{buttonLabel || cmd.toolbarName || cmd.name}
|
||||
</svelte:component>
|
||||
{/if}
|
||||
|
||||
@@ -5,7 +5,16 @@
|
||||
import ToolStripSplitDropDownButton from './ToolStripSplitDropDownButton.svelte';
|
||||
|
||||
export let commands;
|
||||
$: menu = _.compact(commands).map(command => ({ command }));
|
||||
export let hideDisabled = false;
|
||||
export let buttonLabel = null;
|
||||
|
||||
$: menu = _.compact(commands).map(command => (_.isString(command) ? { command } : command));
|
||||
</script>
|
||||
|
||||
<ToolStripCommandButton command={commands[0]} component={ToolStripSplitDropDownButton} {menu} />
|
||||
<ToolStripCommandButton
|
||||
command={commands[0]}
|
||||
component={ToolStripSplitDropDownButton}
|
||||
{menu}
|
||||
{hideDisabled}
|
||||
{buttonLabel}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MapView from '../elements/MapView.svelte';
|
||||
|
||||
export let selection;
|
||||
</script>
|
||||
|
||||
<MapView {selection} />
|
||||
@@ -6,7 +6,7 @@ import SettingsModal from '../settings/SettingsModal.svelte';
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import newQuery from '../query/newQuery';
|
||||
import newQuery, { newDiagram, newQueryDesign } from '../query/newQuery';
|
||||
import saveTabFile from '../utility/saveTabFile';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import getElectron from '../utility/getElectron';
|
||||
@@ -120,6 +120,24 @@ registerCommand({
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.queryDesign',
|
||||
category: 'New',
|
||||
icon: 'img query-design',
|
||||
name: 'Query design',
|
||||
menuName: 'New query design',
|
||||
onClick: () => newQueryDesign(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.diagram',
|
||||
category: 'New',
|
||||
icon: 'img diagram',
|
||||
name: 'ER Diagram',
|
||||
menuName: 'New ER diagram',
|
||||
onClick: () => newDiagram(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.archiveFolder',
|
||||
category: 'New',
|
||||
@@ -467,15 +485,6 @@ registerCommand({
|
||||
onClick: () => getElectron().send('quit-app'),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'new.window',
|
||||
category: 'New',
|
||||
name: 'New Window',
|
||||
keyText: 'CtrlOrCommand+N',
|
||||
testEnabled: () => getElectron() != null,
|
||||
onClick: () => getElectron().send('new-window'),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'app.logout',
|
||||
category: 'App',
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<script context="module">
|
||||
function makeBulletString(value) {
|
||||
return _.pad('', value.length, '•');
|
||||
}
|
||||
|
||||
function highlightSpecialCharacters(value) {
|
||||
value = value.replace(/\n/g, '↲');
|
||||
value = value.replace(/\r/g, '');
|
||||
value = value.replace(/^(\s+)/, makeBulletString);
|
||||
value = value.replace(/(\s+)$/, makeBulletString);
|
||||
value = value.replace(/(\s\s+)/g, makeBulletString);
|
||||
return value;
|
||||
}
|
||||
|
||||
// const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
|
||||
const dateTimeRegex =
|
||||
/^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|()|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
|
||||
|
||||
function formatNumber(value) {
|
||||
if (value >= 10000 || value <= -10000) {
|
||||
if (getBoolSettingsValue('dataGrid.thousandsSeparator', false)) {
|
||||
return value.toLocaleString();
|
||||
} else {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
function formatDateTime(testedString) {
|
||||
const m = testedString.match(dateTimeRegex);
|
||||
return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
import { arrayToHexString } from 'dbgate-tools';
|
||||
|
||||
export let rowData;
|
||||
export let value;
|
||||
export let jsonParsedValue = undefined;
|
||||
</script>
|
||||
|
||||
{#if rowData == null}
|
||||
<span class="null">(No row)</span>
|
||||
{:else if value === null}
|
||||
<span class="null">(NULL)</span>
|
||||
{:else if value === undefined}
|
||||
<span class="null">(No field)</span>
|
||||
{:else if _.isDate(value)}
|
||||
{value.toString()}
|
||||
{:else if value === true}
|
||||
<span class="value">true</span>
|
||||
{:else if value === false}
|
||||
<span class="value">false</span>
|
||||
{:else if _.isNumber(value)}
|
||||
<span class="value">{formatNumber(value)}</span>
|
||||
{:else if _.isString(value) && !jsonParsedValue}
|
||||
{#if dateTimeRegex.test(value)}
|
||||
<span class="value">
|
||||
{formatDateTime(value)}
|
||||
</span>
|
||||
{:else}
|
||||
{highlightSpecialCharacters(value)}
|
||||
{/if}
|
||||
{:else if value?.type == 'Buffer' && _.isArray(value.data)}
|
||||
{#if value.data.length <= 16}
|
||||
<span class="value">{'0x' + arrayToHexString(value.data)}</span>
|
||||
{:else}
|
||||
<span class="null">({value.data.length} bytes)</span>
|
||||
{/if}
|
||||
{:else if value.$oid}
|
||||
<span class="value">ObjectId("{value.$oid}")</span>
|
||||
{:else if _.isPlainObject(value)}
|
||||
<span class="null" title={JSON.stringify(value, undefined, 2)}>(JSON)</span>
|
||||
{:else if _.isArray(value)}
|
||||
<span class="null" title={value.map(x => JSON.stringify(x)).join('\n')}>[{value.length} items]</span>
|
||||
{:else if _.isPlainObject(jsonParsedValue)}
|
||||
<span class="null" title={JSON.stringify(jsonParsedValue, undefined, 2)}>(JSON)</span>
|
||||
{:else if _.isArray(jsonParsedValue)}
|
||||
<span class="null" title={jsonParsedValue.map(x => JSON.stringify(x)).join('\n')}
|
||||
>[{jsonParsedValue.length} items]</span
|
||||
>
|
||||
{:else}
|
||||
{value.toString()}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.null {
|
||||
color: var(--theme-font-3);
|
||||
font-style: italic;
|
||||
}
|
||||
.value {
|
||||
color: var(--theme-icon-green);
|
||||
}
|
||||
</style>
|
||||
@@ -105,19 +105,13 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { changeSetToSql, createChangeSet } from 'dbgate-datalib';
|
||||
import { parseFilter } from 'dbgate-filterparser';
|
||||
import { scriptToSql } from 'dbgate-sqltree';
|
||||
import _ from 'lodash';
|
||||
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
||||
import { extractShellConnection } from '../impexp/createImpExpScript';
|
||||
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import ImportExportModal from '../modals/ImportExportModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { extensions } from '../stores';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
|
||||
@@ -14,9 +14,13 @@
|
||||
export let column;
|
||||
export let conid = undefined;
|
||||
export let database = undefined;
|
||||
export let setSort;
|
||||
export let setSort = undefined;
|
||||
export let addToSort = undefined;
|
||||
export let clearSort = undefined;
|
||||
export let grouping = undefined;
|
||||
export let order = undefined;
|
||||
export let orderIndex = undefined;
|
||||
export let isSortDefined = false;
|
||||
export let allowDefineVirtualReferences = false;
|
||||
export let setGrouping;
|
||||
|
||||
@@ -44,6 +48,9 @@
|
||||
return [
|
||||
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
||||
setSort && { onClick: () => setSort('DESC'), text: 'Sort descending' },
|
||||
isSortDefined && addToSort && !order && { onClick: () => addToSort('ASC'), text: 'Add to sort - ascending' },
|
||||
isSortDefined && addToSort && !order && { onClick: () => addToSort('DESC'), text: 'Add to sort - descending' },
|
||||
order && clearSort && { onClick: () => clearSort(), text: 'Clear sort criteria' },
|
||||
{ onClick: () => copyTextToClipboard(column.columnName), text: 'Copy column name' },
|
||||
|
||||
column.foreignKey && [{ divider: true }, { onClick: openReferencedTable, text: column.foreignKey.refTableName }],
|
||||
@@ -90,11 +97,17 @@
|
||||
{#if order == 'ASC'}
|
||||
<span class="icon">
|
||||
<FontIcon icon="img sort-asc" />
|
||||
{#if orderIndex >= 0}
|
||||
<span class="color-icon-green order-index">{orderIndex + 1}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{#if order == 'DESC'}
|
||||
<span class="icon">
|
||||
<FontIcon icon="img sort-desc" />
|
||||
{#if orderIndex >= 0}
|
||||
<span class="color-icon-green order-index">{orderIndex + 1}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<DropDownButton menu={getMenu} narrow />
|
||||
@@ -106,6 +119,13 @@
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.order-index {
|
||||
font-size: 10pt;
|
||||
margin-left: -3px;
|
||||
margin-right: 2px;
|
||||
top: -1px;
|
||||
position: relative;
|
||||
}
|
||||
.label {
|
||||
flex: 1;
|
||||
min-width: 10px;
|
||||
|
||||
@@ -23,13 +23,16 @@
|
||||
export let filter;
|
||||
export let setFilter;
|
||||
export let showResizeSplitter = false;
|
||||
export let onFocusGrid;
|
||||
export let onGetReference;
|
||||
export let onFocusGrid = null;
|
||||
export let onGetReference = null;
|
||||
export let foreignKey = null;
|
||||
export let conid = null;
|
||||
export let database = null;
|
||||
export let driver = null;
|
||||
export let jslid = null;
|
||||
export let customCommandIcon = null;
|
||||
export let onCustomCommand = null;
|
||||
export let customCommandTooltip = null;
|
||||
|
||||
export let pureName = null;
|
||||
export let schemaName = null;
|
||||
@@ -295,6 +298,11 @@
|
||||
class:isOk
|
||||
placeholder="Filter"
|
||||
/>
|
||||
{#if customCommandIcon && onCustomCommand}
|
||||
<InlineButton on:click={onCustomCommand} title={customCommandTooltip} narrow square>
|
||||
<FontIcon icon={customCommandIcon} />
|
||||
</InlineButton>
|
||||
{/if}
|
||||
{#if conid && database && driver}
|
||||
{#if driver?.databaseEngineTypes?.includes('sql') && foreignKey}
|
||||
<InlineButton on:click={handleShowDictionary} narrow square>
|
||||
@@ -320,6 +328,7 @@
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 10px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
input.isError {
|
||||
|
||||
@@ -1,47 +1,10 @@
|
||||
<script context="module">
|
||||
function makeBulletString(value) {
|
||||
return _.pad('', value.length, '•');
|
||||
}
|
||||
|
||||
function highlightSpecialCharacters(value) {
|
||||
value = value.replace(/\n/g, '↲');
|
||||
value = value.replace(/\r/g, '');
|
||||
value = value.replace(/^(\s+)/, makeBulletString);
|
||||
value = value.replace(/(\s+)$/, makeBulletString);
|
||||
value = value.replace(/(\s\s+)/g, makeBulletString);
|
||||
return value;
|
||||
}
|
||||
|
||||
// const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
|
||||
const dateTimeRegex = /^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|()|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
|
||||
|
||||
function formatNumber(value) {
|
||||
if (value >= 10000 || value <= -10000) {
|
||||
if (getBoolSettingsValue('dataGrid.thousandsSeparator', false)) {
|
||||
return value.toLocaleString();
|
||||
} else {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
function formatDateTime(testedString) {
|
||||
const m = testedString.match(dateTimeRegex);
|
||||
return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}:${m[6]}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _, { isPlainObject, join } from 'lodash';
|
||||
import _ from 'lodash';
|
||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
import { arrayToHexString, isJsonLikeLongString, safeJsonParse } from 'dbgate-tools';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import DictionaryLookupModal from '../modals/DictionaryLookupModal.svelte';
|
||||
import { isJsonLikeLongString, safeJsonParse } from 'dbgate-tools';
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import CellValue from './CellValue.svelte';
|
||||
|
||||
export let rowIndex;
|
||||
export let col;
|
||||
@@ -101,49 +64,7 @@
|
||||
class:isFocusedColumn
|
||||
{style}
|
||||
>
|
||||
{#if rowData == null}
|
||||
<span class="null">(No row)</span>
|
||||
{:else if value === null}
|
||||
<span class="null">(NULL)</span>
|
||||
{:else if value === undefined}
|
||||
<span class="null">(No field)</span>
|
||||
{:else if _.isDate(value)}
|
||||
{value.toString()}
|
||||
{:else if value === true}
|
||||
<span class="value">true</span>
|
||||
{:else if value === false}
|
||||
<span class="value">false</span>
|
||||
{:else if _.isNumber(value)}
|
||||
<span class="value">{formatNumber(value)}</span>
|
||||
{:else if _.isString(value) && !jsonParsedValue}
|
||||
{#if dateTimeRegex.test(value)}
|
||||
<span class="value">
|
||||
{formatDateTime(value)}
|
||||
</span>
|
||||
{:else}
|
||||
{highlightSpecialCharacters(value)}
|
||||
{/if}
|
||||
{:else if value?.type == 'Buffer' && _.isArray(value.data)}
|
||||
{#if value.data.length <= 16}
|
||||
<span class="value">{'0x' + arrayToHexString(value.data)}</span>
|
||||
{:else}
|
||||
<span class="null">({value.data.length} bytes)</span>
|
||||
{/if}
|
||||
{:else if value.$oid}
|
||||
<span class="value">ObjectId("{value.$oid}")</span>
|
||||
{:else if _.isPlainObject(value)}
|
||||
<span class="null" title={JSON.stringify(value, undefined, 2)}>(JSON)</span>
|
||||
{:else if _.isArray(value)}
|
||||
<span class="null" title={value.map(x => JSON.stringify(x)).join('\n')}>[{value.length} items]</span>
|
||||
{:else if _.isPlainObject(jsonParsedValue)}
|
||||
<span class="null" title={JSON.stringify(jsonParsedValue, undefined, 2)}>(JSON)</span>
|
||||
{:else if _.isArray(jsonParsedValue)}
|
||||
<span class="null" title={jsonParsedValue.map(x => JSON.stringify(x)).join('\n')}
|
||||
>[{jsonParsedValue.length} items]</span
|
||||
>
|
||||
{:else}
|
||||
{value.toString()}
|
||||
{/if}
|
||||
<CellValue {rowData} {value} {jsonParsedValue} />
|
||||
|
||||
{#if allowHintField && rowData && _.some(col.hintColumnNames, hintColumnName => rowData[hintColumnName])}
|
||||
<span class="hint"
|
||||
@@ -256,13 +177,6 @@
|
||||
color: var(--theme-font-3);
|
||||
margin-left: 5px;
|
||||
}
|
||||
.null {
|
||||
color: var(--theme-font-3);
|
||||
font-style: italic;
|
||||
}
|
||||
.value {
|
||||
color: var(--theme-icon-green);
|
||||
}
|
||||
|
||||
.autoFillMarker {
|
||||
width: 8px;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
id: 'dataGrid.refresh',
|
||||
category: 'Data grid',
|
||||
name: 'Refresh',
|
||||
keyText: 'F5',
|
||||
keyText: 'F5 | CtrlOrCommand+R',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon reload',
|
||||
@@ -17,7 +17,7 @@
|
||||
id: 'dataGrid.revertRowChanges',
|
||||
category: 'Data grid',
|
||||
name: 'Revert row changes',
|
||||
keyText: 'CtrlOrCommand+R',
|
||||
keyText: 'CtrlOrCommand+U',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.containsChanges,
|
||||
onClick: () => getCurrentDataGrid().revertRowChanges(),
|
||||
});
|
||||
@@ -52,6 +52,16 @@
|
||||
onClick: () => getCurrentDataGrid().insertNewRow(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.cloneRows',
|
||||
category: 'Data grid',
|
||||
name: 'Clone rows',
|
||||
toolbarName: 'Clone row(s)',
|
||||
keyText: 'CtrlOrCommand+Shift+C',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
|
||||
onClick: () => getCurrentDataGrid().cloneRows(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.setNull',
|
||||
category: 'Data grid',
|
||||
@@ -112,6 +122,14 @@
|
||||
onClick: () => getCurrentDataGrid().editJsonDocument(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.openSelectionInMap',
|
||||
category: 'Data grid',
|
||||
name: 'Open selection in map',
|
||||
testEnabled: () => getCurrentDataGrid() != null, // ?.openSelectionInMapEnabled(),
|
||||
onClick: () => getCurrentDataGrid().openSelectionInMap(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.viewJsonDocument',
|
||||
category: 'Data grid',
|
||||
@@ -306,6 +324,8 @@
|
||||
import { apiCall } from '../utility/api';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { isCtrlOrCommandKey, isMac } from '../utility/common';
|
||||
import { selectionCouldBeShownOnMap } from '../elements/MapView.svelte';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
|
||||
export let onLoadNextData = undefined;
|
||||
export let grider = undefined;
|
||||
@@ -334,7 +354,8 @@
|
||||
export let collapsedLeftColumnStore;
|
||||
export let multipleGridsOnTab = false;
|
||||
export let tabControlHiddenTab = false;
|
||||
export let onCustomGridRefresh;
|
||||
export let onCustomGridRefresh = null;
|
||||
export let onOpenQuery = null;
|
||||
export let useEvalFilters = false;
|
||||
export let jslid;
|
||||
// export let generalAllowSave = false;
|
||||
@@ -407,16 +428,44 @@
|
||||
}
|
||||
|
||||
export async function insertNewRow() {
|
||||
if (grider.canInsert) {
|
||||
const rowIndex = grider.insertRow();
|
||||
const cell = [rowIndex, (currentCell && currentCell[1]) || 0];
|
||||
// @ts-ignore
|
||||
currentCell = cell;
|
||||
// @ts-ignore
|
||||
selectedCells = [cell];
|
||||
await tick();
|
||||
scrollIntoView(cell);
|
||||
if (!grider.canInsert) return;
|
||||
const rowIndex = grider.insertRow();
|
||||
const cell = [rowIndex, (currentCell && currentCell[1]) || 0];
|
||||
// @ts-ignore
|
||||
currentCell = cell;
|
||||
// @ts-ignore
|
||||
selectedCells = [cell];
|
||||
await tick();
|
||||
scrollIntoView(cell);
|
||||
}
|
||||
|
||||
export async function cloneRows() {
|
||||
if (!grider.canInsert) return;
|
||||
|
||||
let rowIndex = null;
|
||||
grider.beginUpdate();
|
||||
for (const index of _.sortBy(getSelectedRowIndexes(), x => x)) {
|
||||
if (_.isNumber(index)) {
|
||||
rowIndex = grider.insertRow();
|
||||
|
||||
for (const column of display.columns) {
|
||||
if (column.uniquePath.length > 1) continue;
|
||||
if (column.autoIncrement) continue;
|
||||
|
||||
grider.setCellValue(rowIndex, column.uniqueName, grider.getRowData(index)[column.uniqueName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
grider.endUpdate();
|
||||
|
||||
if (rowIndex == null) return;
|
||||
const cell = [rowIndex, (currentCell && currentCell[1]) || 0];
|
||||
// @ts-ignore
|
||||
currentCell = cell;
|
||||
// @ts-ignore
|
||||
selectedCells = [cell];
|
||||
await tick();
|
||||
scrollIntoView(cell);
|
||||
}
|
||||
|
||||
export function setFixedValue(value) {
|
||||
@@ -530,6 +579,23 @@
|
||||
openJsonDocument(json);
|
||||
}
|
||||
|
||||
export function openSelectionInMap() {
|
||||
const selection = getCellsPublished(selectedCells);
|
||||
if (!selectionCouldBeShownOnMap(selection)) {
|
||||
showModal(ErrorMessageModal, { message: 'There is nothing to be shown on map' });
|
||||
return;
|
||||
}
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Map',
|
||||
icon: 'img map',
|
||||
tabComponent: 'MapTab',
|
||||
},
|
||||
{ editor: selection.map(x => _.omit(x, ['engine'])) }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
function getSelectedExportableCell() {
|
||||
const electron = getElectron();
|
||||
if (electron && selectedCells.length == 1) {
|
||||
@@ -1128,7 +1194,7 @@
|
||||
// console.log('event', event.nativeEvent);
|
||||
}
|
||||
|
||||
if (event.keyCode == keycodes.f2) {
|
||||
if (event.keyCode == keycodes.f2 || event.keyCode == keycodes.enter) {
|
||||
// @ts-ignore
|
||||
dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true });
|
||||
}
|
||||
@@ -1143,7 +1209,20 @@
|
||||
|
||||
handleCursorMove(event);
|
||||
|
||||
if (event.shiftKey && event.keyCode != keycodes.shift && event.keyCode != keycodes.tab) {
|
||||
if (
|
||||
event.shiftKey &&
|
||||
event.keyCode != keycodes.shift &&
|
||||
event.keyCode != keycodes.tab &&
|
||||
event.keyCode != keycodes.ctrl &&
|
||||
event.keyCode != keycodes.leftWindowKey &&
|
||||
event.keyCode != keycodes.rightWindowKey &&
|
||||
!(
|
||||
(event.keyCode >= keycodes.a && event.keyCode <= keycodes.z) ||
|
||||
(event.keyCode >= keycodes.n0 && event.keyCode <= keycodes.n9) ||
|
||||
(event.keyCode >= keycodes.numPad0 && event.keyCode <= keycodes.numPad9) ||
|
||||
event.keyCode == keycodes.dash
|
||||
)
|
||||
) {
|
||||
selectedCells = getCellRange(shiftDragStartCell || currentCell, currentCell);
|
||||
}
|
||||
}
|
||||
@@ -1178,8 +1257,10 @@
|
||||
if (currentCell[0] == 0) return focusFilterEditor(currentCell[1]);
|
||||
return moveCurrentCell(currentCell[0] - 1, currentCell[1], event);
|
||||
case keycodes.downArrow:
|
||||
case keycodes.enter:
|
||||
return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
|
||||
case keycodes.enter:
|
||||
if (!grider.editable) return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
|
||||
break;
|
||||
case keycodes.leftArrow:
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
||||
case keycodes.rightArrow:
|
||||
@@ -1193,25 +1274,31 @@
|
||||
case keycodes.pageDown:
|
||||
return moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event);
|
||||
case keycodes.tab: {
|
||||
if (event.shiftKey) {
|
||||
if (currentCell[1] > 0) {
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
||||
} else {
|
||||
return moveCurrentCell(currentCell[0] - 1, columnSizes.realCount - 1, event);
|
||||
}
|
||||
} else {
|
||||
if (currentCell[1] < columnSizes.realCount - 1) {
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] + 1, event);
|
||||
} else {
|
||||
return moveCurrentCell(currentCell[0] + 1, 0, event);
|
||||
}
|
||||
}
|
||||
return moveCurrentCellWithTabKey(event.shiftKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function moveCurrentCellWithTabKey(isShift) {
|
||||
if (!isRegularCell(currentCell)) return null;
|
||||
|
||||
if (isShift) {
|
||||
if (currentCell[1] > 0) {
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
||||
} else {
|
||||
return moveCurrentCell(currentCell[0] - 1, columnSizes.realCount - 1, event);
|
||||
}
|
||||
} else {
|
||||
if (currentCell[1] < columnSizes.realCount - 1) {
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] + 1, event);
|
||||
} else {
|
||||
return moveCurrentCell(currentCell[0] + 1, 0, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setCellValue(cell, value) {
|
||||
grider.setCellValue(cell[0], realColumnUniqueNames[cell[1]], value);
|
||||
}
|
||||
@@ -1352,10 +1439,24 @@
|
||||
selectAll: action.selectAll,
|
||||
};
|
||||
case 'close': {
|
||||
const [row, col] = currentCell || [];
|
||||
if (domFocusField) domFocusField.focus();
|
||||
// @ts-ignore
|
||||
if (action.mode == 'enter' && row) setTimeout(() => moveCurrentCell(row + 1, col), 0);
|
||||
if (action.mode == 'enter' || action.mode == 'tab' || action.mode == 'shiftTab') {
|
||||
setTimeout(() => {
|
||||
if (isRegularCell(currentCell)) {
|
||||
switch (action.mode) {
|
||||
case 'enter':
|
||||
moveCurrentCell(currentCell[0] + 1, currentCell[1]);
|
||||
break;
|
||||
case 'tab':
|
||||
moveCurrentCellWithTabKey(false);
|
||||
break;
|
||||
case 'shiftTab':
|
||||
moveCurrentCellWithTabKey(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
// if (action.mode == 'save') setTimeout(handleSave, 0);
|
||||
return {};
|
||||
}
|
||||
@@ -1404,6 +1505,7 @@
|
||||
{ command: 'dataGrid.revertAllChanges', hideDisabled: true },
|
||||
{ command: 'dataGrid.deleteSelectedRows' },
|
||||
{ command: 'dataGrid.insertNewRow' },
|
||||
{ command: 'dataGrid.cloneRows' },
|
||||
{ command: 'dataGrid.setNull' },
|
||||
{ placeTag: 'edit' },
|
||||
{ divider: true },
|
||||
@@ -1426,6 +1528,7 @@
|
||||
{ command: 'dataGrid.generateSqlFromData' },
|
||||
{ command: 'dataGrid.openFreeTable' },
|
||||
{ command: 'dataGrid.openChartFromSelection' },
|
||||
{ command: 'dataGrid.openSelectionInMap', hideDisabled: true },
|
||||
{ placeTag: 'chart' }
|
||||
);
|
||||
|
||||
@@ -1457,17 +1560,28 @@
|
||||
{#if !display || (!isDynamicStructure && (!columns || columns.length == 0))}
|
||||
<LoadingInfo wrapper message="Waiting for structure" />
|
||||
{:else if errorMessage}
|
||||
<ErrorInfo message={errorMessage} alignTop />
|
||||
<div>
|
||||
<ErrorInfo message={errorMessage} alignTop />
|
||||
<FormStyledButton value="Reset filter" on:click={() => display.clearFilters()} />
|
||||
<FormStyledButton value="Reset view" on:click={() => display.resetConfig()} />
|
||||
{#if onOpenQuery}
|
||||
<FormStyledButton value="Open Query" on:click={onOpenQuery} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if isDynamicStructure && isLoadedAll && grider?.rowCount == 0}
|
||||
<div>
|
||||
<ErrorInfo
|
||||
alignTop
|
||||
message="No rows loaded, check filter or add new documents. You could copy documents from ohter collections/tables with Copy advanved/Copy as JSON command."
|
||||
message={grider.editable
|
||||
? 'No rows loaded, check filter or add new documents. You could copy documents from ohter collections/tables with Copy advanved/Copy as JSON command.'
|
||||
: 'No rows loaded'}
|
||||
/>
|
||||
{#if display.filterCount > 0}
|
||||
<FormStyledButton value="Reset filter" on:click={() => display.clearFilters()} />
|
||||
{/if}
|
||||
<FormStyledButton value="Add document" on:click={addJsonDocument} />
|
||||
{#if grider.editable}
|
||||
<FormStyledButton value="Add document" on:click={addJsonDocument} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if grider.errors && grider.errors.length > 0}
|
||||
<div>
|
||||
@@ -1526,7 +1640,11 @@
|
||||
{conid}
|
||||
{database}
|
||||
setSort={display.sortable ? order => display.setSort(col.uniqueName, order) : null}
|
||||
order={display.getSortOrder(col.uniqueName)}
|
||||
addToSort={display.sortable ? order => display.addToSort(col.uniqueName, order) : null}
|
||||
order={display.sortable ? display.getSortOrder(col.uniqueName) : null}
|
||||
orderIndex={display.sortable ? display.getSortOrderIndex(col.uniqueName) : -1}
|
||||
isSortDefined={display.sortable ? display.isSortDefined() : false}
|
||||
clearSort={display.sortable ? () => display.clearSort() : null}
|
||||
on:resizeSplitter={e => {
|
||||
// @ts-ignore
|
||||
display.resizeColumn(col.uniqueName, col.width, e.detail);
|
||||
|
||||
@@ -36,18 +36,26 @@
|
||||
break;
|
||||
case keycodes.enter:
|
||||
if (isChangedRef.get()) {
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
onSetValue(parseCellValue(domEditor.value));
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
domEditor.blur();
|
||||
event.preventDefault();
|
||||
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
|
||||
break;
|
||||
case keycodes.tab:
|
||||
if (isChangedRef.get()) {
|
||||
onSetValue(parseCellValue(domEditor.value));
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
domEditor.blur();
|
||||
event.preventDefault();
|
||||
dispatchInsplaceEditor({ type: 'close', mode: event.shiftKey ? 'shiftTab' : 'tab' });
|
||||
break;
|
||||
case keycodes.s:
|
||||
if (isCtrlOrCommandKey(event)) {
|
||||
if (isChangedRef.get()) {
|
||||
onSetValue(parseCellValue(domEditor.value));
|
||||
// grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
@@ -224,4 +224,5 @@
|
||||
frameSelection={!!macroPreview}
|
||||
{grider}
|
||||
{display}
|
||||
onOpenQuery={openQuery}
|
||||
/>
|
||||
|
||||
@@ -167,8 +167,8 @@
|
||||
async function detectSize(tables, domTables) {
|
||||
await tick();
|
||||
const rects = _.values(domTables).map(x => x.getRect());
|
||||
const maxX = _.max(rects.map(x => x.right));
|
||||
const maxY = _.max(rects.map(x => x.bottom));
|
||||
const maxX = rects.length > 0 ? _.max(rects.map(x => x.right)) : 0;
|
||||
const maxY = rects.length > 0 ? _.max(rects.map(x => x.bottom)) : 0;
|
||||
|
||||
canvasWidth = Math.max(3000, maxX + 50);
|
||||
canvasHeight = Math.max(3000, maxY + 50);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
mergeConditions,
|
||||
Source,
|
||||
ResultField,
|
||||
Expression,
|
||||
} from 'dbgate-sqltree';
|
||||
import { EngineDriver } from 'dbgate-types';
|
||||
import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types';
|
||||
@@ -78,25 +79,27 @@ export class DesignerQueryDumper {
|
||||
return select;
|
||||
}
|
||||
|
||||
addConditions(select: Select, tables: DesignerTableInfo[]) {
|
||||
buildConditionFromFilterField(tables: DesignerTableInfo[], filterField: string, getExpression?: Function): Condition {
|
||||
const conditions = [];
|
||||
|
||||
for (const column of this.designer.columns || []) {
|
||||
if (!column.filter) continue;
|
||||
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
|
||||
if (!table) continue;
|
||||
if (!tables.find(x => x.designerId == table.designerId)) continue;
|
||||
if (!column[filterField]) continue;
|
||||
|
||||
if (!column.isCustomExpression) {
|
||||
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
|
||||
if (!table) continue;
|
||||
if (!tables.find(x => x.designerId == table.designerId)) continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const condition = parseFilter(column.filter, findDesignerFilterType(column, this.designer));
|
||||
const condition = parseFilter(column[filterField], findDesignerFilterType(column, this.designer));
|
||||
if (condition) {
|
||||
select.where = mergeConditions(
|
||||
select.where,
|
||||
conditions.push(
|
||||
_.cloneDeepWith(condition, expr => {
|
||||
if (expr.exprType == 'placeholder')
|
||||
return {
|
||||
exprType: 'column',
|
||||
columnName: column.columnName,
|
||||
source: findQuerySource(this.designer, column.designerId),
|
||||
};
|
||||
if (expr.exprType == 'placeholder') {
|
||||
if (getExpression) return getExpression(column);
|
||||
return this.getColumnExpression(column);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -105,33 +108,79 @@ export class DesignerQueryDumper {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (conditions.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (conditions.length == 1) {
|
||||
return conditions[0];
|
||||
}
|
||||
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions,
|
||||
};
|
||||
}
|
||||
|
||||
addConditionsCore(select: Select, tables: DesignerTableInfo[], filterFields, selectField, getExpression?) {
|
||||
const conditions: Condition[] = _.compact(
|
||||
filterFields.map(field => this.buildConditionFromFilterField(tables, field, getExpression))
|
||||
);
|
||||
|
||||
if (conditions.length == 0) {
|
||||
return;
|
||||
}
|
||||
if (conditions.length == 0) {
|
||||
select[selectField] = mergeConditions(select[selectField], conditions[0]);
|
||||
return;
|
||||
}
|
||||
select[selectField] = mergeConditions(select[selectField], {
|
||||
conditionType: 'or',
|
||||
conditions,
|
||||
});
|
||||
}
|
||||
|
||||
addConditions(select: Select, tables: DesignerTableInfo[]) {
|
||||
const additionalFilterCount = this.designer.settings?.additionalFilterCount || 0;
|
||||
const filterFields = ['filter', ..._.range(additionalFilterCount).map(index => `additionalFilter${index + 1}`)];
|
||||
this.addConditionsCore(select, tables, filterFields, 'where');
|
||||
}
|
||||
|
||||
addGroupConditions(select: Select, tables: DesignerTableInfo[], selectIsGrouped: boolean) {
|
||||
for (const column of this.designer.columns || []) {
|
||||
if (!column.groupFilter) continue;
|
||||
const table = (this.designer.tables || []).find(x => x.designerId == column.designerId);
|
||||
if (!table) continue;
|
||||
if (!tables.find(x => x.designerId == table.designerId)) continue;
|
||||
|
||||
const condition = parseFilter(column.groupFilter, findDesignerFilterType(column, this.designer));
|
||||
if (condition) {
|
||||
select.having = mergeConditions(
|
||||
select.having,
|
||||
_.cloneDeepWith(condition, expr => {
|
||||
if (expr.exprType == 'placeholder') {
|
||||
return this.getColumnOutputExpression(column, selectIsGrouped);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
const additionalGroupFilterCount = this.designer.settings?.additionalGroupFilterCount || 0;
|
||||
const filterFields = [
|
||||
'groupFilter',
|
||||
..._.range(additionalGroupFilterCount).map(index => `additionalGroupFilter${index + 1}`),
|
||||
];
|
||||
this.addConditionsCore(select, tables, filterFields, 'having', column =>
|
||||
this.getColumnResultField(column, selectIsGrouped)
|
||||
);
|
||||
}
|
||||
|
||||
getColumnOutputExpression(col, selectIsGrouped): ResultField {
|
||||
getColumnExpression(col): Expression {
|
||||
const source = findQuerySource(this.designer, col.designerId);
|
||||
const { columnName, isCustomExpression, customExpression } = col;
|
||||
|
||||
const res: Expression = isCustomExpression
|
||||
? {
|
||||
exprType: 'raw',
|
||||
sql: customExpression,
|
||||
}
|
||||
: {
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source,
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
getColumnResultField(col, selectIsGrouped): ResultField {
|
||||
const { columnName } = col;
|
||||
let { alias } = col;
|
||||
|
||||
const exprCore = this.getColumnExpression(col);
|
||||
|
||||
if (selectIsGrouped && !col.isGrouped) {
|
||||
// use aggregate
|
||||
const aggregate = col.aggregate == null || col.aggregate == '---' ? 'MAX' : col.aggregate;
|
||||
@@ -142,20 +191,12 @@ export class DesignerQueryDumper {
|
||||
func: aggregate == 'COUNT DISTINCT' ? 'COUNT' : aggregate,
|
||||
argsPrefix: aggregate == 'COUNT DISTINCT' ? 'DISTINCT' : null,
|
||||
alias,
|
||||
args: [
|
||||
{
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source,
|
||||
},
|
||||
],
|
||||
args: [exprCore],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
...exprCore,
|
||||
alias,
|
||||
source,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -179,24 +220,21 @@ export class DesignerQueryDumper {
|
||||
}
|
||||
}
|
||||
|
||||
const topLevelColumns = (this.designer.columns || []).filter(col =>
|
||||
topLevelTables.find(tbl => tbl.designerId == col.designerId)
|
||||
const topLevelColumns = (this.designer.columns || []).filter(
|
||||
col =>
|
||||
topLevelTables.find(tbl => tbl.designerId == col.designerId) || (col.isCustomExpression && col.customExpression)
|
||||
);
|
||||
const selectIsGrouped = !!topLevelColumns.find(x => x.isGrouped || (x.aggregate && x.aggregate != '---'));
|
||||
const outputColumns = topLevelColumns.filter(x => x.isOutput);
|
||||
if (outputColumns.length == 0) {
|
||||
res.selectAll = true;
|
||||
} else {
|
||||
res.columns = outputColumns.map(col => this.getColumnOutputExpression(col, selectIsGrouped));
|
||||
res.columns = outputColumns.map(col => this.getColumnResultField(col, selectIsGrouped));
|
||||
}
|
||||
|
||||
const groupedColumns = topLevelColumns.filter(x => x.isGrouped);
|
||||
if (groupedColumns.length > 0) {
|
||||
res.groupBy = groupedColumns.map(col => ({
|
||||
exprType: 'column',
|
||||
columnName: col.columnName,
|
||||
source: findQuerySource(this.designer, col.designerId),
|
||||
}));
|
||||
res.groupBy = groupedColumns.map(col => this.getColumnExpression(col));
|
||||
}
|
||||
|
||||
const orderColumns = _.sortBy(
|
||||
@@ -205,10 +243,8 @@ export class DesignerQueryDumper {
|
||||
);
|
||||
if (orderColumns.length > 0) {
|
||||
res.orderBy = orderColumns.map(col => ({
|
||||
exprType: 'column',
|
||||
...this.getColumnExpression(col),
|
||||
direction: col.sortOrder < 0 ? 'DESC' : 'ASC',
|
||||
columnName: col.columnName,
|
||||
source: findQuerySource(this.designer, col.designerId),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,14 @@ export type DesignerColumnInfo = {
|
||||
sortOrder?: number;
|
||||
filter?: string;
|
||||
groupFilter?: string;
|
||||
isCustomExpression?: boolean;
|
||||
customExpression?: string;
|
||||
};
|
||||
|
||||
export type DesignerSettings = {
|
||||
isDistinct?: boolean;
|
||||
additionalFilterCount?: number;
|
||||
additionalGroupFilterCount?: number;
|
||||
};
|
||||
|
||||
export type DesignerInfo = {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
export let width;
|
||||
export let isFlex = false;
|
||||
</script>
|
||||
|
||||
<div style={`max-width: ${width}px`}>
|
||||
<div style={`max-width: ${width}px`} class:isFlex>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -12,4 +13,8 @@
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
div.isFlex {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<script lang="ts" context="module">
|
||||
export function selectionCouldBeShownOnMap(selection) {
|
||||
if (selection.length > 0 && _.find(selection, x => isWktGeometry(x.value))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
selection.find(x => x.column.toLowerCase().includes('lat')) &&
|
||||
(selection.find(x => x.column.toLowerCase().includes('lon')) ||
|
||||
selection.find(x => x.column.toLowerCase().includes('lng')))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import leaflet from 'leaflet';
|
||||
import wellknown from 'wellknown';
|
||||
import { isWktGeometry, ScriptWriter, ScriptWriterJson } from 'dbgate-tools';
|
||||
import resizeObserver from '../utility/resizeObserver';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import contextMenu from '../utility/contextMenu';
|
||||
import { saveExportedFile, saveFileToDisk } from '../utility/exportFileTools';
|
||||
import { getCurrentConfig } from '../stores';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
export let selection;
|
||||
|
||||
let refContainer;
|
||||
let map;
|
||||
|
||||
let selectionLayers = [];
|
||||
let geoJson;
|
||||
|
||||
function createColumnsTable(cells) {
|
||||
if (cells.length == 0) return '';
|
||||
return `<table>${cells.map(cell => `<tr><td>${cell.column}</td><td>${cell.value}</td></tr>`).join('\n')}</table>`;
|
||||
}
|
||||
|
||||
function addSelectionToMap() {
|
||||
if (!map) return;
|
||||
if (!selection) return;
|
||||
|
||||
for (const selectionLayer of selectionLayers) {
|
||||
selectionLayer.remove();
|
||||
}
|
||||
selectionLayers = [];
|
||||
|
||||
const selectedRows = _.groupBy(selection || [], 'row');
|
||||
|
||||
const features = [];
|
||||
|
||||
for (const rowKey of _.keys(selectedRows)) {
|
||||
const cells = selectedRows[rowKey];
|
||||
const lat = cells.find(x => x.column.toLowerCase().includes('lat'));
|
||||
const lon = cells.find(x => x.column.toLowerCase().includes('lon') || x.column.toLowerCase().includes('lng'));
|
||||
|
||||
const geoValues = cells.map(x => x.value).filter(isWktGeometry);
|
||||
|
||||
if (lat && lon) {
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
popupContent: createColumnsTable(cells),
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [lon.value, lat.value],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (geoValues.length > 0) {
|
||||
// parse WKT to geoJSON array
|
||||
features.push(
|
||||
...geoValues.map(wellknown).map(geometry => ({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
popupContent: createColumnsTable(cells.filter(x => !isWktGeometry(x.value))),
|
||||
},
|
||||
geometry,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (features.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
geoJson = {
|
||||
type: 'FeatureCollection',
|
||||
features,
|
||||
};
|
||||
|
||||
const geoJsonObj = leaflet
|
||||
.geoJSON(geoJson, {
|
||||
style: function () {
|
||||
return {
|
||||
weight: 2,
|
||||
fillColor: '#ff7800',
|
||||
color: '#ff7800',
|
||||
opacity: 0.8,
|
||||
fillOpacity: 0.4,
|
||||
};
|
||||
},
|
||||
pointToLayer: (feature, latlng) => {
|
||||
return leaflet.circleMarker(latlng, {
|
||||
radius: 7,
|
||||
weight: 2,
|
||||
fillColor: '#ff0000',
|
||||
color: '#ff0000',
|
||||
opacity: 0.9,
|
||||
fillOpacity: 0.9,
|
||||
});
|
||||
},
|
||||
onEachFeature: (feature, layer) => {
|
||||
// does this feature have a property named popupContent?
|
||||
if (feature.properties && feature.properties.popupContent) {
|
||||
layer.bindPopup(feature.properties.popupContent);
|
||||
layer.bindTooltip(feature.properties.popupContent);
|
||||
}
|
||||
},
|
||||
})
|
||||
.addTo(map);
|
||||
// geoJsonObj.bindPopup('This is the Transamerica Pyramid'); //.openPopup();
|
||||
map.fitBounds(geoJsonObj.getBounds());
|
||||
selectionLayers.push(geoJsonObj);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
map = leaflet.map(refContainer).setView([50, 15], 13);
|
||||
|
||||
leaflet
|
||||
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap',
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
addSelectionToMap();
|
||||
// map.fitBounds([
|
||||
// [50, 15],
|
||||
// [50.1, 15],
|
||||
// [50, 15.1],
|
||||
// ]);
|
||||
|
||||
// const marker = leaflet.marker([50, 15]).addTo(map);
|
||||
// <div bind:this={refContainer} class="flex1 map-container" />
|
||||
});
|
||||
|
||||
$: {
|
||||
selection;
|
||||
addSelectionToMap();
|
||||
}
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{
|
||||
text: 'Open on new tab',
|
||||
onClick: () => {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Map',
|
||||
icon: 'img map',
|
||||
tabComponent: 'MapTab',
|
||||
},
|
||||
{ editor: selection.map(x => _.omit(x, ['engine'])) }
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Export to HTML file',
|
||||
onClick: () => {
|
||||
saveFileToDisk(async filePath => {
|
||||
await apiCall('files/export-map', {
|
||||
geoJson,
|
||||
filePath,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={refContainer}
|
||||
use:contextMenu={createMenu}
|
||||
class="flex1"
|
||||
use:resizeObserver={true}
|
||||
on:resize={async e => {
|
||||
await tick();
|
||||
map.invalidateSize();
|
||||
}}
|
||||
/>
|
||||
@@ -13,8 +13,11 @@
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
import TableControl from './TableControl.svelte';
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import _ from 'lodash';
|
||||
|
||||
export let value;
|
||||
export let onChange;
|
||||
@@ -35,8 +38,56 @@
|
||||
}));
|
||||
};
|
||||
|
||||
const addExpressionColumn = () => {
|
||||
onChange(current => ({
|
||||
...current,
|
||||
columns: [...(current.columns || []), { isCustomExpression: true, isOutput: true, designerId: uuidv1() }],
|
||||
}));
|
||||
};
|
||||
|
||||
const addOrCondition = () => {
|
||||
onChange(current => ({
|
||||
...current,
|
||||
settings: {
|
||||
...current?.settings,
|
||||
additionalFilterCount: (current?.settings?.additionalFilterCount ?? 0) + 1,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const removeOrCondition = () => {
|
||||
onChange(current => ({
|
||||
...current,
|
||||
settings: {
|
||||
...current?.settings,
|
||||
additionalFilterCount: (current?.settings?.additionalFilterCount ?? 1) - 1,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const addGroupOrCondition = () => {
|
||||
onChange(current => ({
|
||||
...current,
|
||||
settings: {
|
||||
...current?.settings,
|
||||
additionalGroupFilterCount: (current?.settings?.additionalGroupFilterCount ?? 0) + 1,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const removeGroupOrCondition = () => {
|
||||
onChange(current => ({
|
||||
...current,
|
||||
settings: {
|
||||
...current?.settings,
|
||||
additionalGroupFilterCount: (current?.settings?.additionalGroupFilterCount ?? 1) - 1,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
$: columns = value?.columns;
|
||||
$: tables = value?.tables;
|
||||
$: settings = value?.settings;
|
||||
$: hasGroupedColumn = !!(columns || []).find(x => x.isGrouped);
|
||||
</script>
|
||||
|
||||
@@ -44,18 +95,49 @@
|
||||
<TableControl
|
||||
rows={columns || []}
|
||||
columns={[
|
||||
{ fieldName: 'columnName', header: 'Column/Expression' },
|
||||
{ fieldName: 'columnName', slot: 8, header: 'Column/Expression' },
|
||||
{ fieldName: 'tableDisplayName', header: 'Table', formatter: row => getTableDisplayName(row, tables) },
|
||||
{ fieldName: 'isOutput', header: 'Output', slot: 0 },
|
||||
{ fieldName: 'alias', header: 'Alias', slot: 1 },
|
||||
{ fieldName: 'isGrouped', header: 'Group by', slot: 2 },
|
||||
{ fieldName: 'aggregate', header: 'Aggregate', slot: 3 },
|
||||
{ fieldName: 'sortOrder', header: 'Sort order', slot: 4 },
|
||||
{ fieldName: 'filter', header: 'Filter', slot: 5 },
|
||||
hasGroupedColumn && { fieldName: 'groupFilter', header: 'Group filter', slot: 6 },
|
||||
{ fieldName: 'filter', header: 'Filter', slot: 5, props: { filterField: 'filter' } },
|
||||
..._.range(settings?.additionalFilterCount || 0).map(index => ({
|
||||
fieldName: `additionalFilter${index + 1}`,
|
||||
header: `OR Filter ${index + 2}`,
|
||||
slot: 5,
|
||||
props: { filterField: `additionalFilter${index + 1}` },
|
||||
})),
|
||||
hasGroupedColumn && {
|
||||
fieldName: 'groupFilter',
|
||||
header: 'Group filter',
|
||||
slot: 5,
|
||||
props: { filterField: 'groupFilter' },
|
||||
},
|
||||
..._.range(hasGroupedColumn ? settings?.additionalGroupFilterCount || 0 : 0).map(index => ({
|
||||
fieldName: `additionalGroupFilter${index + 1}`,
|
||||
header: `OR group filter ${index + 2}`,
|
||||
slot: 5,
|
||||
props: { filterField: `additionalGroupFilter${index + 1}` },
|
||||
})),
|
||||
{ fieldName: 'actions', header: '', slot: 7 },
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="8" let:row>
|
||||
{#if row.isCustomExpression}
|
||||
<TextField
|
||||
style="min-width:calc(100% - 9px)"
|
||||
value={row.customExpression}
|
||||
on:input={e => {
|
||||
changeColumn({ ...row, customExpression: e.target.value });
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
{row.columnName}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="0" let:row>
|
||||
<CheckboxField
|
||||
checked={row.isOutput}
|
||||
@@ -67,6 +149,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="1" let:row>
|
||||
<TextField
|
||||
style="min-width:calc(100% - 9px)"
|
||||
value={row.alias}
|
||||
on:input={e => {
|
||||
changeColumn({ ...row, alias: e.target.value });
|
||||
@@ -86,6 +169,7 @@
|
||||
{#if !row.isGrouped}
|
||||
<SelectField
|
||||
isNative
|
||||
style="min-width:calc(100% - 9px)"
|
||||
value={row.aggregate}
|
||||
on:change={e => {
|
||||
changeColumn({ ...row, aggregate: e.detail });
|
||||
@@ -97,6 +181,7 @@
|
||||
<svelte:fragment slot="4" let:row>
|
||||
<SelectField
|
||||
isNative
|
||||
style="min-width:calc(100% - 9px)"
|
||||
value={row.sortOrder}
|
||||
on:change={e => {
|
||||
changeColumn({ ...row, sortOrder: parseInt(e.detail) });
|
||||
@@ -112,21 +197,12 @@
|
||||
]}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="5" let:row>
|
||||
<svelte:fragment slot="5" let:row let:filterField>
|
||||
<DataFilterControl
|
||||
filterType={findDesignerFilterType(row, value)}
|
||||
filter={row.filter}
|
||||
filter={row[filterField]}
|
||||
setFilter={filter => {
|
||||
changeColumn({ ...row, filter });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="6" let:row>
|
||||
<DataFilterControl
|
||||
filterType={findDesignerFilterType(row, value)}
|
||||
filter={row.groupFilter}
|
||||
setFilter={groupFilter => {
|
||||
changeColumn({ ...row, groupFilter });
|
||||
changeColumn({ ...row, [filterField]: filter });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
@@ -134,6 +210,17 @@
|
||||
<InlineButton on:click={() => removeColumn(row)}>Remove</InlineButton>
|
||||
</svelte:fragment>
|
||||
</TableControl>
|
||||
<FormStyledButton value="Add custom expression" on:click={addExpressionColumn} style="width:200px" />
|
||||
<FormStyledButton value="Add OR condition" on:click={addOrCondition} style="width:200px" />
|
||||
{#if settings?.additionalFilterCount > 0}
|
||||
<FormStyledButton value="Remove OR condition" on:click={removeOrCondition} style="width:200px" />
|
||||
{/if}
|
||||
{#if hasGroupedColumn}
|
||||
<FormStyledButton value="Add group OR condition" on:click={addGroupOrCondition} style="width:200px" />
|
||||
{/if}
|
||||
{#if hasGroupedColumn && settings?.additionalGroupFilterCount > 0}
|
||||
<FormStyledButton value="Remove group OR condition" on:click={removeGroupOrCondition} style="width:200px" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -141,4 +228,4 @@
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user