Merge branch 'develop'

This commit is contained in:
Jan Prochazka 2022-01-29 19:31:12 +01:00
commit 03c2a58557
53 changed files with 1543 additions and 130 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@ export interface DisplayColumn {
autoIncrement?: boolean;
isPrimaryKey?: boolean;
foreignKey?: ForeignKeyInfo;
isForeignKeyUnique?: boolean;
isExpandable?: boolean;
isChecked?: boolean;
hintColumnNames?: string[];

View File

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

View File

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

View File

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

View File

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

37
packages/types/appdefs.d.ts vendored Normal file
View File

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

View File

@ -22,9 +22,10 @@ export interface FileFormatDefinition {
}
export interface ThemeDefinition {
className: string;
themeClassName: string;
themeName: string;
themeType: 'light' | 'dark';
themeCss?: string;
}
export interface PluginDefinition {

View File

@ -43,3 +43,4 @@ export * from './dumper';
export * from './dbtypes';
export * from './extensions';
export * from './alter-processor';
export * from './appdefs';

View File

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

View File

@ -26,6 +26,12 @@
$: currentThemeType = $currentThemeDefinition?.themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light';
</script>
<svelte:head>
{#if $currentThemeDefinition?.themeCss}
{@html `<style id="themePlugin">${$currentThemeDefinition?.themeCss}</style>`}
{/if}
</svelte:head>
<div
class={`${$currentTheme} ${currentThemeType} root dbgate-screen`}
use:dragDropFileTarget

View File

@ -0,0 +1,119 @@
<script lang="ts" context="module">
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];
}
</script>
<script lang="ts">
import _ from 'lodash';
import { filterName } from 'dbgate-tools';
import { showModal } from '../modals/modalTools';
import openNewTab from '../utility/openNewTab';
import AppObjectCore from './AppObjectCore.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import { apiCall } from '../utility/api';
import { currentDatabase, currentDatabase } from '../stores';
export let data;
const handleRename = () => {
showModal(InputTextModal, {
value: data.fileName,
label: 'New file name',
header: 'Rename file',
onConfirm: newFile => {
apiCall('apps/rename-file', {
file: data.fileName,
folder: data.folderName,
fileType: data.fileType,
newFile,
});
},
});
};
const handleDelete = () => {
showModal(ConfirmModal, {
message: `Really delete file ${data.fileName}?`,
onConfirm: () => {
apiCall('apps/delete-file', {
file: data.fileName,
folder: data.folderName,
fileType: data.fileType,
});
},
});
};
const handleClick = () => {
if (data.fileType.endsWith('.sql')) {
handleOpenSqlFile();
}
if (data.fileType.endsWith('.json')) {
handleOpenJsonFile();
}
};
const handleOpenSqlFile = () => {
openTextFile(data.fileName, data.fileType, data.folderName, 'QueryTab', 'img sql-file');
};
const handleOpenJsonFile = () => {
openTextFile(data.fileName, data.fileType, data.folderName, 'JsonEditorTab', 'img json');
};
function createMenu() {
return [
{ text: 'Delete', onClick: handleDelete },
{ text: 'Rename', onClick: handleRename },
data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile },
data.fileType.endsWith('.json') && { text: 'Open JSON', onClick: handleOpenJsonFile },
// data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile },
];
}
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.fileLabel}
icon={getAppIcon(data)}
menu={createMenu}
on:click={handleClick}
/>

View File

