diff --git a/CHANGELOG.md b/CHANGELOG.md index 605c8deae..da78e8708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ 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 diff --git a/package.json b/package.json index a9b0fe531..eddfd78ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "5.1.4", + "version": "5.1.5-beta.4", "name": "dbgate-all", "workspaces": [ "packages/*", 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/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/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/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte index d5d0fac83..375d8e86a 100644 --- a/packages/web/src/appobj/DatabaseObjectAppObject.svelte +++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte @@ -345,6 +345,12 @@ }, }, }, + { + label: 'Design perspective query', + tab: 'PerspectiveTab', + forceNewTab: true, + icon: 'img perspective', + }, { label: 'Export', isExport: true, diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index d5d7834f8..14961db3c 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -605,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/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/perspectives/PerspectiveCell.svelte b/packages/web/src/perspectives/PerspectiveCell.svelte index e4750dfd6..b95168b74 100644 --- a/packages/web/src/perspectives/PerspectiveCell.svelte +++ b/packages/web/src/perspectives/PerspectiveCell.svelte @@ -23,7 +23,7 @@ {:else} (no image) {/if} - {:else if _.isArray(value) || _.isPlainObject(value)} + {:else if !value.$oid && (_.isArray(value) || _.isPlainObject(value))} {:else} 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 32a9b97d0..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']); @@ -210,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 => { @@ -291,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, }, @@ -350,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/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/plugins/dbgate-plugin-mongo/src/backend/driver.js b/plugins/dbgate-plugin-mongo/src/backend/driver.js index 3d6c5a015..432593d4b 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/backend/driver.js @@ -209,6 +209,10 @@ const driver = { if (options.countDocuments) { const count = await collection.countDocuments(convertObjectId(options.condition) || {}); return { count }; + } else if (options.aggregate) { + let cursor = await collection.aggregate(options.aggregate); + const rows = await cursor.toArray(); + return { rows: rows.map(transformMongoData) }; } else { // console.log('options.condition', JSON.stringify(options.condition, undefined, 2)); let cursor = await collection.find(convertObjectId(options.condition) || {});