diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml
index ff0a610c9..b8ceda796 100644
--- a/.github/workflows/run-tests.yaml
+++ b/.github/workflows/run-tests.yaml
@@ -4,6 +4,7 @@ on:
branches:
- master
- develop
+ - 'feature/**'
jobs:
test-runner:
diff --git a/integration-tests/__tests__/import-formats.spec.js b/integration-tests/__tests__/import-formats.spec.js
new file mode 100644
index 000000000..21dc38e21
--- /dev/null
+++ b/integration-tests/__tests__/import-formats.spec.js
@@ -0,0 +1,151 @@
+const dbgateApi = require('dbgate-api/src/shell');
+// const jsonLinesWriter = require('dbgate-api/src/shell/jsonLinesWriter');
+const tmp = require('tmp');
+// const dbgatePluginCsv = require('dbgate-plugin-csv/src/backend');
+const fs = require('fs');
+const requirePlugin = require('dbgate-api/src/shell/requirePlugin');
+
+const CSV_DATA = `Issue Number; Title; Github URL; Labels; State; Created At; Updated At; Reporter; Assignee
+801; "Does it 'burst' the database on startup or first lUI load ? "; https://github.com/dbgate/dbgate/issues/801; ""; open; 05/23/2024; 05/23/2024; rgarrigue;
+799; "BUG: latest AppImage crashes on opening in Fedora 39"; https://github.com/dbgate/dbgate/issues/799; ""; open; 05/21/2024; 05/24/2024; BenGraham-Git;
+798; "MongoDB write operations fail"; https://github.com/dbgate/dbgate/issues/798; "bug,solved"; open; 05/21/2024; 05/24/2024; mahmed0715;
+797; "BUG: Unable to open SQL files"; https://github.com/dbgate/dbgate/issues/797; "bug"; open; 05/20/2024; 05/21/2024; cesarValdivia;
+795; "BUG: MS SQL Server connection error (KEY_USAGE_BIT_INCORRECT)"; https://github.com/dbgate/dbgate/issues/795; ""; open; 05/20/2024; 05/20/2024; keskinonur;
+794; "GLIBC_2.29' not found and i have 2.31"; https://github.com/dbgate/dbgate/issues/794; ""; closed; 05/20/2024; 05/21/2024; MFdanGM;
+793; "BUG: PostgresSQL doesn't show tables when connected"; https://github.com/dbgate/dbgate/issues/793; ""; open; 05/20/2024; 05/22/2024; stomper013;
+792; "FEAT: Wayland support"; https://github.com/dbgate/dbgate/issues/792; ""; closed; 05/19/2024; 05/21/2024; VosaXalo;
+`;
+
+async function getReaderRows(reader) {
+ const jsonLinesFileName = tmp.tmpNameSync();
+
+ const writer = await dbgateApi.jsonLinesWriter({
+ fileName: jsonLinesFileName,
+ });
+ await dbgateApi.copyStream(reader, writer);
+
+ const jsonData = fs.readFileSync(jsonLinesFileName, 'utf-8');
+ const rows = jsonData
+ .split('\n')
+ .filter(x => x.trim() !== '')
+ .map(x => JSON.parse(x));
+
+ return rows;
+}
+
+test('csv import test', async () => {
+ const dbgatePluginCsv = requirePlugin('dbgate-plugin-csv');
+
+ const csvFileName = tmp.tmpNameSync();
+
+ fs.writeFileSync(csvFileName, CSV_DATA);
+
+ const reader = await dbgatePluginCsv.shellApi.reader({
+ fileName: csvFileName,
+ });
+
+ const rows = await getReaderRows(reader);
+
+ expect(rows[0].columns).toEqual([
+ { columnName: 'Issue Number' },
+ { columnName: 'Title' },
+ { columnName: 'Github URL' },
+ { columnName: 'Labels' },
+ { columnName: 'State' },
+ { columnName: 'Created At' },
+ { columnName: 'Updated At' },
+ { columnName: 'Reporter' },
+ { columnName: 'Assignee' },
+ ]);
+ expect(rows.length).toEqual(9);
+ expect(rows[1]).toEqual({
+ 'Issue Number': '801',
+ Title: "Does it 'burst' the database on startup or first lUI load ? ",
+ 'Github URL': 'https://github.com/dbgate/dbgate/issues/801',
+ Labels: '',
+ State: 'open',
+ 'Created At': '05/23/2024',
+ 'Updated At': '05/23/2024',
+ Reporter: 'rgarrigue',
+ Assignee: '',
+ });
+});
+
+test('JSON array import test', async () => {
+ const jsonFileName = tmp.tmpNameSync();
+
+ fs.writeFileSync(
+ jsonFileName,
+ JSON.stringify([
+ { id: 1, val: 'v1' },
+ { id: 2, val: 'v2' },
+ ])
+ );
+
+ const reader = await dbgateApi.jsonReader({
+ fileName: jsonFileName,
+ });
+
+ const rows = await getReaderRows(reader);
+
+ expect(rows.length).toEqual(2);
+ expect(rows).toEqual([
+ { id: 1, val: 'v1' },
+ { id: 2, val: 'v2' },
+ ]);
+});
+
+test('JSON object import test', async () => {
+ const jsonFileName = tmp.tmpNameSync();
+
+ fs.writeFileSync(
+ jsonFileName,
+ JSON.stringify({
+ k1: { id: 1, val: 'v1' },
+ k2: { id: 2, val: 'v2' },
+ })
+ );
+
+ const reader = await dbgateApi.jsonReader({
+ fileName: jsonFileName,
+ jsonStyle: 'object',
+ keyField: 'mykey',
+ });
+
+ const rows = await getReaderRows(reader);
+
+ expect(rows.length).toEqual(2);
+ expect(rows).toEqual([
+ { mykey: 'k1', id: 1, val: 'v1' },
+ { mykey: 'k2', id: 2, val: 'v2' },
+ ]);
+});
+
+test('JSON filtered object import test', async () => {
+ const jsonFileName = tmp.tmpNameSync();
+
+ fs.writeFileSync(
+ jsonFileName,
+ JSON.stringify({
+ filtered: {
+ k1: { id: 1, val: 'v1' },
+ k2: { id: 2, val: 'v2' },
+ },
+ })
+ );
+
+ const reader = await dbgateApi.jsonReader({
+ fileName: jsonFileName,
+ jsonStyle: 'object',
+ keyField: 'mykey',
+ rootField: 'filtered',
+ });
+
+ const rows = await getReaderRows(reader);
+
+ expect(rows.length).toEqual(2);
+ expect(rows).toEqual([
+ { mykey: 'k1', id: 1, val: 'v1' },
+ { mykey: 'k2', id: 2, val: 'v2' },
+ ]);
+});
diff --git a/integration-tests/package.json b/integration-tests/package.json
index dcb1e67e4..6a0dea08c 100644
--- a/integration-tests/package.json
+++ b/integration-tests/package.json
@@ -22,6 +22,7 @@
"devDependencies": {
"cross-env": "^7.0.3",
"jest": "^27.0.1",
- "pino-pretty": "^11.2.2"
+ "pino-pretty": "^11.2.2",
+ "tmp": "^0.2.3"
}
}
diff --git a/integration-tests/setupTests.js b/integration-tests/setupTests.js
index b52c4066e..e0f763787 100644
--- a/integration-tests/setupTests.js
+++ b/integration-tests/setupTests.js
@@ -1,4 +1,10 @@
+global.DBGATE_PACKAGES = {
+ 'dbgate-tools': require('dbgate-tools'),
+ 'dbgate-sqltree': require('dbgate-sqltree'),
+};
+
const { prettyFactory } = require('pino-pretty');
+const tmp = require('tmp');
const pretty = prettyFactory({
colorize: true,
@@ -20,3 +26,5 @@ global.console = {
process.stdout.write(messages.join(' ') + '\n');
},
};
+
+tmp.setGracefulCleanup();
diff --git a/integration-tests/tools.js b/integration-tests/tools.js
index 846c27659..4ab8c20d4 100644
--- a/integration-tests/tools.js
+++ b/integration-tests/tools.js
@@ -1,8 +1,3 @@
-global.DBGATE_PACKAGES = {
- 'dbgate-tools': require('dbgate-tools'),
- 'dbgate-sqltree': require('dbgate-sqltree'),
-};
-
const requireEngineDriver = require('dbgate-api/src/utility/requireEngineDriver');
const crypto = require('crypto');
diff --git a/package.json b/package.json
index ec6297e8c..ac6acbc09 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"build:api": "yarn workspace dbgate-api build",
"build:web:docker": "yarn workspace dbgate-web build",
"build:plugins:frontend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:frontend",
+ "build:plugins:backend": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn build:backend",
"build:plugins:frontend:watch": "workspaces-run --parallel --only=\"dbgate-plugin-*\" -- yarn build:frontend:watch",
"storage-json": "dbmodel model-to-json storage-db packages/api/src/storageModel.js --commonjs",
"plugins:copydist": "workspaces-run --only=\"dbgate-plugin-*\" -- yarn copydist",
diff --git a/packages/api/package.json b/packages/api/package.json
index 4812aefd0..c2813e3e8 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -57,6 +57,7 @@
"rimraf": "^3.0.0",
"simple-encryptor": "^4.0.0",
"ssh2": "^1.11.0",
+ "stream-json": "^1.8.0",
"tar": "^6.0.5"
},
"scripts": {
diff --git a/packages/api/src/controllers/jsldata.js b/packages/api/src/controllers/jsldata.js
index 0cf04a978..448ba5d3f 100644
--- a/packages/api/src/controllers/jsldata.js
+++ b/packages/api/src/controllers/jsldata.js
@@ -18,11 +18,14 @@ function readFirstLine(file) {
}
if (reader.hasNextLine()) {
reader.nextLine((err, line) => {
- if (err) reject(err);
- resolve(line);
+ if (err) {
+ reader.close(() => reject(err)); // Ensure reader is closed on error
+ return;
+ }
+ reader.close(() => resolve(line)); // Ensure reader is closed after reading
});
} else {
- resolve(null);
+ reader.close(() => resolve(null)); // Properly close if no lines are present
}
});
});
diff --git a/packages/api/src/shell/index.js b/packages/api/src/shell/index.js
index 253bb053a..1a2bef628 100644
--- a/packages/api/src/shell/index.js
+++ b/packages/api/src/shell/index.js
@@ -6,7 +6,7 @@ const copyStream = require('./copyStream');
const fakeObjectReader = require('./fakeObjectReader');
const consoleObjectWriter = require('./consoleObjectWriter');
const jsonLinesWriter = require('./jsonLinesWriter');
-const jsonArrayWriter = require('./jsonArrayWriter');
+const jsonWriter = require('./jsonWriter');
const jsonLinesReader = require('./jsonLinesReader');
const sqlDataWriter = require('./sqlDataWriter');
const jslDataReader = require('./jslDataReader');
@@ -29,6 +29,7 @@ const modifyJsonLinesReader = require('./modifyJsonLinesReader');
const dataDuplicator = require('./dataDuplicator');
const dbModelToJson = require('./dbModelToJson');
const jsonToDbModel = require('./jsonToDbModel');
+const jsonReader = require('./jsonReader');
const dbgateApi = {
queryReader,
@@ -37,8 +38,9 @@ const dbgateApi = {
tableReader,
copyStream,
jsonLinesWriter,
- jsonArrayWriter,
jsonLinesReader,
+ jsonReader,
+ jsonWriter,
sqlDataWriter,
fakeObjectReader,
consoleObjectWriter,
diff --git a/packages/api/src/shell/jsonArrayWriter.js b/packages/api/src/shell/jsonArrayWriter.js
deleted file mode 100644
index ed18b86f6..000000000
--- a/packages/api/src/shell/jsonArrayWriter.js
+++ /dev/null
@@ -1,52 +0,0 @@
-const { getLogger } = require('dbgate-tools');
-const fs = require('fs');
-const stream = require('stream');
-
-const logger = getLogger('jsonArrayWriter');
-
-class StringifyStream extends stream.Transform {
- constructor() {
- super({ objectMode: true });
- this.wasHeader = false;
- this.wasRecord = false;
- }
- _transform(chunk, encoding, done) {
- let skip = false;
-
- if (!this.wasHeader) {
- skip = chunk.__isStreamHeader;
- this.wasHeader = true;
- }
- if (!skip) {
- if (!this.wasRecord) {
- this.push('[\n');
- } else {
- this.push(',\n');
- }
- this.wasRecord = true;
-
- this.push(JSON.stringify(chunk));
- }
- done();
- }
-
- _flush(done) {
- if (!this.wasRecord) {
- this.push('[]\n');
- } else {
- this.push('\n]\n');
- }
- done();
- }
-}
-
-async function jsonArrayWriter({ fileName, encoding = 'utf-8' }) {
- logger.info(`Writing file ${fileName}`);
- const stringify = new StringifyStream();
- const fileStream = fs.createWriteStream(fileName, encoding);
- stringify.pipe(fileStream);
- stringify['finisher'] = fileStream;
- return stringify;
-}
-
-module.exports = jsonArrayWriter;
diff --git a/packages/api/src/shell/jsonLinesReader.js b/packages/api/src/shell/jsonLinesReader.js
index b226e487e..db36d966f 100644
--- a/packages/api/src/shell/jsonLinesReader.js
+++ b/packages/api/src/shell/jsonLinesReader.js
@@ -2,6 +2,7 @@ const fs = require('fs');
const stream = require('stream');
const byline = require('byline');
const { getLogger } = require('dbgate-tools');
+const download = require('./download');
const logger = getLogger('jsonLinesReader');
class ParseStream extends stream.Transform {
@@ -35,8 +36,10 @@ class ParseStream extends stream.Transform {
async function jsonLinesReader({ fileName, encoding = 'utf-8', limitRows = undefined }) {
logger.info(`Reading file ${fileName}`);
+ const downloadedFile = await download(fileName);
+
const fileStream = fs.createReadStream(
- fileName,
+ downloadedFile,
// @ts-ignore
encoding
);
diff --git a/packages/api/src/shell/jsonReader.js b/packages/api/src/shell/jsonReader.js
new file mode 100644
index 000000000..48423f3ca
--- /dev/null
+++ b/packages/api/src/shell/jsonReader.js
@@ -0,0 +1,84 @@
+const fs = require('fs');
+const stream = require('stream');
+const byline = require('byline');
+const { getLogger } = require('dbgate-tools');
+const { parser } = require('stream-json');
+const { pick } = require('stream-json/filters/Pick');
+const { streamArray } = require('stream-json/streamers/StreamArray');
+const { streamObject } = require('stream-json/streamers/StreamObject');
+const download = require('./download');
+
+const logger = getLogger('jsonReader');
+
+
+class ParseStream extends stream.Transform {
+ constructor({ limitRows, jsonStyle, keyField }) {
+ super({ objectMode: true });
+ this.wasHeader = false;
+ this.limitRows = limitRows;
+ this.jsonStyle = jsonStyle;
+ this.keyField = keyField || '_key';
+ this.rowsWritten = 0;
+ }
+ _transform(chunk, encoding, done) {
+ if (!this.wasHeader) {
+ this.push({
+ __isStreamHeader: true,
+ __isDynamicStructure: true,
+ });
+
+ this.wasHeader = true;
+ }
+ if (!this.limitRows || this.rowsWritten < this.limitRows) {
+ if (this.jsonStyle === 'object') {
+ this.push({
+ ...chunk.value,
+ [this.keyField]: chunk.key,
+ });
+ } else {
+ this.push(chunk.value);
+ }
+
+ this.rowsWritten += 1;
+ }
+ done();
+ }
+}
+
+async function jsonReader({
+ fileName,
+ jsonStyle,
+ keyField = '_key',
+ rootField = null,
+ encoding = 'utf-8',
+ limitRows = undefined,
+}) {
+ logger.info(`Reading file ${fileName}`);
+
+ const downloadedFile = await download(fileName);
+ const fileStream = fs.createReadStream(
+ downloadedFile,
+ // @ts-ignore
+ encoding
+ );
+ const parseJsonStream = parser();
+ fileStream.pipe(parseJsonStream);
+
+ const parseStream = new ParseStream({ limitRows, jsonStyle, keyField });
+
+ const tramsformer = jsonStyle === 'object' ? streamObject() : streamArray();
+
+ if (rootField) {
+ const filterStream = pick({ filter: rootField });
+ parseJsonStream.pipe(filterStream);
+ filterStream.pipe(tramsformer);
+ } else {
+ parseJsonStream.pipe(tramsformer);
+ }
+
+ tramsformer.pipe(parseStream);
+
+ return parseStream;
+}
+
+module.exports = jsonReader;
diff --git a/packages/api/src/shell/jsonWriter.js b/packages/api/src/shell/jsonWriter.js
new file mode 100644
index 000000000..a0ce19f24
--- /dev/null
+++ b/packages/api/src/shell/jsonWriter.js
@@ -0,0 +1,97 @@
+const { getLogger } = require('dbgate-tools');
+const fs = require('fs');
+const stream = require('stream');
+const _ = require('lodash');
+
+const logger = getLogger('jsonArrayWriter');
+
+class StringifyStream extends stream.Transform {
+ constructor({ jsonStyle, keyField, rootField }) {
+ super({ objectMode: true });
+ this.wasHeader = false;
+ this.wasRecord = false;
+ this.jsonStyle = jsonStyle;
+ this.keyField = keyField || '_key';
+ this.rootField = rootField;
+ }
+ _transform(chunk, encoding, done) {
+ let skip = false;
+
+ if (!this.wasHeader) {
+ skip = chunk.__isStreamHeader;
+ this.wasHeader = true;
+ }
+ if (!skip) {
+ if (!this.wasRecord) {
+ if (this.rootField) {
+ if (this.jsonStyle === 'object') {
+ this.push(`{"${this.rootField}": {\n`);
+ } else {
+ this.push(`{"${this.rootField}": [\n`);
+ }
+ } else {
+ if (this.jsonStyle === 'object') {
+ this.push('{\n');
+ } else {
+ this.push('[\n');
+ }
+ }
+ } else {
+ this.push(',\n');
+ }
+ this.wasRecord = true;
+
+ if (this.jsonStyle === 'object') {
+ const key = chunk[this.keyField] ?? chunk[Object.keys(chunk)[0]];
+ this.push(`"${key}": ${JSON.stringify(_.omit(chunk, [this.keyField]))}`);
+ } else {
+ this.push(JSON.stringify(chunk));
+ }
+ }
+ done();
+ }
+
+ _flush(done) {
+ if (!this.wasRecord) {
+ if (this.rootField) {
+ if (this.jsonStyle === 'object') {
+ this.push(`{"${this.rootField}": {}}\n`);
+ } else {
+ this.push(`{"${this.rootField}": []}\n`);
+ }
+ } else {
+ if (this.jsonStyle === 'object') {
+ this.push('{}\n');
+ } else {
+ this.push('[]\n');
+ }
+ }
+ } else {
+ if (this.rootField) {
+ if (this.jsonStyle === 'object') {
+ this.push('\n}}\n');
+ } else {
+ this.push('\n]}\n');
+ }
+ } else {
+ if (this.jsonStyle === 'object') {
+ this.push('\n}\n');
+ } else {
+ this.push('\n]\n');
+ }
+ }
+ }
+ done();
+ }
+}
+
+async function jsonWriter({ fileName, jsonStyle, keyField = '_key', rootField, encoding = 'utf-8' }) {
+ logger.info(`Writing file ${fileName}`);
+ const stringify = new StringifyStream({ jsonStyle, keyField, rootField });
+ const fileStream = fs.createWriteStream(fileName, encoding);
+ stringify.pipe(fileStream);
+ stringify['finisher'] = fileStream;
+ return stringify;
+}
+
+module.exports = jsonWriter;
diff --git a/packages/api/src/shell/modifyJsonLinesReader.js b/packages/api/src/shell/modifyJsonLinesReader.js
index 7aa007369..dec137ba0 100644
--- a/packages/api/src/shell/modifyJsonLinesReader.js
+++ b/packages/api/src/shell/modifyJsonLinesReader.js
@@ -66,7 +66,7 @@ class ParseStream extends stream.Transform {
...obj,
...update.fields,
},
- (v, k) => v.$$undefined$$
+ (v, k) => v?.$$undefined$$
);
}
}
diff --git a/packages/web/src/appobj/ArchiveFileAppObject.svelte b/packages/web/src/appobj/ArchiveFileAppObject.svelte
index 20457d54f..97874510d 100644
--- a/packages/web/src/appobj/ArchiveFileAppObject.svelte
+++ b/packages/web/src/appobj/ArchiveFileAppObject.svelte
@@ -65,18 +65,18 @@
diff --git a/packages/web/src/impexp/SourceTargetConfig.svelte b/packages/web/src/impexp/SourceTargetConfig.svelte
index 241c02ecb..d25e55128 100644
--- a/packages/web/src/impexp/SourceTargetConfig.svelte
+++ b/packages/web/src/impexp/SourceTargetConfig.svelte
@@ -196,6 +196,7 @@
width: 20vw;
margin-left: var(--dim-large-form-margin);
margin-bottom: var(--dim-large-form-margin);
+ border: 1px solid var(--theme-border);
}
.label {
diff --git a/packages/web/src/impexp/createImpExpScript.ts b/packages/web/src/impexp/createImpExpScript.ts
index 5ea6c3b4d..cb001994f 100644
--- a/packages/web/src/impexp/createImpExpScript.ts
+++ b/packages/web/src/impexp/createImpExpScript.ts
@@ -192,7 +192,7 @@ export function normalizeExportColumnMap(colmap) {
return null;
}
-export default async function createImpExpScript(extensions, values, addEditorInfo = true, forceScript = false) {
+export default async function createImpExpScript(extensions, values, forceScript = false) {
const config = getCurrentConfig();
const script =
config.allowShellScripting || forceScript
@@ -233,10 +233,6 @@ export default async function createImpExpScript(extensions, values, addEditorIn
script.copyStream(sourceVar, targetVar, colmapVar);
script.endLine();
}
- if (addEditorInfo) {
- script.comment('@ImportExportConfigurator');
- script.comment(JSON.stringify(values));
- }
return script.getScript(values.schedule);
}
diff --git a/packages/web/src/modals/ChangeDownloadUrlModal.svelte b/packages/web/src/modals/ChangeDownloadUrlModal.svelte
index 86ffb41ec..980f0aaf1 100644
--- a/packages/web/src/modals/ChangeDownloadUrlModal.svelte
+++ b/packages/web/src/modals/ChangeDownloadUrlModal.svelte
@@ -8,6 +8,7 @@
import { closeCurrentModal } from './modalTools';
export let onConfirm;
+ export let url;
const handleSubmit = e => {
onConfirm(e.detail.url);
@@ -15,7 +16,7 @@
};
-
+
Download imported file from web
diff --git a/packages/web/src/modals/ImportExportModal.svelte b/packages/web/src/modals/ImportExportModal.svelte
deleted file mode 100644
index 1d1c6dc78..000000000
--- a/packages/web/src/modals/ImportExportModal.svelte
+++ /dev/null
@@ -1,203 +0,0 @@
-
-
-
-
-
- Import/Export
- {#if busy}
-
- {/if}
-
-
-
-
-
-
- {#if busy}
-
- {/if}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {#if busy}
- Stop
- {:else}
- Run
- {/if}
- Generate script
-
- Close
-
-
-
-
-
-
diff --git a/packages/web/src/plugins/fileformats.ts b/packages/web/src/plugins/fileformats.ts
index a65aef9df..94b49411e 100644
--- a/packages/web/src/plugins/fileformats.ts
+++ b/packages/web/src/plugins/fileformats.ts
@@ -13,7 +13,33 @@ const jsonFormat = {
storageType: 'json',
extension: 'json',
name: 'JSON',
- writerFunc: 'jsonArrayWriter',
+ readerFunc: 'jsonReader',
+ writerFunc: 'jsonWriter',
+
+ args: [
+ {
+ type: 'select',
+ name: 'jsonStyle',
+ label: 'JSON style',
+ options: [
+ { name: 'Array', value: '' },
+ { name: 'Object', value: 'object' },
+ ],
+ apiName: 'jsonStyle',
+ },
+ {
+ type: 'text',
+ name: 'keyField',
+ label: 'Key field (only for "Object" style)',
+ apiName: 'keyField',
+ },
+ {
+ type: 'text',
+ name: 'rootField',
+ label: 'Root field',
+ apiName: 'rootField',
+ },
+ ],
};
const sqlFormat = {
@@ -38,7 +64,7 @@ const jsonQuickExport = {
label: 'JSON',
extension: 'json',
createWriter: fileName => ({
- functionName: 'jsonArrayWriter',
+ functionName: 'jsonWriter',
props: {
fileName,
},
diff --git a/packages/web/src/tabs/ImportExportTab.svelte b/packages/web/src/tabs/ImportExportTab.svelte
index 2e065d33f..b7d0a664c 100644
--- a/packages/web/src/tabs/ImportExportTab.svelte
+++ b/packages/web/src/tabs/ImportExportTab.svelte
@@ -1,22 +1,39 @@
+
+
-
-
-
-
+
+
+
+
+
- {#if busy}
-
- {/if}
-
+ {#if busy}
+
+ {/if}
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if busy}
+ Stop
+ {:else}
+ Run
+ {/if}
+ Generate script
+
+
+
diff --git a/packages/web/src/tabs/ShellTab.svelte b/packages/web/src/tabs/ShellTab.svelte
index 05a4eea74..6a8100067 100644
--- a/packages/web/src/tabs/ShellTab.svelte
+++ b/packages/web/src/tabs/ShellTab.svelte
@@ -23,15 +23,6 @@
onClick: () => getCurrentEditor().copyNodeScript(),
});
- registerCommand({
- id: 'shell.openWizard',
- category: 'Shell',
- name: 'Open wizard',
- // testEnabled: () => getCurrentEditor()?.openWizardEnabled(),
- onClick: () => getCurrentEditor().openWizard(),
- });
-
- const configRegex = /\s*\/\/\s*@ImportExportConfigurator\s*\n\s*\/\/\s*(\{[^\n]+\})\n/;
const requireRegex = /\s*(\/\/\s*@require\s+[^\n]+)\n/g;
const initRegex = /([^\n]+\/\/\s*@init)/g;
@@ -47,8 +38,6 @@
import { registerFileCommands } from '../commands/stdCommands';
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
- import ImportExportModal from '../modals/ImportExportModal.svelte';
- import { showModal } from '../modals/modalTools';
import AceEditor from '../query/AceEditor.svelte';
import RunnerOutputPane from '../query/RunnerOutputPane.svelte';
import useEditorData from '../query/useEditorData';
@@ -60,7 +49,7 @@
import { showSnackbarError } from '../utility/snackbar';
import useEffect from '../utility/useEffect';
import useTimerLabel from '../utility/useTimerLabel';
-
+
export let tabid;
const tabVisible: any = getContext('tabVisible');
@@ -149,19 +138,6 @@
copyTextToClipboard(resp);
}
- // export function openWizardEnabled() {
- // return ($editorValue || '').match(configRegex);
- // }
-
- export function openWizard() {
- const jsonTextMatch = ($editorValue || '').match(configRegex);
- if (jsonTextMatch) {
- showModal(ImportExportModal, { initialValues: JSON.parse(jsonTextMatch[1]) });
- } else {
- showSnackbarError('No wizard info found');
- }
- }
-
function getActiveScript() {
const selectedText = domEditor.getEditor().getSelectedText();
const editorText = $editorValue;
@@ -208,7 +184,6 @@
return [
{ command: 'shell.execute' },
{ command: 'shell.kill' },
- { command: 'shell.openWizard' },
{ divider: true },
{ command: 'shell.toggleComment' },
{ divider: true },
diff --git a/packages/web/src/utility/exportFileTools.ts b/packages/web/src/utility/exportFileTools.ts
index 39ce5d676..4704aefed 100644
--- a/packages/web/src/utility/exportFileTools.ts
+++ b/packages/web/src/utility/exportFileTools.ts
@@ -42,8 +42,8 @@ export async function exportSqlDump(outputFile, connection, databaseName, pureFi
onOpenResult:
pureFileName && !getElectron()
? () => {
- downloadFromApi(`uploads/get?file=${pureFileName}`, 'file.sql');
- }
+ downloadFromApi(`uploads/get?file=${pureFileName}`, 'file.sql');
+ }
: null,
openResultLabel: 'Download SQL file',
});
@@ -226,17 +226,18 @@ export async function downloadFromApi(route: string, donloadName: string) {
method: 'GET',
headers: resolveApiHeaders(),
})
- .then((res) => res.blob())
- .then((blob) => {
+ .then(res => res.blob())
+ .then(blob => {
const objUrl = URL.createObjectURL(blob);
- const a = document.createElement("a");
+ const a = document.createElement('a');
document.body.appendChild(a);
a.download = donloadName;
a.href = objUrl;
a.click();
a.remove();
setTimeout(() => {
- URL.revokeObjectURL(objUrl)
- })
- })
+ URL.revokeObjectURL(objUrl);
+ });
+ });
}
+
diff --git a/packages/web/src/utility/importExportTools.ts b/packages/web/src/utility/importExportTools.ts
new file mode 100644
index 000000000..bed2b20db
--- /dev/null
+++ b/packages/web/src/utility/importExportTools.ts
@@ -0,0 +1,15 @@
+import openNewTab from './openNewTab';
+
+export function openImportExportTab(editorProps, additionalProps = {}) {
+ openNewTab(
+ {
+ tabComponent: 'ImportExportTab',
+ title: 'Import/Export',
+ icon: 'img export',
+ props: additionalProps,
+ },
+ {
+ editor: editorProps,
+ }
+ );
+}
diff --git a/packages/web/src/utility/openElectronFile.ts b/packages/web/src/utility/openElectronFile.ts
index a7f1e4fef..c4e0b09d0 100644
--- a/packages/web/src/utility/openElectronFile.ts
+++ b/packages/web/src/utility/openElectronFile.ts
@@ -1,17 +1,17 @@
import { showModal } from '../modals/modalTools';
import { get } from 'svelte/store';
import newQuery from '../query/newQuery';
-import ImportExportModal from '../modals/ImportExportModal.svelte';
import getElectron from './getElectron';
import { currentDatabase, extensions, getCurrentDatabase } from '../stores';
import { getUploadListener } from './uploadFiles';
-import {getConnectionLabel, getDatabaseFileLabel } from 'dbgate-tools';
+import { getConnectionLabel, getDatabaseFileLabel } from 'dbgate-tools';
import { apiCall } from './api';
import openNewTab from './openNewTab';
import { openJsonDocument } from '../tabs/JsonTab.svelte';
import { SAVED_FILE_HANDLERS } from '../appobj/SavedFileAppObject.svelte';
import _ from 'lodash';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
+import { openImportExportTab } from './importExportTools';
export function canOpenByElectron(file, extensions) {
if (!file) return false;
@@ -178,17 +178,30 @@ export function openElectronFileCore(filePath, extensions) {
shortName: parsed.name,
});
} else {
- showModal(ImportExportModal, {
- openedFile: {
- filePath,
- storageType: format.storageType,
- shortName: parsed.name,
- },
- importToCurrentTarget: true,
- initialValues: {
+ openImportExportTab(
+ {
sourceStorageType: format.storageType,
},
- });
+ {
+ openedFile: {
+ filePath,
+ storageType: format.storageType,
+ shortName: parsed.name,
+ },
+ }
+ );
+
+ // showModal(ImportExportModal, {
+ // openedFile: {
+ // filePath,
+ // storageType: format.storageType,
+ // shortName: parsed.name,
+ // },
+ // importToCurrentTarget: true,
+ // initialValues: {
+ // sourceStorageType: format.storageType,
+ // },
+ // });
}
}
}
diff --git a/packages/web/src/utility/uploadFiles.ts b/packages/web/src/utility/uploadFiles.ts
index 88d6a8d89..ddbcfa503 100644
--- a/packages/web/src/utility/uploadFiles.ts
+++ b/packages/web/src/utility/uploadFiles.ts
@@ -5,9 +5,9 @@ import getElectron from './getElectron';
import resolveApi, { resolveApiHeaders } from './resolveApi';
import { findFileFormat } from '../plugins/fileformats';
import { showModal } from '../modals/modalTools';
-import ImportExportModal from '../modals/ImportExportModal.svelte';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import openNewTab from './openNewTab';
+import { openImportExportTab } from './importExportTools';
let uploadListener;
@@ -79,13 +79,23 @@ export default function uploadFiles(files) {
uploadListener(fileData);
} else {
if (findFileFormat(ext, fileData.storageType)) {
- showModal(ImportExportModal, {
- uploadedFile: fileData,
- importToCurrentTarget: true,
- initialValues: {
+ openImportExportTab(
+ {
sourceStorageType: fileData.storageType,
},
- });
+ {
+ uploadedFile: fileData,
+ importToCurrentTarget: true,
+ }
+ );
+
+ // showModal(ImportExportModal, {
+ // uploadedFile: fileData,
+ // importToCurrentTarget: true,
+ // initialValues: {
+ // sourceStorageType: fileData.storageType,
+ // },
+ // });
}
}
diff --git a/packages/web/src/widgets/SavedFilesList.svelte b/packages/web/src/widgets/SavedFilesList.svelte
index 9d7989039..44d8da920 100644
--- a/packages/web/src/widgets/SavedFilesList.svelte
+++ b/packages/web/src/widgets/SavedFilesList.svelte
@@ -20,6 +20,7 @@
const queryFiles = useFiles({ folder: 'query' });
const sqliteFiles = useFiles({ folder: 'sqlite' });
const diagramFiles = useFiles({ folder: 'diagrams' });
+ const jobFiles = useFiles({ folder: 'jobs' });
const perspectiveFiles = useFiles({ folder: 'perspectives' });
$: files = [
@@ -31,11 +32,12 @@
...($sqliteFiles || []),
...($diagramFiles || []),
...($perspectiveFiles || []),
+ ...($jobFiles || []),
];
function handleRefreshFiles() {
apiCall('files/refresh', {
- folders: ['sql', 'shell', 'markdown', 'charts', 'query', 'sqlite', 'diagrams', 'perspectives'],
+ folders: ['sql', 'shell', 'markdown', 'charts', 'query', 'sqlite', 'diagrams', 'perspectives', 'jobs'],
});
}
diff --git a/plugins/dbgate-plugin-csv/package.json b/plugins/dbgate-plugin-csv/package.json
index 08fefed64..72bc027a3 100644
--- a/plugins/dbgate-plugin-csv/package.json
+++ b/plugins/dbgate-plugin-csv/package.json
@@ -32,10 +32,11 @@
"prepublishOnly": "yarn build"
},
"devDependencies": {
- "csv": "^5.3.2",
+ "csv": "^6.3.10",
"dbgate-plugin-tools": "^1.0.7",
+ "line-reader": "^0.4.0",
"lodash": "^4.17.21",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
}
-}
\ No newline at end of file
+}
diff --git a/plugins/dbgate-plugin-csv/src/backend/reader.js b/plugins/dbgate-plugin-csv/src/backend/reader.js
index 6c07518f5..53d0ad335 100644
--- a/plugins/dbgate-plugin-csv/src/backend/reader.js
+++ b/plugins/dbgate-plugin-csv/src/backend/reader.js
@@ -2,8 +2,32 @@ const zipObject = require('lodash/zipObject');
const csv = require('csv');
const fs = require('fs');
const stream = require('stream');
+const lineReader = require('line-reader');
let dbgateApi;
+
+function readFirstLine(file) {
+ return new Promise((resolve, reject) => {
+ lineReader.open(file, (err, reader) => {
+ if (err) {
+ reject(err);
+ return;
+ }
+ if (reader.hasNextLine()) {
+ reader.nextLine((err, line) => {
+ if (err) {
+ reader.close(() => reject(err)); // Ensure reader is closed on error
+ return;
+ }
+ reader.close(() => resolve(line)); // Ensure reader is closed after reading
+ });
+ } else {
+ reader.close(() => resolve(null)); // Properly close if no lines are present
+ }
+ });
+ });
+}
+
class CsvPrepareStream extends stream.Transform {
constructor({ header }) {
super({ objectMode: true });
@@ -46,13 +70,29 @@ class CsvPrepareStream extends stream.Transform {
async function reader({ fileName, encoding = 'utf-8', header = true, delimiter, limitRows = undefined }) {
console.log(`Reading file ${fileName}`);
+ const downloadedFile = await dbgateApi.download(fileName);
+
+ if (!delimiter) {
+ // auto detect delimiter
+ // read first line from downloadedFile
+ const firstLine = await readFirstLine(downloadedFile);
+ if (firstLine) {
+ const delimiterCounts = {
+ ',': firstLine.replace(/[^,]/g, '').length,
+ ';': firstLine.replace(/[^;]/g, '').length,
+ '|': firstLine.replace(/[^|]/g, '').length,
+ };
+
+ delimiter = Object.keys(delimiterCounts).reduce((a, b) => (delimiterCounts[a] > delimiterCounts[b] ? a : b), ',');
+ }
+ }
const csvStream = csv.parse({
// @ts-ignore
delimiter,
skip_lines_with_error: true,
to_line: limitRows ? limitRows + 1 : undefined,
+ ltrim: true,
});
- const downloadedFile = await dbgateApi.download(fileName);
const fileStream = fs.createReadStream(downloadedFile, encoding);
const csvPrepare = new CsvPrepareStream({ header });
fileStream.pipe(csvStream);
diff --git a/plugins/dbgate-plugin-csv/src/frontend/index.js b/plugins/dbgate-plugin-csv/src/frontend/index.js
index 54ae04114..ef43d8ded 100644
--- a/plugins/dbgate-plugin-csv/src/frontend/index.js
+++ b/plugins/dbgate-plugin-csv/src/frontend/index.js
@@ -17,6 +17,7 @@ const fileFormat = {
name: 'delimiter',
label: 'Delimiter',
options: [
+ { name: 'Auto-detect', value: '' },
{ name: 'Comma (,)', value: ',' },
{ name: 'Semicolon (;)', value: ';' },
{ name: 'Tab', value: '\t' },
diff --git a/yarn.lock b/yarn.lock
index 97bc9f2ef..66c1a2057 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3116,30 +3116,30 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
-csv-generate@^3.4.3:
- version "3.4.3"
- resolved "https://registry.yarnpkg.com/csv-generate/-/csv-generate-3.4.3.tgz#bc42d943b45aea52afa896874291da4b9108ffff"
- integrity sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==
+csv-generate@^4.4.1:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/csv-generate/-/csv-generate-4.4.1.tgz#729781ace8d1b92f6bfb407d1ab9548728c55681"
+ integrity sha512-O/einO0v4zPmXaOV+sYqGa02VkST4GP5GLpWBNHEouIU7pF3kpGf3D0kCCvX82ydIY4EKkOK+R8b1BYsRXravg==
-csv-parse@^4.16.3:
- version "4.16.3"
- resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-4.16.3.tgz#7ca624d517212ebc520a36873c3478fa66efbaf7"
- integrity sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==
+csv-parse@^5.5.6:
+ version "5.5.6"
+ resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.6.tgz#0d726d58a60416361358eec291a9f93abe0b6b1a"
+ integrity sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==
-csv-stringify@^5.6.5:
- version "5.6.5"
- resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-5.6.5.tgz#c6d74badda4b49a79bf4e72f91cce1e33b94de00"
- integrity sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==
+csv-stringify@^6.5.1:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/csv-stringify/-/csv-stringify-6.5.1.tgz#a31837dd35e34787e3c248159c982a21af964d94"
+ integrity sha512-+9lpZfwpLntpTIEpFbwQyWuW/hmI/eHuJZD1XzeZpfZTqkf1fyvBbBLXTJJMsBuuS11uTShMqPwzx4A6ffXgRQ==
-csv@^5.3.2:
- version "5.5.3"
- resolved "https://registry.yarnpkg.com/csv/-/csv-5.5.3.tgz#cd26c1e45eae00ce6a9b7b27dcb94955ec95207d"
- integrity sha512-QTaY0XjjhTQOdguARF0lGKm5/mEq9PD9/VhZZegHDIBq2tQwgNpHc3dneD4mGo2iJs+fTKv5Bp0fZ+BRuY3Z0g==
+csv@^6.3.10:
+ version "6.3.10"
+ resolved "https://registry.yarnpkg.com/csv/-/csv-6.3.10.tgz#960a3a9cef08573ecca2d80ddb71152aca383088"
+ integrity sha512-5NYZG4AN2ZUthmNxIudgBEdMPUnbQHu9V4QTzBPqQzUP3KQsFiJo+8HQ0+oVxj1PomIT1/f67VI1QH/hsrZLKA==
dependencies:
- csv-generate "^3.4.3"
- csv-parse "^4.16.3"
- csv-stringify "^5.6.5"
- stream-transform "^2.1.3"
+ csv-generate "^4.4.1"
+ csv-parse "^5.5.6"
+ csv-stringify "^6.5.1"
+ stream-transform "^3.3.2"
dashdash@^1.12.0:
version "1.14.1"
@@ -7456,11 +7456,6 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
-mixme@^0.5.1:
- version "0.5.10"
- resolved "https://registry.yarnpkg.com/mixme/-/mixme-0.5.10.tgz#d653b2984b75d9018828f1ea333e51717ead5f51"
- integrity sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q==
-
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
@@ -9750,12 +9745,22 @@ stoppable@^1.1.0:
resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"
integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==
-stream-transform@^2.1.3:
- version "2.1.3"
- resolved "https://registry.yarnpkg.com/stream-transform/-/stream-transform-2.1.3.tgz#a1c3ecd72ddbf500aa8d342b0b9df38f5aa598e3"
- integrity sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==
+stream-chain@^2.2.5:
+ version "2.2.5"
+ resolved "https://registry.yarnpkg.com/stream-chain/-/stream-chain-2.2.5.tgz#b30967e8f14ee033c5b9a19bbe8a2cba90ba0d09"
+ integrity sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==
+
+stream-json@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/stream-json/-/stream-json-1.8.0.tgz#53f486b2e3b4496c506131f8d7260ba42def151c"
+ integrity sha512-HZfXngYHUAr1exT4fxlbc1IOce1RYxp2ldeaf97LYCOPSoOqY/1Psp7iGvpb+6JIOgkra9zDYnPX01hGAHzEPw==
dependencies:
- mixme "^0.5.1"
+ stream-chain "^2.2.5"
+
+stream-transform@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/stream-transform/-/stream-transform-3.3.2.tgz#398c67b2f3b6ed5d04ceadde9e412bda8416c8ab"
+ integrity sha512-v64PUnPy9Qw94NGuaEMo+9RHQe4jTBYf+NkTtqkCgeuiNo8NlL0LtLR7fkKWNVFtp3RhIm5Dlxkgm5uz7TDimQ==
streamsearch@^1.1.0:
version "1.1.0"
@@ -10182,6 +10187,11 @@ tmp@^0.0.33:
dependencies:
os-tmpdir "~1.0.2"
+tmp@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae"
+ integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==
+
tmpl@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"