@ -0,0 +1,96 @@
<script lang="ts" context="module">
export const extractKey = data => data.name;
export const createMatcher = data => filter => filterName(filter, data.name);
</script>
<script lang="ts">
import _, { find } from 'lodash';
import { filterName } from 'dbgate-tools';
import { currentApplication, currentDatabase } from '../stores';
import AppObjectCore from './AppObjectCore.svelte';
import { showModal } from '../modals/modalTools';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import { apiCall } from '../utility/api';
import { useConnectionList } from '../utility/metadataLoaders';
export let data;
$: connections = useConnectionList();
const handleDelete = () => {
showModal(ConfirmModal, {
message: `Really delete application ${data.name}?`,
onConfirm: () => {
apiCall('apps/delete-folder', { folder: data.name });
},
});
};
const handleRename = () => {
const { name } = data;
showModal(InputTextModal, {
value: name,
label: 'New application name',
header: 'Rename application',
onConfirm: async newFolder => {
await apiCall('apps/rename-folder', {
folder: data.name,
newFolder: newFolder,
});
if ($currentApplication == data.name) {
$currentApplication = newFolder;
}
},
});
};
function setOnCurrentDb(value) {
apiCall('connections/update-database', {
conid: $currentDatabase?.connection?._id,
database: $currentDatabase?.name,
values: {
[`useApp:${data.name}`]: value,
},
});
}
function createMenu() {
return [
{ text: 'Delete', onClick: handleDelete },
{ text: 'Rename', onClick: handleRename },
$currentDatabase && [
!isOnCurrentDb($currentDatabase, $connections) && {
text: 'Enable on current database',
onClick: () => setOnCurrentDb(true),
},
isOnCurrentDb($currentDatabase, $connections) && {
text: 'Disable on current database',
onClick: () => setOnCurrentDb(false),
},
],
];
}
function isOnCurrentDb(currentDb, connections) {
const conn = connections.find(x => x._id == currentDb?.connection?._id);
const db = conn?.databases?.find(x => x.name == currentDb?.name);
return db && db[`useApp:${data.name}`];
}
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.name}
icon={'img app'}
statusIcon={isOnCurrentDb($currentDatabase, $connections) ? 'icon check' : null}
statusTitle={`Application ${data.name} is used for database ${$currentDatabase?.name}`}
isBold={data.name == $currentApplication}
on:click={() => ($currentApplication = data.name)}
menu={createMenu}
/>

View File

@ -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';
@ -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();
</script>
<AppObjectCore

View File

@ -1,7 +1,8 @@
<script lang="ts" context="module">
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,
});
},
})),
],
];
}
</script>
@ -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();
</script>
<AppObjectCore

View File

@ -30,7 +30,7 @@ import runCommand from './runCommand';
function themeCommand(theme: ThemeDefinition) {
return {
text: theme.themeName,
onClick: () => 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',

View File

@ -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' },
],
];
}
</script>

View File

@ -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}
/>
</td>
{/each}

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@
allowChangeColor: true,
appendTableSystemMenu: true,
customizeStyle: true,
allowDefineVirtualReferences: true,
}}
referenceComponent={DiagramDesignerReference}
/>

View File

@ -21,6 +21,7 @@
allowChangeColor: false,
appendTableSystemMenu: false,
customizeStyle: false,
allowDefineVirtualReferences: false,
}}
referenceComponent={QueryDesignerReference}
/>

View File

@ -9,11 +9,13 @@
export let name;
export let options;
export let isClearable = false;
export let selectFieldComponent = SelectField;
const { values, setFieldValue } = getFormContext();
</script>
<SelectField
<svelte:component
this={selectFieldComponent}
{...$$restProps}
value={$values && $values[name]}
options={_.compact(options)}

View File

@ -0,0 +1,48 @@
<script lang="ts">
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);
}
}
}
</script>
<SelectField
isNative
{...$$restProps}
{value}
on:change={e => {
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,
})),
]}
/>

View File

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

View File

@ -1,10 +1,11 @@
<script lang="ts">
import FormProvider from '../forms/FormProvider.svelte';
import _ from 'lodash';
import FormSubmit from '../forms/FormSubmit.svelte';
import FormStyledButton from '../elements/FormStyledButton.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
import { useTableInfo } from '../utility/metadataLoaders';
import { useAppFolders, useConnectionList, useTableInfo, useUsedApps } from '../utility/metadataLoaders';
import TableControl from '../elements/TableControl.svelte';
import TextField from '../forms/TextField.svelte';
import FormTextField from '../forms/FormTextField.svelte';
@ -19,6 +20,10 @@
} from '../utility/dictionaryDescriptionTools';
import { includes } from 'lodash';
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
import FormSelectField from '../forms/FormSelectField.svelte';
import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte';
import { currentDatabase } from '../stores';
import { filterAppsForDatabase } from '../utility/appTools';
export let conid;
export let database;
@ -28,18 +33,41 @@
$: tableInfo = useTableInfo({ conid, database, schemaName, pureName });
$: descriptionInfo = getDictionaryDescription($tableInfo, conid, database, true);
$: apps = useUsedApps();
$: appFolders = useAppFolders();
$: connections = useConnectionList();
const values = writable({});
$: descriptionInfo = getDictionaryDescription($tableInfo, conid, database, $apps, $connections, true);
const values = writable({ targetApplication: '#new' } as any);
function initValues(descriptionInfo) {
$values = {
targetApplication: $values.targetApplication,
columns: descriptionInfo.expression,
delimiter: descriptionInfo.delimiter,
};
}
$: if (descriptionInfo) initValues(descriptionInfo);
$: {
if (descriptionInfo) initValues(descriptionInfo);
}
$: {
if ($values.targetApplication == '#new' && $currentDatabase) {
const filtered = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $apps || []);
const common = _.intersection(
($appFolders || []).map(x => x.name),
filtered.map(x => x.name)
);
if (common.length > 0) {
$values = {
...$values,
targetApplication: common[0],
};
}
}
}
</script>
<FormProviderCore {values}>
@ -75,7 +103,14 @@
<FormTextField name="delimiter" label="Delimiter" />
<FormCheckboxField name="useForAllDatabases" label="Use for all databases" />
<FormSelectField
label="Target application"
name="targetApplication"
disableInitialize
selectFieldComponent={TargetApplicationSelect}
/>
<!-- <FormCheckboxField name="useForAllDatabases" label="Use for all databases" /> -->
<svelte:fragment slot="footer">
<FormSubmit
@ -89,7 +124,7 @@
database,
$values.columns,
$values.delimiter,
$values.useForAllDatabases
$values.targetApplication
);
onConfirm();
}}

