Skip to content

Commit

Permalink
Added /query/structure interface for datasources
Browse files Browse the repository at this point in the history
  • Loading branch information
miguel-lansdorf committed Sep 13, 2023
1 parent b1a0d8c commit 5e71b1f
Show file tree
Hide file tree
Showing 7 changed files with 635 additions and 5 deletions.
3 changes: 2 additions & 1 deletion api/src/api_models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
ApiKeyIDRequest,
} from './api';
import { Role, RolePermission, RoleCreateOrUpdateRequest, RoleIDRequest } from './role';
import { QueryParams, QueryRequest, HttpParams } from './query';
import { QueryParams, QueryRequest, HttpParams, QueryStructureRequest } from './query';
import { Job, JobFilterObject, JobListRequest, JobPaginationResponse, JobSortObject, JobRunRequest } from './job';
import { Config, ConfigDescription, ConfigGetRequest, ConfigUpdateRequest } from './config';
import {
Expand Down Expand Up @@ -157,6 +157,7 @@ export default {
QueryParams,
QueryRequest,
HttpParams,
QueryStructureRequest,

Job,
JobFilterObject,
Expand Down
66 changes: 65 additions & 1 deletion api/src/api_models/query.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Type } from 'class-transformer';
import { IsBoolean, IsIn, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator';
import { IsBoolean, IsIn, IsNumber, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator';
import { ApiModel, ApiModelProperty, SwaggerDefinitionConstant } from 'swagger-express-ts';
import { Authentication } from './base';

Expand Down Expand Up @@ -155,3 +155,67 @@ export class HttpParams {
})
url: string;
}

@ApiModel({
description: 'Query Structure object',
name: 'QueryStructureRequest',
})
export class QueryStructureRequest {
@IsIn(['TABLES', 'COLUMNS', 'DATA', 'INDEXES', 'COUNT'])
@ApiModelProperty({
description: `type of query.
TABLES = get all tables in database
COLUMNS = get column structure of table
DATA = get data of table
INDEXES = get indexes of table
COUNT = get total number of rows in table`,
required: true,
enum: ['TABLES', 'COLUMNS', 'DATA', 'INDEXES', 'COUNT'],
})
query_type: 'TABLES' | 'COLUMNS' | 'DATA' | 'INDEXES' | 'COUNT';

@IsIn(['postgresql', 'mysql'])
@ApiModelProperty({
description: 'datasource type of query',
required: true,
enum: ['postgresql', 'mysql'],
})
type: 'postgresql' | 'mysql';

@IsString()
@ApiModelProperty({
description: 'datasource key',
required: true,
})
key: string;

@IsString()
@ApiModelProperty({
description: 'table schema',
required: true,
})
table_schema: string;

@IsString()
@ApiModelProperty({
description: 'table schema',
required: true,
})
table_name: string;

@IsNumber()
@IsOptional()
@ApiModelProperty({
description: 'Limit of query results. Default = 20',
required: false,
})
limit?: number;

@IsNumber()
@IsOptional()
@ApiModelProperty({
description: 'Offset of query results, Default = 0',
required: false,
})
offset?: number;
}
36 changes: 35 additions & 1 deletion api/src/controller/query.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { controller, httpPost, interfaces } from 'inversify-express-utils';
import { ApiOperationPost, ApiPath } from 'swagger-express-ts';
import { QueryService } from '../services/query.service';
import { validate } from '../middleware/validation';
import { QueryRequest } from '../api_models/query';
import { QueryRequest, QueryStructureRequest } from '../api_models/query';
import permission from '../middleware/permission';
import { PERMISSIONS } from '../services/role.service';
import { ApiKey } from '../api_models/api';
Expand Down Expand Up @@ -54,4 +54,38 @@ export class QueryController implements interfaces.Controller {
next(error);
}
}

@ApiOperationPost({
path: '/structure',
description: 'query structure of selected datasource',
parameters: {
body: { description: 'Query Structure object', required: true, model: 'QueryStructureRequest' },
},
responses: {
200: { description: 'Query result' },
500: { description: 'ApiError', model: 'ApiError' },
},
})
@httpPost(
'/structure',
permission({ match: 'all', permissions: [PERMISSIONS.DASHBOARD_MANAGE] }),
validate(QueryStructureRequest),
)
public async queryStructure(req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
try {
const { query_type, type, key, table_schema, table_name, limit, offset } = req.body as QueryStructureRequest;
const result = await this.queryService.queryStructure(
query_type,
type,
key,
table_schema,
table_name,
limit,
offset,
);
res.json(result);
} catch (error) {
next(error);
}
}
}
142 changes: 142 additions & 0 deletions api/src/services/query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,148 @@ export class QueryService {
}
}

