diff --git a/packages/api/package.json b/packages/api/package.json
index 0cb34267..721a664d 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -44,7 +44,6 @@
"line-reader": "^0.4.0",
"lodash": "^4.17.21",
"ncp": "^2.0.0",
- "nedb-promises": "^4.0.1",
"node-cron": "^2.0.3",
"node-ssh-forward": "^0.7.2",
"portfinder": "^1.0.28",
diff --git a/packages/api/src/controllers/apps.js b/packages/api/src/controllers/apps.js
new file mode 100644
index 00000000..c6504874
--- /dev/null
+++ b/packages/api/src/controllers/apps.js
@@ -0,0 +1,264 @@
+const fs = require('fs-extra');
+const _ = require('lodash');
+const path = require('path');
+const { appdir } = require('../utility/directories');
+const socket = require('../utility/socket');
+const connections = require('./connections');
+
+module.exports = {
+ folders_meta: true,
+ async folders() {
+ const folders = await fs.readdir(appdir());
+ return [
+ ...folders.map(name => ({
+ name,
+ })),
+ ];
+ },
+
+ createFolder_meta: true,
+ async createFolder({ folder }) {
+ const name = await this.getNewAppFolder({ name: folder });
+ await fs.mkdir(path.join(appdir(), name));
+ socket.emitChanged('app-folders-changed');
+ return name;
+ },
+
+ files_meta: true,
+ async files({ folder }) {
+ const dir = path.join(appdir(), folder);
+ if (!(await fs.exists(dir))) return [];
+ const files = await fs.readdir(dir);
+
+ function fileType(ext, type) {
+ return files
+ .filter(name => name.endsWith(ext))
+ .map(name => ({
+ name: name.slice(0, -ext.length),
+ label: path.parse(name.slice(0, -ext.length)).base,
+ type,
+ }));
+ }
+
+ return [
+ ...fileType('.command.sql', 'command.sql'),
+ ...fileType('.query.sql', 'query.sql'),
+ ...fileType('.config.json', 'config.json'),
+ ];
+ },
+
+ async emitChangedDbApp(folder) {
+ const used = await this.getUsedAppFolders();
+ if (used.includes(folder)) {
+ socket.emitChanged('used-apps-changed');
+ }
+ },
+
+ refreshFiles_meta: true,
+ async refreshFiles({ folder }) {
+ socket.emitChanged(`app-files-changed-${folder}`);
+ },
+
+ refreshFolders_meta: true,
+ async refreshFolders() {
+ socket.emitChanged(`app-folders-changed`);
+ },
+
+ deleteFile_meta: true,
+ async deleteFile({ folder, file, fileType }) {
+ await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
+ socket.emitChanged(`app-files-changed-${folder}`);
+ this.emitChangedDbApp(folder);
+ },
+
+ renameFile_meta: true,
+ async renameFile({ folder, file, newFile, fileType }) {
+ await fs.rename(
+ path.join(path.join(appdir(), folder), `${file}.${fileType}`),
+ path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
+ );
+ socket.emitChanged(`app-files-changed-${folder}`);
+ this.emitChangedDbApp(folder);
+ },
+
+ renameFolder_meta: true,
+ async renameFolder({ folder, newFolder }) {
+ const uniqueName = await this.getNewAppFolder({ name: newFolder });
+ await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
+ socket.emitChanged(`app-folders-changed`);
+ },
+
+ deleteFolder_meta: true,
+ async deleteFolder({ folder }) {
+ if (!folder) throw new Error('Missing folder parameter');
+ await fs.rmdir(path.join(appdir(), folder), { recursive: true });
+ socket.emitChanged(`app-folders-changed`);
+ },
+
+ async getNewAppFolder({ name }) {
+ if (!(await fs.exists(path.join(appdir(), name)))) return name;
+ let index = 2;
+ while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
+ index += 1;
+ }
+ return `${name}${index}`;
+ },
+
+ getUsedAppFolders_meta: true,
+ async getUsedAppFolders() {
+ const list = await connections.list();
+ const apps = [];
+
+ for (const connection of list) {
+ for (const db of connection.databases || []) {
+ for (const key of _.keys(db || {})) {
+ if (key.startsWith('useApp:') && db[key]) {
+ apps.push(key.substring('useApp:'.length));
+ }
+ }
+ }
+ }
+
+ return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
+ },
+
+ getUsedApps_meta: true,
+ async getUsedApps() {
+ const apps = await this.getUsedAppFolders();
+ const res = [];
+
+ for (const folder of apps) {
+ res.push(await this.loadApp({ folder }));
+ }
+ return res;
+ },
+
+ // getAppsForDb_meta: true,
+ // async getAppsForDb({ conid, database }) {
+ // const connection = await connections.get({ conid });
+ // if (!connection) return [];
+ // const db = (connection.databases || []).find(x => x.name == database);
+ // const apps = [];
+ // const res = [];
+ // if (db) {
+ // for (const key of _.keys(db || {})) {
+ // if (key.startsWith('useApp:') && db[key]) {
+ // apps.push(key.substring('useApp:'.length));
+ // }
+ // }
+ // }
+ // for (const folder of apps) {
+ // res.push(await this.loadApp({ folder }));
+ // }
+ // return res;
+ // },
+
+ loadApp_meta: true,
+ async loadApp({ folder }) {
+ const res = {
+ queries: [],
+ commands: [],
+ name: folder,
+ };
+ const dir = path.join(appdir(), folder);
+ if (await fs.exists(dir)) {
+ const files = await fs.readdir(dir);
+
+ async function processType(ext, field) {
+ for (const file of files) {
+ if (file.endsWith(ext)) {
+ res[field].push({
+ name: file.slice(0, -ext.length),
+ sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
+ });
+ }
+ }
+ }
+
+ await processType('.command.sql', 'commands');
+ await processType('.query.sql', 'queries');
+ }
+
+ try {
+ res.virtualReferences = JSON.parse(
+ await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
+ );
+ } catch (err) {
+ res.virtualReferences = [];
+ }
+ try {
+ res.dictionaryDescriptions = JSON.parse(
+ await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
+ );
+ } catch (err) {
+ res.dictionaryDescriptions = [];
+ }
+
+ return res;
+ },
+
+ async saveConfigFile(appFolder, filename, filterFunc, newItem) {
+ const file = path.join(appdir(), appFolder, filename);
+
+ let json;
+ try {
+ json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
+ } catch (err) {
+ json = [];
+ }
+
+ if (filterFunc) {
+ json = json.filter(filterFunc);
+ }
+
+ json = [...json, newItem];
+
+ await fs.writeFile(file, JSON.stringify(json, undefined, 2));
+
+ socket.emitChanged(`app-files-changed-${appFolder}`);
+ socket.emitChanged('used-apps-changed');
+ },
+
+ saveVirtualReference_meta: true,
+ async saveVirtualReference({ appFolder, schemaName, pureName, refSchemaName, refTableName, columns }) {
+ await this.saveConfigFile(
+ appFolder,
+ 'virtual-references.config.json',
+ columns.length == 1
+ ? x =>
+ !(
+ x.schemaName == schemaName &&
+ x.pureName == pureName &&
+ x.columns.length == 1 &&
+ x.columns[0].columnName == columns[0].columnName
+ )
+ : null,
+ {
+ schemaName,
+ pureName,
+ refSchemaName,
+ refTableName,
+ columns,
+ }
+ );
+ return true;
+ },
+
+ saveDictionaryDescription_meta: true,
+ async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
+ await this.saveConfigFile(
+ appFolder,
+ 'dictionary-descriptions.config.json',
+ x => !(x.schemaName == schemaName && x.pureName == pureName),
+ {
+ schemaName,
+ pureName,
+ expression,
+ columns,
+ delimiter,
+ }
+ );
+
+ return true;
+ },
+};
diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js
index 5ec9873d..ee71c169 100644
--- a/packages/api/src/controllers/connections.js
+++ b/packages/api/src/controllers/connections.js
@@ -1,7 +1,6 @@
const path = require('path');
const { fork } = require('child_process');
const _ = require('lodash');
-const nedb = require('nedb-promises');
const fs = require('fs-extra');
const { datadir, filesdir } = require('../utility/directories');
@@ -9,6 +8,7 @@ const socket = require('../utility/socket');
const { encryptConnection } = require('../utility/crypting');
const { handleProcessCommunication } = require('../utility/processComm');
const { pickSafeConnectionInfo } = require('../utility/crypting');
+const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
const processArgs = require('../utility/processArgs');
@@ -136,7 +136,7 @@ module.exports = {
const dir = datadir();
if (!portalConnections) {
// @ts-ignore
- this.datastore = nedb.create(path.join(dir, 'connections.jsonl'));
+ this.datastore = new JsonLinesDatabase(path.join(dir, 'connections.jsonl'));
}
},
@@ -173,18 +173,22 @@ module.exports = {
let res;
const encrypted = encryptConnection(connection);
if (connection._id) {
- res = await this.datastore.update(_.pick(connection, '_id'), encrypted);
+ res = await this.datastore.update(encrypted);
} else {
res = await this.datastore.insert(encrypted);
}
socket.emitChanged('connection-list-changed');
+ socket.emitChanged('used-apps-changed');
+ // for (const db of connection.databases || []) {
+ // socket.emitChanged(`db-apps-changed-${connection._id}-${db.name}`);
+ // }
return res;
},
update_meta: true,
async update({ _id, values }) {
if (portalConnections) return;
- const res = await this.datastore.update({ _id }, { $set: values });
+ const res = await this.datastore.patch(_id, values);
socket.emitChanged('connection-list-changed');
return res;
},
@@ -192,22 +196,24 @@ module.exports = {
updateDatabase_meta: true,
async updateDatabase({ conid, database, values }) {
if (portalConnections) return;
- const conn = await this.datastore.find({ _id: conid });
- let databases = conn[0].databases || [];
+ const conn = await this.datastore.get(conid);
+ let databases = (conn && conn.databases) || [];
if (databases.find(x => x.name == database)) {
databases = databases.map(x => (x.name == database ? { ...x, ...values } : x));
} else {
databases = [...databases, { name: database, ...values }];
}
- const res = await this.datastore.update({ _id: conid }, { $set: { databases } });
+ const res = await this.datastore.patch(conid, { databases });
socket.emitChanged('connection-list-changed');
+ socket.emitChanged('used-apps-changed');
+ // socket.emitChanged(`db-apps-changed-${conid}-${database}`);
return res;
},
delete_meta: true,
async delete(connection) {
if (portalConnections) return;
- const res = await this.datastore.remove(_.pick(connection, '_id'));
+ const res = await this.datastore.remove(connection._id);
socket.emitChanged('connection-list-changed');
return res;
},
@@ -215,8 +221,8 @@ module.exports = {
get_meta: true,
async get({ conid }) {
if (portalConnections) return portalConnections.find(x => x._id == conid) || null;
- const res = await this.datastore.find({ _id: conid });
- return res[0] || null;
+ const res = await this.datastore.get(conid);
+ return res || null;
},
newSqliteDatabase_meta: true,
diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js
index 78a29778..14ea526a 100644
--- a/packages/api/src/controllers/files.js
+++ b/packages/api/src/controllers/files.js
@@ -1,12 +1,13 @@
const uuidv1 = require('uuid/v1');
const fs = require('fs-extra');
const path = require('path');
-const { filesdir, archivedir, resolveArchiveFolder, uploadsdir } = require('../utility/directories');
+const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir } = require('../utility/directories');
const getChartExport = require('../utility/getChartExport');
const hasPermission = require('../utility/hasPermission');
const socket = require('../utility/socket');
const scheduler = require('./scheduler');
const getDiagramExport = require('../utility/getDiagramExport');
+const apps = require('./apps');
function serialize(format, data) {
if (format == 'text') return data;
@@ -74,6 +75,11 @@ module.exports = {
encoding: 'utf-8',
});
return deserialize(format, text);
+ } else if (folder.startsWith('app:')) {
+ const text = await fs.readFile(path.join(appdir(), folder.substring('app:'.length), file), {
+ encoding: 'utf-8',
+ });
+ return deserialize(format, text);
} else {
if (!hasPermission(`files/${folder}/read`)) return null;
const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' });
@@ -88,6 +94,12 @@ module.exports = {
await fs.writeFile(path.join(dir, file), serialize(format, data));
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
return true;
+ } else if (folder.startsWith('app:')) {
+ const app = folder.substring('app:'.length);
+ await fs.writeFile(path.join(appdir(), app, file), serialize(format, data));
+ socket.emitChanged(`app-files-changed-${app}`);
+ apps.emitChangedDbApp(folder);
+ return true;
} else {
if (!hasPermission(`files/${folder}/write`)) return false;
const dir = path.join(filesdir(), folder);
diff --git a/packages/api/src/main.js b/packages/api/src/main.js
index 5cb5ec46..2ad5588d 100644
--- a/packages/api/src/main.js
+++ b/packages/api/src/main.js
@@ -19,6 +19,7 @@ const runners = require('./controllers/runners');
const jsldata = require('./controllers/jsldata');
const config = require('./controllers/config');
const archive = require('./controllers/archive');
+const apps = require('./controllers/apps');
const uploads = require('./controllers/uploads');
const plugins = require('./controllers/plugins');
const files = require('./controllers/files');
@@ -157,6 +158,7 @@ function useAllControllers(app, electron) {
useController(app, electron, '/files', files);
useController(app, electron, '/scheduler', scheduler);
useController(app, electron, '/query-history', queryHistory);
+ useController(app, electron, '/apps', apps);
}
function initializeElectronSender(electronSender) {
diff --git a/packages/api/src/utility/JsonLinesDatabase.js b/packages/api/src/utility/JsonLinesDatabase.js
new file mode 100644
index 00000000..9f4243bb
--- /dev/null
+++ b/packages/api/src/utility/JsonLinesDatabase.js
@@ -0,0 +1,142 @@
+const AsyncLock = require('async-lock');
+const fs = require('fs-extra');
+const uuidv1 = require('uuid/v1');
+
+const lock = new AsyncLock();
+
+// const lineReader = require('line-reader');
+// const { fetchNextLineFromReader } = require('./JsonLinesDatastore');
+
+class JsonLinesDatabase {
+ constructor(filename) {
+ this.filename = filename;
+ this.data = [];
+ this.loadedOk = false;
+ this.loadPerformed = false;
+ }
+
+ async _save() {
+ if (!this.loadedOk) {
+ // don't override data
+ return;
+ }
+ await fs.writeFile(this.filename, this.data.map(x => JSON.stringify(x)).join('\n'));
+ }
+
+ async _ensureLoaded() {
+ if (!this.loadPerformed) {
+ await lock.acquire('reader', async () => {
+ if (!this.loadPerformed) {
+ if (!(await fs.exists(this.filename))) {
+ this.loadedOk = true;
+ this.loadPerformed = true;
+ return;
+ }
+ try {
+ const text = await fs.readFile(this.filename, { encoding: 'utf-8' });
+ this.data = text
+ .split('\n')
+ .filter(x => x.trim())
+ .map(x => JSON.parse(x));
+ this.loadedOk = true;
+ } catch (err) {
+ console.error(`Error loading file ${this.filename}`, err);
+ }
+ this.loadPerformed = true;
+ }
+ });
+ }
+ }
+
+ async insert(obj) {
+ await this._ensureLoaded();
+ if (obj._id && (await this.get(obj._id))) {
+ throw new Error(`Cannot insert duplicate ID ${obj._id} into ${this.filename}`);
+ }
+ const elem = obj._id
+ ? obj
+ : {
+ ...obj,
+ _id: uuidv1(),
+ };
+ this.data.push(elem);
+ await this._save();
+ return elem;
+ }
+
+ async get(id) {
+ await this._ensureLoaded();
+ return this.data.find(x => x._id == id);
+ }
+
+ async find(cond) {
+ await this._ensureLoaded();
+ if (cond) {
+ return this.data.filter(x => {
+ for (const key of Object.keys(cond)) {
+ if (x[key] != cond[key]) return false;
+ }
+ return true;
+ });
+ } else {
+ return this.data;
+ }
+ }
+
+ async update(obj) {
+ await this._ensureLoaded();
+ this.data = this.data.map(x => (x._id == obj._id ? obj : x));
+ await this._save();
+ return obj;
+ }
+
+ async patch(id, values) {
+ await this._ensureLoaded();
+ this.data = this.data.map(x => (x._id == id ? { ...x, ...values } : x));
+ await this._save();
+ return this.data.find(x => x._id == id);
+ }
+
+ async remove(id) {
+ await this._ensureLoaded();
+ const removed = this.data.find(x => x._id == id);
+ this.data = this.data.filter(x => x._id != id);
+ await this._save();
+ return removed;
+ }
+
+ // async _openReader() {
+ // return new Promise((resolve, reject) =>
+ // lineReader.open(this.filename, (err, reader) => {
+ // if (err) reject(err);
+ // resolve(reader);
+ // })
+ // );
+ // }
+
+ // async _read() {
+ // this.data = [];
+ // if (!(await fs.exists(this.filename))) return;
+ // try {
+ // const reader = await this._openReader();
+ // for (;;) {
+ // const line = await fetchNextLineFromReader(reader);
+ // if (!line) break;
+ // this.data.push(JSON.parse(line));
+ // }
+ // } catch (err) {
+ // console.error(`Error loading file ${this.filename}`, err);
+ // }
+ // }
+
+ // async _write() {
+ // const fw = fs.createWriteStream(this.filename);
+ // for (const obj of this.data) {
+ // await fw.write(JSON.stringify(obj));
+ // await fw.write('\n');
+ // }
+ // await fw.end();
+ // }
+}
+
+module.exports = JsonLinesDatabase;
diff --git a/packages/api/src/utility/JsonLinesDatastore.js b/packages/api/src/utility/JsonLinesDatastore.js
index b9965ae8..37a7220f 100644
--- a/packages/api/src/utility/JsonLinesDatastore.js
+++ b/packages/api/src/utility/JsonLinesDatastore.js
@@ -4,7 +4,7 @@ const lock = new AsyncLock();
const stableStringify = require('json-stable-stringify');
const { evaluateCondition } = require('dbgate-sqltree');
-async function fetchNextLine(reader) {
+function fetchNextLineFromReader(reader) {
return new Promise((resolve, reject) => {
if (!reader.hasNextLine()) {
resolve(null);
@@ -62,7 +62,7 @@ class JsonLinesDatastore {
async _readLine(parse) {
for (;;) {
- const line = await fetchNextLine(this.reader);
+ const line = await fetchNextLineFromReader(this.reader);
if (!line) {
// EOF
return null;
diff --git a/packages/api/src/utility/directories.js b/packages/api/src/utility/directories.js
index 9af898e1..35b5c887 100644
--- a/packages/api/src/utility/directories.js
+++ b/packages/api/src/utility/directories.js
@@ -38,6 +38,7 @@ const rundir = dirFunc('run', true);
const uploadsdir = dirFunc('uploads', true);
const pluginsdir = dirFunc('plugins');
const archivedir = dirFunc('archive');
+const appdir = dirFunc('apps');
const filesdir = dirFunc('files');
function packagedPluginsDir() {
@@ -103,6 +104,7 @@ module.exports = {
rundir,
uploadsdir,
archivedir,
+ appdir,
ensureDirectory,
pluginsdir,
filesdir,
diff --git a/packages/datalib/src/GridDisplay.ts b/packages/datalib/src/GridDisplay.ts
index e347af3e..29604143 100644
--- a/packages/datalib/src/GridDisplay.ts
+++ b/packages/datalib/src/GridDisplay.ts
@@ -28,6 +28,7 @@ export interface DisplayColumn {
autoIncrement?: boolean;
isPrimaryKey?: boolean;
foreignKey?: ForeignKeyInfo;
+ isForeignKeyUnique?: boolean;
isExpandable?: boolean;
isChecked?: boolean;
hintColumnNames?: string[];
diff --git a/packages/datalib/src/TableGridDisplay.ts b/packages/datalib/src/TableGridDisplay.ts
index cd8227a2..e9480835 100644
--- a/packages/datalib/src/TableGridDisplay.ts
+++ b/packages/datalib/src/TableGridDisplay.ts
@@ -1,5 +1,5 @@
import _ from 'lodash';
-import { filterName } from 'dbgate-tools';
+import { filterName, isTableColumnUnique } from 'dbgate-tools';
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
import {
TableInfo,
@@ -79,10 +79,11 @@ export class TableGridDisplay extends GridDisplay {
...col,
isChecked: this.isColumnChecked(col),
hintColumnNames:
- this.getFkDictionaryDescription(col.foreignKey)?.columns?.map(
+ this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)?.columns?.map(
columnName => `hint_${col.uniqueName}_${columnName}`
) || null,
- hintColumnDelimiter: this.getFkDictionaryDescription(col.foreignKey)?.delimiter,
+ hintColumnDelimiter: this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)
+ ?.delimiter,
isExpandable: !!col.foreignKey,
})) || []
);
@@ -203,7 +204,8 @@ export class TableGridDisplay extends GridDisplay {
}
getFkTarget(column: DisplayColumn) {
- const { uniqueName, foreignKey } = column;
+ const { uniqueName, foreignKey, isForeignKeyUnique } = column;
+ if (!isForeignKeyUnique) return null;
const pureName = foreignKey.refTableName;
const schemaName = foreignKey.refSchemaName;
return this.findTable({ schemaName, pureName });
@@ -230,7 +232,7 @@ export class TableGridDisplay extends GridDisplay {
const uniquePath = [...parentPath, col.columnName];
const uniqueName = uniquePath.join('.');
// console.log('this.config.addedColumns', this.config.addedColumns, uniquePath);
- return {
+ const res = {
...col,
pureName: table.pureName,
schemaName: table.schemaName,
@@ -241,7 +243,19 @@ export class TableGridDisplay extends GridDisplay {
foreignKey:
table.foreignKeys &&
table.foreignKeys.find(fk => fk.columns.length == 1 && fk.columns[0].columnName == col.columnName),
+ isForeignKeyUnique: false,
};
+
+ if (res.foreignKey) {
+ const refTableInfo = this.dbinfo.tables.find(
+ x => x.schemaName == res.foreignKey.refSchemaName && x.pureName == res.foreignKey.refTableName
+ );
+ if (refTableInfo && isTableColumnUnique(refTableInfo, res.foreignKey.columns[0].refColumnName)) {
+ res.isForeignKeyUnique = true;
+ }
+ }
+
+ return res;
}
addAddedColumnsToSelect(
diff --git a/packages/tools/src/createBulkInsertStreamBase.ts b/packages/tools/src/createBulkInsertStreamBase.ts
index eed503a4..068912c3 100644
--- a/packages/tools/src/createBulkInsertStreamBase.ts
+++ b/packages/tools/src/createBulkInsertStreamBase.ts
@@ -29,18 +29,18 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options):
// console.log('ANALYSING', name, structure);
if (structure && options.dropIfExists) {
console.log(`Dropping table ${fullNameQuoted}`);
- await driver.query(pool, `DROP TABLE ${fullNameQuoted}`);
+ await driver.script(pool, `DROP TABLE ${fullNameQuoted}`);
}
if (options.createIfNotExists && (!structure || options.dropIfExists)) {
console.log(`Creating table ${fullNameQuoted}`);
const dmp = driver.createDumper();
dmp.createTable(prepareTableForImport({ ...writable.structure, ...name }));
console.log(dmp.s);
- await driver.query(pool, dmp.s);
+ await driver.script(pool, dmp.s);
structure = await driver.analyseSingleTable(pool, name);
}
if (options.truncate) {
- await driver.query(pool, `TRUNCATE TABLE ${fullNameQuoted}`);
+ await driver.script(pool, `TRUNCATE TABLE ${fullNameQuoted}`);
}
writable.columnNames = _intersection(
diff --git a/packages/tools/src/structureTools.ts b/packages/tools/src/structureTools.ts
index 8c6d9248..f3d08351 100644
--- a/packages/tools/src/structureTools.ts
+++ b/packages/tools/src/structureTools.ts
@@ -1,4 +1,4 @@
-import { DatabaseInfo, TableInfo } from 'dbgate-types';
+import { DatabaseInfo, TableInfo, ApplicationDefinition } from 'dbgate-types';
import _flatten from 'lodash/flatten';
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
@@ -90,3 +90,31 @@ function fillDatabaseExtendedInfo(db: DatabaseInfo): DatabaseInfo {
export function extendDatabaseInfo(db: DatabaseInfo): DatabaseInfo {
return fillDatabaseExtendedInfo(addTableDependencies(db));
}
+
+export function extendDatabaseInfoFromApps(db: DatabaseInfo, apps: ApplicationDefinition[]): DatabaseInfo {
+ if (!db || !apps) return db;
+ const dbExt = {
+ ...db,
+ tables: db.tables.map(table => ({
+ ...table,
+ foreignKeys: [
+ ...(table.foreignKeys || []),
+ ..._flatten(apps.map(app => app.virtualReferences || []))
+ .filter(fk => fk.pureName == table.pureName && fk.schemaName == table.schemaName)
+ .map(fk => ({ ...fk, constraintType: 'foreignKey', isVirtual: true })),
+ ],
+ })),
+ } as DatabaseInfo;
+ return addTableDependencies(dbExt);
+}
+
+export function isTableColumnUnique(table: TableInfo, column: string) {
+ if (table.primaryKey && table.primaryKey.columns.length == 1 && table.primaryKey.columns[0].columnName == column) {
+ return true;
+ }
+ const uqs = [...(table.uniques || []), ...(table.indexes || []).filter(x => x.isUnique)];
+ if (uqs.find(uq => uq.columns.length == 1 && uq.columns[0].columnName == column)) {
+ return true;
+ }
+ return false;
+}
diff --git a/packages/tools/src/tableTransforms.ts b/packages/tools/src/tableTransforms.ts
index 994161b7..403426cf 100644
--- a/packages/tools/src/tableTransforms.ts
+++ b/packages/tools/src/tableTransforms.ts
@@ -4,6 +4,9 @@ import _cloneDeep from 'lodash/cloneDeep';
export function prepareTableForImport(table: TableInfo): TableInfo {
const res = _cloneDeep(table);
res.foreignKeys = [];
+ res.indexes = [];
+ res.uniques = [];
+ res.checks = [];
if (res.primaryKey) res.primaryKey.constraintName = null;
return res;
}
diff --git a/packages/types/appdefs.d.ts b/packages/types/appdefs.d.ts
new file mode 100644
index 00000000..b9826beb
--- /dev/null
+++ b/packages/types/appdefs.d.ts
@@ -0,0 +1,37 @@
+interface ApplicationCommand {
+ name: string;
+ sql: string;
+}
+
+interface ApplicationQuery {
+ name: string;
+ sql: string;
+}
+
+interface VirtualReferenceDefinition {
+ pureName: string;
+ schemaName?: string;
+ refSchemaName?: string;
+ refTableName: string;
+ columns: {
+ columnName: string;
+ refColumnName: string;
+ }[];
+}
+
+interface DictionaryDescriptionDefinition {
+ pureName: string;
+ schemaName?: string;
+ expression: string;
+ columns: string[];
+ delimiter: string;
+}
+
+export interface ApplicationDefinition {
+ name: string;
+
+ queries: ApplicationQuery[];
+ commands: ApplicationCommand[];
+ virtualReferences: VirtualReferenceDefinition[];
+ dictionaryDescriptions: DictionaryDescriptionDefinition[];
+}
diff --git a/packages/types/extensions.d.ts b/packages/types/extensions.d.ts
index fca442fe..a60dcff7 100644
--- a/packages/types/extensions.d.ts
+++ b/packages/types/extensions.d.ts
@@ -22,9 +22,10 @@ export interface FileFormatDefinition {
}
export interface ThemeDefinition {
- className: string;
+ themeClassName: string;
themeName: string;
themeType: 'light' | 'dark';
+ themeCss?: string;
}
export interface PluginDefinition {
diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts
index aed04c88..e7f8ad0a 100644
--- a/packages/types/index.d.ts
+++ b/packages/types/index.d.ts
@@ -43,3 +43,4 @@ export * from './dumper';
export * from './dbtypes';
export * from './extensions';
export * from './alter-processor';
+export * from './appdefs';
diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte
index b77d962c..a0527321 100644
--- a/packages/web/src/App.svelte
+++ b/packages/web/src/App.svelte
@@ -15,6 +15,7 @@
import { subscribeConnectionPingers } from './utility/connectionsPinger';
import { subscribePermissionCompiler } from './utility/hasPermission';
import { apiCall } from './utility/api';
+ import { getUsedApps } from './utility/metadataLoaders';
let loadedApi = false;
@@ -30,7 +31,8 @@
const settings = await apiCall('config/get-settings');
const connections = await apiCall('connections/list');
const config = await apiCall('config/get');
- loadedApi = settings && connections && config;
+ const apps = await getUsedApps();
+ loadedApi = settings && connections && config && apps;
if (loadedApi) {
subscribeApiDependendStores();
diff --git a/packages/web/src/Screen.svelte b/packages/web/src/Screen.svelte
index 8d2e919d..a05fbbd8 100644
--- a/packages/web/src/Screen.svelte
+++ b/packages/web/src/Screen.svelte
@@ -26,6 +26,12 @@
$: currentThemeType = $currentThemeDefinition?.themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light';
+
+ {#if $currentThemeDefinition?.themeCss}
+ {@html ``}
+ {/if}
+
+
+ async function openTextFile(fileName, fileType, folderName, tabComponent, icon) {
+ const connProps: any = {};
+ let tooltip = undefined;
+
+ const resp = await apiCall('files/load', {
+ folder: 'app:' + folderName,
+ file: fileName + '.' + fileType,
+ format: 'text',
+ });
+
+ openNewTab(
+ {
+ title: fileName,
+ icon,
+ tabComponent,
+ tooltip,
+ props: {
+ savedFile:fileName + '.' + fileType,
+ savedFolder: 'app:' + folderName,
+ savedFormat: 'text',
+ appFolder: folderName,
+ ...connProps,
+ },
+ },
+ { editor: resp }
+ );
+ }
+
+ export const extractKey = data => data.fileName;
+ export const createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
+ const APP_ICONS = {
+ 'config.json': 'img json',
+ 'command.sql': 'img app-command',
+ 'query.sql': 'img app-query',
+ };
+
+ function getAppIcon(data) {
+ return APP_ICONS[data.fileType];
+ }
+
+
+
+
+
diff --git a/packages/web/src/appobj/AppFolderAppObject.svelte b/packages/web/src/appobj/AppFolderAppObject.svelte
new file mode 100644
index 00000000..a04f1976
--- /dev/null
+++ b/packages/web/src/appobj/AppFolderAppObject.svelte
@@ -0,0 +1,96 @@
+
+
+
+
+
($currentApplication = data.name)}
+ menu={createMenu}
+/>
diff --git a/packages/web/src/appobj/ArchiveFileAppObject.svelte b/packages/web/src/appobj/ArchiveFileAppObject.svelte
index 83e24c4a..bc2b3cbf 100644
--- a/packages/web/src/appobj/ArchiveFileAppObject.svelte
+++ b/packages/web/src/appobj/ArchiveFileAppObject.svelte
@@ -81,7 +81,7 @@
markArchiveFileAsDataSheet,
markArchiveFileAsReadonly,
} from '../utility/archiveTools';
-import { apiCall } from '../utility/api';
+ import { apiCall } from '../utility/api';
export let data;
diff --git a/packages/web/src/appobj/ConnectionAppObject.svelte b/packages/web/src/appobj/ConnectionAppObject.svelte
index afc8e0ab..d7577220 100644
--- a/packages/web/src/appobj/ConnectionAppObject.svelte
+++ b/packages/web/src/appobj/ConnectionAppObject.svelte
@@ -26,7 +26,7 @@
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
import getElectron from '../utility/getElectron';
import getConnectionLabel from '../utility/getConnectionLabel';
- import { getDatabaseList } from '../utility/metadataLoaders';
+ import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
import { getLocalStorage } from '../utility/storageCache';
import { apiCall } from '../utility/api';
@@ -97,7 +97,7 @@
value: 'newdb',
label: 'Database name',
onConfirm: name =>
- apiCall('server-connections/create-database', {
+ apiCall('server-connections/create-database', {
conid: data._id,
name,
}),
@@ -153,7 +153,7 @@
],
data.singleDatabase && [
{ divider: true },
- getDatabaseMenuItems(data, data.defaultDatabase, $extensions, $currentDatabase),
+ getDatabaseMenuItems(data, data.defaultDatabase, $extensions, $currentDatabase, $apps),
],
];
};
@@ -186,6 +186,8 @@
statusTitle = null;
}
}
+
+ $: apps = useUsedApps();
export const extractKey = props => props.name;
- export function getDatabaseMenuItems(connection, name, $extensions, $currentDatabase) {
+ export function getDatabaseMenuItems(connection, name, $extensions, $currentDatabase, $apps) {
+ const apps = filterAppsForDatabase(connection, name, $apps);
const handleNewQuery = () => {
const tooltip = `${getConnectionLabel(connection)}\n${name}`;
openNewTab({
@@ -157,8 +158,20 @@
openJsonDocument(db, name);
};
+ async function handleConfirmSql(sql) {
+ const resp = await apiCall('database-connections/run-script', { conid: connection._id, database: name, sql });
+ const { errorMessage } = resp || {};
+ if (errorMessage) {
+ showModal(ErrorMessageModal, { title: 'Error when executing script', message: errorMessage });
+ } else {
+ showSnackbarSuccess('Saved to database');
+ }
+ }
+
const driver = findEngineDriver(connection, getExtensions());
+ const commands = _.flatten((apps || []).map(x => x.commands || []));
+
return [
{ onClick: handleNewQuery, text: 'New query', isNewQuery: true },
!driver?.dialect?.nosql && { onClick: handleNewTable, text: 'New table' },
@@ -180,6 +193,20 @@
_.get($currentDatabase, 'connection._id') == _.get(connection, '_id') &&
_.get($currentDatabase, 'name') == name && { onClick: handleDisconnect, text: 'Disconnect' },
+
+ commands.length > 0 && [
+ { divider: true },
+ commands.map((cmd: any) => ({
+ text: cmd.name,
+ onClick: () => {
+ showModal(ConfirmSqlModal, {
+ sql: cmd.sql,
+ onConfirm: () => handleConfirmSql(cmd.sql),
+ engine: driver.engine,
+ });
+ },
+ })),
+ ],
];
}
@@ -207,18 +234,22 @@
import { showSnackbarSuccess } from '../utility/snackbar';
import { findEngineDriver } from 'dbgate-tools';
import InputTextModal from '../modals/InputTextModal.svelte';
- import { getDatabaseInfo } from '../utility/metadataLoaders';
+ import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
import { openJsonDocument } from '../tabs/JsonTab.svelte';
import { apiCall } from '../utility/api';
+ import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
+ import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
+ import { filterAppsForDatabase } from '../utility/appTools';
export let data;
export let passProps;
function createMenu() {
- return getDatabaseMenuItems(data.connection, data.name, $extensions, $currentDatabase);
+ return getDatabaseMenuItems(data.connection, data.name, $extensions, $currentDatabase, $apps);
}
$: isPinned = !!$pinnedDatabases.find(x => x.name == data.name && x.connection?._id == data.connection?._id);
+ $: apps = useUsedApps();
currentTheme.set(theme.className),
+ onClick: () => currentTheme.set(theme.themeClassName),
// onPreview: () => {
// const old = get(currentTheme);
// currentTheme.set(css);
@@ -128,6 +128,23 @@ registerCommand({
},
});
+registerCommand({
+ id: 'new.application',
+ category: 'New',
+ icon: 'img app',
+ name: 'Application',
+ onClick: () => {
+ showModal(InputTextModal, {
+ value: '',
+ label: 'New application name',
+ header: 'Create application',
+ onConfirm: async folder => {
+ apiCall('apps/create-folder', { folder });
+ },
+ });
+ },
+});
+
registerCommand({
id: 'new.table',
category: 'New',
diff --git a/packages/web/src/datagrid/ColumnHeaderControl.svelte b/packages/web/src/datagrid/ColumnHeaderControl.svelte
index 762ed303..17e142a4 100644
--- a/packages/web/src/datagrid/ColumnHeaderControl.svelte
+++ b/packages/web/src/datagrid/ColumnHeaderControl.svelte
@@ -7,6 +7,8 @@
import { isTypeDateTime } from 'dbgate-tools';
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject.svelte';
import { copyTextToClipboard } from '../utility/clipboard';
+ import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
+ import { showModal } from '../modals/modalTools';
export let column;
export let conid = undefined;
@@ -14,6 +16,7 @@
export let setSort;
export let grouping = undefined;
export let order = undefined;
+ export let allowDefineVirtualReferences = false;
export let setGrouping;
const openReferencedTable = () => {
@@ -26,6 +29,16 @@
});
};
+ const handleDefineVirtualForeignKey = () => {
+ showModal(VirtualForeignKeyEditorModal, {
+ schemaName: column.schemaName,
+ pureName: column.pureName,
+ conid,
+ database,
+ columnName: column.columnName,
+ });
+ };
+
function getMenu() {
return [
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
@@ -49,6 +62,11 @@
{ onClick: () => setGrouping('GROUP:MONTH'), text: 'Group by MONTH' },
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
],
+
+ allowDefineVirtualReferences && [
+ { divider: true },
+ { onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
+ ],
];
}
diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte
index 6ef9b98f..e3f6175f 100644
--- a/packages/web/src/datagrid/DataGridCore.svelte
+++ b/packages/web/src/datagrid/DataGridCore.svelte
@@ -303,6 +303,7 @@
export let errorMessage = undefined;
export let pureName = undefined;
export let schemaName = undefined;
+ export let allowDefineVirtualReferences = false;
export let isLoadedAll;
export let loadedTime;
@@ -1425,6 +1426,7 @@
}}
setGrouping={display.groupable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null}
grouping={display.getGrouping(col.uniqueName)}
+ {allowDefineVirtualReferences}
/>
{/each}
diff --git a/packages/web/src/datagrid/TableDataGrid.svelte b/packages/web/src/datagrid/TableDataGrid.svelte
index 2dc81a4f..e2e9331c 100644
--- a/packages/web/src/datagrid/TableDataGrid.svelte
+++ b/packages/web/src/datagrid/TableDataGrid.svelte
@@ -7,7 +7,7 @@
TableGridDisplay,
} from 'dbgate-datalib';
import { getFilterValueExpression } from 'dbgate-filterparser';
- import { findEngineDriver } from 'dbgate-tools';
+ import { extendDatabaseInfoFromApps, findEngineDriver } from 'dbgate-tools';
import _ from 'lodash';
import { writable } from 'svelte/store';
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
@@ -16,9 +16,11 @@
import {
useConnectionInfo,
+ useConnectionList,
useDatabaseInfo,
useDatabaseServerVersion,
useServerVersion,
+ useUsedApps,
} from '../utility/metadataLoaders';
import DataGrid from './DataGrid.svelte';
@@ -46,6 +48,9 @@
$: connection = useConnectionInfo({ conid });
$: dbinfo = useDatabaseInfo({ conid, database });
$: serverVersion = useDatabaseServerVersion({ conid, database });
+ $: apps = useUsedApps();
+ $: extendedDbInfo = extendDatabaseInfoFromApps($dbinfo, $apps);
+ $: connections = useConnectionList();
// $: console.log('serverVersion', $serverVersion);
@@ -64,10 +69,10 @@
setConfig,
cache,
setCache,
- $dbinfo,
+ extendedDbInfo,
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
$serverVersion,
- table => getDictionaryDescription(table, conid, database)
+ table => getDictionaryDescription(table, conid, database, $apps, $connections)
)
: null;
@@ -80,10 +85,10 @@
setConfig,
cache,
setCache,
- $dbinfo,
+ extendedDbInfo,
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
$serverVersion,
- table => getDictionaryDescription(table, conid, database)
+ table => getDictionaryDescription(table, conid, database, $apps, $connections)
)
: null;
@@ -159,6 +164,7 @@
macroCondition={macro => macro.type == 'transformValue'}
onReferenceSourceChanged={reference ? handleReferenceSourceChanged : null}
multipleGridsOnTab={multipleGridsOnTab || !!reference}
+ allowDefineVirtualReferences
onReferenceClick={value => {
if (value && value.referenceId && reference && reference.referenceId == value.referenceId) {
// reference not changed
diff --git a/packages/web/src/designer/Designer.svelte b/packages/web/src/designer/Designer.svelte
index 9c0676a7..f7f13ff4 100644
--- a/packages/web/src/designer/Designer.svelte
+++ b/packages/web/src/designer/Designer.svelte
@@ -29,7 +29,7 @@
import DesignerTable from './DesignerTable.svelte';
import { isConnectedByReference } from './designerTools';
import uuidv1 from 'uuid/v1';
- import { getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
+ import { getTableInfo, useDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
import cleanupDesignColumns from './cleanupDesignColumns';
import _ from 'lodash';
import { writable } from 'svelte/store';
@@ -46,6 +46,7 @@
import { showModal } from '../modals/modalTools';
import ChooseColorModal from '../modals/ChooseColorModal.svelte';
import { currentThemeDefinition } from '../stores';
+ import { extendDatabaseInfoFromApps } from 'dbgate-tools';
export let value;
export let onChange;
@@ -67,10 +68,12 @@
const targetDragColumn$ = writable(null);
const dbInfo = settings?.updateFromDbInfo ? useDatabaseInfo({ conid, database }) : null;
+ $: dbInfoExtended = $dbInfo ? extendDatabaseInfoFromApps($dbInfo, $apps) : null;
$: tables = value?.tables as any[];
$: references = value?.references as any[];
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
+ $: apps = useUsedApps();
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
@@ -94,8 +97,8 @@
}
$: {
- if (dbInfo) {
- updateFromDbInfo($dbInfo);
+ if (dbInfoExtended) {
+ updateFromDbInfo(dbInfoExtended as any);
}
}
@@ -104,13 +107,13 @@
}
$: {
- if (dbInfo && value?.autoLayout) {
- performAutoActions($dbInfo);
+ if (dbInfoExtended && value?.autoLayout) {
+ performAutoActions(dbInfoExtended);
}
}
function updateFromDbInfo(db = 'auto') {
- if (db == 'auto' && dbInfo) db = $dbInfo;
+ if (db == 'auto' && dbInfo) db = dbInfoExtended as any;
if (!settings?.updateFromDbInfo || !db) return;
onChange(current => {
@@ -372,8 +375,8 @@
};
const handleAddTableReferences = async table => {
- if (!dbInfo) return;
- const db = $dbInfo;
+ if (!dbInfoExtended) return;
+ const db = dbInfoExtended;
if (!db) return;
callChange(current => {
return getTablesWithReferences(db, table, current);
@@ -692,13 +695,17 @@
if (css) css += '\n';
css += cssItem;
}
+ if ($currentThemeDefinition?.themeCss) {
+ if (css) css += '\n';
+ css += $currentThemeDefinition?.themeCss;
+ }
saveFileToDisk(async filePath => {
await apiCall('files/export-diagram', {
filePath,
html: domCanvas.outerHTML,
css,
themeType: $currentThemeDefinition?.themeType,
- themeClassName: $currentThemeDefinition?.className,
+ themeClassName: $currentThemeDefinition?.themeClassName,
});
});
}
diff --git a/packages/web/src/designer/DesignerTable.svelte b/packages/web/src/designer/DesignerTable.svelte
index d8fb21e0..ff7d9b9d 100644
--- a/packages/web/src/designer/DesignerTable.svelte
+++ b/packages/web/src/designer/DesignerTable.svelte
@@ -9,6 +9,7 @@
import InputTextModal from '../modals/InputTextModal.svelte';
import { showModal } from '../modals/modalTools';
import { currentThemeDefinition } from '../stores';
+ import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
import contextMenu from '../utility/contextMenu';
import moveDrag from '../utility/moveDrag';
import ColumnLine from './ColumnLine.svelte';
@@ -166,6 +167,15 @@
});
};
+ const handleDefineVirtualForeignKey = table => {
+ showModal(VirtualForeignKeyEditorModal, {
+ schemaName: table.schemaName,
+ pureName: table.pureName,
+ conid,
+ database,
+ });
+ };
+
function createMenu() {
return [
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
@@ -185,6 +195,11 @@
settings?.allowAddAllReferences &&
!isMultipleTableSelection && { text: 'Add references', onClick: () => onAddAllReferences(table) },
settings?.allowChangeColor && { text: 'Change color', onClick: () => onChangeTableColor(table) },
+ settings?.allowDefineVirtualReferences &&
+ !isMultipleTableSelection && {
+ text: 'Define virtual foreign key',
+ onClick: () => handleDefineVirtualForeignKey(table),
+ },
settings?.appendTableSystemMenu &&
!isMultipleTableSelection && [{ divider: true }, createDatabaseObjectMenu({ ...table, conid, database })],
];
diff --git a/packages/web/src/designer/DiagramDesigner.svelte b/packages/web/src/designer/DiagramDesigner.svelte
index 7f0ec71b..d5f62433 100644
--- a/packages/web/src/designer/DiagramDesigner.svelte
+++ b/packages/web/src/designer/DiagramDesigner.svelte
@@ -21,6 +21,7 @@
allowChangeColor: true,
appendTableSystemMenu: true,
customizeStyle: true,
+ allowDefineVirtualReferences: true,
}}
referenceComponent={DiagramDesignerReference}
/>
diff --git a/packages/web/src/designer/QueryDesigner.svelte b/packages/web/src/designer/QueryDesigner.svelte
index 7eb74fa2..508a970b 100644
--- a/packages/web/src/designer/QueryDesigner.svelte
+++ b/packages/web/src/designer/QueryDesigner.svelte
@@ -21,6 +21,7 @@
allowChangeColor: false,
appendTableSystemMenu: false,
customizeStyle: false,
+ allowDefineVirtualReferences: false,
}}
referenceComponent={QueryDesignerReference}
/>
diff --git a/packages/web/src/forms/FormSelectFieldRaw.svelte b/packages/web/src/forms/FormSelectFieldRaw.svelte
index e16fc6e8..d67d7660 100644
--- a/packages/web/src/forms/FormSelectFieldRaw.svelte
+++ b/packages/web/src/forms/FormSelectFieldRaw.svelte
@@ -9,11 +9,13 @@
export let name;
export let options;
export let isClearable = false;
+ export let selectFieldComponent = SelectField;
const { values, setFieldValue } = getFormContext();
-
+ import _ from 'lodash';
+ import { createEventDispatcher } from 'svelte';
+
+ import SelectField from '../forms/SelectField.svelte';
+ import { currentDatabase } from '../stores';
+ import { filterAppsForDatabase } from '../utility/appTools';
+ import { useAppFolders, useUsedApps } from '../utility/metadataLoaders';
+
+ export let value = '#new';
+ export let disableInitialize = false;
+
+ const dispatch = createEventDispatcher();
+
+ $: appFolders = useAppFolders();
+ $: usedApps = useUsedApps();
+
+ $: {
+ if (!disableInitialize && value == '#new' && $currentDatabase) {
+ const filtered = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $usedApps || []);
+ const common = _.intersection(
+ ($appFolders || []).map(x => x.name),
+ filtered.map(x => x.name)
+ );
+ if (common.length > 0) {
+ value = common[0] as string;
+ dispatch('change', value);
+ }
+ }
+ }
+
+
+ {
+ value = e.detail;
+ dispatch('change', value);
+ }}
+ options={[
+ { label: '(New application linked to current DB)', value: '#new' },
+ ...($appFolders || []).map(app => ({
+ label: app.name,
+ value: app.name,
+ })),
+ ]}
+/>
diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte
index 2c79d71f..90aebcfe 100644
--- a/packages/web/src/icons/FontIcon.svelte
+++ b/packages/web/src/icons/FontIcon.svelte
@@ -26,6 +26,7 @@
'icon version': 'mdi mdi-ticket-confirmation',
'icon pin': 'mdi mdi-pin',
'icon arrange': 'mdi mdi-arrange-send-to-back',
+ 'icon app': 'mdi mdi-layers-triple',
'icon columns': 'mdi mdi-view-column',
'icon columns-outline': 'mdi mdi-view-column-outline',
@@ -126,6 +127,9 @@
'img diagram': 'mdi mdi-graph color-icon-blue',
'img yaml': 'mdi mdi-code-brackets color-icon-red',
'img compare': 'mdi mdi-compare color-icon-red',
+ 'img app': 'mdi mdi-layers-triple color-icon-magenta',
+ 'img app-command': 'mdi mdi-flash color-icon-green',
+ 'img app-query': 'mdi mdi-view-comfy color-icon-magenta',
'img add': 'mdi mdi-plus-circle color-icon-green',
'img minus': 'mdi mdi-minus-circle color-icon-red',
diff --git a/packages/web/src/modals/DefineDictionaryDescriptionModal.svelte b/packages/web/src/modals/DefineDictionaryDescriptionModal.svelte
index 899e768c..50b32757 100644
--- a/packages/web/src/modals/DefineDictionaryDescriptionModal.svelte
+++ b/packages/web/src/modals/DefineDictionaryDescriptionModal.svelte
@@ -1,10 +1,11 @@
@@ -75,7 +103,14 @@
-
+
+
+
- export const className = 'theme-dark';
+ export const themeClassName = 'theme-dark';
export const themeName = 'Dark';
export const themeType = 'dark';
diff --git a/packages/web/src/plugins/ThemeLight.svelte b/packages/web/src/plugins/ThemeLight.svelte
index 1956d393..fbc44e3b 100644
--- a/packages/web/src/plugins/ThemeLight.svelte
+++ b/packages/web/src/plugins/ThemeLight.svelte
@@ -1,5 +1,5 @@
diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts
index 068ba507..8bc5649f 100644
--- a/packages/web/src/stores.ts
+++ b/packages/web/src/stores.ts
@@ -64,6 +64,7 @@ export const openedModals = writable([]);
export const openedSnackbars = writable([]);
export const nullStore = readable(null, () => {});
export const currentArchive = writableWithStorage('default', 'currentArchive');
+export const currentApplication = writableWithStorage(null, 'currentApplication');
export const isFileDragActive = writable(false);
export const selectedCellsCallback = writable(null);
export const loadingPluginStore = writable({
@@ -72,7 +73,7 @@ export const loadingPluginStore = writable({
});
export const currentThemeDefinition = derived([currentTheme, extensions], ([$currentTheme, $extensions]) =>
- $extensions.themes.find(x => x.className == $currentTheme)
+ $extensions.themes.find(x => x.themeClassName == $currentTheme)
);
subscribeCssVariable(selectedWidget, x => (x ? 1 : 0), '--dim-visible-left-panel');
diff --git a/packages/web/src/tableeditor/ForeignKeyEditorModal.svelte b/packages/web/src/tableeditor/ForeignKeyEditorModal.svelte
index c13fb04c..4c8a8f1d 100644
--- a/packages/web/src/tableeditor/ForeignKeyEditorModal.svelte
+++ b/packages/web/src/tableeditor/ForeignKeyEditorModal.svelte
@@ -81,7 +81,7 @@
value={fullNameToString({ pureName: refTableName, schemaName: refSchemaName })}
isNative
notSelected
- options={(dbInfo?.tables || []).map(tbl => ({
+ options={_.sortBy(dbInfo?.tables || [], ['schemaName', 'pureName']).map(tbl => ({
label: fullNameToLabel(tbl),
value: fullNameToString(tbl),
}))}
diff --git a/packages/web/src/tableeditor/VirtualForeignKeyEditorModal.svelte b/packages/web/src/tableeditor/VirtualForeignKeyEditorModal.svelte
new file mode 100644
index 00000000..8124ba50
--- /dev/null
+++ b/packages/web/src/tableeditor/VirtualForeignKeyEditorModal.svelte
@@ -0,0 +1,190 @@
+
+
+
+
+ Virtual foreign key
+
+
+
+
+ {
+ const appFolder = await saveDbToApp(conid, database, dstApp);
+ await apiCall('apps/save-virtual-reference', {
+ appFolder,
+ schemaName,
+ pureName,
+ refSchemaName,
+ refTableName,
+ columns,
+ });
+ closeCurrentModal();
+ }}
+ />
+
+
+
+
+
+
+
diff --git a/packages/web/src/tabs/JsonEditorTab.svelte b/packages/web/src/tabs/JsonEditorTab.svelte
new file mode 100644
index 00000000..e9a1e51e
--- /dev/null
+++ b/packages/web/src/tabs/JsonEditorTab.svelte
@@ -0,0 +1,83 @@
+
+
+
+
+ setEditorData(e.detail)}
+ on:focus={() => {
+ activator.activate();
+ invalidateCommands();
+ }}
+ bind:this={domEditor}
+ mode="json"
+/>
diff --git a/packages/web/src/tabs/YamlEditorTab.svelte b/packages/web/src/tabs/YamlEditorTab.svelte
index 6cab222f..1540137b 100644
--- a/packages/web/src/tabs/YamlEditorTab.svelte
+++ b/packages/web/src/tabs/YamlEditorTab.svelte
@@ -31,7 +31,7 @@
const tabVisible: any = getContext('tabVisible');
- export const activator = createActivator('MarkdownEditorTab', false);
+ export const activator = createActivator('YamlEditorTab', false);
let domEditor;
@@ -63,8 +63,6 @@
function createMenu() {
return [
- { command: 'yaml.preview' },
- { divider: true },
{ command: 'yaml.toggleComment' },
{ divider: true },
{ command: 'yaml.save' },
diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js
index e3002e0d..b0d7f714 100644
--- a/packages/web/src/tabs/index.js
+++ b/packages/web/src/tabs/index.js
@@ -15,6 +15,7 @@ import * as FavoriteEditorTab from './FavoriteEditorTab.svelte';
import * as QueryDesignTab from './QueryDesignTab.svelte';
import * as CommandListTab from './CommandListTab.svelte';
import * as YamlEditorTab from './YamlEditorTab.svelte';
+import * as JsonEditorTab from './JsonEditorTab.svelte';
import * as CompareModelTab from './CompareModelTab.svelte';
import * as JsonTab from './JsonTab.svelte';
import * as ChangelogTab from './ChangelogTab.svelte';
@@ -38,6 +39,7 @@ export default {
QueryDesignTab,
CommandListTab,
YamlEditorTab,
+ JsonEditorTab,
CompareModelTab,
JsonTab,
ChangelogTab,
diff --git a/packages/web/src/utility/appTools.ts b/packages/web/src/utility/appTools.ts
new file mode 100644
index 00000000..506e1aaa
--- /dev/null
+++ b/packages/web/src/utility/appTools.ts
@@ -0,0 +1,33 @@
+import { ApplicationDefinition, StoredConnection } from 'dbgate-types';
+import { apiCall } from '../utility/api';
+
+export async function saveDbToApp(conid: string, database: string, app: string) {
+ if (app == '#new') {
+ const folder = await apiCall('apps/create-folder', { folder: database });
+
+ await apiCall('connections/update-database', {
+ conid,
+ database,
+ values: {
+ [`useApp:${folder}`]: true,
+ },
+ });
+
+ return folder;
+ }
+
+ await apiCall('connections/update-database', {
+ conid,
+ database,
+ values: {
+ [`useApp:${app}`]: true,
+ },
+ });
+
+ return app;
+}
+
+export function filterAppsForDatabase(connection, database: string, $apps): ApplicationDefinition[] {
+ const db = (connection?.databases || []).find(x => x.name == database);
+ return $apps.filter(app => db && db[`useApp:${app.name}`]);
+}
diff --git a/packages/web/src/utility/dictionaryDescriptionTools.ts b/packages/web/src/utility/dictionaryDescriptionTools.ts
index 0454ccc7..87cc9557 100644
--- a/packages/web/src/utility/dictionaryDescriptionTools.ts
+++ b/packages/web/src/utility/dictionaryDescriptionTools.ts
@@ -1,7 +1,8 @@
import { DictionaryDescription } from 'dbgate-datalib';
-import { TableInfo } from 'dbgate-types';
+import { ApplicationDefinition, TableInfo } from 'dbgate-types';
import _ from 'lodash';
-import { getLocalStorage, setLocalStorage, removeLocalStorage } from './storageCache';
+import { apiCall } from './api';
+import { filterAppsForDatabase, saveDbToApp } from './appTools';
function checkDescriptionColumns(columns: string[], table: TableInfo) {
if (!columns?.length) return false;
@@ -14,17 +15,20 @@ export function getDictionaryDescription(
table: TableInfo,
conid: string,
database: string,
+ apps: ApplicationDefinition[],
+ connections,
skipCheckSaved: boolean = false
): DictionaryDescription {
- const keySpecific = `dictionary_spec_${table.schemaName}||${table.pureName}||${conid}||${database}`;
- const keyCommon = `dictionary_spec_${table.schemaName}||${table.pureName}`;
+ const conn = connections.find(x => x._id == conid);
+ const dbApps = filterAppsForDatabase(conn, database, apps);
- const cachedSpecific = getLocalStorage(keySpecific);
- const cachedCommon = getLocalStorage(keyCommon);
+ const cached = _.flatten(dbApps.map(x => x.dictionaryDescriptions || [])).find(
+ x => x.pureName == table.pureName && x.schemaName == table.schemaName
+ );
- if (cachedSpecific && (skipCheckSaved || checkDescriptionColumns(cachedSpecific.columns, table)))
- return cachedSpecific;
- if (cachedCommon && (skipCheckSaved || checkDescriptionColumns(cachedCommon.columns, table))) return cachedCommon;
+ if (cached && (skipCheckSaved || checkDescriptionColumns(cached.columns, table))) {
+ return cached;
+ }
const descColumn = table.columns.find(x => x?.dataType?.toLowerCase()?.includes('char'));
if (descColumn) {
@@ -57,29 +61,22 @@ export function changeDelimitedColumnList(columns, columnName, isChecked) {
return parsed.join(',');
}
-export function saveDictionaryDescription(
+export async function saveDictionaryDescription(
table: TableInfo,
conid: string,
database: string,
expression: string,
delimiter: string,
- useForAllDatabases: boolean
+ targetApplication: string
) {
- const keySpecific = `dictionary_spec_${table.schemaName}||${table.pureName}||${conid}||${database}`;
- const keyCommon = `dictionary_spec_${table.schemaName}||${table.pureName}`;
+ const appFolder = await saveDbToApp(conid, database, targetApplication);
- removeLocalStorage(keySpecific);
- if (useForAllDatabases) removeLocalStorage(keyCommon);
-
- const description = {
+ await apiCall('apps/save-dictionary-description', {
+ appFolder,
+ schemaName: table.schemaName,
+ pureName: table.pureName,
columns: parseDelimitedColumnList(expression),
expression,
delimiter,
- };
-
- if (useForAllDatabases) {
- setLocalStorage(keyCommon, description);
- } else {
- setLocalStorage(keySpecific, description);
- }
+ });
}
diff --git a/packages/web/src/utility/metadataLoaders.ts b/packages/web/src/utility/metadataLoaders.ts
index 3fcf6fa6..c6edbef8 100644
--- a/packages/web/src/utility/metadataLoaders.ts
+++ b/packages/web/src/utility/metadataLoaders.ts
@@ -103,6 +103,30 @@ const archiveFilesLoader = ({ folder }) => ({
reloadTrigger: `archive-files-changed-${folder}`,
});
+const appFoldersLoader = () => ({
+ url: 'apps/folders',
+ params: {},
+ reloadTrigger: `app-folders-changed`,
+});
+
+const appFilesLoader = ({ folder }) => ({
+ url: 'apps/files',
+ params: { folder },
+ reloadTrigger: `app-files-changed-${folder}`,
+});
+
+// const dbAppsLoader = ({ conid, database }) => ({
+// url: 'apps/get-apps-for-db',
+// params: { conid, database },
+// reloadTrigger: `db-apps-changed-${conid}-${database}`,
+// });
+
+const usedAppsLoader = ({ conid, database }) => ({
+ url: 'apps/get-used-apps',
+ params: { },
+ reloadTrigger: `used-apps-changed`,
+});
+
const serverStatusLoader = () => ({
url: 'server-connections/server-status',
params: {},
@@ -401,6 +425,36 @@ export function useArchiveFolders(args = {}) {
return useCore(archiveFoldersLoader, args);
}
+export function getAppFiles(args) {
+ return getCore(appFilesLoader, args);
+}
+export function useAppFiles(args) {
+ return useCore(appFilesLoader, args);
+}
+
+export function getAppFolders(args = {}) {
+ return getCore(appFoldersLoader, args);
+}
+export function useAppFolders(args = {}) {
+ return useCore(appFoldersLoader, args);
+}
+
+
+
+export function getUsedApps(args = {}) {
+ return getCore(usedAppsLoader, args);
+}
+export function useUsedApps(args = {}) {
+ return useCore(usedAppsLoader, args);
+}
+
+// export function getDbApps(args = {}) {
+// return getCore(dbAppsLoader, args);
+// }
+// export function useDbApps(args = {}) {
+// return useCore(dbAppsLoader, args);
+// }
+
export function getInstalledPlugins(args = {}) {
return getCore(installedPluginsLoader, args) || [];
}
diff --git a/packages/web/src/widgets/AppFilesList.svelte b/packages/web/src/widgets/AppFilesList.svelte
new file mode 100644
index 00000000..f010027f
--- /dev/null
+++ b/packages/web/src/widgets/AppFilesList.svelte
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ({
+ fileName: file.name,
+ folderName: folder,
+ fileType: file.type,
+ fileLabel: file.label,
+ }))}
+ groupFunc={data => APP_LABELS[data.fileType] || 'App config'}
+ module={appFileAppObject}
+ {filter}
+ />
+
diff --git a/packages/web/src/widgets/AppFolderList.svelte b/packages/web/src/widgets/AppFolderList.svelte
new file mode 100644
index 00000000..84aa0871
--- /dev/null
+++ b/packages/web/src/widgets/AppFolderList.svelte
@@ -0,0 +1,39 @@
+
+
+
+
+
+ runCommand('new.application')} title="Create new application">
+
+
+
+
+
+
+
+
+
diff --git a/packages/web/src/widgets/AppWidget.svelte b/packages/web/src/widgets/AppWidget.svelte
new file mode 100644
index 00000000..13074843
--- /dev/null
+++ b/packages/web/src/widgets/AppWidget.svelte
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/web/src/widgets/WidgetContainer.svelte b/packages/web/src/widgets/WidgetContainer.svelte
index 14802429..c53900b5 100644
--- a/packages/web/src/widgets/WidgetContainer.svelte
+++ b/packages/web/src/widgets/WidgetContainer.svelte
@@ -6,6 +6,7 @@
import PluginsWidget from './PluginsWidget.svelte';
import CellDataWidget from './CellDataWidget.svelte';
import HistoryWidget from './HistoryWidget.svelte';
+ import AppWidget from './AppWidget.svelte';
@@ -25,3 +26,6 @@
{#if $selectedWidget == 'cell-data'}
{/if}
+{#if $selectedWidget == 'app'}
+
+{/if}
diff --git a/packages/web/src/widgets/WidgetIconPanel.svelte b/packages/web/src/widgets/WidgetIconPanel.svelte
index 440bca65..29a88a07 100644
--- a/packages/web/src/widgets/WidgetIconPanel.svelte
+++ b/packages/web/src/widgets/WidgetIconPanel.svelte
@@ -40,6 +40,11 @@
name: 'cell-data',
title: 'Selected cell data detail view',
},
+ {
+ icon: 'icon app',
+ name: 'app',
+ title: 'Application layers',
+ },
// {
// icon: 'icon settings',
// name: 'settings',
diff --git a/yarn.lock b/yarn.lock
index 307082a1..a674047b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1844,11 +1844,6 @@ async-lock@^1.2.6:
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.8.tgz#7b02bdfa2de603c0713acecd11184cf97bbc7c4c"
integrity sha512-G+26B2jc0Gw0EG/WN2M6IczuGepBsfR1+DtqLnyFSH4p2C668qkOCtEkGNVEaaNAVlYwEMazy1+/jnLxltBkIQ==
-async@0.2.10:
- version "0.2.10"
- resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
- integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E=
-
async@>=0.6.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
@@ -2080,13 +2075,6 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
-binary-search-tree@0.2.5:
- version "0.2.5"
- resolved "https://registry.yarnpkg.com/binary-search-tree/-/binary-search-tree-0.2.5.tgz#7dbb3b210fdca082450dad2334c304af39bdc784"
- integrity sha1-fbs7IQ/coIJFDa0jNMMErzm9x4Q=
- dependencies:
- underscore "~1.4.4"
-
bindings@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
@@ -6811,13 +6799,6 @@ local-access@^1.0.1:
resolved "https://registry.yarnpkg.com/local-access/-/local-access-1.1.0.tgz#e007c76ba2ca83d5877ba1a125fc8dfe23ba4798"
integrity sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==
-localforage@^1.3.0:
- version "1.7.3"
- resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.7.3.tgz#0082b3ca9734679e1bd534995bdd3b24cf10f204"
- integrity sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==
- dependencies:
- lie "3.1.1"
-
localforage@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1"
@@ -7315,7 +7296,7 @@ mkdirp@0.3.0:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=
-mkdirp@0.x, mkdirp@^0.5.1, mkdirp@~0.5.1:
+mkdirp@0.x, mkdirp@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
@@ -7485,24 +7466,6 @@ ncp@^2.0.0:
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
-nedb-promises@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/nedb-promises/-/nedb-promises-4.0.1.tgz#4d0bd1553d045acca5d6713ad76eb97aa830b390"
- integrity sha512-I6nVZ0zjjYGfja2UU8lDSEzjfQTS8bo+8jvn7apILpynYDKzLpl6YRfdPa+uRSUYDN9bH45wJ+gvRWcOjO2g5g==
- dependencies:
- nedb "^1.8.0"
-
-nedb@^1.8.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/nedb/-/nedb-1.8.0.tgz#0e3502cd82c004d5355a43c9e55577bd7bd91d88"
- integrity sha1-DjUCzYLABNU1WkPJ5VV3vXvZHYg=
- dependencies:
- async "0.2.10"
- binary-search-tree "0.2.5"
- localforage "^1.3.0"
- mkdirp "~0.5.1"
- underscore "~1.4.4"
-
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@@ -10693,11 +10656,6 @@ undefsafe@^2.0.2:
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.0.tgz#3ccdcbb824230fc6bf234ad0ddcd83dff4eafe5f"
integrity sha512-sCs4H3pCytsb5K7i072FAEC9YlSYFIbosvM0tAKAlpSSUgD7yC1iXSEGdl5XrDKQ1YUB+p/HDzYrSG2H2Vl36g==
-underscore@~1.4.4:
- version "1.4.4"
- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
- integrity sha1-YaajIBBiKvoHljvzJSA88SI51gQ=
-
union-value@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"