View File

@ -6,7 +6,7 @@
import { closeCurrentModal, showModal } from './modalTools';
import DefineDictionaryDescriptionModal from './DefineDictionaryDescriptionModal.svelte';
import ScrollableTableControl from '../elements/ScrollableTableControl.svelte';
import { getTableInfo } from '../utility/metadataLoaders';
import { getTableInfo, useConnectionList, useUsedApps } from '../utility/metadataLoaders';
import { getDictionaryDescription } from '../utility/dictionaryDescriptionTools';
import { onMount } from 'svelte';
import { dumpSqlSelect } from 'dbgate-sqltree';
@ -33,6 +33,9 @@
let checkedKeys = [];
$: apps = useUsedApps();
$: connections = useConnectionList();
function defineDescription() {
showModal(DefineDictionaryDescriptionModal, {
conid,
@ -45,7 +48,7 @@
async function reload() {
tableInfo = await getTableInfo({ conid, database, schemaName, pureName });
description = getDictionaryDescription(tableInfo, conid, database);
description = getDictionaryDescription(tableInfo, conid, database, $apps, $connections);
if (!tableInfo || !description) return;
if (tableInfo?.primaryKey?.columns?.length != 1) return;
@ -112,6 +115,8 @@
$: {
search;
$apps;
$connections;
reload();
}

View File

@ -1,5 +1,5 @@
<script context="module">
export const className = 'theme-dark';
export const themeClassName = 'theme-dark';
export const themeName = 'Dark';
export const themeType = 'dark';
</script>

View File

@ -1,5 +1,5 @@
<script context="module">
export const className = 'theme-light';
export const themeClassName = 'theme-light';
export const themeName = 'Light';
export const themeType = 'light';
</script>

View File

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

View File

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

View File

@ -0,0 +1,190 @@
<script lang="ts">
import FormStyledButton from '../elements/FormStyledButton.svelte';
import FormProvider from '../forms/FormProvider.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import ModalBase from '../modals/ModalBase.svelte';
import { closeCurrentModal } from '../modals/modalTools';
import { fullNameFromString, fullNameToLabel, fullNameToString } from 'dbgate-tools';
import SelectField from '../forms/SelectField.svelte';
import _ from 'lodash';
import { useDatabaseInfo, useTableInfo } from '../utility/metadataLoaders';
import { onMount } from 'svelte';
import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte';
import { apiCall } from '../utility/api';
import { saveDbToApp } from '../utility/appTools';
export let conid;
export let database;
export let schemaName;
export let pureName;
export let columnName;
let dstApp;
const dbInfo = useDatabaseInfo({ conid, database });
const tableInfo = useTableInfo({ conid, database, schemaName, pureName });
let columns = [];
let refTableName = null;
let refSchemaName = null;
$: refTableInfo = $dbInfo?.tables?.find(x => x.pureName == refTableName && x.schemaName == refSchemaName);
// $dbInfo?.views?.find(x => x.pureName == refTableName && x.schemaName == refSchemaName);
onMount(() => {
if (columnName) {
columns = [
...columns,
{
columnName,
},
];
}
});
</script>
<FormProvider>
<ModalBase {...$$restProps}>
<svelte:fragment slot="header">Virtual foreign key</svelte:fragment>
<div class="largeFormMarker">
<div class="row">
<div class="label col-3">Referenced table</div>
<div class="col-9">
<SelectField
value={fullNameToString({ pureName: refTableName, schemaName: refSchemaName })}
isNative
notSelected
options={[
..._.sortBy($dbInfo?.tables || [], ['schemaName', 'pureName']),
// ..._.sortBy($dbInfo?.views || [], ['schemaName', 'pureName']),
].map(tbl => ({
label: fullNameToLabel(tbl),
value: fullNameToString(tbl),
}))}
on:change={e => {
if (e.detail) {
const name = fullNameFromString(e.detail);
refTableName = name.pureName;
refSchemaName = name.schemaName;
}
}}
/>
</div>
</div>
<div class="row">
<div class="col-5 mr-1">
Base column - {$tableInfo.pureName}
</div>
<div class="col-5 ml-1">
Ref column - {refTableName || '(table not set)'}
</div>
</div>
{#each columns as column, index}
<div class="row">
<div class="col-5 mr-1">
{#key column.columnName}
<SelectField
value={column.columnName}
isNative
notSelected
options={$tableInfo.columns.map(col => ({
label: col.columnName,
value: col.columnName,
}))}
on:change={e => {
if (e.detail) {
columns = columns.map((col, i) => (i == index ? { ...col, columnName: e.detail } : col));
}
}}
/>
{/key}
</div>
<div class="col-5 ml-1">
{#key column.refColumnName}
<SelectField
value={column.refColumnName}
isNative
notSelected
options={(refTableInfo?.columns || []).map(col => ({
label: col.columnName,
value: col.columnName,
}))}
on:change={e => {
if (e.detail) {
columns = columns.map((col, i) => (i == index ? { ...col, refColumnName: e.detail } : col));
}
}}
/>
{/key}
</div>
<div class="col-2 button">
<FormStyledButton
value="Delete"
on:click={e => {
const x = [...columns];
x.splice(index, 1);
columns = x;
}}
/>
</div>
</div>
{/each}
<FormStyledButton
type="button"
value="Add column"
on:click={() => {
columns = [...columns, {}];
}}
/>
<div class="row">
<div class="label col-3">Target application</div>
<div class="col-9">
<TargetApplicationSelect bind:value={dstApp} />
</div>
</div>
</div>
<svelte:fragment slot="footer">
<FormSubmit
value={'Save'}
on:click={async () => {
const appFolder = await saveDbToApp(conid, database, dstApp);
await apiCall('apps/save-virtual-reference', {
appFolder,
schemaName,
pureName,
refSchemaName,
refTableName,
columns,
});
closeCurrentModal();
}}
/>
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
</svelte:fragment>
</ModalBase>
</FormProvider>
<style>
.row {
margin: var(--dim-large-form-margin);
display: flex;
}
.row .label {
white-space: nowrap;
align-self: center;
}
.button {
align-self: center;
text-align: right;
}
</style>

View File

@ -0,0 +1,83 @@
<script lang="ts" context="module">
const getCurrentEditor = () => getActiveComponent('JsonEditorTab');
registerFileCommands({
idPrefix: 'json',
category: 'Json',
getCurrentEditor,
folder: 'yaml',
format: 'text',
fileExtension: 'json',
toggleComment: true,
findReplace: true,
});
</script>
<script lang="ts">
import { getContext } from 'svelte';
import { registerFileCommands } from '../commands/stdCommands';
import AceEditor from '../query/AceEditor.svelte';
import useEditorData from '../query/useEditorData';
import invalidateCommands from '../commands/invalidateCommands';
import createActivator, { getActiveComponent } from '../utility/createActivator';
export let tabid;
const tabVisible: any = getContext('tabVisible');
export const activator = createActivator('JsonEditorTab', false);
let domEditor;
$: if ($tabVisible && domEditor) {
domEditor?.getEditor()?.focus();
}
export function getData() {
return $editorState.value || '';
}
export function toggleComment() {
domEditor.getEditor().execCommand('togglecomment');
}
export function find() {
domEditor.getEditor().execCommand('find');
}
export function replace() {
domEditor.getEditor().execCommand('replace');
}
export function getTabId() {
return tabid;
}
const { editorState, editorValue, setEditorData, saveToStorage } = useEditorData({ tabid });
function createMenu() {
return [
{ command: 'json.toggleComment' },
{ divider: true },
{ command: 'json.save' },
{ command: 'json.saveAs' },
{ divider: true },
{ command: 'json.find' },
{ command: 'json.replace' },
];
}
</script>
<AceEditor
value={$editorState.value || ''}
menu={createMenu()}
on:input={e => setEditorData(e.detail)}
on:focus={() => {
activator.activate();
invalidateCommands();
}}
bind:this={domEditor}
mode="json"
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,101 @@
<script lang="ts" context="module">
const APP_LABELS = {
'command.sql': 'SQL commands',
'query.sql': 'SQL queries',
};
const COMMAND_TEMPLATE = `-- Write SQL command here
-- After save, you can execute it from database context menu, for all databases, which use this application
`;
const QUERY_TEMPLATE = `-- Write SQL query here
-- After save, you can view it in tables list, for all databases, which use this application
`;
</script>
<script lang="ts">
import { createFreeTableModel } from 'dbgate-datalib';
import _ from 'lodash';
import AppObjectList from '../appobj/AppObjectList.svelte';
import * as appFileAppObject from '../appobj/AppFileAppObject.svelte';
import CloseSearchButton from '../elements/CloseSearchButton.svelte';
import DropDownButton from '../elements/DropDownButton.svelte';
import InlineButton from '../elements/InlineButton.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import { showModal } from '../modals/modalTools';
import newQuery from '../query/newQuery';
import { currentApplication } from '../stores';
import { apiCall } from '../utility/api';
import { markArchiveFileAsDataSheet } from '../utility/archiveTools';
import { useAppFiles, useArchiveFolders } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
let filter = '';
$: folder = $currentApplication;
$: files = useAppFiles({ folder });
const handleRefreshFiles = () => {
apiCall('apps/refresh-files', { folder });
};
function handleNewSqlFile(fileType, header, initialData) {
showModal(InputTextModal, {
value: '',
label: 'New file name',
header,
onConfirm: async file => {
newQuery({
title: file,
initialData,
// @ts-ignore
savedFile: file + '.' + fileType,
savedFolder: 'app:' + $currentApplication,
savedFormat: 'text',
appFolder: $currentApplication,
});
},
});
}
function createAddMenu() {
return [
{
text: 'New SQL command',
onClick: () => handleNewSqlFile('command.sql', 'Create new SQL command', COMMAND_TEMPLATE),
},
// { text: 'New query view', onClick: () => handleNewSqlFile('query.sql', 'Create new SQL query', QUERY_TEMPLATE) },
];
}
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search application files" bind:value={filter} />
<CloseSearchButton bind:filter />
<DropDownButton icon="icon plus-thick" menu={createAddMenu} />
<InlineButton on:click={handleRefreshFiles} title="Refresh files of selected application">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>
<AppObjectList
list={($files || []).map(file => ({
fileName: file.name,
folderName: folder,
fileType: file.type,
fileLabel: file.label,
}))}
groupFunc={data => APP_LABELS[data.fileType] || 'App config'}
module={appFileAppObject}
{filter}
/>
</WidgetsInnerContainer>

View File

@ -0,0 +1,39 @@
<script lang="ts">
import _ from 'lodash';
import AppObjectList from '../appobj/AppObjectList.svelte';
import * as appFolderAppObject from '../appobj/AppFolderAppObject.svelte';
import runCommand from '../commands/runCommand';
import CloseSearchButton from '../elements/CloseSearchButton.svelte';
import InlineButton from '../elements/InlineButton.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import { apiCall } from '../utility/api';
import { useAppFolders } from '../utility/metadataLoaders';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
let filter = '';
$: folders = useAppFolders();
const handleRefreshFolders = () => {
apiCall('apps/refresh-folders');
};
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search applications" bind:value={filter} />
<CloseSearchButton bind:filter />
<InlineButton on:click={() => runCommand('new.application')} title="Create new application">
<FontIcon icon="icon plus-thick" />
</InlineButton>
<InlineButton on:click={handleRefreshFolders} title="Refresh application list">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>
<AppObjectList list={_.sortBy($folders, 'name')} module={appFolderAppObject} {filter} />
</WidgetsInnerContainer>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import AppFilesList from './AppFilesList.svelte';
import WidgetColumnBar from './WidgetColumnBar.svelte';
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
import { useFavorites } from '../utility/metadataLoaders';
import AppFolderList from './AppFolderList.svelte';
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title="Applications" name="apps" height="30%" storageName="appsWidget">
<AppFolderList />
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Application files" name="files" storageName="appFilesWidget">
<AppFilesList />
</WidgetColumnBarItem>
</WidgetColumnBar>

View File

@ -6,6 +6,7 @@
import PluginsWidget from './PluginsWidget.svelte';
import CellDataWidget from './CellDataWidget.svelte';
import HistoryWidget from './HistoryWidget.svelte';
import AppWidget from './AppWidget.svelte';
</script>
<DatabaseWidget hidden={$selectedWidget != 'database'} />
@ -25,3 +26,6 @@
{#if $selectedWidget == 'cell-data'}
<CellDataWidget />
{/if}
{#if $selectedWidget == 'app'}
<AppWidget />
{/if}

View File

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

View File

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