SYNC: Merge pull request #5 from dbgate/feature/firestore

This commit is contained in:
Jan Prochazka
2025-07-24 10:59:56 +02:00
committed by Diflow
parent 0cf9ddb1cd
commit c171f93c93
22 changed files with 1353 additions and 69 deletions

41
.vscode/launch.json vendored
View File

@@ -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>/**"]
} }
] ]
} }

View File

@@ -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

View File

@@ -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",

View File

@@ -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),
}; };
} }

View File

@@ -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) {

View File

@@ -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;
} }

View File

@@ -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,
}; };

View File

@@ -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':

View File

@@ -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[];

View File

@@ -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);

View File

@@ -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;
} }

View File

@@ -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;
};

View File

@@ -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';

View File

@@ -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' }

View 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>

View 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>

View File

@@ -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}

View File

@@ -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">

View File

@@ -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">

View 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
View 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

837
yarn.lock

File diff suppressed because it is too large Load Diff