mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-17 21:26:00 +00:00
SYNC: Merge pull request #5 from dbgate/feature/firestore
This commit is contained in:
41
.vscode/launch.json
vendored
41
.vscode/launch.json
vendored
@@ -1,20 +1,41 @@
|
|||||||
{
|
{
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
|
"name": "Debug App",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Launch API",
|
|
||||||
"skipFiles": [
|
|
||||||
"<node_internals>/**"
|
|
||||||
],
|
|
||||||
"program": "${workspaceFolder}/packages/api/src/index.js",
|
"program": "${workspaceFolder}/packages/api/src/index.js",
|
||||||
"outFiles": [
|
"envFile": "${workspaceFolder}/packages/api/.env",
|
||||||
"${workspaceFolder}/**/*.js"
|
"args": ["--listen-api"],
|
||||||
]
|
"console": "integratedTerminal",
|
||||||
|
"restart": true,
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"skipFiles": ["<node_internals>/**"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug App (Break on Start)",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/packages/api/src/index.js",
|
||||||
|
"args": ["--listen-api"],
|
||||||
|
"envFile": "${workspaceFolder}/.env",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"restart": true,
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"skipFiles": ["<node_internals>/**"],
|
||||||
|
"stopOnEntry": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Attach to Process",
|
||||||
|
"type": "node",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 9229,
|
||||||
|
"restart": true,
|
||||||
|
"localRoot": "${workspaceFolder}",
|
||||||
|
"remoteRoot": "${workspaceFolder}",
|
||||||
|
"skipFiles": ["<node_internals>/**"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ DEVWEB=1
|
|||||||
# DISABLE_SHELL=1
|
# DISABLE_SHELL=1
|
||||||
# HIDE_APP_EDITOR=1
|
# HIDE_APP_EDITOR=1
|
||||||
|
|
||||||
|
|
||||||
# DEVWEB=1
|
# DEVWEB=1
|
||||||
# LOGINS=admin,test
|
# LOGINS=admin,test
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "env-cmd -f .env node src/index.js --listen-api",
|
"start": "env-cmd -f .env node src/index.js --listen-api",
|
||||||
|
"start:debug": "env-cmd -f .env node --inspect src/index.js --listen-api",
|
||||||
"start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api",
|
"start:portal": "env-cmd -f env/portal/.env node src/index.js --listen-api",
|
||||||
"start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api",
|
"start:singledb": "env-cmd -f env/singledb/.env node src/index.js --listen-api",
|
||||||
"start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api",
|
"start:auth": "env-cmd -f env/auth/.env node src/index.js --listen-api",
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ function getDisplayColumn(basePath, columnName, display: CollectionGridDisplay)
|
|||||||
isPartitionKey: !!display?.collection?.partitionKey?.find(x => x.columnName == uniqueName),
|
isPartitionKey: !!display?.collection?.partitionKey?.find(x => x.columnName == uniqueName),
|
||||||
isClusterKey: !!display?.collection?.clusterKey?.find(x => x.columnName == uniqueName),
|
isClusterKey: !!display?.collection?.clusterKey?.find(x => x.columnName == uniqueName),
|
||||||
isUniqueKey: !!display?.collection?.uniqueKey?.find(x => x.columnName == uniqueName),
|
isUniqueKey: !!display?.collection?.uniqueKey?.find(x => x.columnName == uniqueName),
|
||||||
|
hasAutoValue: !!display?.collection?.autoValueColumns?.find(x => x.columnName == uniqueName),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { hexStringToArray, parseNumberSafe } from 'dbgate-tools';
|
|||||||
import { FilterBehaviour, TransformType } from 'dbgate-types';
|
import { FilterBehaviour, TransformType } from 'dbgate-types';
|
||||||
|
|
||||||
const binaryCondition =
|
const binaryCondition =
|
||||||
(operator, numberDualTesting = false) =>
|
(operator, filterBehaviour: FilterBehaviour = {}) =>
|
||||||
value => {
|
value => {
|
||||||
|
const { passNumbers, allowNumberDualTesting } = filterBehaviour;
|
||||||
const numValue = parseNumberSafe(value);
|
const numValue = parseNumberSafe(value);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
numberDualTesting &&
|
allowNumberDualTesting &&
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
!isNaN(numValue)
|
!isNaN(numValue)
|
||||||
) {
|
) {
|
||||||
@@ -43,6 +45,21 @@ const binaryCondition =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (passNumbers && !isNaN(numValue)) {
|
||||||
|
return {
|
||||||
|
conditionType: 'binary',
|
||||||
|
operator,
|
||||||
|
left: {
|
||||||
|
exprType: 'placeholder',
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
exprType: 'value',
|
||||||
|
value: numValue,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
conditionType: 'binary',
|
conditionType: 'binary',
|
||||||
operator,
|
operator,
|
||||||
@@ -462,18 +479,18 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
|
|||||||
null: () => word('NULL').map(unaryCondition('isNull')),
|
null: () => word('NULL').map(unaryCondition('isNull')),
|
||||||
isEmpty: r => r.empty.map(unaryCondition('isEmpty')),
|
isEmpty: r => r.empty.map(unaryCondition('isEmpty')),
|
||||||
isNotEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')),
|
isNotEmpty: r => r.not.then(r.empty).map(unaryCondition('isNotEmpty')),
|
||||||
true: () => P.regexp(/true/i).map(binaryFixedValueCondition('1')),
|
true: () => P.regexp(/true/i).map(binaryFixedValueCondition(filterBehaviour.passBooleans ? true : '1')),
|
||||||
false: () => P.regexp(/false/i).map(binaryFixedValueCondition('0')),
|
false: () => P.regexp(/false/i).map(binaryFixedValueCondition(filterBehaviour.passBooleans ? false : '0')),
|
||||||
trueNum: () => word('1').map(binaryFixedValueCondition('1')),
|
trueNum: () => word('1').map(binaryFixedValueCondition('1')),
|
||||||
falseNum: () => word('0').map(binaryFixedValueCondition('0')),
|
falseNum: () => word('0').map(binaryFixedValueCondition('0')),
|
||||||
|
|
||||||
eq: r => word('=').then(r.value).map(binaryCondition('=', filterBehaviour.allowNumberDualTesting)),
|
eq: r => word('=').then(r.value).map(binaryCondition('=', filterBehaviour)),
|
||||||
ne: r => word('!=').then(r.value).map(binaryCondition('<>', filterBehaviour.allowNumberDualTesting)),
|
ne: r => word('!=').then(r.value).map(binaryCondition('<>', filterBehaviour)),
|
||||||
ne2: r => word('<>').then(r.value).map(binaryCondition('<>', filterBehaviour.allowNumberDualTesting)),
|
ne2: r => word('<>').then(r.value).map(binaryCondition('<>', filterBehaviour)),
|
||||||
le: r => word('<=').then(r.value).map(binaryCondition('<=', filterBehaviour.allowNumberDualTesting)),
|
le: r => word('<=').then(r.value).map(binaryCondition('<=', filterBehaviour)),
|
||||||
ge: r => word('>=').then(r.value).map(binaryCondition('>=', filterBehaviour.allowNumberDualTesting)),
|
ge: r => word('>=').then(r.value).map(binaryCondition('>=', filterBehaviour)),
|
||||||
lt: r => word('<').then(r.value).map(binaryCondition('<', filterBehaviour.allowNumberDualTesting)),
|
lt: r => word('<').then(r.value).map(binaryCondition('<', filterBehaviour)),
|
||||||
gt: r => word('>').then(r.value).map(binaryCondition('>', filterBehaviour.allowNumberDualTesting)),
|
gt: r => word('>').then(r.value).map(binaryCondition('>', filterBehaviour)),
|
||||||
startsWith: r => word('^').then(r.value).map(likeCondition('like', '#VALUE#%')),
|
startsWith: r => word('^').then(r.value).map(likeCondition('like', '#VALUE#%')),
|
||||||
endsWith: r => word('$').then(r.value).map(likeCondition('like', '%#VALUE#')),
|
endsWith: r => word('$').then(r.value).map(likeCondition('like', '%#VALUE#')),
|
||||||
contains: r => word('+').then(r.value).map(likeCondition('like', '%#VALUE#%')),
|
contains: r => word('+').then(r.value).map(likeCondition('like', '%#VALUE#%')),
|
||||||
@@ -526,8 +543,12 @@ const createParser = (filterBehaviour: FilterBehaviour) => {
|
|||||||
allowedElements.push('exists', 'notExists');
|
allowedElements.push('exists', 'notExists');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterBehaviour.supportArrayTesting) {
|
if (filterBehaviour.supportEmptyArrayTesting) {
|
||||||
allowedElements.push('emptyArray', 'notEmptyArray');
|
allowedElements.push('emptyArray');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterBehaviour.supportNotEmptyArrayTesting) {
|
||||||
|
allowedElements.push('notEmptyArray');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterBehaviour.supportNullTesting) {
|
if (filterBehaviour.supportNullTesting) {
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ function areDifferentRowCounts(db1: DatabaseInfo, db2: DatabaseInfo) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
export class DatabaseAnalyser<TClient = any> {
|
||||||
export class DatabaseAnalyser {
|
|
||||||
structure: DatabaseInfo;
|
structure: DatabaseInfo;
|
||||||
modifications: DatabaseModification[];
|
modifications: DatabaseModification[];
|
||||||
singleObjectFilter: any;
|
singleObjectFilter: any;
|
||||||
@@ -51,7 +50,7 @@ export class DatabaseAnalyser {
|
|||||||
dialect: SqlDialect;
|
dialect: SqlDialect;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
|
||||||
constructor(public dbhan: DatabaseHandle, public driver: EngineDriver, version) {
|
constructor(public dbhan: DatabaseHandle<TClient>, public driver: EngineDriver, version) {
|
||||||
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(version)) || driver?.dialect;
|
this.dialect = (driver?.dialectByVersion && driver?.dialectByVersion(version)) || driver?.dialect;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const stringFilterBehaviour: FilterBehaviour = {
|
|||||||
export const logicalFilterBehaviour: FilterBehaviour = {
|
export const logicalFilterBehaviour: FilterBehaviour = {
|
||||||
supportBooleanValues: true,
|
supportBooleanValues: true,
|
||||||
supportNullTesting: true,
|
supportNullTesting: true,
|
||||||
|
supportBooleanOrNull: true,
|
||||||
supportSqlCondition: true,
|
supportSqlCondition: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,7 +37,8 @@ export const datetimeFilterBehaviour: FilterBehaviour = {
|
|||||||
|
|
||||||
export const mongoFilterBehaviour: FilterBehaviour = {
|
export const mongoFilterBehaviour: FilterBehaviour = {
|
||||||
supportEquals: true,
|
supportEquals: true,
|
||||||
supportArrayTesting: true,
|
supportEmptyArrayTesting: true,
|
||||||
|
supportNotEmptyArrayTesting: true,
|
||||||
supportNumberLikeComparison: true,
|
supportNumberLikeComparison: true,
|
||||||
supportStringInclusion: true,
|
supportStringInclusion: true,
|
||||||
supportBooleanValues: true,
|
supportBooleanValues: true,
|
||||||
@@ -57,11 +59,38 @@ export const evalFilterBehaviour: FilterBehaviour = {
|
|||||||
allowStringToken: true,
|
allowStringToken: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const firestoreFilterBehaviours: FilterBehaviour = {
|
||||||
|
supportEquals: true,
|
||||||
|
supportEmpty: false,
|
||||||
|
supportNumberLikeComparison: true,
|
||||||
|
supportDatetimeComparison: false,
|
||||||
|
supportNullTesting: true,
|
||||||
|
supportBooleanValues: true,
|
||||||
|
supportEmptyArrayTesting: true,
|
||||||
|
|
||||||
|
supportStringInclusion: false,
|
||||||
|
supportDatetimeSymbols: false,
|
||||||
|
supportExistsTesting: false,
|
||||||
|
supportSqlCondition: false,
|
||||||
|
|
||||||
|
allowStringToken: true,
|
||||||
|
allowNumberToken: true,
|
||||||
|
allowHexString: true,
|
||||||
|
allowNumberDualTesting: false,
|
||||||
|
allowObjectIdTesting: false,
|
||||||
|
|
||||||
|
passBooleans: true,
|
||||||
|
passNumbers: true,
|
||||||
|
|
||||||
|
disableOr: true,
|
||||||
|
};
|
||||||
|
|
||||||
export const standardFilterBehaviours: { [id: string]: FilterBehaviour } = {
|
export const standardFilterBehaviours: { [id: string]: FilterBehaviour } = {
|
||||||
numberFilterBehaviour,
|
numberFilterBehaviour,
|
||||||
stringFilterBehaviour,
|
stringFilterBehaviour,
|
||||||
logicalFilterBehaviour,
|
logicalFilterBehaviour,
|
||||||
datetimeFilterBehaviour,
|
datetimeFilterBehaviour,
|
||||||
mongoFilterBehaviour,
|
mongoFilterBehaviour,
|
||||||
|
firestoreFilterBehaviours,
|
||||||
evalFilterBehaviour,
|
evalFilterBehaviour,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,6 +75,37 @@ export function parseCellValue(value, editorTypes?: DataEditorTypesBehaviour) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (editorTypes?.parseGeopointAsDollar) {
|
||||||
|
const m = value.match(/^([\d\.]+)\s*°\s*([NS]),\s*([\d\.]+)\s*°\s*([EW])$/i);
|
||||||
|
if (m) {
|
||||||
|
let latitude = parseFloat(m[1]);
|
||||||
|
const latDir = m[2].toUpperCase();
|
||||||
|
let longitude = parseFloat(m[3]);
|
||||||
|
const lonDir = m[4].toUpperCase();
|
||||||
|
|
||||||
|
if (latDir === 'S') latitude = -latitude;
|
||||||
|
if (lonDir === 'W') longitude = -longitude;
|
||||||
|
|
||||||
|
return {
|
||||||
|
$geoPoint: {
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editorTypes?.parseFsDocumentRefAsDollar) {
|
||||||
|
const trimmedValue = value.replace(/\s/g, '');
|
||||||
|
if (trimmedValue.startsWith('$ref:')) {
|
||||||
|
return {
|
||||||
|
$fsDocumentRef: {
|
||||||
|
documentPath: trimmedValue.slice(5),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (editorTypes?.parseJsonNull) {
|
if (editorTypes?.parseJsonNull) {
|
||||||
if (value == 'null') return null;
|
if (value == 'null') return null;
|
||||||
}
|
}
|
||||||
@@ -246,6 +277,32 @@ export function stringifyCellValue(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (editorTypes?.parseGeopointAsDollar) {
|
||||||
|
if (value?.$geoPoint) {
|
||||||
|
const { latitude, longitude } = value.$geoPoint;
|
||||||
|
if (_isNumber(latitude) && _isNumber(longitude)) {
|
||||||
|
const latAbs = Math.abs(latitude);
|
||||||
|
const lonAbs = Math.abs(longitude);
|
||||||
|
const latDir = latitude >= 0 ? 'N' : 'S';
|
||||||
|
const lonDir = longitude >= 0 ? 'E' : 'W';
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: `${latAbs}° ${latDir}, ${lonAbs}° ${lonDir}`,
|
||||||
|
gridStyle: 'valueCellStyle',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editorTypes?.parseFsDocumentRefAsDollar) {
|
||||||
|
if (value?.$fsDocumentRef) {
|
||||||
|
return {
|
||||||
|
value: `$ref: ${value.$fsDocumentRef.documentPath ?? ''}`,
|
||||||
|
gridStyle: 'valueCellStyle',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_isArray(value)) {
|
if (_isArray(value)) {
|
||||||
switch (intent) {
|
switch (intent) {
|
||||||
case 'gridCellIntent':
|
case 'gridCellIntent':
|
||||||
|
|||||||
2
packages/types/dbinfo.d.ts
vendored
2
packages/types/dbinfo.d.ts
vendored
@@ -108,6 +108,8 @@ export interface CollectionInfo extends DatabaseObjectInfo {
|
|||||||
// unique combination of columns (should be contatenation of partitionKey and clusterKey)
|
// unique combination of columns (should be contatenation of partitionKey and clusterKey)
|
||||||
uniqueKey?: ColumnReference[];
|
uniqueKey?: ColumnReference[];
|
||||||
|
|
||||||
|
autoValueColumns?: ColumnReference[];
|
||||||
|
|
||||||
// partition key columns
|
// partition key columns
|
||||||
partitionKey?: ColumnReference[];
|
partitionKey?: ColumnReference[];
|
||||||
|
|
||||||
|
|||||||
28
packages/types/engines.d.ts
vendored
28
packages/types/engines.d.ts
vendored
@@ -23,6 +23,28 @@ export interface StreamOptions {
|
|||||||
info?: (info) => void;
|
info?: (info) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CollectionOperationInfo =
|
||||||
|
| {
|
||||||
|
type: 'createCollection';
|
||||||
|
collection: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'dropCollection';
|
||||||
|
collection: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'renameCollection';
|
||||||
|
collection: string;
|
||||||
|
newName: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'cloneCollection';
|
||||||
|
collection: string;
|
||||||
|
newName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface RunScriptOptions {
|
export interface RunScriptOptions {
|
||||||
useTransaction: boolean;
|
useTransaction: boolean;
|
||||||
logScriptItems?: boolean;
|
logScriptItems?: boolean;
|
||||||
@@ -120,6 +142,8 @@ export interface DataEditorTypesBehaviour {
|
|||||||
parseHexAsBuffer?: boolean;
|
parseHexAsBuffer?: boolean;
|
||||||
parseObjectIdAsDollar?: boolean;
|
parseObjectIdAsDollar?: boolean;
|
||||||
parseDateAsDollar?: boolean;
|
parseDateAsDollar?: boolean;
|
||||||
|
parseGeopointAsDollar?: boolean;
|
||||||
|
parseFsDocumentRefAsDollar?: boolean;
|
||||||
|
|
||||||
explicitDataType?: boolean;
|
explicitDataType?: boolean;
|
||||||
supportNumberType?: boolean;
|
supportNumberType?: boolean;
|
||||||
@@ -217,7 +241,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
|||||||
defaultSocketPath?: string;
|
defaultSocketPath?: string;
|
||||||
authTypeLabel?: string;
|
authTypeLabel?: string;
|
||||||
importExportArgs?: any[];
|
importExportArgs?: any[];
|
||||||
connect({ server, port, user, password, database }): Promise<DatabaseHandle<TClient>>;
|
connect({ server, port, user, password, database, certificateJson }): Promise<DatabaseHandle<TClient>>;
|
||||||
close(dbhan: DatabaseHandle<TClient>): Promise<any>;
|
close(dbhan: DatabaseHandle<TClient>): Promise<any>;
|
||||||
query(dbhan: DatabaseHandle<TClient>, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
query(dbhan: DatabaseHandle<TClient>, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
||||||
stream(dbhan: DatabaseHandle<TClient>, sql: string, options: StreamOptions);
|
stream(dbhan: DatabaseHandle<TClient>, sql: string, options: StreamOptions);
|
||||||
@@ -264,7 +288,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
|||||||
dropDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
|
dropDatabase(dbhan: DatabaseHandle<TClient>, name: string): Promise;
|
||||||
getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any;
|
getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any;
|
||||||
script(dbhan: DatabaseHandle<TClient>, sql: string, options?: RunScriptOptions): Promise;
|
script(dbhan: DatabaseHandle<TClient>, sql: string, options?: RunScriptOptions): Promise;
|
||||||
operation(dbhan: DatabaseHandle<TClient>, operation: {}, options?: RunScriptOptions): Promise;
|
operation(dbhan: DatabaseHandle<TClient>, operation: CollectionOperationInfo, options?: RunScriptOptions): Promise;
|
||||||
getNewObjectTemplates(): NewObjectTemplate[];
|
getNewObjectTemplates(): NewObjectTemplate[];
|
||||||
// direct call of dbhan.client method, only some methods could be supported, on only some drivers
|
// direct call of dbhan.client method, only some methods could be supported, on only some drivers
|
||||||
callMethod(dbhan: DatabaseHandle<TClient>, method, args);
|
callMethod(dbhan: DatabaseHandle<TClient>, method, args);
|
||||||
|
|||||||
9
packages/types/filter-type.d.ts
vendored
9
packages/types/filter-type.d.ts
vendored
@@ -9,11 +9,18 @@ export interface FilterBehaviour {
|
|||||||
supportExistsTesting?: boolean;
|
supportExistsTesting?: boolean;
|
||||||
supportBooleanValues?: boolean;
|
supportBooleanValues?: boolean;
|
||||||
supportSqlCondition?: boolean;
|
supportSqlCondition?: boolean;
|
||||||
supportArrayTesting?: boolean;
|
supportEmptyArrayTesting?: boolean;
|
||||||
|
supportNotEmptyArrayTesting?: boolean;
|
||||||
|
supportBooleanOrNull?: boolean;
|
||||||
|
|
||||||
allowStringToken?: boolean;
|
allowStringToken?: boolean;
|
||||||
allowNumberToken?: boolean;
|
allowNumberToken?: boolean;
|
||||||
allowHexString?: boolean;
|
allowHexString?: boolean;
|
||||||
allowNumberDualTesting?: boolean;
|
allowNumberDualTesting?: boolean;
|
||||||
allowObjectIdTesting?: boolean;
|
allowObjectIdTesting?: boolean;
|
||||||
|
|
||||||
|
passBooleans?: boolean;
|
||||||
|
passNumbers?: boolean;
|
||||||
|
|
||||||
|
disableOr?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
89
packages/types/query.d.ts
vendored
89
packages/types/query.d.ts
vendored
@@ -15,3 +15,92 @@ export interface QueryResult {
|
|||||||
columns?: QueryResultColumn[];
|
columns?: QueryResultColumn[];
|
||||||
rowsAffected?: number;
|
rowsAffected?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LeftOperand = {
|
||||||
|
exprType: 'placeholder' | 'column';
|
||||||
|
columnName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RightOperand = {
|
||||||
|
exprType: 'value';
|
||||||
|
value: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BinaryCondition = {
|
||||||
|
conditionType: 'binary';
|
||||||
|
operator: '=' | '!=' | '<>' | '<' | '<=' | '>' | '>=';
|
||||||
|
left: LeftOperand;
|
||||||
|
right: RightOperand;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AndCondition = {
|
||||||
|
conditionType: 'and';
|
||||||
|
conditions: FilterCondition[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrCondition = {
|
||||||
|
conditionType: 'or';
|
||||||
|
conditions: FilterCondition[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NullCondition = {
|
||||||
|
conditionType: 'isNull' | 'isNotNull';
|
||||||
|
expr: LeftOperand;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NotCondition = {
|
||||||
|
conditionType: 'not';
|
||||||
|
condition: FilterCondition;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LikeCondition = {
|
||||||
|
conditionType: 'like';
|
||||||
|
left: LeftOperand;
|
||||||
|
right: RightOperand;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PredicateCondition = {
|
||||||
|
conditionType: 'specificPredicate';
|
||||||
|
predicate: 'exists' | 'notExists' | 'emptyArray' | 'notEmptyArray';
|
||||||
|
expr: LeftOperand;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InCondition = {
|
||||||
|
conditionType: 'in';
|
||||||
|
expr: LeftOperand;
|
||||||
|
values: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterCondition =
|
||||||
|
| BinaryCondition
|
||||||
|
| AndCondition
|
||||||
|
| OrCondition
|
||||||
|
| NullCondition
|
||||||
|
| NotCondition
|
||||||
|
| LikeCondition
|
||||||
|
| PredicateCondition
|
||||||
|
| InCondition;
|
||||||
|
|
||||||
|
export type SortItem = {
|
||||||
|
columnName: string;
|
||||||
|
direction?: 'ASC' | 'DESC';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AggregateColumn = {
|
||||||
|
aggregateFunction: 'count' | 'sum' | 'avg' | 'min' | 'max';
|
||||||
|
columnArgument?: string;
|
||||||
|
alias: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CollectionAggregate = {
|
||||||
|
condition?: FilterCondition;
|
||||||
|
groupByColumns: string[];
|
||||||
|
aggregateColumns: AggregateColumn[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FullQueryOptions = {
|
||||||
|
condition?: FilterCondition;
|
||||||
|
sort?: SortItem[];
|
||||||
|
limit?: number;
|
||||||
|
skip?: number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -122,7 +122,11 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
|
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
|
||||||
import registerCommand from '../commands/registerCommand';
|
import registerCommand from '../commands/registerCommand';
|
||||||
import { extractShellConnection, extractShellConnectionHostable, extractShellHostConnection } from '../impexp/createImpExpScript';
|
import {
|
||||||
|
extractShellConnection,
|
||||||
|
extractShellConnectionHostable,
|
||||||
|
extractShellHostConnection,
|
||||||
|
} from '../impexp/createImpExpScript';
|
||||||
import { apiCall } from '../utility/api';
|
import { apiCall } from '../utility/api';
|
||||||
|
|
||||||
import { registerMenu } from '../utility/contextMenu';
|
import { registerMenu } from '../utility/contextMenu';
|
||||||
|
|||||||
@@ -80,11 +80,12 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterBehaviour.supportArrayTesting) {
|
if (filterBehaviour.supportNotEmptyArrayTesting) {
|
||||||
res.push(
|
res.push({ onClick: () => setFilter('NOT EMPTY ARRAY'), text: 'Array is not empty' });
|
||||||
{ onClick: () => setFilter('NOT EMPTY ARRAY'), text: 'Array is not empty' },
|
}
|
||||||
{ onClick: () => setFilter('EMPTY ARRAY'), text: 'Array is empty' }
|
|
||||||
);
|
if (filterBehaviour.supportEmptyArrayTesting) {
|
||||||
|
res.push({ onClick: () => setFilter('EMPTY ARRAY'), text: 'Array is empty' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterBehaviour.supportNullTesting) {
|
if (filterBehaviour.supportNullTesting) {
|
||||||
@@ -132,7 +133,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterBehaviour.supportBooleanValues && filterBehaviour.supportNullTesting) {
|
if (filterBehaviour.supportBooleanOrNull) {
|
||||||
res.push(
|
res.push(
|
||||||
{ onClick: () => setFilter('TRUE, NULL'), text: 'Is True or NULL' },
|
{ onClick: () => setFilter('TRUE, NULL'), text: 'Is True or NULL' },
|
||||||
{ onClick: () => setFilter('FALSE, NULL'), text: 'Is False or NULL' }
|
{ onClick: () => setFilter('FALSE, NULL'), text: 'Is False or NULL' }
|
||||||
|
|||||||
58
packages/web/src/forms/FormJsonFileInputField.svelte
Normal file
58
packages/web/src/forms/FormJsonFileInputField.svelte
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SimpleFilesInput, { ProcessedFile } from '../impexp/SimpleFilesInput.svelte';
|
||||||
|
import { FileParseResult, parseFileAsJson } from '../utility/parseFileAsJson';
|
||||||
|
import { getFormContext } from './FormProviderCore.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let buttonLabel: string = 'Choose File';
|
||||||
|
export let name: string;
|
||||||
|
export let disabled: boolean = false;
|
||||||
|
export let accept: string = '.json,application/json';
|
||||||
|
export let templateProps = {};
|
||||||
|
|
||||||
|
const { template, setFieldValue, values } = getFormContext();
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let fileName: string | null = null;
|
||||||
|
$: hasValue = $values?.[name] != null;
|
||||||
|
$: displayLabel = getDisplayLabel(buttonLabel, hasValue, fileName);
|
||||||
|
|
||||||
|
async function handleFileChange(fileData: ProcessedFile): Promise<void> {
|
||||||
|
const parseResult: FileParseResult = await parseFileAsJson(fileData.file);
|
||||||
|
|
||||||
|
if (parseResult.success) {
|
||||||
|
fileName = fileData.name;
|
||||||
|
setFieldValue(name, parseResult.data);
|
||||||
|
dispatch('change', {
|
||||||
|
success: true,
|
||||||
|
data: parseResult.data,
|
||||||
|
fileName: fileData.name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fileName = null;
|
||||||
|
setFieldValue(name, null);
|
||||||
|
dispatch('change', {
|
||||||
|
success: false,
|
||||||
|
error: parseResult.error,
|
||||||
|
fileName: fileData.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayLabel(baseLabel: string, hasValue: boolean, fileName: string | null): string {
|
||||||
|
if (!hasValue) {
|
||||||
|
return baseLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
return `${baseLabel} (${fileName})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseLabel} (JSON loaded)`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component this={template} type="file" {label} {disabled} {...templateProps}>
|
||||||
|
<SimpleFilesInput label={displayLabel} {accept} {disabled} onChange={handleFileChange} {...$$restProps} />
|
||||||
|
</svelte:component>
|
||||||
105
packages/web/src/impexp/SimpleFilesInput.svelte
Normal file
105
packages/web/src/impexp/SimpleFilesInput.svelte
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<script context="module" lang="ts">
|
||||||
|
export type ProcessedFile = {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
lastModified: number;
|
||||||
|
content: string | ArrayBuffer | null;
|
||||||
|
file: File;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export let disabled: boolean = false;
|
||||||
|
export let label: string = 'Choose File';
|
||||||
|
export let onChange: ((fileData: ProcessedFile | ProcessedFile[]) => void) | null = null;
|
||||||
|
export let accept: string = '*';
|
||||||
|
export let multiple: boolean = false;
|
||||||
|
|
||||||
|
let fileInput: HTMLInputElement;
|
||||||
|
|
||||||
|
function handleFileChange(event: Event): void {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const files = target.files;
|
||||||
|
if (!files || files.length < 0 || !onChange) return;
|
||||||
|
|
||||||
|
if (multiple) {
|
||||||
|
const processedFiles = Array.from(files).map(processFile);
|
||||||
|
Promise.all(processedFiles).then((results: ProcessedFile[]) => {
|
||||||
|
onChange(results);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
processFile(files[0]).then((result: ProcessedFile) => {
|
||||||
|
onChange(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processFile(file: File): Promise<ProcessedFile> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||||
|
resolve({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
lastModified: file.lastModified,
|
||||||
|
content: e.target?.result || null,
|
||||||
|
file: file,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerFileInput(): void {
|
||||||
|
fileInput.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input
|
||||||
|
{disabled}
|
||||||
|
bind:this={fileInput}
|
||||||
|
type="file"
|
||||||
|
{accept}
|
||||||
|
{multiple}
|
||||||
|
on:change={handleFileChange}
|
||||||
|
style="display: none;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button {disabled} on:click={triggerFileInput} class="file-input-btn">
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.file-input-btn {
|
||||||
|
border: 1px solid var(--theme-bg-button-inv-2);
|
||||||
|
padding: 5px;
|
||||||
|
margin: 2px;
|
||||||
|
background-color: var(--theme-bg-button-inv);
|
||||||
|
color: var(--theme-font-inv-1);
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-btn:hover:not(:disabled) {
|
||||||
|
background-color: var(--theme-bg-button-inv-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-btn:active:not(:disabled) {
|
||||||
|
background-color: var(--theme-bg-button-inv-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-btn:focus {
|
||||||
|
outline: 2px solid var(--theme-bg-button-inv-2);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-btn:disabled {
|
||||||
|
background-color: var(--theme-bg-button-inv-3);
|
||||||
|
color: var(--theme-font-inv-3);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -173,8 +173,10 @@
|
|||||||
|
|
||||||
{#if storageType == 'database' || storageType == 'query'}
|
{#if storageType == 'database' || storageType == 'query'}
|
||||||
<FormConnectionSelect name={connectionIdField} label="Server" {direction} />
|
<FormConnectionSelect name={connectionIdField} label="Server" {direction} />
|
||||||
|
{#if !$connectionInfo?.singleDatabase}
|
||||||
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label="Database" />
|
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label="Database" />
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
{#if storageType == 'database'}
|
{#if storageType == 'database'}
|
||||||
<FormSchemaSelect
|
<FormSchemaSelect
|
||||||
conidName={connectionIdField}
|
conidName={connectionIdField}
|
||||||
|
|||||||
@@ -63,7 +63,9 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<FormRadioGroupItem name="joinOperator" value=" " text="And" />
|
<FormRadioGroupItem name="joinOperator" value=" " text="And" />
|
||||||
|
{#if !filterBehaviour.disableOr}
|
||||||
<FormRadioGroupItem name="joinOperator" value="," text="Or" />
|
<FormRadioGroupItem name="joinOperator" value="," text="Or" />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
import FormDropDownTextField from '../forms/FormDropDownTextField.svelte';
|
import FormDropDownTextField from '../forms/FormDropDownTextField.svelte';
|
||||||
import { getConnectionLabel } from 'dbgate-tools';
|
import { getConnectionLabel } from 'dbgate-tools';
|
||||||
import { _t } from '../translations';
|
import { _t } from '../translations';
|
||||||
|
import FilesInput from '../impexp/FilesInput.svelte';
|
||||||
|
import SimpleFilesInput from '../impexp/SimpleFilesInput.svelte';
|
||||||
|
import FormJsonFileInputField from '../forms/FormJsonFileInputField.svelte';
|
||||||
|
|
||||||
export let getDatabaseList;
|
export let getDatabaseList;
|
||||||
export let currentConnection;
|
export let currentConnection;
|
||||||
@@ -462,6 +465,10 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if driver?.showConnectionField('certificateJson', $values, showConnectionFieldArgs)}
|
||||||
|
<FormJsonFileInputField disabled={isConnected} label="Service account key JSON" name="certificateJson" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if driver}
|
{#if driver}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 mr-1">
|
<div class="col-6 mr-1">
|
||||||
|
|||||||
38
packages/web/src/utility/parseFileAsJson.js
Normal file
38
packages/web/src/utility/parseFileAsJson.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* @template [T = any]
|
||||||
|
* @typedef {Object} FileParseResultSuccess
|
||||||
|
* @property {true} success
|
||||||
|
* @property {T} data
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} FileParseResultError
|
||||||
|
* @property {false} success
|
||||||
|
* @property {string} error
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template [T = any]
|
||||||
|
* @typedef {FileParseResultSuccess<T> | FileParseResultError} FileParseResult
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template [T = any]
|
||||||
|
* @param {File} file
|
||||||
|
* @returns {Promise<FileParseResult<T>>}
|
||||||
|
*/
|
||||||
|
export async function parseFileAsJson(file) {
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown parsing error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
20
silent
Normal file
20
silent
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
DEVMODE=1
|
||||||
|
SHELL_SCRIPTING=1
|
||||||
|
# LOCAL_DBGATE_CLOUD=1
|
||||||
|
# LOCAL_DBGATE_IDENTITY=1
|
||||||
|
|
||||||
|
# CLOUD_UPGRADE_FILE=c:\test\upg\upgrade.zip
|
||||||
|
|
||||||
|
# PERMISSIONS=~widgets/app,~widgets/plugins
|
||||||
|
# DISABLE_SHELL=1
|
||||||
|
# HIDE_APP_EDITOR=1
|
||||||
|
|
||||||
|
# DEVWEB=1
|
||||||
|
# LOGINS=admin,test
|
||||||
|
|
||||||
|
# LOGIN_PASSWORD_admin=admin
|
||||||
|
# LOGIN_PERMISSIONS_admin=*
|
||||||
|
|
||||||
|
# LOGIN_PASSWORD_test=test
|
||||||
|
# LOGIN_PERMISSIONS_test=~*, widgets/database
|
||||||
|
# WORKSPACE_DIR=/home/jena/dbgate-data-2
|
||||||
Reference in New Issue
Block a user