private getDBStructureSql(): string {
return 'SELECT table_schema, table_name, table_type FROM information_schema.tables ORDER BY table_schema, table_name';
}

private getColumnStructureSql(type: string, table_schema: string, table_name: string): string {
if (type === 'postgresql') {
const attrelid = `'${table_schema}.${table_name}'::regclass`;
return `
SELECT
ordinal_position,
UPPER(pc.contype) AS column_key,
pg_get_constraintdef(pc.oid) AS column_key_text,
column_name,
format_type(atttypid, atttypmod) AS column_type,
is_nullable,
column_default,
pg_catalog.col_description(${attrelid}, ordinal_position) AS column_comment
FROM
information_schema.columns
JOIN pg_attribute pa ON pa.attrelid = ${attrelid}
AND attname = column_name
LEFT JOIN pg_constraint pc ON pc.conrelid = ${attrelid} AND ordinal_position = any(pc.conkey)
WHERE
table_name = '${table_name}' AND table_schema = '${table_schema}';
`;
}
if (type === 'mysql') {
return `
SELECT ordinal_position, column_key, column_name, column_type, is_nullable, column_default, column_comment
FROM information_schema.columns
WHERE table_name = '${table_name}' AND table_schema = '${table_schema}'
`;
}
return '';
}

private getDataSql(table_schema: string, table_name: string, limit: number, offset: number): string {
return `
SELECT *
FROM ${table_schema}.${table_name}
LIMIT ${limit} OFFSET ${offset}`;
}

private getIndexesSql(type: string, table_schema: string, table_name: string): string {
if (type === 'postgresql') {
return `
SELECT
ix.relname AS index_name,
upper(am.amname) AS index_algorithm,
indisunique AS is_unique,
pg_get_indexdef(indexrelid) AS index_definition,
CASE WHEN position(' WHERE ' IN pg_get_indexdef(indexrelid)) > 0 THEN
regexp_replace(pg_get_indexdef(indexrelid), '.+WHERE ', '')
WHEN position(' WITH ' IN pg_get_indexdef(indexrelid)) > 0 THEN
regexp_replace(pg_get_indexdef(indexrelid), '.+WITH ', '')
ELSE
''
END AS condition,
pg_catalog.obj_description(i.indexrelid, 'pg_class') AS comment
FROM
pg_index i
JOIN pg_class t ON t.oid = i.indrelid
JOIN pg_class ix ON ix.oid = i.indexrelid
JOIN pg_namespace n ON t.relnamespace = n.oid
JOIN pg_am AS am ON ix.relam = am.oid
WHERE
t.relname = '${table_name}' AND n.nspname = '${table_schema}';
`;
}
if (type === 'mysql') {
return `
SELECT
sub_part AS index_length,
index_name AS index_name,
index_type AS index_algorithm,
CASE non_unique WHEN 0 THEN 'TRUE' ELSE 'FALSE' END AS is_unique,
column_name AS column_name
FROM
information_schema.statistics
WHERE
table_name = '${table_name}' AND table_schema = '${table_schema}'
ORDER BY
seq_in_index ASC;
`;
}
return '';
}

private getCountSql(table_schema: string, table_name: string): string {
return `
SELECT count(*) AS total
FROM ${table_schema}.${table_name}
`;
}

async queryStructure(
query_type: string,
type: string,
key: string,
table_schema: string,
table_name: string,
limit = 20,
offset = 0,
): Promise<any> {
let sql: string;
switch (query_type) {
case 'TABLES':
sql = this.getDBStructureSql();
break;
case 'COLUMNS':
sql = this.getColumnStructureSql(type, table_schema, table_name);
break;
case 'DATA':
sql = this.getDataSql(table_schema, table_name, limit, offset);
break;
case 'INDEXES':
sql = this.getIndexesSql(type, table_schema, table_name);
break;
case 'COUNT':
sql = this.getCountSql(table_schema, table_name);
break;

default:
return null;
}

let result;
switch (type) {
case 'postgresql':
result = await this.postgresqlQuery(key, sql);
break;

case 'mysql':
result = await this.mysqlQuery(key, sql);
break;

default:
return null;
}
return result;
}

async query(
type: string,
key: string,
Expand Down
Loading

0 comments on commit 5e71b1f

Please sign in to comment.