diff --git a/.github/workflows/build-docker-beta.yaml b/.github/workflows/build-docker-beta.yaml deleted file mode 100644 index ea0e52263..000000000 --- a/.github/workflows/build-docker-beta.yaml +++ /dev/null @@ -1,58 +0,0 @@ -name: Docker image BETA - -# on: [push] - -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+-docker.[0-9]+' - -jobs: - build: - - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ubuntu-18.04] - - steps: - - name: Context - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v2 - with: - fetch-depth: 1 - - name: Use Node.js 14.x - uses: actions/setup-node@v1 - with: - node-version: 14.x - - name: yarn install - run: | - # yarn --version - # yarn config set network-timeout 300000 - yarn install - - name: setCurrentVersion - run: | - yarn setCurrentVersion - - name: Prepare docker image - run: | - yarn run prepare:docker - - name: Build docker image - run: | - docker build ./docker -t dbgate - - name: Push docker image - run: | - docker tag dbgate dbgate/dbgate:beta - docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - docker push dbgate/dbgate:beta - - name: Build alpine docker image - run: | - docker build ./docker -t dbgate -f docker/Dockerfile-alpine - - name: Push alpine docker image - run: | - docker tag dbgate dbgate/dbgate:beta-alpine - docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - docker push dbgate/dbgate:beta-alpine diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml index df9729ae7..1764abab3 100644 --- a/.github/workflows/build-docker.yaml +++ b/.github/workflows/build-docker.yaml @@ -1,17 +1,11 @@ name: Docker image -# on: [push] - on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - # - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - -# on: -# push: -# branches: -# - production + - 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-docker.[0-9]+' jobs: build: @@ -30,12 +24,43 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 1 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + dbgate/dbgate + flavor: | + latest=false + tags: | + type=raw,value=beta,enable=${{ contains(github.ref_name, '-docker.') || contains(github.ref_name, '-beta.') }} + + type=match,pattern=\d+.\d+.\d+,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }} + type=raw,value=latest,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }} + + - name: Docker alpine meta + id: alpmeta + uses: docker/metadata-action@v4 + with: + images: | + dbgate/dbgate + flavor: | + latest=false + tags: | + type=raw,value=beta-alpine,enable=${{ contains(github.ref_name, '-docker.') || contains(github.ref_name, '-beta.') }} + + type=match,pattern=\d+.\d+.\d+,suffix=-alpine,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }} + type=raw,value=alpine,enable=${{ !contains(github.ref_name, '-docker.') && !contains(github.ref_name, '-beta.') }} + - name: Use Node.js 14.x uses: actions/setup-node@v1 with: node-version: 14.x - name: yarn install run: | + # yarn --version + # yarn config set network-timeout 300000 yarn install - name: setCurrentVersion run: | @@ -43,19 +68,28 @@ jobs: - name: Prepare docker image run: | yarn run prepare:docker - - name: Build docker image - run: | - docker build ./docker -t dbgate - - name: Push docker image - run: | - docker tag dbgate dbgate/dbgate - docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - docker push dbgate/dbgate - - name: Build alpine docker image - run: | - docker build ./docker -t dbgate -f docker/Dockerfile-alpine - - name: Push alpine docker image - run: | - docker tag dbgate dbgate/dbgate:alpine - docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - docker push dbgate/dbgate:alpine + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v3 + with: + push: true + context: ./docker + tags: ${{ steps.meta.outputs.tags }} + platforms: linux/amd64,linux/arm64,linux/arm/v7 + + - name: Build and push alpine + uses: docker/build-push-action@v3 + with: + push: true + context: ./docker + file: ./docker/Dockerfile-alpine + tags: ${{ steps.alpmeta.outputs.tags }} + platforms: linux/amd64,linux/arm64,linux/arm/v7 diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f3e2de9..da78e8708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,32 @@ Builds: - linux - application for linux - win - application for Windows +### 5.1.5 +- ADDED: Support perspectives for MongoDB - MongoDB query designer +- ADDED: Show JSON content directly in the overview #395 +- CHANGED: OSX Command H shortcut for hiding window #390 +- ADDED: Uppercase Autocomplete Suggestions #389 +- FIXED: Record view left/right arrows cause start record number to be treated as string #388 +- FIXED: MongoDb ObjectId behaviour not consistent in nested objects #387 +- FIXED: demo.dbgate.org - beta version crash 5.1.5-beta.3 #386 +- ADDED: connect via socket - configurable via environment variables #358 + +### 5.1.4 +- ADDED: Drop database commands #384 +- ADDED: Customizable Redis key separator #379 +- ADDED: ARM support for docker images +- ADDED: Version tags for docker images +- ADDED: Better SQL command splitting and highlighting +- ADDED: Unsaved marker for SQL files + +### 5.1.3 +- ADDED: Editing multiline cell values #378 #371 #359 +- ADDED: Truncate table #333 +- ADDED: Perspectives - show row count +- ADDED: Query - error markers in gutter area +- ADDED: Query - ability to execute query elements from gutter +- FIXED: Correct error line numbers returned from queries + ### 5.1.2 - FIXED: MongoDb any export function does not work. #373 - ADDED: Query Designer short order more flexibility #372 diff --git a/misc/play-dark-mode.svg b/misc/play-dark-mode.svg new file mode 100644 index 000000000..093288891 --- /dev/null +++ b/misc/play-dark-mode.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/misc/play-light-mode.svg b/misc/play-light-mode.svg new file mode 100644 index 000000000..51bdf59a7 --- /dev/null +++ b/misc/play-light-mode.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/package.json b/package.json index 8c00752fa..eeff09400 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "5.1.3-beta.1", + "version": "5.1.5", "name": "dbgate-all", "workspaces": [ "packages/*", diff --git a/packages/api/package.json b/packages/api/package.json index e3f0c90dc..8190e5805 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -25,7 +25,7 @@ "compare-versions": "^3.6.0", "cors": "^2.8.5", "cross-env": "^6.0.3", - "dbgate-query-splitter": "^4.9.0", + "dbgate-query-splitter": "^4.9.2", "dbgate-sqltree": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1", "debug": "^4.3.4", diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index 7aad6ebb1..63b3c872c 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -53,6 +53,8 @@ function getPortalCollections() { databaseUrl: process.env[`URL_${id}`], useDatabaseUrl: !!process.env[`URL_${id}`], databaseFile: process.env[`FILE_${id}`], + socketPath: process.env[`SOCKET_PATH_${id}`], + authType: process.env[`AUTH_TYPE_${id}`] || (process.env[`SOCKET_PATH_${id}`] ? 'socket' : undefined), defaultDatabase: process.env[`DATABASE_${id}`] || (process.env[`FILE_${id}`] ? getDatabaseFileLabel(process.env[`FILE_${id}`]) : null), diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index 7f6ad2b6d..5b06d3078 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -152,4 +152,13 @@ module.exports = { opened.subprocess.send({ msgtype: 'createDatabase', name }); return { status: 'ok' }; }, + + dropDatabase_meta: true, + async dropDatabase({ conid, name }, req) { + testConnectionPermission(conid, req); + const opened = await this.ensureOpened(conid); + if (opened.connection.isReadOnly) return false; + opened.subprocess.send({ msgtype: 'dropDatabase', name }); + return { status: 'ok' }; + }, }; diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index 3cd88c580..88f5ef8d9 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -177,7 +177,7 @@ async function handleQueryData({ msgid, sql }, skipReadonlyCheck = false) { const res = await driver.query(systemConnection, sql); process.send({ msgtype: 'response', msgid, ...res }); } catch (err) { - process.send({ msgtype: 'response', msgid, errorMessage: err.message }); + process.send({ msgtype: 'response', msgid, errorMessage: err.message || 'Error executing SQL script' }); } } diff --git a/packages/api/src/proc/serverConnectionProcess.js b/packages/api/src/proc/serverConnectionProcess.js index 42e5cb8d9..592071040 100644 --- a/packages/api/src/proc/serverConnectionProcess.js +++ b/packages/api/src/proc/serverConnectionProcess.js @@ -2,7 +2,6 @@ const stableStringify = require('json-stable-stringify'); const { extractBoolSettingsValue, extractIntSettingsValue } = require('dbgate-tools'); const childProcessChecker = require('../utility/childProcessChecker'); const requireEngineDriver = require('../utility/requireEngineDriver'); -const { decryptConnection } = require('../utility/crypting'); const connectUtility = require('../utility/connectUtility'); const { handleProcessCommunication } = require('../utility/processComm'); @@ -81,14 +80,16 @@ function handlePing() { lastPing = new Date().getTime(); } -async function handleCreateDatabase({ name }) { +async function handleDatabaseOp(op, { name }) { const driver = requireEngineDriver(storedConnection); systemConnection = await connectUtility(driver, storedConnection, 'app'); - console.log(`RUNNING SCRIPT: CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`); - if (driver.createDatabase) { - await driver.createDatabase(systemConnection, name); + if (driver[op]) { + await driver[op](systemConnection, name); } else { - await driver.query(systemConnection, `CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`); + const dmp = driver.createDumper(); + dmp[op](name); + console.log(`RUNNING SCRIPT: ${dmp.s}`); + await driver.query(systemConnection, dmp.s); } await handleRefresh(); } @@ -96,7 +97,8 @@ async function handleCreateDatabase({ name }) { const messageHandlers = { connect: handleConnect, ping: handlePing, - createDatabase: handleCreateDatabase, + createDatabase: props => handleDatabaseOp('createDatabase', props), + dropDatabase: props => handleDatabaseOp('dropDatabase', props), }; async function handleMessage({ msgtype, ...other }) { diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js index 2c179c413..459e934f5 100644 --- a/packages/api/src/proc/sessionProcess.js +++ b/packages/api/src/proc/sessionProcess.js @@ -101,8 +101,9 @@ class TableWriter { } class StreamHandler { - constructor(resultIndexHolder, resolve) { + constructor(resultIndexHolder, resolve, startLine) { this.recordset = this.recordset.bind(this); + this.startLine = startLine; this.row = this.row.bind(this); // this.error = this.error.bind(this); this.done = this.done.bind(this); @@ -155,14 +156,21 @@ class StreamHandler { this.resolve(); } info(info) { + if (info && info.line != null) { + info = { + ...info, + line: this.startLine + info.line, + }; + } process.send({ msgtype: 'info', info }); } } -function handleStream(driver, resultIndexHolder, sql) { +function handleStream(driver, resultIndexHolder, sqlItem) { return new Promise((resolve, reject) => { - const handler = new StreamHandler(resultIndexHolder, resolve); - driver.stream(systemConnection, sql, handler); + const start = sqlItem.trimStart || sqlItem.start; + const handler = new StreamHandler(resultIndexHolder, resolve, start && start.line); + driver.stream(systemConnection, sqlItem.text, handler); }); } @@ -221,7 +229,10 @@ async function handleExecuteQuery({ sql }) { const resultIndexHolder = { value: 0, }; - for (const sqlItem of splitQuery(sql, driver.getQuerySplitterOptions('stream'))) { + for (const sqlItem of splitQuery(sql, { + ...driver.getQuerySplitterOptions('stream'), + returnRichInfo: true, + })) { await handleStream(driver, resultIndexHolder, sqlItem); // const handler = new StreamHandler(resultIndex); // const stream = await driver.stream(systemConnection, sqlItem, handler); diff --git a/packages/api/src/shell/importDatabase.js b/packages/api/src/shell/importDatabase.js index 7ad1fbfc1..6cb52961d 100644 --- a/packages/api/src/shell/importDatabase.js +++ b/packages/api/src/shell/importDatabase.js @@ -47,7 +47,7 @@ async function importDatabase({ connection = undefined, systemConnection = undef const downloadedFile = await download(inputFile); const fileStream = fs.createReadStream(downloadedFile, 'utf-8'); - const splittedStream = splitQueryStream(fileStream, driver.getQuerySplitterOptions()); + const splittedStream = splitQueryStream(fileStream, driver.getQuerySplitterOptions('script')); const importStream = new ImportStream(pool, driver); // @ts-ignore splittedStream.pipe(importStream); diff --git a/packages/datalib/src/PerspectiveCache.ts b/packages/datalib/src/PerspectiveCache.ts index d26a6697a..e706b8ae4 100644 --- a/packages/datalib/src/PerspectiveCache.ts +++ b/packages/datalib/src/PerspectiveCache.ts @@ -5,6 +5,7 @@ import _zip from 'lodash/zip'; import _difference from 'lodash/difference'; import debug from 'debug'; import stableStringify from 'json-stable-stringify'; +import { PerspectiveDataPattern } from './PerspectiveDataPattern'; const dbg = debug('dbgate:PerspectiveCache'); @@ -34,6 +35,7 @@ export class PerspectiveCacheTable { pureName: string; bindingColumns?: string[]; dataColumns: string[]; + allColumns?: boolean; loadedAll: boolean; loadedRows: any[] = []; bindingGroups: { [bindingKey: string]: PerspectiveBindingGroup } = {}; @@ -86,14 +88,23 @@ export class PerspectiveCache { constructor() {} tables: { [tableKey: string]: PerspectiveCacheTable } = {}; + dataPatterns: PerspectiveDataPattern[] = []; getTableCache(props: PerspectiveDataLoadProps) { const tableKey = stableStringify( - _pick(props, ['schemaName', 'pureName', 'bindingColumns', 'databaseConfig', 'orderBy', 'condition']) + _pick(props, [ + 'schemaName', + 'pureName', + 'bindingColumns', + 'databaseConfig', + 'orderBy', + 'sqlCondition', + 'mongoCondition', + ]) ); let res = this.tables[tableKey]; - if (res && _difference(props.dataColumns, res.dataColumns).length > 0) { + if (res && _difference(props.dataColumns, res.dataColumns).length > 0 && !res.allColumns) { dbg('Delete cache because incomplete columns', props.pureName, res.dataColumns); // we have incomplete cache @@ -113,5 +124,6 @@ export class PerspectiveCache { clear() { this.tables = {}; + this.dataPatterns = []; } } diff --git a/packages/datalib/src/PerspectiveConfig.ts b/packages/datalib/src/PerspectiveConfig.ts index 1f8ec5a89..d7af928f6 100644 --- a/packages/datalib/src/PerspectiveConfig.ts +++ b/packages/datalib/src/PerspectiveConfig.ts @@ -7,6 +7,13 @@ import uuidv1 from 'uuid/v1'; // uncheckedColumns: string[]; // } +export type PerspectiveDatabaseEngineType = 'sqldb' | 'docdb'; + +export interface PerspectiveDatabaseConfig { + conid: string; + database: string; +} + export interface PerspectiveCustomJoinConfig { refNodeDesignerId: string; referenceDesignerId: string; diff --git a/packages/datalib/src/PerspectiveDataLoader.ts b/packages/datalib/src/PerspectiveDataLoader.ts index 716b89bd0..1c9df1439 100644 --- a/packages/datalib/src/PerspectiveDataLoader.ts +++ b/packages/datalib/src/PerspectiveDataLoader.ts @@ -1,19 +1,40 @@ import { Condition, Expression, Select } from 'dbgate-sqltree'; import { PerspectiveDataLoadProps } from './PerspectiveDataProvider'; import debug from 'debug'; +import _zipObject from 'lodash/zipObject'; +import _mapValues from 'lodash/mapValues'; +import _isArray from 'lodash/isArray'; +import { safeJsonParse } from 'dbgate-tools'; + +function normalizeLoadedRow(row) { + return _mapValues(row, v => safeJsonParse(v) || v); +} + +function normalizeResult(result) { + if (_isArray(result)) { + return result.map(normalizeLoadedRow); + } + if (result.errorMessage) { + return result; + } + return { + ...result, + errorMessage: 'Unspecified error', + }; +} const dbg = debug('dbgate:PerspectiveDataLoader'); export class PerspectiveDataLoader { constructor(public apiCall) {} - buildCondition(props: PerspectiveDataLoadProps): Condition { - const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props; + buildSqlCondition(props: PerspectiveDataLoadProps): Condition { + const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, sqlCondition } = props; const conditions = []; - if (condition) { - conditions.push(condition); + if (sqlCondition) { + conditions.push(sqlCondition); } if (bindingColumns?.length == 1) { @@ -38,8 +59,26 @@ export class PerspectiveDataLoader { : null; } - async loadGrouping(props: PerspectiveDataLoadProps) { - const { schemaName, pureName, bindingColumns, bindingValues, dataColumns } = props; + buildMongoCondition(props: PerspectiveDataLoadProps): {} { + const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, mongoCondition } = props; + + const conditions = []; + + if (mongoCondition) { + conditions.push(mongoCondition); + } + + if (bindingColumns?.length == 1) { + conditions.push({ + [bindingColumns[0]]: { $in: bindingValues.map(x => x[0]) }, + }); + } + + return conditions.length == 1 ? conditions[0] : conditions.length > 0 ? { $and: conditions } : null; + } + + async loadGroupingSqlDb(props: PerspectiveDataLoadProps) { + const { schemaName, pureName, bindingColumns } = props; const bindingColumnExpressions = bindingColumns.map( columnName => @@ -71,13 +110,13 @@ export class PerspectiveDataLoader { }, ...bindingColumnExpressions, ], - where: this.buildCondition(props), + where: this.buildSqlCondition(props), }; select.groupBy = bindingColumnExpressions; if (dbg?.enabled) { - dbg(`LOAD COUNTS, table=${props.pureName}, columns=${props.dataColumns?.join(',')}`); + dbg(`LOAD COUNTS, table=${props.pureName}, columns=${bindingColumns?.join(',')}`); } const response = await this.apiCall('database-connections/sql-select', { @@ -93,8 +132,63 @@ export class PerspectiveDataLoader { })); } - async loadData(props: PerspectiveDataLoadProps) { - const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props; + async loadGroupingDocDb(props: PerspectiveDataLoadProps) { + const { schemaName, pureName, bindingColumns } = props; + + const aggregate = [ + { $match: this.buildMongoCondition(props) }, + { + $group: { + _id: _zipObject( + bindingColumns, + bindingColumns.map(col => '$' + col) + ), + count: { $sum: 1 }, + }, + }, + ]; + + if (dbg?.enabled) { + dbg(`LOAD COUNTS, table=${props.pureName}, columns=${bindingColumns?.join(',')}`); + } + + const response = await this.apiCall('database-connections/collection-data', { + conid: props.databaseConfig.conid, + database: props.databaseConfig.database, + options: { + pureName, + aggregate, + }, + }); + + if (response.errorMessage) return response; + return response.rows.map(row => ({ + ...row._id, + _perspective_group_size_: parseInt(row.count), + })); + } + + async loadGrouping(props: PerspectiveDataLoadProps) { + const { engineType } = props; + switch (engineType) { + case 'sqldb': + return this.loadGroupingSqlDb(props); + case 'docdb': + return this.loadGroupingDocDb(props); + } + } + + async loadDataSqlDb(props: PerspectiveDataLoadProps) { + const { + schemaName, + pureName, + bindingColumns, + bindingValues, + dataColumns, + orderBy, + sqlCondition: condition, + engineType, + } = props; if (dataColumns?.length == 0) { return []; @@ -113,16 +207,19 @@ export class PerspectiveDataLoader { }, })), selectAll: !dataColumns, - orderBy: orderBy?.map(({ columnName, order }) => ({ - exprType: 'column', - columnName, - direction: order, - source: { - name: { schemaName, pureName }, - }, - })), + orderBy: + orderBy?.length > 0 + ? orderBy?.map(({ columnName, order }) => ({ + exprType: 'column', + columnName, + direction: order, + source: { + name: { schemaName, pureName }, + }, + })) + : null, range: props.range, - where: this.buildCondition(props), + where: this.buildSqlCondition(props), }; if (dbg?.enabled) { @@ -143,8 +240,76 @@ export class PerspectiveDataLoader { return response.rows; } - async loadRowCount(props: PerspectiveDataLoadProps) { - const { schemaName, pureName, bindingColumns, bindingValues, dataColumns, orderBy, condition } = props; + getDocDbLoadOptions(props: PerspectiveDataLoadProps, useSort: boolean) { + const { pureName } = props; + const res: any = { + pureName, + condition: this.buildMongoCondition(props), + skip: props.range?.offset, + limit: props.range?.limit, + }; + if (useSort && props.orderBy?.length > 0) { + res.sort = _zipObject( + props.orderBy.map(col => col.columnName), + props.orderBy.map(col => (col.order == 'DESC' ? -1 : 1)) + ); + } + + return res; + } + + async loadDataDocDb(props: PerspectiveDataLoadProps) { + const { + schemaName, + pureName, + bindingColumns, + bindingValues, + dataColumns, + orderBy, + sqlCondition: condition, + engineType, + } = props; + + if (dbg?.enabled) { + dbg( + `LOAD DATA, collection=${props.pureName}, columns=${props.dataColumns?.join(',')}, range=${ + props.range?.offset + },${props.range?.limit}` + ); + } + + const options = this.getDocDbLoadOptions(props, true); + + const response = await this.apiCall('database-connections/collection-data', { + conid: props.databaseConfig.conid, + database: props.databaseConfig.database, + options, + }); + + if (response.errorMessage) return response; + return response.rows; + } + + async loadData(props: PerspectiveDataLoadProps) { + const { engineType } = props; + switch (engineType) { + case 'sqldb': + return normalizeResult(await this.loadDataSqlDb(props)); + case 'docdb': + return normalizeResult(await this.loadDataDocDb(props)); + } + } + + async loadRowCountSqlDb(props: PerspectiveDataLoadProps) { + const { + schemaName, + pureName, + bindingColumns, + bindingValues, + dataColumns, + orderBy, + sqlCondition: condition, + } = props; const select: Select = { commandType: 'select', @@ -158,7 +323,7 @@ export class PerspectiveDataLoader { alias: 'count', }, ], - where: this.buildCondition(props), + where: this.buildSqlCondition(props), }; const response = await this.apiCall('database-connections/sql-select', { @@ -170,4 +335,39 @@ export class PerspectiveDataLoader { if (response.errorMessage) return response; return response.rows[0]; } + + async loadRowCountDocDb(props: PerspectiveDataLoadProps) { + const { + schemaName, + pureName, + bindingColumns, + bindingValues, + dataColumns, + orderBy, + sqlCondition: condition, + } = props; + + const options = { + ...this.getDocDbLoadOptions(props, false), + countDocuments: true, + }; + + const response = await this.apiCall('database-connections/collection-data', { + conid: props.databaseConfig.conid, + database: props.databaseConfig.database, + options, + }); + + return response; + } + + async loadRowCount(props: PerspectiveDataLoadProps) { + const { engineType } = props; + switch (engineType) { + case 'sqldb': + return this.loadRowCountSqlDb(props); + case 'docdb': + return this.loadRowCountDocDb(props); + } + } } diff --git a/packages/datalib/src/PerspectiveDataPattern.ts b/packages/datalib/src/PerspectiveDataPattern.ts new file mode 100644 index 000000000..876ffc00a --- /dev/null +++ b/packages/datalib/src/PerspectiveDataPattern.ts @@ -0,0 +1,95 @@ +import { PerspectiveDataLoader } from './PerspectiveDataLoader'; +import { PerspectiveDataLoadProps } from './PerspectiveDataProvider'; +import _isString from 'lodash/isString'; +import _isPlainObject from 'lodash/isPlainObject'; +import _isNumber from 'lodash/isNumber'; +import _isBoolean from 'lodash/isBoolean'; +import _isArray from 'lodash/isArray'; +import { safeJsonParse } from 'dbgate-tools'; + +export type PerspectiveDataPatternColumnType = 'null' | 'oid' | 'string' | 'number' | 'boolean' | 'json'; + +export interface PerspectiveDataPatternColumn { + name: string; + types: PerspectiveDataPatternColumnType[]; + columns: PerspectiveDataPatternColumn[]; +} + +export interface PerspectiveDataPattern { + conid: string; + database: string; + schemaName?: string; + pureName: string; + columns: PerspectiveDataPatternColumn[]; +} + +export type PerspectiveDataPatternDict = { [designerId: string]: PerspectiveDataPattern }; + +function detectValueType(value): PerspectiveDataPatternColumnType { + if (_isString(value)) return 'string'; + if (_isNumber(value)) return 'number'; + if (_isBoolean(value)) return 'boolean'; + if (value?.$oid) return 'oid'; + if (_isPlainObject(value) || _isArray(value)) return 'json'; + if (value == null) return 'null'; +} + +function addObjectToColumns(columns: PerspectiveDataPatternColumn[], row) { + if (_isPlainObject(row)) { + for (const key of Object.keys(row)) { + let column: PerspectiveDataPatternColumn = columns.find(x => x.name == key); + if (!column) { + column = { + name: key, + types: [], + columns: [], + }; + columns.push(column); + } + const value = row[key]; + const type = detectValueType(value); + if (!column.types.includes(type)) { + column.types.push(type); + } + if (_isPlainObject(value)) { + addObjectToColumns(column.columns, value); + } + if (_isArray(value)) { + for (const item of value) { + addObjectToColumns(column.columns, item); + } + } + if (_isString(value)) { + const json = safeJsonParse(value); + if (json && (_isPlainObject(json) || _isArray(json))) { + if (!column.types.includes('json')) { + column.types.push('json'); + } + if (_isPlainObject(json)) { + addObjectToColumns(column.columns, json); + } + if (_isArray(json)) { + for (const item of json) { + addObjectToColumns(column.columns, item); + } + } + } + } + } + } +} + +export function analyseDataPattern( + patternBase: Omit, + rows: any[] +): PerspectiveDataPattern { + const res: PerspectiveDataPattern = { + ...patternBase, + columns: [], + }; + // console.log('ROWS', rows); + for (const row of rows) { + addObjectToColumns(res.columns, row); + } + return res; +} diff --git a/packages/datalib/src/PerspectiveDataProvider.ts b/packages/datalib/src/PerspectiveDataProvider.ts index 17e3bcac6..5c471c55e 100644 --- a/packages/datalib/src/PerspectiveDataProvider.ts +++ b/packages/datalib/src/PerspectiveDataProvider.ts @@ -1,24 +1,21 @@ 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'; +import { PerspectiveDataPatternDict } from './PerspectiveDataPattern'; +import { PerspectiveDatabaseConfig, PerspectiveDatabaseEngineType } from './PerspectiveConfig'; 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; + schemaName?: string; pureName: string; - dataColumns: string[]; + dataColumns?: string[]; + allColumns?: boolean; orderBy: { columnName: string; order: 'ASC' | 'DESC'; @@ -27,11 +24,17 @@ export interface PerspectiveDataLoadProps { bindingValues?: any[][]; range?: RangeDefinition; topCount?: number; - condition?: Condition; + sqlCondition?: Condition; + mongoCondition?: any; + engineType: PerspectiveDatabaseEngineType; } export class PerspectiveDataProvider { - constructor(public cache: PerspectiveCache, public loader: PerspectiveDataLoader) {} + constructor( + public cache: PerspectiveCache, + public loader: PerspectiveDataLoader, + public dataPatterns: PerspectiveDataPatternDict + ) {} async loadData(props: PerspectiveDataLoadProps): Promise<{ rows: any[]; incomplete: boolean }> { dbg('load data', props); // console.log('LOAD DATA', props); @@ -182,6 +185,7 @@ export class PerspectiveDataProvider { // load missing rows tableCache.dataColumns = props.dataColumns; + tableCache.allColumns = props.allColumns; const nextRows = await this.loader.loadData({ ...props, diff --git a/packages/datalib/src/PerspectiveDisplay.ts b/packages/datalib/src/PerspectiveDisplay.ts index 7bf8d3e14..f7f5521e7 100644 --- a/packages/datalib/src/PerspectiveDisplay.ts +++ b/packages/datalib/src/PerspectiveDisplay.ts @@ -3,6 +3,8 @@ import _max from 'lodash/max'; import _range from 'lodash/max'; import _fill from 'lodash/fill'; import _findIndex from 'lodash/findIndex'; +import _isPlainObject from 'lodash/isPlainObject'; +import _isArray from 'lodash/isArray'; import debug from 'debug'; const dbg = debug('dbgate:PerspectiveDisplay'); @@ -126,14 +128,14 @@ export class PerspectiveDisplay { fillColumns(children: PerspectiveTreeNode[], parentNodes: PerspectiveTreeNode[]) { for (const child of children) { - if (child.isCheckedColumn || child.isCheckedNode) { + if (child.generatesHiearchicGridColumn || child.generatesDataGridColumn) { this.processColumn(child, parentNodes); } } } processColumn(node: PerspectiveTreeNode, parentNodes: PerspectiveTreeNode[]) { - if (node.isCheckedColumn) { + if (node.generatesDataGridColumn) { const column = new PerspectiveDisplayColumn(this); column.title = node.columnTitle; column.dataField = node.dataField; @@ -145,7 +147,7 @@ export class PerspectiveDisplay { this.columns.push(column); } - if (node.isExpandable && node.isCheckedNode) { + if (node.generatesHiearchicGridColumn) { const countBefore = this.columns.length; this.fillColumns(node.childNodes, [...parentNodes, node]); @@ -167,13 +169,30 @@ export class PerspectiveDisplay { // return _findIndex(this.columns, x => x.dataNode.designerId == node.designerId); // } + extractArray(value) { + if (_isArray(value)) return value; + if (_isPlainObject(value)) return [value]; + return []; + } + collectRows(sourceRows: any[], nodes: PerspectiveTreeNode[]): CollectedPerspectiveDisplayRow[] { // console.log('********** COLLECT ROWS', sourceRows); - const columnNodes = nodes.filter(x => x.isCheckedColumn); - const treeNodes = nodes.filter(x => x.isCheckedNode); + const columnNodes = nodes.filter(x => x.generatesDataGridColumn); + const treeNodes = nodes.filter(x => x.generatesHiearchicGridColumn); - // console.log('columnNodes', columnNodes); - // console.log('treeNodes', treeNodes); + // console.log( + // 'columnNodes', + // columnNodes.map(x => x.title) + // ); + // console.log( + // 'treeNodes', + // treeNodes.map(x => x.title) + // ); + + // console.log( + // 'nodes', + // nodes.map(x => x.title) + // ); const columnIndexes = columnNodes.map(node => this.findColumnIndexFromNode(node)); @@ -181,13 +200,14 @@ export class PerspectiveDisplay { for (const sourceRow of sourceRows) { // console.log('PROCESS SOURCE', sourceRow); // row.startIndex = startIndex; - const rowData = columnNodes.map(node => sourceRow[node.codeName]); + const rowData = columnNodes.map(node => sourceRow[node.columnName]); const subRowCollections = []; for (const node of treeNodes) { + // console.log('sourceRow[node.fieldName]', node.fieldName, sourceRow[node.fieldName]); if (sourceRow[node.fieldName]) { const subrows = { - rows: this.collectRows(sourceRow[node.fieldName], node.childNodes), + rows: this.collectRows(this.extractArray(sourceRow[node.fieldName]), node.childNodes), }; subRowCollections.push(subrows); } diff --git a/packages/datalib/src/PerspectiveTreeNode.ts b/packages/datalib/src/PerspectiveTreeNode.ts index 0e69f32be..4518c149f 100644 --- a/packages/datalib/src/PerspectiveTreeNode.ts +++ b/packages/datalib/src/PerspectiveTreeNode.ts @@ -1,4 +1,5 @@ import { + CollectionInfo, ColumnInfo, DatabaseInfo, ForeignKeyInfo, @@ -7,13 +8,15 @@ import { TableInfo, ViewInfo, } from 'dbgate-types'; -import { equalFullName } from 'dbgate-tools'; +import { equalFullName, isCollectionInfo, isTableInfo, isViewInfo } from 'dbgate-tools'; import { ChangePerspectiveConfigFunc, createPerspectiveNodeConfig, MultipleDatabaseInfo, PerspectiveConfig, PerspectiveCustomJoinConfig, + PerspectiveDatabaseConfig, + PerspectiveDatabaseEngineType, PerspectiveFilterColumnInfo, PerspectiveNodeConfig, PerspectiveReferenceConfig, @@ -27,17 +30,14 @@ import _uniqBy from 'lodash/uniqBy'; import _sortBy from 'lodash/sortBy'; import _cloneDeepWith from 'lodash/cloneDeepWith'; import _findIndex from 'lodash/findIndex'; -import { - PerspectiveDatabaseConfig, - PerspectiveDataLoadProps, - PerspectiveDataProvider, -} from './PerspectiveDataProvider'; +import { 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'; import uuidv1 from 'uuid/v1'; +import { PerspectiveDataPatternColumn } from './PerspectiveDataPattern'; export interface PerspectiveDataLoadPropsWithNode { props: PerspectiveDataLoadProps; @@ -79,7 +79,7 @@ export abstract class PerspectiveTreeNode { this.parentNodeConfig = parentNode?.nodeConfig; } readonly nodeConfig: PerspectiveNodeConfig; - readonly parentNodeConfig: PerspectiveNodeConfig; + parentNodeConfig: PerspectiveNodeConfig; // defaultChecked: boolean; abstract get title(); abstract get codeName(); @@ -108,6 +108,18 @@ export abstract class PerspectiveTreeNode { get namedObject(): NamedObjectInfo { return null; } + get tableNodeOrParent(): PerspectiveTableNode { + if (this instanceof PerspectiveTableNode) { + return this; + } + if (this.parentNode == null) { + return null; + } + return this.parentNode.tableNodeOrParent; + } + get engineType(): PerspectiveDatabaseEngineType { + return null; + } abstract getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps; get isRoot() { return this.parentNode == null; @@ -119,6 +131,12 @@ export abstract class PerspectiveTreeNode { get isSortable() { return false; } + get generatesHiearchicGridColumn() { + return this.isExpandable && this.isCheckedNode; + } + get generatesDataGridColumn() { + return this.isCheckedColumn; + } matchChildRow(parentRow: any, childRow: any): boolean { return true; } @@ -271,14 +289,15 @@ export abstract class PerspectiveTreeNode { [field]: isIncluded ? [...(n[field] || []), this.codeName] : (n[field] || []).filter(x => x != this.codeName), }); - const [cfgChanged, nodeCfg] = this.parentNode?.ensureNodeConfig(cfg); + const [cfgChanged, nodeCfg] = this.parentNode?.tableNodeOrParent?.ensureNodeConfig(cfg); - return { + const res = { ...cfgChanged, nodes: cfgChanged.nodes.map(n => - n.designerId == (this.parentNode?.designerId || nodeCfg?.designerId) ? changedFields(n) : n + n.designerId == (this.parentNode?.tableNodeOrParent?.designerId || nodeCfg?.designerId) ? changedFields(n) : n ), }; + return res; }); } @@ -292,11 +311,15 @@ export abstract class PerspectiveTreeNode { ...this.childNodes.map(x => x.childDataColumn), ..._flatten(this.childNodes.filter(x => x.isExpandable && x.isChecked).map(x => x.getChildMatchColumns())), ...this.getParentMatchColumns(), + ...this.childNodes + .filter(x => x instanceof PerspectivePatternColumnNode) + .filter(x => this.nodeConfig?.checkedColumns?.find(y => y.startsWith(x.codeName + '::'))) + .map(x => x.columnName), ]) ); } - getChildrenCondition(source = null): Condition { + getChildrenSqlCondition(source = null): Condition { const conditions = _compact([ ...this.childNodes.map(x => x.parseFilterCondition(source)), ...this.buildParentFilterConditions(), @@ -313,7 +336,18 @@ export abstract class PerspectiveTreeNode { }; } - getOrderBy(table: TableInfo | ViewInfo): PerspectiveDataLoadProps['orderBy'] { + getChildrenMongoCondition(source = null): {} { + const conditions = _compact([...this.childNodes.map(x => x.parseFilterCondition(source))]); + if (conditions.length == 0) { + return null; + } + if (conditions.length == 1) { + return conditions[0]; + } + return { $and: conditions }; + } + + getOrderBy(table: TableInfo | ViewInfo | CollectionInfo): PerspectiveDataLoadProps['orderBy'] { const res = _compact( this.childNodes.map(node => { const sort = this.nodeConfig?.sort?.find(x => x.columnName == node.columnName); @@ -325,11 +359,15 @@ export abstract class PerspectiveTreeNode { } }) ); - return res.length > 0 - ? res - : (table as TableInfo)?.primaryKey?.columns.map(x => ({ columnName: x.columnName, order: 'ASC' })) || [ - { columnName: table?.columns[0].columnName, order: 'ASC' }, - ]; + if (res.length > 0) return res; + const pkColumns = (table as TableInfo)?.primaryKey?.columns.map(x => ({ + columnName: x.columnName, + order: 'ASC' as 'ASC', + })); + if (pkColumns) return pkColumns; + const columns = (table as TableInfo | ViewInfo)?.columns; + if (columns) return [{ columnName: columns[0].columnName, order: 'ASC' }]; + return [{ columnName: '_id', order: 'ASC' }]; } getBaseTables() { @@ -390,7 +428,9 @@ export abstract class PerspectiveTreeNode { return ( (this.parentNode?.isRoot || this.parentNode?.supportsParentFilter) && this.parentNode?.databaseConfig?.conid == this.databaseConfig?.conid && - this.parentNode?.databaseConfig?.database == this.databaseConfig?.database + this.parentNode?.databaseConfig?.database == this.databaseConfig?.database && + this.engineType == 'sqldb' && + this.parentNode?.engineType == 'sqldb' ); } @@ -438,7 +478,7 @@ export abstract class PerspectiveTreeNode { conditionType: 'and', conditions: _compact([ ...lastNode.getParentJoinCondition(lastAlias, this.namedObject.pureName), - leafNode.getChildrenCondition({ alias: 'pert_0' }), + leafNode.getChildrenSqlCondition({ alias: 'pert_0' }), ]), }; @@ -496,6 +536,10 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode { ); } + get engineType() { + return this.parentNode.engineType; + } + matchChildRow(parentRow: any, childRow: any): boolean { if (!this.foreignKey) return false; return parentRow[this.foreignKey.columns[0].columnName] == childRow[this.foreignKey.columns[0].refColumnName]; @@ -552,7 +596,8 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode { dataColumns: this.getDataLoadColumns(), databaseConfig: this.databaseConfig, orderBy: this.getOrderBy(this.refTable), - condition: this.getChildrenCondition(), + sqlCondition: this.getChildrenSqlCondition(), + engineType: 'sqldb', }; } @@ -573,6 +618,7 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode { get fieldName() { return this.codeName + 'Ref'; + // return this.codeName ; } get title() { @@ -670,6 +716,7 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode { pureName: this.foreignKey.refTableName, conid: this.databaseConfig.conid, database: this.databaseConfig.database, + objectTypeField: this.table.objectTypeField, }; } return null; @@ -693,9 +740,216 @@ export class PerspectiveTableColumnNode extends PerspectiveTreeNode { } } +export class PerspectivePatternColumnNode extends PerspectiveTreeNode { + foreignKey: ForeignKeyInfo; + refTable: TableInfo; + + constructor( + public table: TableInfo | ViewInfo | CollectionInfo, + public column: PerspectiveDataPatternColumn, + public tableColumn: ColumnInfo, + dbs: MultipleDatabaseInfo, + config: PerspectiveConfig, + setConfig: ChangePerspectiveConfigFunc, + dataProvider: PerspectiveDataProvider, + databaseConfig: PerspectiveDatabaseConfig, + parentNode: PerspectiveTreeNode, + designerId: string + ) { + super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig, designerId); + this.parentNodeConfig = this.tableNodeOrParent?.nodeConfig; + } + + get isChildColumn() { + return this.parentNode instanceof PerspectivePatternColumnNode; + } + + // 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; + // }); + // } + + // createReferenceConfigColumns(): PerspectiveReferenceConfig['columns'] { + // return this.foreignKey?.columns?.map(col => ({ + // source: col.columnName, + // target: col.refColumnName, + // })); + // } + + getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps { + return null; + } + + get generatesHiearchicGridColumn() { + // console.log('generatesHiearchicGridColumn', this.parentTableNode?.nodeConfig?.checkedColumns, this.codeName + '::'); + return !!this.tableNodeOrParent?.nodeConfig?.checkedColumns?.find(x => x.startsWith(this.codeName + '::')); + } + + // get generatesHiearchicGridColumn() { + // // return this.config &&; + // } + + get icon() { + if (this.column.types.includes('json')) { + return 'img json'; + } + return 'img column'; + } + + get codeName() { + if (this.parentNode instanceof PerspectivePatternColumnNode) { + return `${this.parentNode.codeName}::${this.column.name}`; + } + return this.column.name; + } + + get columnName() { + return this.column.name; + } + + get fieldName() { + return this.column.name; + } + + get title() { + return this.column.name; + } + + get isExpandable() { + return this.column.columns.length > 0; + } + + get isSortable() { + return !this.isChildColumn; + } + + get filterType(): FilterType { + if (this.tableColumn) return getFilterType(this.tableColumn.dataType); + return 'mongo'; + } + + generateChildNodes(): PerspectiveTreeNode[] { + return this.column.columns.map( + column => + new PerspectivePatternColumnNode( + this.table, + column, + this.tableColumn, + this.dbs, + this.config, + this.setConfig, + this.dataProvider, + this.databaseConfig, + this, + null + ) + ); + return []; + // 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 + // ); + } + + get filterInfo(): PerspectiveFilterColumnInfo { + if (this.isChildColumn) { + return null; + } + + return { + columnName: this.columnName, + filterType: this.filterType, + pureName: this.table.pureName, + schemaName: this.table.schemaName, + foreignKey: this.foreignKey, + }; + } + + parseFilterCondition(source = null): {} { + const filter = this.getFilter(); + if (!filter) return null; + const condition = parseFilter(filter, 'mongo'); + if (!condition) return null; + return _cloneDeepWith(condition, expr => { + if (expr.__placeholder__) { + return { + [this.columnName]: expr.__placeholder__, + }; + } + }); + } + + // 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() { + // return `${this.collection.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, + public table: TableInfo | ViewInfo | CollectionInfo, dbs: MultipleDatabaseInfo, config: PerspectiveConfig, setConfig: ChangePerspectiveConfigFunc, @@ -707,14 +961,22 @@ export class PerspectiveTableNode extends PerspectiveTreeNode { super(dbs, config, setConfig, parentNode, dataProvider, databaseConfig, designerId); } + get engineType(): PerspectiveDatabaseEngineType { + return isCollectionInfo(this.table) ? 'docdb' : 'sqldb'; + } + getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps { + const isMongo = isCollectionInfo(this.table); return { schemaName: this.table.schemaName, pureName: this.table.pureName, dataColumns: this.getDataLoadColumns(), + allColumns: isMongo, databaseConfig: this.databaseConfig, orderBy: this.getOrderBy(this.table), - condition: this.getChildrenCondition(), + sqlCondition: isMongo ? null : this.getChildrenSqlCondition(), + mongoCondition: isMongo ? this.getChildrenMongoCondition() : null, + engineType: isMongo ? 'docdb' : 'sqldb', }; } @@ -756,6 +1018,7 @@ export class PerspectiveTableNode extends PerspectiveTreeNode { pureName: this.table.pureName, conid: this.databaseConfig.conid, database: this.databaseConfig.database, + objectTypeField: this.table.objectTypeField, }; } @@ -770,64 +1033,6 @@ export class PerspectiveTableNode extends PerspectiveTreeNode { }; } } - -// 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, @@ -872,7 +1077,8 @@ export class PerspectiveTableReferenceNode extends PerspectiveTableNode { dataColumns: this.getDataLoadColumns(), databaseConfig: this.databaseConfig, orderBy: this.getOrderBy(this.table), - condition: this.getChildrenCondition(), + sqlCondition: this.getChildrenSqlCondition(), + engineType: 'sqldb', }; } @@ -934,7 +1140,7 @@ export class PerspectiveTableReferenceNode extends PerspectiveTableNode { export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode { constructor( public customJoin: PerspectiveCustomJoinConfig, - table: TableInfo | ViewInfo, + table: TableInfo | ViewInfo | CollectionInfo, dbs: MultipleDatabaseInfo, config: PerspectiveConfig, setConfig: ChangePerspectiveConfigFunc, @@ -966,6 +1172,8 @@ export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode { getNodeLoadProps(parentRows: any[]): PerspectiveDataLoadProps { // console.log('CUSTOM JOIN', this.customJoin); // console.log('this.getDataLoadColumns()', this.getDataLoadColumns()); + const isMongo = isCollectionInfo(this.table); + return { schemaName: this.table.schemaName, pureName: this.table.pureName, @@ -975,9 +1183,12 @@ export class PerspectiveCustomJoinTreeNode extends PerspectiveTableNode { stableStringify ), dataColumns: this.getDataLoadColumns(), + allColumns: isMongo, databaseConfig: this.databaseConfig, orderBy: this.getOrderBy(this.table), - condition: this.getChildrenCondition(), + sqlCondition: isMongo ? null : this.getChildrenSqlCondition(), + mongoCondition: isMongo ? this.getChildrenMongoCondition() : null, + engineType: isMongo ? 'docdb' : 'sqldb', }; } @@ -1082,7 +1293,7 @@ function findDesignerIdForNode( } export function getTableChildPerspectiveNodes( - table: TableInfo | ViewInfo, + table: TableInfo | ViewInfo | CollectionInfo, dbs: MultipleDatabaseInfo, config: PerspectiveConfig, setConfig: ChangePerspectiveConfigFunc, @@ -1093,25 +1304,59 @@ export function getTableChildPerspectiveNodes( if (!table) return []; const db = parentNode.db; - const columnNodes = table.columns.map(col => - findDesignerIdForNode( - config, - parentNode, - designerId => - new PerspectiveTableColumnNode( - col, - table, - dbs, - config, - setConfig, - dataProvider, - databaseConfig, - parentNode, - designerId - ) - ) - ); + const pattern = dataProvider?.dataPatterns?.[parentNode.designerId]; + const tableOrView = isTableInfo(table) || isViewInfo(table) ? table : null; + + const columnNodes = + tableOrView?.columns?.map(col => + findDesignerIdForNode(config, parentNode, designerId => + pattern?.columns?.find(x => x.name == col.columnName)?.types.includes('json') + ? new PerspectivePatternColumnNode( + table, + pattern?.columns?.find(x => x.name == col.columnName), + col, + dbs, + config, + setConfig, + dataProvider, + databaseConfig, + parentNode, + designerId + ) + : new PerspectiveTableColumnNode( + col, + tableOrView, + dbs, + config, + setConfig, + dataProvider, + databaseConfig, + parentNode, + designerId + ) + ) + ) || + pattern?.columns?.map(col => + findDesignerIdForNode( + config, + parentNode, + designerId => + new PerspectivePatternColumnNode( + table, + col, + null, + dbs, + config, + setConfig, + dataProvider, + databaseConfig, + parentNode, + designerId + ) + ) + ) || + []; // if (!columnNodes.find(x => x.isChecked)) { // const circularColumns = columnNodes.filter(x => x.isCircular).map(x => x.columnName); // const defaultColumns = getPerspectiveDefaultColumns(table, db, circularColumns); @@ -1173,6 +1418,7 @@ export function getTableChildPerspectiveNodes( const db = dbs?.[newConfig.conid]?.[newConfig.database]; const table = db?.tables?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); const view = db?.views?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); + const collection = db?.collections?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); const join: PerspectiveCustomJoinConfig = { refNodeDesignerId: node.designerId, @@ -1189,11 +1435,11 @@ export function getTableChildPerspectiveNodes( : ref.columns.map(col => ({ baseColumnName: col.target, refColumnName: col.source })), }; - if (table || view) { + if (table || view || collection) { customs.push( new PerspectiveCustomJoinTreeNode( join, - table || view, + table || view || collection, dbs, config, setConfig, @@ -1210,34 +1456,5 @@ export function getTableChildPerspectiveNodes( res.push(..._sortBy(customs, '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, - // null - // ) - // ); - // } - // } - // } - // res.push(..._sortBy(customs, 'title')); - return res; } diff --git a/packages/datalib/src/index.ts b/packages/datalib/src/index.ts index dace2858f..d4626bb57 100644 --- a/packages/datalib/src/index.ts +++ b/packages/datalib/src/index.ts @@ -19,3 +19,4 @@ export * from './PerspectiveDataProvider'; export * from './PerspectiveCache'; export * from './PerspectiveConfig'; export * from './processPerspectiveDefaultColunns'; +export * from './PerspectiveDataPattern'; diff --git a/packages/datalib/src/processPerspectiveDefaultColunns.ts b/packages/datalib/src/processPerspectiveDefaultColunns.ts index 060c2890b..2e106acf7 100644 --- a/packages/datalib/src/processPerspectiveDefaultColunns.ts +++ b/packages/datalib/src/processPerspectiveDefaultColunns.ts @@ -1,8 +1,17 @@ import { findForeignKeyForColumn } from 'dbgate-tools'; import { DatabaseInfo, TableInfo, ViewInfo } from 'dbgate-types'; import { createPerspectiveNodeConfig, MultipleDatabaseInfo, PerspectiveConfig } from './PerspectiveConfig'; +import { PerspectiveDataPattern, PerspectiveDataPatternDict } from './PerspectiveDataPattern'; import { PerspectiveTableNode } from './PerspectiveTreeNode'; +const namePredicates = [ + x => x.toLowerCase() == 'name', + x => x.toLowerCase() == 'title', + x => x.toLowerCase().includes('name'), + x => x.toLowerCase().includes('title'), + x => x.toLowerCase().includes('subject'), +]; + function getPerspectiveDefaultColumns( table: TableInfo | ViewInfo, db: DatabaseInfo, @@ -10,13 +19,7 @@ function getPerspectiveDefaultColumns( ): [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'), + ...namePredicates, x => table.columns .find(y => y.columnName == x) @@ -44,9 +47,20 @@ function getPerspectiveDefaultColumns( return [[columns[0]], null]; } +function getPerspectiveDefaultCollectionColumns(pattern: PerspectiveDataPattern): string[] { + const columns = pattern.columns.map(x => x.name); + const predicates = [...namePredicates, x => pattern.columns.find(y => y.name == x)?.types?.includes('string')]; + + for (const predicate of predicates) { + const col = columns.find(predicate); + if (col) return [col]; + } +} + export function perspectiveNodesHaveStructure( config: PerspectiveConfig, dbInfos: MultipleDatabaseInfo, + dataPatterns: PerspectiveDataPatternDict, conid: string, database: string ) { @@ -56,8 +70,10 @@ export function perspectiveNodesHaveStructure( const table = db.tables.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); const view = db.views.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); + const collection = db.collections.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); - if (!table && !view) return false; + if (!table && !view && !collection) return false; + if (collection && !dataPatterns?.[node.designerId]) return false; } return true; @@ -66,18 +82,20 @@ export function perspectiveNodesHaveStructure( export function shouldProcessPerspectiveDefaultColunns( config: PerspectiveConfig, dbInfos: MultipleDatabaseInfo, + dataPatterns: PerspectiveDataPatternDict, conid: string, database: string ) { const nodesNotProcessed = config.nodes.filter(x => !x.defaultColumnsProcessed); if (nodesNotProcessed.length == 0) return false; - return perspectiveNodesHaveStructure(config, dbInfos, conid, database); + return perspectiveNodesHaveStructure(config, dbInfos, dataPatterns, conid, database); } function processPerspectiveDefaultColunnsStep( config: PerspectiveConfig, dbInfos: MultipleDatabaseInfo, + dataPatterns: PerspectiveDataPatternDict, conid: string, database: string ) { @@ -107,6 +125,7 @@ function processPerspectiveDefaultColunnsStep( const table = db.tables.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); const view = db.views.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); + const collection = db.collections.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); if (table || view) { const treeNode = root.findNodeByDesignerId(node.designerId); @@ -181,6 +200,22 @@ function processPerspectiveDefaultColunnsStep( }; } } + + if (collection) { + const defaultColumns = getPerspectiveDefaultCollectionColumns(dataPatterns?.[node.designerId]); + return { + ...config, + nodes: config.nodes.map(n => + n.designerId == node.designerId + ? { + ...n, + defaultColumnsProcessed: true, + checkedColumns: defaultColumns, + } + : n + ), + }; + } } return null; @@ -199,11 +234,12 @@ function markAllProcessed(config: PerspectiveConfig): PerspectiveConfig { export function processPerspectiveDefaultColunns( config: PerspectiveConfig, dbInfos: MultipleDatabaseInfo, + dataPatterns: PerspectiveDataPatternDict, conid: string, database: string ): PerspectiveConfig { while (config.nodes.filter(x => !x.defaultColumnsProcessed).length > 0) { - const newConfig = processPerspectiveDefaultColunnsStep(config, dbInfos, conid, database); + const newConfig = processPerspectiveDefaultColunnsStep(config, dbInfos, dataPatterns, conid, database); if (!newConfig) { return markAllProcessed(config); } diff --git a/packages/datalib/src/tests/PerspectiveDisplay.test.ts b/packages/datalib/src/tests/PerspectiveDisplay.test.ts index d2c74f757..82bbedc82 100644 --- a/packages/datalib/src/tests/PerspectiveDisplay.test.ts +++ b/packages/datalib/src/tests/PerspectiveDisplay.test.ts @@ -1,4 +1,3 @@ -import { TableInfo } from 'dbgate-types'; import { PerspectiveDisplay } from '../PerspectiveDisplay'; import { PerspectiveTableNode } from '../PerspectiveTreeNode'; import { chinookDbInfo } from './chinookDbInfo'; @@ -13,6 +12,7 @@ test('test flat view', () => { const configColumns = processPerspectiveDefaultColunns( createPerspectiveConfig({ pureName: 'Artist' }), { conid: { db: chinookDbInfo } }, + null, 'conid', 'db' ); @@ -47,7 +47,7 @@ test('test one level nesting', () => { columns: [{ source: 'ArtistId', target: 'ArtistId' }], }); - const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, 'conid', 'db'); + const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, null, 'conid', 'db'); // const config = createPerspectiveConfig({ pureName: 'Artist' }); // config.nodes[0].checkedColumns = ['Album']; @@ -107,7 +107,7 @@ test('test two level nesting', () => { designerId: '2', columns: [{ source: 'AlbumId', target: 'AlbumId' }], }); - const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, 'conid', 'db'); + const configColumns = processPerspectiveDefaultColunns(config, { conid: { db: chinookDbInfo } }, null, 'conid', 'db'); const root = new PerspectiveTableNode( artistTable, diff --git a/packages/datalib/src/tests/PerspectiveDisplayNoSql.test.ts b/packages/datalib/src/tests/PerspectiveDisplayNoSql.test.ts new file mode 100644 index 000000000..aa08e0f83 --- /dev/null +++ b/packages/datalib/src/tests/PerspectiveDisplayNoSql.test.ts @@ -0,0 +1,98 @@ +import { PerspectiveDisplay } from '../PerspectiveDisplay'; +import { PerspectiveTableNode } from '../PerspectiveTreeNode'; +import { createPerspectiveConfig, PerspectiveNodeConfig } from '../PerspectiveConfig'; +import { processPerspectiveDefaultColunns } from '../processPerspectiveDefaultColunns'; +import { DatabaseAnalyser } from 'dbgate-tools'; +import { analyseDataPattern } from '../PerspectiveDataPattern'; +import { PerspectiveDataProvider } from '../PerspectiveDataProvider'; + +const accountData = [ + { + name: 'jan', + email: 'jan@foo.co', + follows: [{ name: 'lucie' }, { name: 'petr' }], + nested: { email: 'jan@nest.cz' }, + }, + { + name: 'romeo', + email: 'romeo@foo.co', + follows: [{ name: 'julie' }, { name: 'wiliam' }], + nested: { email: 'romeo@nest.cz' }, + }, +]; + +function createDisplay(cfgFunc?: (cfg: PerspectiveNodeConfig) => void) { + const collectionInfo = { + objectTypeField: 'collections', + pureName: 'Account', + }; + const dbInfo = { + ...DatabaseAnalyser.createEmptyStructure(), + collections: [collectionInfo], + }; + const config = createPerspectiveConfig({ pureName: 'Account' }); + const dataPatterns = { + [config.rootDesignerId]: analyseDataPattern( + { + conid: 'conid', + database: 'db', + pureName: 'Account', + }, + accountData + ), + }; + const configColumns = processPerspectiveDefaultColunns( + config, + { conid: { db: dbInfo } }, + dataPatterns, + 'conid', + 'db' + ); + if (cfgFunc) { + cfgFunc(configColumns.nodes[0]); + } + const root = new PerspectiveTableNode( + collectionInfo, + { conid: { db: dbInfo } }, + configColumns, + null, + new PerspectiveDataProvider(null, null, dataPatterns), + { conid: 'conid', database: 'db' }, + null, + configColumns.rootDesignerId + ); + + const display = new PerspectiveDisplay(root, accountData); + + return display; +} + +test('test nosql display', () => { + const display = createDisplay(); + + expect(display.rows.length).toEqual(2); + expect(display.rows[0].rowData).toEqual(['jan']); + expect(display.rows[1].rowData).toEqual(['romeo']); +}); + +test('test nosql nested array display', () => { + const display = createDisplay(cfg => { + cfg.checkedColumns = ['name', 'follows::name']; + }); + + expect(display.rows.length).toEqual(4); + expect(display.rows[0].rowData).toEqual(['jan', 'lucie']); + expect(display.rows[1].rowData).toEqual([undefined, 'petr']); + expect(display.rows[2].rowData).toEqual(['romeo', 'julie']); + expect(display.rows[3].rowData).toEqual([undefined, 'wiliam']); +}); + +test('test nosql nested object', () => { + const display = createDisplay(cfg => { + cfg.checkedColumns = ['name', 'nested::email']; + }); + + expect(display.rows.length).toEqual(2); + expect(display.rows[0].rowData).toEqual(['jan', 'jan@nest.cz']); + expect(display.rows[1].rowData).toEqual(['romeo', 'romeo@nest.cz']); +}); diff --git a/packages/tools/package.json b/packages/tools/package.json index 17001d95a..52e8d17ac 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -31,7 +31,7 @@ "typescript": "^4.4.3" }, "dependencies": { - "dbgate-query-splitter": "^4.9.0", + "dbgate-query-splitter": "^4.9.2", "dbgate-sqltree": "^5.0.0-alpha.1", "debug": "^4.3.4", "json-stable-stringify": "^1.0.1", diff --git a/packages/tools/src/SqlDumper.ts b/packages/tools/src/SqlDumper.ts index 063989a3f..c8bb5c6af 100644 --- a/packages/tools/src/SqlDumper.ts +++ b/packages/tools/src/SqlDumper.ts @@ -32,6 +32,12 @@ export class SqlDumper implements AlterProcessor { dialect: SqlDialect; indentLevel = 0; + static keywordsCase = 'upperCase'; + static convertKeywordCase(keyword: any): string { + if (this.keywordsCase == 'lowerCase') return keyword?.toString()?.toLowerCase(); + return keyword?.toString()?.toUpperCase(); + } + constructor(driver: EngineDriver) { this.driver = driver; this.dialect = driver.dialect; @@ -60,10 +66,10 @@ export class SqlDumper implements AlterProcessor { this.putRaw("'"); } putByteArrayValue(value) { - this.putRaw('NULL'); + this.put('^null'); } putValue(value) { - if (value === null) this.putRaw('NULL'); + if (value === null) this.put('^null'); else if (value === true) this.putRaw('1'); else if (value === false) this.putRaw('0'); else if (_isString(value)) this.putStringValue(value); @@ -71,7 +77,7 @@ export class SqlDumper implements AlterProcessor { else if (_isDate(value)) this.putStringValue(new Date(value).toISOString()); else if (value?.type == 'Buffer' && _isArray(value?.data)) this.putByteArrayValue(value?.data); else if (_isPlainObject(value) || _isArray(value)) this.putStringValue(JSON.stringify(value)); - else this.putRaw('NULL'); + else this.put('^null'); } putCmd(format, ...args) { this.put(format, ...args); @@ -92,7 +98,7 @@ export class SqlDumper implements AlterProcessor { case 'k': { if (value) { - this.putRaw(value.toUpperCase()); + this.putRaw(SqlDumper.convertKeywordCase(value)); } } break; @@ -128,7 +134,7 @@ export class SqlDumper implements AlterProcessor { switch (c) { case '^': while (i < length && format[i].match(/[a-z0-9_]/i)) { - this.putRaw(format[i].toUpperCase()); + this.putRaw(SqlDumper.convertKeywordCase(format[i])); i++; } break; @@ -181,6 +187,14 @@ export class SqlDumper implements AlterProcessor { this.put(' ^auto_increment'); } + createDatabase(name: string) { + this.putCmd('^create ^database %i', name); + } + + dropDatabase(name: string) { + this.putCmd('^drop ^database %i', name); + } + specialColumnOptions(column) {} columnDefinition(column: ColumnInfo, { includeDefault = true, includeNullable = true, includeCollate = true } = {}) { diff --git a/packages/tools/src/driverBase.ts b/packages/tools/src/driverBase.ts index 9037df9b2..76d2dc202 100644 --- a/packages/tools/src/driverBase.ts +++ b/packages/tools/src/driverBase.ts @@ -16,6 +16,7 @@ const dialect = { isSparse: false, isPersisted: false, }, + defaultSchemaName: null, }; export const driverBase = { diff --git a/packages/tools/src/structureTools.ts b/packages/tools/src/structureTools.ts index f3d083514..660c5779c 100644 --- a/packages/tools/src/structureTools.ts +++ b/packages/tools/src/structureTools.ts @@ -1,4 +1,4 @@ -import { DatabaseInfo, TableInfo, ApplicationDefinition } from 'dbgate-types'; +import { DatabaseInfo, TableInfo, ApplicationDefinition, ViewInfo, CollectionInfo } from 'dbgate-types'; import _flatten from 'lodash/flatten'; export function addTableDependencies(db: DatabaseInfo): DatabaseInfo { @@ -118,3 +118,15 @@ export function isTableColumnUnique(table: TableInfo, column: string) { } return false; } + +export function isTableInfo(obj: { objectTypeField?: string }): obj is TableInfo { + return obj.objectTypeField == 'tables'; +} + +export function isViewInfo(obj: { objectTypeField?: string }): obj is ViewInfo { + return obj.objectTypeField == 'views'; +} + +export function isCollectionInfo(obj: { objectTypeField?: string }): obj is CollectionInfo { + return obj.objectTypeField == 'collections'; +} diff --git a/packages/types/dialect.d.ts b/packages/types/dialect.d.ts index 70b241f1f..a8a1b2378 100644 --- a/packages/types/dialect.d.ts +++ b/packages/types/dialect.d.ts @@ -9,6 +9,7 @@ export interface SqlDialect { fallbackDataType?: string; explicitDropConstraint?: boolean; anonymousPrimaryKey?: boolean; + defaultSchemaName?: string; enableConstraintsPerTable?: boolean; dropColumnDependencies?: string[]; diff --git a/packages/types/dumper.d.ts b/packages/types/dumper.d.ts index 9d35fbdee..8c3f0147b 100644 --- a/packages/types/dumper.d.ts +++ b/packages/types/dumper.d.ts @@ -14,6 +14,8 @@ export interface SqlDumper extends AlterProcessor { putValue(value: string | number | Date); putCollection(delimiter: string, collection: T[], lambda: (item: T) => void); transform(type: TransformType, dumpExpr: () => void); + createDatabase(name: string); + dropDatabase(name: string); endCommand(); allowIdentityInsert(table: NamedObjectInfo, allow: boolean); diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index cc15d56d2..6673b64a9 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -89,9 +89,7 @@ export interface EngineDriver { ): Promise; analyseSingleTable(pool: any, name: NamedObjectInfo): Promise; getVersion(pool: any): Promise<{ version: string }>; - listDatabases( - pool: any - ): Promise< + listDatabases(pool: any): Promise< { name: string; }[] @@ -112,7 +110,8 @@ export interface EngineDriver { updateCollection(pool: any, changeSet: any): Promise; getCollectionUpdateScript(changeSet: any): string; createDatabase(pool: any, name: string): Promise; - getQuerySplitterOptions(usage: 'stream' | 'script'): any; + dropDatabase(pool: any, name: string): Promise; + getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor'): any; script(pool: any, sql: string): Promise; getNewObjectTemplates(): NewObjectTemplate[]; // direct call of pool method, only some methods could be supported, on only some drivers diff --git a/packages/web/package.json b/packages/web/package.json index 0be497b59..ce096543a 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -24,7 +24,7 @@ "chartjs-adapter-moment": "^1.0.0", "cross-env": "^7.0.3", "dbgate-datalib": "^5.0.0-alpha.1", - "dbgate-query-splitter": "^4.9.0", + "dbgate-query-splitter": "^4.9.2", "dbgate-sqltree": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1", "dbgate-types": "^5.0.0-alpha.1", diff --git a/packages/web/public/global.css b/packages/web/public/global.css index df96b6ae1..e211eef0b 100644 --- a/packages/web/public/global.css +++ b/packages/web/public/global.css @@ -167,3 +167,33 @@ textarea { color: var(--theme-font-1); border: 1px solid var(--theme-border); } + +.ace_gutter-cell.ace-gutter-sql-run { + background-repeat: no-repeat; + background-position: 2px center; + + /* content: 'â–¶'; + margin-right: 3px; */ + + /* border-radius: 20px 0px 0px 20px; */ + /* Change the color of the breakpoint if you want + box-shadow: 0px 0px 1px 1px #248c46 inset; */ +} + +.theme-type-light .ace_gutter-cell.ace-gutter-sql-run { + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMS4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMTcuODA0IDE3LjgwNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTcuODA0IDE3LjgwNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8Zz4KCTxnIGlkPSJjOThfcGxheSI+CgkJPHBhdGggZmlsbD0nIzQ0NCcgZD0iTTIuMDY3LDAuMDQzQzIuMjEtMC4wMjgsMi4zNzItMC4wMDgsMi40OTMsMC4wODVsMTMuMzEyLDguNTAzYzAuMDk0LDAuMDc4LDAuMTU0LDAuMTkxLDAuMTU0LDAuMzEzCgkJCWMwLDAuMTItMC4wNjEsMC4yMzctMC4xNTQsMC4zMTRMMi40OTIsMTcuNzE3Yy0wLjA3LDAuMDU3LTAuMTYyLDAuMDg3LTAuMjUsMC4wODdsLTAuMTc2LTAuMDQKCQkJYy0wLjEzNi0wLjA2NS0wLjIyMi0wLjIwNy0wLjIyMi0wLjM2MVYwLjQwMkMxLjg0NCwwLjI1LDEuOTMsMC4xMDcsMi4wNjcsMC4wNDN6Ii8+Cgk8L2c+Cgk8ZyBpZD0iQ2FwYV8xXzc4XyI+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==); +} + +.theme-type-dark .ace_gutter-cell.ace-gutter-sql-run { + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMS4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMTcuODA0IDE3LjgwNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTcuODA0IDE3LjgwNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8Zz4KCTxnIGlkPSJjOThfcGxheSI+CgkJPHBhdGggZmlsbD0nI2RkZCcgZD0iTTIuMDY3LDAuMDQzQzIuMjEtMC4wMjgsMi4zNzItMC4wMDgsMi40OTMsMC4wODVsMTMuMzEyLDguNTAzYzAuMDk0LDAuMDc4LDAuMTU0LDAuMTkxLDAuMTU0LDAuMzEzCgkJCWMwLDAuMTItMC4wNjEsMC4yMzctMC4xNTQsMC4zMTRMMi40OTIsMTcuNzE3Yy0wLjA3LDAuMDU3LTAuMTYyLDAuMDg3LTAuMjUsMC4wODdsLTAuMTc2LTAuMDQKCQkJYy0wLjEzNi0wLjA2NS0wLjIyMi0wLjIwNy0wLjIyMi0wLjM2MVYwLjQwMkMxLjg0NCwwLjI1LDEuOTMsMC4xMDcsMi4wNjcsMC4wNDN6Ii8+Cgk8L2c+Cgk8ZyBpZD0iQ2FwYV8xXzc4XyI+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==); +} + +.ace_gutter-cell.ace-gutter-sql-run:hover { + background-color: var(--theme-bg-2); +} + +.ace_gutter-cell.ace-gutter-current-part { + /* background-color: var(--theme-bg-2); */ + font-weight: bold; + color: var(--theme-font-hover); +} \ No newline at end of file diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 67c6b22cb..6938737e8 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -19,6 +19,7 @@ import AppTitleProvider from './utility/AppTitleProvider.svelte'; import getElectron from './utility/getElectron'; import AppStartInfo from './widgets/AppStartInfo.svelte'; + import SettingsListener from './utility/SettingsListener.svelte'; let loadedApi = false; let loadedPlugins = false; @@ -79,6 +80,7 @@ {#if loadedPlugins} + {:else} { + showModal(ConfirmModal, { + message: `Really drop database ${name}? All opened sessions with this database will be forcefully closed.`, + onConfirm: () => + apiCall('server-connections/drop-database', { + conid: connection._id, + name, + }), + }); + }; + const handleNewCollection = () => { showModal(InputTextModal, { value: '', @@ -218,6 +229,30 @@ }); }; + const handleQueryDesigner = () => { + openNewTab({ + title: 'Query #', + icon: 'img query-design', + tabComponent: 'QueryDesignTab', + props: { + conid: connection._id, + database: name, + }, + }); + }; + + const handleNewPerspective = () => { + openNewTab({ + title: 'Perspective #', + icon: 'img perspective', + tabComponent: 'PerspectiveTab', + props: { + conid: connection._id, + database: name, + }, + }); + }; + async function handleConfirmSql(sql) { saveScriptToDatabase({ conid: connection._id, database: name }, sql, false); } @@ -233,13 +268,21 @@ { onClick: handleNewQuery, text: 'New query', isNewQuery: true }, driver?.databaseEngineTypes?.includes('sql') && { onClick: handleNewTable, text: 'New table' }, driver?.databaseEngineTypes?.includes('document') && { onClick: handleNewCollection, text: 'New collection' }, + driver?.databaseEngineTypes?.includes('sql') && { onClick: handleQueryDesigner, text: 'Design query' }, + driver?.databaseEngineTypes?.includes('sql') && { + onClick: handleNewPerspective, + text: 'Design perspective query', + }, { divider: true }, isSqlOrDoc && !connection.isReadOnly && { onClick: handleImport, text: 'Import wizard' }, isSqlOrDoc && { onClick: handleExport, text: 'Export wizard' }, driver?.databaseEngineTypes?.includes('sql') && { onClick: handleSqlRestore, text: 'Restore/import SQL dump' }, driver?.supportsDatabaseDump && { onClick: handleSqlDump, text: 'Backup/export SQL dump' }, + isSqlOrDoc && + !connection.isReadOnly && + !connection.singleDatabase && { onClick: handleDropDatabase, text: 'Drop database' }, { divider: true }, - isSqlOrDoc && { onClick: handleShowDiagram, text: 'Show diagram' }, + driver?.databaseEngineTypes?.includes('sql') && { onClick: handleShowDiagram, text: 'Show diagram' }, isSqlOrDoc && { onClick: handleSqlGenerator, text: 'SQL Generator' }, isSqlOrDoc && { onClick: handleOpenJsonModel, text: 'Open model as JSON' }, isSqlOrDoc && { onClick: handleExportModel, text: 'Export DB model - experimental' }, diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte index b13cabf6e..375d8e86a 100644 --- a/packages/web/src/appobj/DatabaseObjectAppObject.svelte +++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte @@ -9,7 +9,7 @@ schemaName, ...(columns?.map(({ columnName }) => ({ childName: columnName })) || []) ); - export const createTitle = ({ pureName }) => pureName; + export const createTitle = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName); export const databaseObjectIcons = { tables: 'img table', @@ -52,7 +52,12 @@ icon: 'img table-structure', }, { - label: 'Open perspective', + label: 'Design query', + isQueryDesigner: true, + requiresWriteAccess: true, + }, + { + label: 'Design perspective query', tab: 'PerspectiveTab', forceNewTab: true, icon: 'img perspective', @@ -80,11 +85,6 @@ isDuplicateTable: true, requiresWriteAccess: true, }, - { - label: 'Query designer', - isQueryDesigner: true, - requiresWriteAccess: true, - }, { label: 'Show diagram', isDiagram: true, @@ -155,7 +155,11 @@ icon: 'img view-structure', }, { - label: 'Open perspective', + label: 'Design query', + isQueryDesigner: true, + }, + { + label: 'Design perspective query', tab: 'PerspectiveTab', forceNewTab: true, icon: 'img perspective', @@ -164,10 +168,6 @@ label: 'Drop view', isDrop: true, }, - { - label: 'Query designer', - isQueryDesigner: true, - }, { divider: true, }, @@ -345,6 +345,12 @@ }, }, }, + { + label: 'Design perspective query', + tab: 'PerspectiveTab', + forceNewTab: true, + icon: 'img perspective', + }, { label: 'Export', isExport: true, @@ -586,6 +592,16 @@ } } + function getObjectTitle(connection, schemaName, pureName) { + const driver = findEngineDriver(connection, getExtensions()); + + const defaultSchema = driver?.dialect?.defaultSchemaName; + if (schemaName && defaultSchema && schemaName != defaultSchema) { + return `${schemaName}.${pureName}`; + } + return pureName; + } + export async function openDatabaseObjectDetail( tabComponent, scriptTemplate, @@ -603,7 +619,7 @@ openNewTab( { - title: scriptTemplate ? 'Query #' : pureName, + title: scriptTemplate ? 'Query #' : getObjectTitle(connection, schemaName, pureName), tooltip, icon: icon || (scriptTemplate ? 'img sql-file' : databaseObjectIcons[objectTypeField]), tabComponent: scriptTemplate ? 'QueryTab' : tabComponent, diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index 951a67597..14961db3c 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -127,6 +127,9 @@ registerCommand({ name: 'Query design', menuName: 'New query design', onClick: () => newQueryDesign(), + testEnabled: () => + getCurrentDatabase() && + findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql'), }); registerCommand({ @@ -144,6 +147,9 @@ registerCommand({ icon: 'img diagram', name: 'ER Diagram', menuName: 'New ER diagram', + testEnabled: () => + getCurrentDatabase() && + findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql'), onClick: () => newDiagram(), }); @@ -599,7 +605,7 @@ export function registerFileCommands({ registerCommand({ id: idPrefix + '.replace', category, - keyText: 'CtrlOrCommand+H', + keyText: isMac() ? 'Alt+Command+F' : 'CtrlOrCommand+H', name: 'Replace', testEnabled: () => getCurrentEditor() != null, onClick: () => getCurrentEditor().replace(), diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index 83b7bc7f1..15e4f998b 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -200,7 +200,7 @@ id: 'dataGrid.hideColumn', category: 'Data grid', name: 'Hide column', - keyText: 'CtrlOrCommand+H', + keyText: isMac() ? 'Alt+Command+F' : 'CtrlOrCommand+H', testEnabled: () => getCurrentDataGrid() != null, onClick: () => getCurrentDataGrid().hideColumn(), }); diff --git a/packages/web/src/datagrid/JslDataGridCore.svelte b/packages/web/src/datagrid/JslDataGridCore.svelte index 94ea6df22..cb2afde3e 100644 --- a/packages/web/src/datagrid/JslDataGridCore.svelte +++ b/packages/web/src/datagrid/JslDataGridCore.svelte @@ -65,7 +65,7 @@ let changeIndex = 0; let rowCountLoaded = null; - const throttleLoadNext = _.throttle(() => domGrid.resetLoadedAll(), 500); + const throttleLoadNext = _.throttle(() => domGrid?.resetLoadedAll(), 500); const handleJslDataStats = stats => { if (stats.changeIndex < changeIndex) return; diff --git a/packages/web/src/designer/ColumnLine.svelte b/packages/web/src/designer/ColumnLine.svelte index 096d542d1..4a3284e8a 100644 --- a/packages/web/src/designer/ColumnLine.svelte +++ b/packages/web/src/designer/ColumnLine.svelte @@ -61,6 +61,9 @@ } $: sortOrderProps = settings?.getSortOrderProps ? settings?.getSortOrderProps(designerId, column.columnName) : null; + $: iconOverride = settings?.getColumnIconOverride + ? settings?.getColumnIconOverride(designerId, column.columnName) + : null;
{/if} - + {#if designerColumn?.filter} {/if} diff --git a/packages/web/src/designer/Designer.svelte b/packages/web/src/designer/Designer.svelte index cc4dad1da..2dfabbda4 100644 --- a/packages/web/src/designer/Designer.svelte +++ b/packages/web/src/designer/Designer.svelte @@ -479,7 +479,7 @@ const rect = e.target.getBoundingClientRect(); var json = JSON.parse(data); const { objectTypeField } = json; - if (objectTypeField != 'tables' && objectTypeField != 'views') return; + if (objectTypeField != 'tables' && objectTypeField != 'views' && objectTypeField != 'collections') return; json.designerId = `${json.pureName}-${uuidv1()}`; json.left = e.clientX - rect.left; json.top = e.clientY - rect.top; @@ -941,6 +941,7 @@ .empty { margin: 50px; font-size: 20px; + position: absolute; } .canvas { position: relative; diff --git a/packages/web/src/designer/DesignerTable.svelte b/packages/web/src/designer/DesignerTable.svelte index a7bab85fe..ca3162057 100644 --- a/packages/web/src/designer/DesignerTable.svelte +++ b/packages/web/src/designer/DesignerTable.svelte @@ -213,6 +213,8 @@ !isMultipleTableSelection && [{ divider: true }, createDatabaseObjectMenu({ ...table, conid, database })], ]; } + + // $: console.log('COLUMNS', columns);
diff --git a/packages/web/src/elements/Pager.svelte b/packages/web/src/elements/Pager.svelte index 0f3af1384..d2db8556b 100644 --- a/packages/web/src/elements/Pager.svelte +++ b/packages/web/src/elements/Pager.svelte @@ -22,7 +22,7 @@
{ - skip -= limit; + skip = parseInt(skip) - parseInt(limit); if (skip < 0) skip = 0; dispatch('load'); }} @@ -35,7 +35,7 @@ dispatch('load')} on:keydown={handleKeyDown} /> { - skip += limit; + skip = parseInt(skip) + parseInt(limit); dispatch('load'); }} > diff --git a/packages/web/src/elements/TabCloseButton.svelte b/packages/web/src/elements/TabCloseButton.svelte new file mode 100644 index 000000000..93355ef61 --- /dev/null +++ b/packages/web/src/elements/TabCloseButton.svelte @@ -0,0 +1,31 @@ + + + { + mousein = true; + }} + on:mouseleave={() => { + mousein = false; + }} +> + + + + diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 57302b6e8..21f5063f1 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -46,6 +46,7 @@ 'icon file': 'mdi mdi-file', 'icon loading': 'mdi mdi-loading mdi-spin', 'icon close': 'mdi mdi-close', + 'icon unsaved': 'mdi mdi-record', 'icon stop': 'mdi mdi-close-octagon', 'icon filter': 'mdi mdi-filter', 'icon filter-off': 'mdi mdi-filter-off', diff --git a/packages/web/src/modals/CloseTabModal.svelte b/packages/web/src/modals/CloseTabModal.svelte new file mode 100644 index 000000000..ef8de7380 --- /dev/null +++ b/packages/web/src/modals/CloseTabModal.svelte @@ -0,0 +1,47 @@ + + + + + Confirm close tabs + +
+ Following files are modified, really close tabs? After closing, you could reopen them in history + + widget +
+ + {#each tabs as tab} +
{tab.title}
+ {/each} + + + { + closeCurrentModal(); + onConfirm(); + }} + /> + { + closeCurrentModal(); + onCancel(); + }} + /> + +
+
diff --git a/packages/web/src/modals/InsertJoinModal.svelte b/packages/web/src/modals/InsertJoinModal.svelte index ed19eb7bf..e1cd01777 100644 --- a/packages/web/src/modals/InsertJoinModal.svelte +++ b/packages/web/src/modals/InsertJoinModal.svelte @@ -1,4 +1,5 @@ - + {#if value !== undefined} {#if displayType == 'json'} @@ -23,6 +23,8 @@ {:else} (no image) {/if} + {:else if !value.$oid && (_.isArray(value) || _.isPlainObject(value))} + {:else} {/if} diff --git a/packages/web/src/perspectives/PerspectiveDesigner.svelte b/packages/web/src/perspectives/PerspectiveDesigner.svelte index 7a8b8b1c4..7fb2fd625 100644 --- a/packages/web/src/perspectives/PerspectiveDesigner.svelte +++ b/packages/web/src/perspectives/PerspectiveDesigner.svelte @@ -3,10 +3,12 @@ createPerspectiveNodeConfig, MultipleDatabaseInfo, PerspectiveConfig, + PerspectiveDataPatternDict, perspectiveNodesHaveStructure, PerspectiveTreeNode, switchPerspectiveReferenceDirection, } from 'dbgate-datalib'; + import { CollectionInfo } from 'dbgate-types'; import _ from 'lodash'; import { tick } from 'svelte'; import runCommand from '../commands/runCommand'; @@ -18,6 +20,7 @@ export let config: PerspectiveConfig; export let dbInfos: MultipleDatabaseInfo; + export let dataPatterns: PerspectiveDataPatternDict; export let root: PerspectiveTreeNode; export let conid; @@ -27,22 +30,39 @@ export let onClickTableHeader = null; - function createDesignerModel(config: PerspectiveConfig, dbInfos: MultipleDatabaseInfo) { + function createDesignerModel( + config: PerspectiveConfig, + dbInfos: MultipleDatabaseInfo, + dataPatterns: PerspectiveDataPatternDict + ) { return { ...config, tables: _.compact( config.nodes.map(node => { - const table = dbInfos?.[node.conid || conid]?.[node.database || database]?.tables?.find( + const db = dbInfos?.[node.conid || conid]?.[node.database || database]; + const table = db?.tables?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); + const view = db?.views?.find(x => x.pureName == node.pureName && x.schemaName == node.schemaName); + let collection: CollectionInfo & { columns?: any[] } = db?.collections?.find( x => x.pureName == node.pureName && x.schemaName == node.schemaName ); - const view = dbInfos?.[node.conid || conid]?.[node.database || database]?.views?.find( - x => x.pureName == node.pureName && x.schemaName == node.schemaName - ); - if (!table && !view) return null; + + if (collection) { + const pattern = dataPatterns?.[node.designerId]; + if (!pattern) return null; + collection = { + ...collection, + columns: + pattern?.columns.map(x => ({ + columnName: x.name, + })) || [], + }; + } + + if (!table && !view && !collection) return null; const { designerId } = node; return { - ...(table || view), + ...(table || view || collection), left: node?.position?.x || 0, top: node?.position?.y || 0, alias: node.alias, @@ -55,7 +75,7 @@ function handleChange(value, skipUndoChain, settings) { setConfig(oldValue => { - const newValue = _.isFunction(value) ? value(createDesignerModel(oldValue, dbInfos)) : value; + const newValue = _.isFunction(value) ? value(createDesignerModel(oldValue, dbInfos, dataPatterns)) : value; let isArranged = oldValue.isArranged; if (settings?.isCalledFromArrange) { isArranged = true; @@ -122,11 +142,11 @@ }); } - async function detectAutoArrange(config: PerspectiveConfig, dbInfos, root) { + async function detectAutoArrange(config: PerspectiveConfig, dbInfos, dataPatterns, root) { if ( root && config.nodes.find(x => !x.position) && - perspectiveNodesHaveStructure(config, dbInfos, conid, database) && + perspectiveNodesHaveStructure(config, dbInfos, dataPatterns, conid, database) && config.nodes.every(x => root?.findNodeByDesignerId(x.designerId)) ) { await tick(); @@ -134,7 +154,7 @@ } } - $: detectAutoArrange(config, dbInfos, root); + $: detectAutoArrange(config, dbInfos, dataPatterns, root); // $: console.log('DESIGNER ROOT', root); @@ -221,6 +241,14 @@ const orderIndex = sort.length > 1 ? _.findIndex(sort, x => x.columnName == columnName) : -1; return { order, orderIndex }; }, + getColumnIconOverride: (designerId, columnName) => { + const pattern = dataPatterns?.[designerId]; + const column = pattern?.columns.find(x => x.name == columnName); + if (column?.types?.includes('json')) { + return 'img json'; + } + return null; + }, isColumnFiltered: (designerId, columnName) => { return !!config.nodes.find(x => x.designerId == designerId)?.filters?.[columnName]; }, @@ -277,6 +305,6 @@ onClickTableHeader, }} referenceComponent={QueryDesignerReference} - value={createDesignerModel(config, dbInfos)} + value={createDesignerModel(config, dbInfos, dataPatterns)} onChange={handleChange} /> diff --git a/packages/web/src/perspectives/PerspectiveTable.svelte b/packages/web/src/perspectives/PerspectiveTable.svelte index 4f7990c4a..09277e76d 100644 --- a/packages/web/src/perspectives/PerspectiveTable.svelte +++ b/packages/web/src/perspectives/PerspectiveTable.svelte @@ -16,6 +16,7 @@ ChangePerspectiveConfigFunc, PerspectiveConfig, PerspectiveDisplay, + PerspectivePatternColumnNode, PerspectiveTableColumnNode, PerspectiveTreeNode, PERSPECTIVE_PAGE_SIZE, @@ -41,6 +42,24 @@ import { getFilterValueExpression } from 'dbgate-filterparser'; import StatusBarTabItem from '../widgets/StatusBarTabItem.svelte'; + const TABS_BY_FIELD = { + tables: { + text: 'table', + tabComponent: 'TableDataTab', + icon: 'img table', + }, + views: { + text: 'view', + tabComponent: 'ViewDataTab', + icon: 'img view', + }, + collections: { + text: 'collection', + tabComponent: 'CollectionDataTab', + icon: 'img collection', + }, + }; + const dbg = debug('dbgate:PerspectiveTable'); export const activator = createActivator('PerspectiveTable', true, ['Designer']); @@ -57,6 +76,7 @@ let errorMessage; let rowCount; let isLoading = false; + let isLoadQueued = false; const lastVisibleRowIndexRef = createRef(0); const disableLoadNextRef = createRef(false); @@ -121,6 +141,12 @@ } async function loadData(node: PerspectiveTreeNode, counts) { + if (isLoading) { + isLoadQueued = true; + return; + } else { + isLoadQueued = false; + } // console.log('LOADING', node); if (!node) return; const rows = []; @@ -147,6 +173,10 @@ // loadProps.push(child.getNodeLoadProps()); // } // } + + if (isLoadQueued) { + loadData(root, $loadedCounts); + } } export function openJson() { @@ -199,24 +229,28 @@ const tableNode = root?.findNodeByDesignerId(tableNodeDesignerId); if (tableNode?.headerTableAttributes) { - const { pureName, schemaName, conid, database } = tableNode?.headerTableAttributes; - res.push({ - text: `Open table ${pureName}`, - onClick: () => { - openNewTab({ - title: pureName, - icon: 'img table', - tabComponent: 'TableDataTab', - props: { - schemaName, - pureName, - conid: conid, - database: database, - objectTypeField: 'tables', - }, - }); - }, - }); + const { pureName, schemaName, conid, database, objectTypeField } = tableNode?.headerTableAttributes; + console.log('objectTypeField', objectTypeField); + const tab = TABS_BY_FIELD[objectTypeField]; + if (tab) { + res.push({ + text: `Open ${tab.text} ${pureName}`, + onClick: () => { + openNewTab({ + title: pureName, + icon: tab.icon, + tabComponent: tab.tabComponent, + props: { + schemaName, + pureName, + conid: conid, + database: database, + objectTypeField, + }, + }); + }, + }); + } } const setColumnDisplay = type => { @@ -280,42 +314,39 @@ const value = display.rows[rowIndex].rowData[columnIndex]; const { dataNode } = column; - if (dataNode instanceof PerspectiveTableColumnNode) { + if ( + dataNode.filterInfo && + (dataNode instanceof PerspectiveTableColumnNode || dataNode instanceof PerspectivePatternColumnNode) + ) { const { table } = dataNode; - let tabComponent = null; - let icon = null; - let objectTypeField = null; - if (dataNode.isTable) { - tabComponent = 'TableDataTab'; - icon = 'img table'; - objectTypeField = 'tables'; - } - if (dataNode.isView) { - tabComponent = 'ViewDataTab'; - icon = 'img view'; - objectTypeField = 'views'; - } - if (tabComponent) { + + const tab = TABS_BY_FIELD[table.objectTypeField]; + const filterExpression = getFilterValueExpression( + value, + dataNode instanceof PerspectiveTableColumnNode ? dataNode.column.dataType : null + ); + + if (tab) { res.push({ - text: 'Open filtered table', + text: 'Open filtered grid', onClick: () => { openNewTab( { title: table.pureName, - icon, - tabComponent, + icon: tab.icon, + tabComponent: tab.tabComponent, props: { schemaName: table.schemaName, pureName: table.pureName, conid, database, - objectTypeField, + objectTypeField: table.objectTypeField, }, }, { grid: { filters: { - [dataNode.columnName]: getFilterValueExpression(value, dataNode.column.dataType), + [dataNode.columnName]: filterExpression, }, // isFormView: true, }, @@ -339,7 +370,7 @@ ...n, filters: { ...n.filters, - [dataNode.columnName]: getFilterValueExpression(value, dataNode.column.dataType), + [dataNode.columnName]: filterExpression, }, } : n diff --git a/packages/web/src/perspectives/PerspectiveView.svelte b/packages/web/src/perspectives/PerspectiveView.svelte index e5d644404..a260f296b 100644 --- a/packages/web/src/perspectives/PerspectiveView.svelte +++ b/packages/web/src/perspectives/PerspectiveView.svelte @@ -65,6 +65,7 @@ import { sleep } from '../utility/common'; import FontIcon from '../icons/FontIcon.svelte'; import InlineButton from '../buttons/InlineButton.svelte'; + import { usePerspectiveDataPatterns } from '../utility/usePerspectiveDataPatterns'; const dbg = debug('dbgate:PerspectiveView'); @@ -128,17 +129,21 @@ } $: dbInfos = useMultipleDatabaseInfo(perspectiveDatabases); + $: loader = new PerspectiveDataLoader(apiCall); + $: dataPatterns = usePerspectiveDataPatterns({ conid, database }, config, cache, $dbInfos, loader); $: rootObject = config?.nodes?.find(x => x.designerId == config?.rootDesignerId); $: rootDb = rootObject ? $dbInfos?.[rootObject.conid || conid]?.[rootObject.database || database] : null; $: tableInfo = rootDb?.tables.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName); $: viewInfo = rootDb?.views.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName); + $: collectionInfo = rootDb?.collections.find( + x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName + ); - $: loader = new PerspectiveDataLoader(apiCall); - $: dataProvider = new PerspectiveDataProvider(cache, loader); + $: dataProvider = new PerspectiveDataProvider(cache, loader, $dataPatterns); $: root = - tableInfo || viewInfo + tableInfo || viewInfo || collectionInfo ? new PerspectiveTableNode( - tableInfo || viewInfo, + tableInfo || viewInfo || collectionInfo, $dbInfos, config, setConfig, @@ -151,13 +156,14 @@ $: tempRoot = root?.findNodeByDesignerId(tempRootDesignerId); $: { - if (shouldProcessPerspectiveDefaultColunns(config, $dbInfos, conid, database)) { - setConfig(cfg => processPerspectiveDefaultColunns(cfg, $dbInfos, conid, database)); + if (shouldProcessPerspectiveDefaultColunns(config, $dbInfos, $dataPatterns, conid, database)) { + setConfig(cfg => processPerspectiveDefaultColunns(cfg, $dbInfos, $dataPatterns, conid, database)); } } // $: console.log('PERSPECTIVE', config); // $: console.log('VIEW ROOT', root); + // $: console.log('dataPatterns', $dataPatterns); @@ -205,6 +211,7 @@ {database} {setConfig} dbInfos={$dbInfos} + dataPatterns={$dataPatterns} {root} onClickTableHeader={designerId => { sleep(100).then(() => { diff --git a/packages/web/src/query/AceEditor.svelte b/packages/web/src/query/AceEditor.svelte index 8544f3b33..2968ec365 100644 --- a/packages/web/src/query/AceEditor.svelte +++ b/packages/web/src/query/AceEditor.svelte @@ -155,6 +155,8 @@ export let readOnly = false; export let splitterOptions = null; export let onKeyDown = null; + export let onExecuteFragment = null; + export let errorMessages = null; const tabVisible: any = getContext('tabVisible'); @@ -166,7 +168,8 @@ let queryParts = []; let currentPart = null; - let currentPartMarker = null; + let currentPartLines = []; + // let currentPartMarker = null; let queryParserWorker; @@ -183,16 +186,26 @@ return editor; } - export function getCurrentCommandText(): string { - if (currentPart != null) return currentPart.text; - if (!editor) return ''; - const selectedText = editor.getSelectedText(); - if (selectedText) return selectedText; - if (editor.getHighlightActiveLine()) { - const line = editor.getSelectionRange().start.row; - return editor.session.getLine(line); + export function getCurrentCommandText(): { text: string; line?: number } { + if (currentPart != null) { + return { + text: currentPart.text, + line: currentPart.trimStart.line, + }; } - return ''; + if (!editor) return { text: '' }; + const selectedText = editor.getSelectedText(); + if (selectedText) { + return { + text: selectedText, + line: editor.getSelectionRange().start.row, + }; + } + const line = editor.getSelectionRange().start.row; + return { + text: editor.session.getLine(line), + line, + }; } export function getCodeCompletionCommandText() { @@ -285,8 +298,37 @@ function processParserResult(data) { queryParts = data; - editor.setHighlightActiveLine(queryParts.length <= 1); + // editor.setHighlightActiveLine(queryParts.length <= 1); changedCurrentQueryPart(); + updateAnnotations(); + } + + function updateAnnotations() { + if (!mode?.includes('sql')) return; + + // console.log('UPDATING ANNOTATIONS'); + + editor?.session?.setAnnotations([ + ...(queryParts || []) + .filter(part => !(errorMessages || []).find(err => err.line == part.trimStart.line)) + .map(part => ({ + row: part.trimStart.line, + text: part.text, + className: currentPartLines.includes(part.trimStart.line) + ? 'ace-gutter-sql-run ace-gutter-current-part' + : 'ace-gutter-sql-run', // className: 'ace-gutter-sql-run', + })), + ...(errorMessages || []).map(error => ({ + row: error.line, + text: error.message, + type: 'error', + })), + ]); + } + + $: { + errorMessages; + updateAnnotations(); } const handleContextMenu = e => { @@ -304,8 +346,6 @@ function changedQueryParts() { const editor = getEditor(); if (splitterOptions && editor && queryParserWorker) { - const editor = getEditor(); - const message = { text: editor.getValue(), options: { @@ -329,19 +369,20 @@ function changedCurrentQueryPart() { if (queryParts.length <= 1) { removeCurrentPartMarker(); + updateAnnotations(); return; } const selectionRange = editor.getSelectionRange(); - if ( - selectionRange.start.row != selectionRange.end.row || - selectionRange.start.column != selectionRange.end.column - ) { - removeCurrentPartMarker(); - currentPart = null; - return; - } + // if ( + // selectionRange.start.row != selectionRange.end.row || + // selectionRange.start.column != selectionRange.end.column + // ) { + // removeCurrentPartMarker(); + // currentPart = null; + // return; + // } const cursor = selectionRange.start; const part = queryParts.find( @@ -350,25 +391,39 @@ ((cursor.row == x.end.line && cursor.column <= x.end.column) || cursor.row < x.end.line) ); - if (part?.text != currentPart?.text || part?.start?.position != currentPart?.start?.position) { + if ( + part?.text != currentPart?.text || + part?.start?.position != currentPart?.start?.position || + part?.end?.position != currentPart?.end?.position + ) { removeCurrentPartMarker(); currentPart = part; if (currentPart) { const start = currentPart.trimStart || currentPart.start; const end = currentPart.trimEnd || currentPart.end; - currentPartMarker = editor - .getSession() - .addMarker(new ace.Range(start.line, start.column, end.line, end.column), 'ace_active-line', 'text'); + if (start && end) { + currentPartLines = _.range(start.line, end.line + 1); + for (const row of currentPartLines) { + if ((queryParts || []).find(part => part.trimStart.line == row)) { + continue; + } + editor.getSession().addGutterDecoration(row, 'ace-gutter-current-part'); + } + } + // currentPartMarker = editor + // .getSession() + // .addMarker(new ace.Range(start.line, start.column, end.line, end.column), 'ace_active-line', 'text'); } + updateAnnotations(); } } function removeCurrentPartMarker() { - if (currentPartMarker != null) { - editor.getSession().removeMarker(currentPartMarker); - currentPartMarker = null; + for (const row of currentPartLines) { + editor.getSession().removeGutterDecoration(row, 'ace-gutter-current-part'); } + currentPartLines = []; } onMount(() => { @@ -380,6 +435,7 @@ editor.getSession().setMode('ace/mode/' + mode); editor.setTheme('ace/theme/' + theme); editor.setValue(value, 1); + editor.setHighlightActiveLine(false); contentBackup = value; setEventCallBacks(); if (options) { @@ -392,6 +448,77 @@ editor.container.addEventListener('contextmenu', handleContextMenu); editor.keyBinding.addKeyboardHandler(handleKeyDown); changedQueryParts(); + + // editor.session.addGutterDecoration(0, 'ace-gutter-sql-run'); + + // editor.session.setAnnotations([ + // { + // row: 1, + // text: 'SQL SCRIPT 0', + // type: 'gutter', + // }, + // ]); + + // editor.getSession().setAnnotations([ + // { + // row: 0, + // // column: 0, + // text: 'Error Message', // Or the Json reply from the parser + // type: 'error', // also "warning" and "information" + // }, + // { + // row: 1, + // // column: 0, + // text: 'SELECT * FROM \n22222', // Or the Json reply from the parser + // // type: 'info', // also "warning" and "information" + // className: 'ace-gutter-sql-run', + // }, + // ]); + + // editor.on('guttermousemove', e => console.log('MOVE', e.target), true); + // editor.on('guttermouseout', e => console.log('OUT', e.target), true); + // editor.on('guttermouseleave', e => console.log('LEAVE', e.target), true); + // editor.session.setBreakpoint(0); + + // editor.on( + // 'gutterclick', + // e => { + // const row = e.getDocumentPosition().row; + + // const part = (queryParts || []).find(part => part.trimStart.line == row); + // if (part && onExecuteFragment) { + // onExecuteFragment(part.text); + // e.stop(); + // } + // }, + // true + // ); + + editor.on( + 'guttermousedown', + e => { + const row = e.getDocumentPosition().row; + + const part = (queryParts || []).find(part => part.trimStart.line == row); + if (part && onExecuteFragment) { + onExecuteFragment(part.text, part.trimStart.line); + e.stop(); + editor.moveCursorTo(part.trimStart.line, 0); + editor.selection.clearSelection(); + } + }, + true + ); + + // editor.session.gutterRenderer = { + // getWidth: function (session, lastLineNumber, config) { + // return lastLineNumber.toString().length * config.characterWidth; + // }, + // getText: function (session, row) { + // return (row + 1).toString(); + // // return String.fromCharCode(row + 65); + // }, + // }; }); onDestroy(() => { @@ -427,6 +554,7 @@ editor.on('focus', () => dispatch('focus')); editor.setReadOnly(readOnly); + editor.on('change', () => { const content = editor.getValue(); value = content; diff --git a/packages/web/src/query/MessageView.svelte b/packages/web/src/query/MessageView.svelte index 99d47cc78..20d0a6e08 100644 --- a/packages/web/src/query/MessageView.svelte +++ b/packages/web/src/query/MessageView.svelte @@ -18,6 +18,7 @@ export let items: any[]; export let showProcedure = false; export let showLine = false; + export let startLine = 0; $: time0 = items[0] && new Date(items[0].time).getTime(); @@ -58,7 +59,7 @@ {row.procedure || ''} {/if} {#if showLine} - {row.line || ''} + {row.line == null ? '' : row.line + 1 + startLine} {/if} {/each} diff --git a/packages/web/src/query/SocketMessageView.svelte b/packages/web/src/query/SocketMessageView.svelte index cc48ffcfa..5e1bddf41 100644 --- a/packages/web/src/query/SocketMessageView.svelte +++ b/packages/web/src/query/SocketMessageView.svelte @@ -13,8 +13,11 @@ export let eventName; export let executeNumber; export let showNoMessagesAlert = false; + export let startLine = 0; + export let onChangeErrors = null; const cachedMessagesRef = createRef([]); + const lastErrorMessageCountRef = createRef(0); let displayedMessages = []; @@ -44,11 +47,26 @@ } } + $: { + if (onChangeErrors) { + const errors = displayedMessages.filter(x => x.severity == 'error' && x.line != null); + if (lastErrorMessageCountRef.get() != errors.length) { + onChangeErrors( + errors.map(err => ({ + ...err, + line: err.line == null ? null : err.line + startLine, + })) + ); + lastErrorMessageCountRef.set(errors.length); + } + } + } + $: $effect; {#if showNoMessagesAlert && (!displayedMessages || displayedMessages.length == 0)} {:else} - + {/if} diff --git a/packages/web/src/query/SqlEditor.svelte b/packages/web/src/query/SqlEditor.svelte index 68fc206f3..b7870756f 100644 --- a/packages/web/src/query/SqlEditor.svelte +++ b/packages/web/src/query/SqlEditor.svelte @@ -32,7 +32,7 @@ return domEditor.getEditor(); } - export function getCurrentCommandText(): string { + export function getCurrentCommandText(): { text: string; line?: number } { return domEditor.getCurrentCommandText(); } diff --git a/packages/web/src/query/codeCompletion.ts b/packages/web/src/query/codeCompletion.ts index e59503b59..ddb662098 100644 --- a/packages/web/src/query/codeCompletion.ts +++ b/packages/web/src/query/codeCompletion.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import { addCompleter, setCompleters } from 'ace-builds/src-noconflict/ext-language_tools'; import { getDatabaseInfo } from '../utility/metadataLoaders'; import analyseQuerySources from './analyseQuerySources'; +import { getStringSettingsValue } from '../settings/settingsTools'; const COMMON_KEYWORDS = [ 'select', @@ -78,13 +79,21 @@ export function mountCodeCompletion({ conid, database, editor, getText }) { const line = session.getLine(cursor.row).slice(0, cursor.column); const dbinfo = await getDatabaseInfo({ conid, database }); - let list = COMMON_KEYWORDS.map(word => ({ - name: word, - value: word, - caption: word, - meta: 'keyword', - score: 800, - })); + const convertUpper = getStringSettingsValue('sqlEditor.sqlCommandsCase', 'upperCase') == 'upperCase'; + + let list = COMMON_KEYWORDS.map(word => { + if (convertUpper) { + word = word.toUpperCase(); + } + + return { + name: word, + value: word, + caption: word, + meta: 'keyword', + score: 800, + }; + }); if (dbinfo) { const colMatch = line.match(/([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)?$/); diff --git a/packages/web/src/settings/ConnectionDriverFields.svelte b/packages/web/src/settings/ConnectionDriverFields.svelte index 9f9310916..ebe5e5186 100644 --- a/packages/web/src/settings/ConnectionDriverFields.svelte +++ b/packages/web/src/settings/ConnectionDriverFields.svelte @@ -173,6 +173,10 @@ /> {/if} +{#if driver?.showConnectionField('treeKeySeparator', $values)} + +{/if} + {#if driver?.showConnectionField('windowsDomain', $values)} {/if} diff --git a/packages/web/src/settings/SettingsModal.svelte b/packages/web/src/settings/SettingsModal.svelte index 99e0ca357..ebf2d9a14 100644 --- a/packages/web/src/settings/SettingsModal.svelte +++ b/packages/web/src/settings/SettingsModal.svelte @@ -111,6 +111,19 @@ ORDER BY defaultValue="30" disabled={values['connection.autoRefresh'] === false} /> + +
SQL editor
+ +
Application theme
diff --git a/packages/web/src/settings/settingsTools.ts b/packages/web/src/settings/settingsTools.ts index 7c27ad203..dbf739eee 100644 --- a/packages/web/src/settings/settingsTools.ts +++ b/packages/web/src/settings/settingsTools.ts @@ -21,3 +21,10 @@ export function getBoolSettingsValue(name, defaultValue) { if (res == null) return defaultValue; return !!res; } + +export function getStringSettingsValue(name, defaultValue) { + const settings = getCurrentSettings(); + const res = settings[name]; + if (res == null) return defaultValue; + return res; +} diff --git a/packages/web/src/tabs/QueryTab.svelte b/packages/web/src/tabs/QueryTab.svelte index 9166fcbaa..c987fd851 100644 --- a/packages/web/src/tabs/QueryTab.svelte +++ b/packages/web/src/tabs/QueryTab.svelte @@ -52,7 +52,7 @@ import useEditorData from '../query/useEditorData'; import { extensions } from '../stores'; import applyScriptTemplate from '../utility/applyScriptTemplate'; - import { changeTab } from '../utility/common'; + import { changeTab, markTabUnsaved } from '../utility/common'; import { getDatabaseInfo, useConnectionInfo } from '../utility/metadataLoaders'; import SocketMessageView from '../query/SocketMessageView.svelte'; import useEffect from '../utility/useEffect'; @@ -86,10 +86,11 @@ let busy = false; let executeNumber = 0; + let executeStartLine = 0; let visibleResultTabs = false; let sessionId = null; let resultCount; - + let errorMessages; let domEditor; $: connection = useConnectionInfo({ conid }); @@ -143,13 +144,14 @@ return !!conid && (!$connection?.isReadOnly || driver?.readOnlySessions); } - async function executeCore(sql) { + async function executeCore(sql, startLine = 0) { if (busy) return; if (!sql || !sql.trim()) { showSnackbarError('Skipped executing empty query'); return; } + executeStartLine = startLine; executeNumber++; visibleResultTabs = true; @@ -179,13 +181,14 @@ } export async function executeCurrent() { - const sql = domEditor.getCurrentCommandText(); - await executeCore(sql); + const cmd = domEditor.getCurrentCommandText(); + await executeCore(cmd.text, cmd.line); } export async function execute() { const selectedText = domEditor.getEditor().getSelectedText(); - await executeCore(selectedText || $editorValue); + const startLine = domEditor.getEditor().getSelectionRange().start.row; + await executeCore(selectedText || $editorValue, selectedText ? startLine : 0); } export async function kill() { @@ -257,6 +260,10 @@ : null, }); + function handleChangeErrors(errors) { + errorMessages = errors; + } + function createMenu() { return [ { command: 'query.execute' }, @@ -276,6 +283,8 @@ } const quickExportHandlerRef = createQuickExportHandlerRef(); + + let isInitialized = false; @@ -286,21 +295,32 @@ engine={$connection && $connection.engine} {conid} {database} - splitterOptions={driver?.getQuerySplitterOptions('script')} + splitterOptions={driver?.getQuerySplitterOptions('editor')} value={$editorState.value || ''} menu={createMenu()} - on:input={e => setEditorData(e.detail)} + on:input={e => { + setEditorData(e.detail); + if (isInitialized) { + markTabUnsaved(tabid); + } + errorMessages = []; + }} on:focus={() => { activator.activate(); invalidateCommands(); + setTimeout(() => { + isInitialized = true; + }, 100); }} bind:this={domEditor} + onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)} + {errorMessages} /> {:else} setEditorData(e.detail)} on:focus={() => { @@ -318,8 +338,10 @@ eventName={sessionId ? `session-info-${sessionId}` : null} on:messageClick={handleMesageClick} {executeNumber} + startLine={executeStartLine} showProcedure showLine + onChangeErrors={handleChangeErrors} />
diff --git a/packages/web/src/utility/SettingsListener.svelte b/packages/web/src/utility/SettingsListener.svelte new file mode 100644 index 000000000..7dedd6a87 --- /dev/null +++ b/packages/web/src/utility/SettingsListener.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/web/src/utility/common.ts b/packages/web/src/utility/common.ts index 5fb98c0ff..e5c083cb2 100644 --- a/packages/web/src/utility/common.ts +++ b/packages/web/src/utility/common.ts @@ -1,4 +1,4 @@ -import { openedTabs } from '../stores'; +import { getOpenedTabs, openedTabs } from '../stores'; import _ from 'lodash'; import getElectron from './getElectron'; @@ -18,6 +18,16 @@ export function changeTab(tabid, changeFunc) { openedTabs.update(files => files.map(tab => (tab.tabid == tabid ? changeFunc(tab) : tab))); } +export function markTabUnsaved(tabid) { + const tab = getOpenedTabs().find(x => x.tabid == tabid); + if (tab.unsaved) return; + openedTabs.update(files => files.map(tab => (tab.tabid == tabid ? { ...tab, unsaved: true } : tab))); +} + +export function markTabSaved(tabid) { + openedTabs.update(files => files.map(tab => (tab.tabid == tabid ? { ...tab, unsaved: false } : tab))); +} + export function setSelectedTabFunc(files, tabid) { return [ ...(files || []).filter(x => x.tabid != tabid).map(x => ({ ...x, selected: false })), diff --git a/packages/web/src/utility/saveTabFile.ts b/packages/web/src/utility/saveTabFile.ts index 778807fcf..60351ecdb 100644 --- a/packages/web/src/utility/saveTabFile.ts +++ b/packages/web/src/utility/saveTabFile.ts @@ -1,7 +1,7 @@ import { derived, get } from 'svelte/store'; import { showModal } from '../modals/modalTools'; import { openedTabs } from '../stores'; -import { changeTab } from './common'; +import { changeTab, markTabSaved } from './common'; import SaveFileModal from '../modals/SaveFileModal.svelte'; import registerCommand from '../commands/registerCommand'; import { apiCall } from './api'; @@ -24,12 +24,14 @@ export default async function saveTabFile(editor, saveMode, folder, format, file if (savedFilePath) { await apiCall('files/save-as', { filePath: savedFilePath, data, format }); } + markTabSaved(tabid); }; const onSave = (title, newProps) => { changeTab(tabid, tab => ({ ...tab, title, + unsaved: false, props: { ...tab.props, savedFormat: format, diff --git a/packages/web/src/utility/usePerspectiveDataPatterns.ts b/packages/web/src/utility/usePerspectiveDataPatterns.ts new file mode 100644 index 000000000..f29c44cea --- /dev/null +++ b/packages/web/src/utility/usePerspectiveDataPatterns.ts @@ -0,0 +1,125 @@ +import { + analyseDataPattern, + MultipleDatabaseInfo, + PerspectiveCache, + PerspectiveConfig, + PerspectiveDatabaseConfig, + PerspectiveDataLoadProps, + PerspectiveDataPattern, + PerspectiveDataPatternDict, +} from 'dbgate-datalib'; +import { PerspectiveDataLoader } from 'dbgate-datalib/lib/PerspectiveDataLoader'; +import { writable, Readable } from 'svelte/store'; + +export function getPerspectiveDataPatternsFromCache( + databaseConfig: PerspectiveDatabaseConfig, + config: PerspectiveConfig, + cache: PerspectiveCache, + dbInfos: MultipleDatabaseInfo +): PerspectiveDataPatternDict { + const res = {}; + + for (const node of config.nodes) { + const conid = node.conid || databaseConfig.conid; + const database = node.database || databaseConfig.database; + const { schemaName, pureName } = node; + + const cached = cache.dataPatterns.find( + x => x.conid == conid && x.database == database && x.schemaName == schemaName && x.pureName == pureName + ); + if (cached) { + res[node.designerId] = cached; + } + } + + return res; +} + +export async function getPerspectiveDataPatterns( + databaseConfig: PerspectiveDatabaseConfig, + config: PerspectiveConfig, + cache: PerspectiveCache, + dbInfos: MultipleDatabaseInfo, + dataLoader: PerspectiveDataLoader +): Promise { + const res = {}; + + for (const node of config.nodes) { + const conid = node.conid || databaseConfig.conid; + const database = node.database || databaseConfig.database; + const { schemaName, pureName } = node; + + const cached = cache.dataPatterns.find( + x => x.conid == conid && x.database == database && x.schemaName == schemaName && x.pureName == pureName + ); + if (cached) { + res[node.designerId] = cached; + continue; + } + + const db = dbInfos?.[conid]?.[database]; + + if (!db) continue; + + const table = db.tables?.find(x => x.pureName == pureName && x.schemaName == schemaName); + const view = db.views?.find(x => x.pureName == pureName && x.schemaName == schemaName); + const collection = db.collections?.find(x => x.pureName == pureName && x.schemaName == schemaName); + if (!table && !view && !collection) continue; + + // console.log('LOAD PATTERN FOR', pureName); + + const props: PerspectiveDataLoadProps = { + databaseConfig: { conid, database }, + engineType: collection ? 'docdb' : 'sqldb', + schemaName, + pureName, + orderBy: table?.primaryKey + ? table?.primaryKey.columns.map(x => ({ columnName: x.columnName, order: 'ASC' })) + : table || view + ? [{ columnName: (table || view).columns[0].columnName, order: 'ASC' }] + : null, + range: { + offset: 0, + limit: 10, + }, + }; + // console.log('LOAD PROPS', props); + const rows = await dataLoader.loadData(props); + + if (rows.errorMessage) { + console.error('Error loading pattern for', pureName, ':', rows.errorMessage); + continue; + } + + // console.log('PATTERN ROWS', rows); + + const pattern = analyseDataPattern( + { + conid, + database, + pureName, + schemaName, + }, + rows + ); + + cache.dataPatterns.push(pattern); + res[node.designerId] = pattern; + } + + return res; +} + +export function usePerspectiveDataPatterns( + databaseConfig: PerspectiveDatabaseConfig, + config: PerspectiveConfig, + cache: PerspectiveCache, + dbInfos: MultipleDatabaseInfo, + dataLoader: PerspectiveDataLoader +): Readable { + const cached = getPerspectiveDataPatternsFromCache(databaseConfig, config, cache, dbInfos); + const promise = getPerspectiveDataPatterns(databaseConfig, config, cache, dbInfos, dataLoader); + const res = writable(cached); + promise.then(value => res.set(value)); + return res; +} diff --git a/packages/web/src/widgets/TabsPanel.svelte b/packages/web/src/widgets/TabsPanel.svelte index 7244a70c9..fe9ad5742 100644 --- a/packages/web/src/widgets/TabsPanel.svelte +++ b/packages/web/src/widgets/TabsPanel.svelte @@ -1,5 +1,23 @@