Add Logic to Edit And Update Database on Both Client And Server
Summary: This logic relies on the logic on client side, specifically `SqliteDatabaseDriver.java` in specifying some keys to retrieve useful data in `currentStructure`. This will be error-prone if developer writes their own data driver which doesn't follow the same naming as the data driver used as model. Also, this diff adds util file to deal with query for updating database on client side. I decided to construct query clause on server side so that the client side change is not needed. Reviewed By: jknoxville Differential Revision: D21788241 fbshipit-source-id: cf9a920c3e5b7b29f619bc3f00e68616b3445cab
This commit is contained in:
committed by
Facebook GitHub Bot
parent
fe17ddce1a
commit
5a2221e6cd
73
desktop/plugins/databases/UpdateQueryUtil.tsx
Normal file
73
desktop/plugins/databases/UpdateQueryUtil.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Value} from 'flipper';
|
||||||
|
|
||||||
|
const INT_DATA_TYPE = ['INTEGER', 'LONG', 'INT', 'BIGINT'];
|
||||||
|
const FLOAT_DATA_TYPE = ['REAL', 'DOUBLE'];
|
||||||
|
const BLOB_DATA_TYPE = ['BLOB'];
|
||||||
|
|
||||||
|
export function convertStringToValue(
|
||||||
|
types: {[key: string]: {type: string; nullable: boolean}},
|
||||||
|
key: string,
|
||||||
|
value: string | null,
|
||||||
|
): Value {
|
||||||
|
if (value !== null && types.hasOwnProperty(key)) {
|
||||||
|
const {type, nullable} = types[key];
|
||||||
|
if (value.length <= 0 && nullable) {
|
||||||
|
return {type: 'null', value: null};
|
||||||
|
}
|
||||||
|
if (INT_DATA_TYPE.indexOf(type) >= 0) {
|
||||||
|
return {type: 'integer', value: parseInt(value, 10)};
|
||||||
|
} else if (FLOAT_DATA_TYPE.indexOf(type) >= 0) {
|
||||||
|
return {type: 'float', value: parseFloat(value)};
|
||||||
|
} else if (BLOB_DATA_TYPE.indexOf(type) >= 0) {
|
||||||
|
return {type: 'blob', value};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if no type found assume type is nullable string
|
||||||
|
if (value === null) {
|
||||||
|
return {type: 'null', value: null};
|
||||||
|
} else {
|
||||||
|
return {type: 'string', value};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function constructQueryClause(
|
||||||
|
values: {[key: string]: Value},
|
||||||
|
connector: string,
|
||||||
|
): string {
|
||||||
|
return Object.entries(values).reduce(
|
||||||
|
(clauses, [key, val]: [string, Value], idx) => {
|
||||||
|
const {type, value} = val;
|
||||||
|
const valueString =
|
||||||
|
type === 'null'
|
||||||
|
? 'NULL'
|
||||||
|
: type === 'string' || type === 'blob'
|
||||||
|
? `'${value}'`
|
||||||
|
: `${value}`;
|
||||||
|
if (idx <= 0) {
|
||||||
|
return `${key}=${valueString}`;
|
||||||
|
} else {
|
||||||
|
return `${clauses} ${connector} ${key}=${valueString}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function constructUpdateQuery(
|
||||||
|
table: string,
|
||||||
|
where: {[key: string]: Value},
|
||||||
|
change: {[key: string]: Value},
|
||||||
|
): string {
|
||||||
|
return `UPDATE ${table}
|
||||||
|
SET ${constructQueryClause(change, ',')}
|
||||||
|
WHERE ${constructQueryClause(where, 'AND')}`;
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ import {DatabaseClient} from './ClientProtocol';
|
|||||||
import ButtonNavigation from './ButtonNavigation';
|
import ButtonNavigation from './ButtonNavigation';
|
||||||
import DatabaseDetailSidebar from './DatabaseDetailSidebar';
|
import DatabaseDetailSidebar from './DatabaseDetailSidebar';
|
||||||
import DatabaseStructure from './DatabaseStructure';
|
import DatabaseStructure from './DatabaseStructure';
|
||||||
|
import {convertStringToValue, constructUpdateQuery} from './UpdateQueryUtil';
|
||||||
import sqlFormatter from 'sql-formatter';
|
import sqlFormatter from 'sql-formatter';
|
||||||
import dateFormat from 'dateformat';
|
import dateFormat from 'dateformat';
|
||||||
|
|
||||||
@@ -977,6 +978,106 @@ export default class DatabasesPlugin extends FlipperPlugin<
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onRowEdited(change: {[key: string]: string | null}) {
|
||||||
|
const {
|
||||||
|
selectedDatabaseTable,
|
||||||
|
currentStructure,
|
||||||
|
viewMode,
|
||||||
|
currentPage,
|
||||||
|
} = this.state;
|
||||||
|
const highlightedRowIdx = currentPage?.highlightedRows[0] ?? -1;
|
||||||
|
const row =
|
||||||
|
highlightedRowIdx >= 0
|
||||||
|
? currentPage?.rows[currentPage?.highlightedRows[0]]
|
||||||
|
: undefined;
|
||||||
|
const columns = currentPage?.columns;
|
||||||
|
// currently only allow to edit data shown in Data tab
|
||||||
|
if (
|
||||||
|
viewMode !== 'data' ||
|
||||||
|
selectedDatabaseTable === null ||
|
||||||
|
currentStructure === null ||
|
||||||
|
currentPage === null ||
|
||||||
|
row === undefined ||
|
||||||
|
columns === undefined ||
|
||||||
|
// only trigger when there is change
|
||||||
|
Object.keys(change).length <= 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// check if the table has primary key to use for query
|
||||||
|
// This is assumed data are in the same format as in SqliteDatabaseDriver.java
|
||||||
|
const primaryKeyIdx = currentStructure.columns.indexOf('primary_key');
|
||||||
|
const nameKeyIdx = currentStructure.columns.indexOf('column_name');
|
||||||
|
const typeIdx = currentStructure.columns.indexOf('data_type');
|
||||||
|
const nullableIdx = currentStructure.columns.indexOf('nullable');
|
||||||
|
if (primaryKeyIdx < 0 && nameKeyIdx < 0 && typeIdx < 0) {
|
||||||
|
console.error(
|
||||||
|
'primary_key, column_name, and/or data_type cannot be empty',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const primaryColumnIndexes = currentStructure.rows
|
||||||
|
.reduce((acc, row) => {
|
||||||
|
const primary = row[primaryKeyIdx];
|
||||||
|
if (primary.type === 'boolean' && primary.value) {
|
||||||
|
const name = row[nameKeyIdx];
|
||||||
|
return name.type === 'string' ? acc.concat(name.value) : acc;
|
||||||
|
} else {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}, [] as Array<string>)
|
||||||
|
.map((name) => columns.indexOf(name))
|
||||||
|
.filter((idx) => idx >= 0);
|
||||||
|
// stop if no primary key to distinguish unique query
|
||||||
|
if (primaryColumnIndexes.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = currentStructure.rows.reduce((acc, row) => {
|
||||||
|
const nameValue = row[nameKeyIdx];
|
||||||
|
const name = nameValue.type === 'string' ? nameValue.value : null;
|
||||||
|
const typeValue = row[typeIdx];
|
||||||
|
const type = typeValue.type === 'string' ? typeValue.value : null;
|
||||||
|
const nullableValue =
|
||||||
|
nullableIdx < 0 ? {type: 'null', value: null} : row[nullableIdx];
|
||||||
|
const nullable = nullableValue.value !== false;
|
||||||
|
if (name !== null && type !== null) {
|
||||||
|
acc[name] = {type, nullable};
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as {[key: string]: {type: string; nullable: boolean}});
|
||||||
|
|
||||||
|
const changeValue = Object.entries(change).reduce(
|
||||||
|
(acc, [key, value]: [string, string | null]) => {
|
||||||
|
acc[key] = convertStringToValue(types, key, value);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as {[key: string]: Value},
|
||||||
|
);
|
||||||
|
this.dispatchAction({
|
||||||
|
type: 'Execute',
|
||||||
|
query: constructUpdateQuery(
|
||||||
|
selectedDatabaseTable,
|
||||||
|
primaryColumnIndexes.reduce((acc, idx) => {
|
||||||
|
acc[columns[idx]] = row[idx];
|
||||||
|
return acc;
|
||||||
|
}, {} as {[key: string]: Value}),
|
||||||
|
changeValue,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.dispatchAction({
|
||||||
|
type: 'UpdatePage',
|
||||||
|
...produce(currentPage, (draft) =>
|
||||||
|
Object.entries(changeValue).forEach(([key, value]: [string, Value]) => {
|
||||||
|
const columnIdx = draft.columns.indexOf(key);
|
||||||
|
if (columnIdx >= 0) {
|
||||||
|
draft.rows[highlightedRowIdx][columnIdx] = value;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderTable(page: Page | null) {
|
renderTable(page: Page | null) {
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user