Add databases plugin v0 (android) (#441)
Summary: Adds a plugin for listing the databases, tables and contents of those tables in an android app. Right now, works with sqlite, but it should be generic enough to work with other db types. ## Changelog Add initial version of android databases plugin Creating a PR, I may need to do some cleaning up, but this is to kick off that process. Pull Request resolved: https://github.com/facebook/flipper/pull/441 Reviewed By: danielbuechele Differential Revision: D15288831 Pulled By: jknoxville fbshipit-source-id: 6379ad60d640ea6b0a9473acc03dd6ea81a3a8d4
This commit is contained in:
committed by
Facebook Github Bot
parent
f20a781bca
commit
a630b50a8f
@@ -11,6 +11,7 @@ apply plugin: 'maven'
|
||||
android {
|
||||
compileSdkVersion rootProject.compileSdkVersion
|
||||
buildToolsVersion rootProject.buildToolsVersion
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.minSdkVersion
|
||||
@@ -67,6 +68,8 @@ android {
|
||||
|
||||
testImplementation deps.mockito
|
||||
testImplementation deps.robolectric
|
||||
testImplementation deps.testCore
|
||||
testImplementation deps.testRules
|
||||
testImplementation deps.hamcrest
|
||||
testImplementation deps.junit
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/**
|
||||
* 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.
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.sample;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.example.ExampleFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
@@ -51,6 +52,7 @@ public final class FlipperInitializer {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
client.addPlugin(new ExampleFlipperPlugin());
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.start();
|
||||
|
||||
final OkHttpClient okHttpClient =
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.sample;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
public class Database1Helper extends SQLiteOpenHelper {
|
||||
|
||||
// If you change the database schema, you must increment the database version.
|
||||
public static final int DATABASE_VERSION = 4;
|
||||
private static final String SQL_CREATE_FIRST_TABLE =
|
||||
"CREATE TABLE "
|
||||
+ "db1_first_table"
|
||||
+ " ("
|
||||
+ "_id INTEGER PRIMARY KEY,"
|
||||
+ "db1_col0_text TEXT,"
|
||||
+ "db1_col1_integer INTEGER,"
|
||||
+ "db1_col2_float FLOAT,"
|
||||
+ "db1_col3_blob TEXT,"
|
||||
+ "db1_col4_null TEXT DEFAULT NULL,"
|
||||
+ "db1_col5 TEXT,"
|
||||
+ "db1_col6 TEXT,"
|
||||
+ "db1_col7 TEXT,"
|
||||
+ "db1_col8 TEXT,"
|
||||
+ "db1_col9 TEXT"
|
||||
+ ")";
|
||||
private static final String SQL_CREATE_SECOND_TABLE =
|
||||
"CREATE TABLE "
|
||||
+ "db1_empty_table"
|
||||
+ " ("
|
||||
+ "_id INTEGER PRIMARY KEY,"
|
||||
+ "column1 TEXT,"
|
||||
+ "column2 TEXT)";
|
||||
|
||||
public Database1Helper(Context context) {
|
||||
super(context, "database1.db", null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SQL_CREATE_FIRST_TABLE);
|
||||
db.execSQL(SQL_CREATE_SECOND_TABLE);
|
||||
insertSampleData(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// This database is only a cache for online data, so its upgrade policy is
|
||||
// to simply to discard the data and start over
|
||||
db.execSQL("DROP TABLE IF EXISTS first_table");
|
||||
db.execSQL("DROP TABLE IF EXISTS second_table");
|
||||
db.execSQL("DROP TABLE IF EXISTS db1_first_table");
|
||||
db.execSQL("DROP TABLE IF EXISTS db1_empty_table");
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
onUpgrade(db, oldVersion, newVersion);
|
||||
}
|
||||
|
||||
public void insertSampleData(SQLiteDatabase db) {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put("db1_col0_text", "Long text data for testing resizing");
|
||||
contentValues.put("db1_col1_integer", 1000 + i);
|
||||
contentValues.put("db1_col2_float", 1000.465f + i);
|
||||
contentValues.put("db1_col3_blob", new byte[] {0, 0, 0, 1, 1, 0, 1, 1});
|
||||
contentValues.put("db1_col5", "db_1_column5_value");
|
||||
contentValues.put("db1_col6", "db_1_column6_value");
|
||||
contentValues.put("db1_col7", "db_1_column7_value");
|
||||
contentValues.put("db1_col8", "db_1_column8_value");
|
||||
contentValues.put("db1_col9", "db_1_column9_value");
|
||||
db.insert("db1_first_table", null, contentValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.sample;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
public class Database2Helper extends SQLiteOpenHelper {
|
||||
|
||||
// If you change the database schema, you must increment the database version.
|
||||
public static final int DATABASE_VERSION = 4;
|
||||
private static final String SQL_CREATE_FIRST_TABLE =
|
||||
"CREATE TABLE "
|
||||
+ "db2_first_table"
|
||||
+ " ("
|
||||
+ "_id INTEGER PRIMARY KEY,"
|
||||
+ "column1 TEXT,"
|
||||
+ "column2 TEXT)";
|
||||
private static final String SQL_CREATE_SECOND_TABLE =
|
||||
"CREATE TABLE "
|
||||
+ "db2_second_table"
|
||||
+ " ("
|
||||
+ "_id INTEGER PRIMARY KEY,"
|
||||
+ "column1 TEXT,"
|
||||
+ "column2 TEXT)";
|
||||
|
||||
public Database2Helper(Context context) {
|
||||
super(context, "database2.db", null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SQL_CREATE_FIRST_TABLE);
|
||||
db.execSQL(SQL_CREATE_SECOND_TABLE);
|
||||
insertSampleData(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// This database is only a cache for online data, so its upgrade policy is
|
||||
// to simply to discard the data and start over
|
||||
db.execSQL("DROP TABLE IF EXISTS first_table");
|
||||
db.execSQL("DROP TABLE IF EXISTS second_table");
|
||||
db.execSQL("DROP TABLE IF EXISTS db2_first_table");
|
||||
db.execSQL("DROP TABLE IF EXISTS db2_second_table");
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
onUpgrade(db, oldVersion, newVersion);
|
||||
}
|
||||
|
||||
public void insertSampleData(SQLiteDatabase db) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put("column1", "Long text data for testing resizing");
|
||||
contentValues.put(
|
||||
"column2",
|
||||
"extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra extra Long text data for testing resizing");
|
||||
db.insert("db2_first_table", null, contentValues);
|
||||
db.insert("db2_second_table", null, contentValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
/**
|
||||
* 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.
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.sample;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.database.DatabaseUtils;
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
@@ -33,5 +34,11 @@ public class FlipperSampleApplication extends Application {
|
||||
.edit()
|
||||
.putInt("SomeKey", 1337)
|
||||
.apply();
|
||||
|
||||
Database1Helper db1Helper = new Database1Helper(this);
|
||||
Database2Helper db2Helper = new Database2Helper(this);
|
||||
|
||||
DatabaseUtils.queryNumEntries(db1Helper.getReadableDatabase(), "db1_first_table", null, null);
|
||||
DatabaseUtils.queryNumEntries(db2Helper.getReadableDatabase(), "db2_first_table", null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.databases;
|
||||
|
||||
/**
|
||||
* Interface to describe a Database object. The DatabaseDescriptor#name() is visible and displayed
|
||||
* to the user
|
||||
*/
|
||||
public interface DatabaseDescriptor {
|
||||
String name();
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.databases;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.StringDef;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abstract class allowing to implement different drivers interfacing with Databases.
|
||||
*
|
||||
* @param <DESCRIPTOR> A DatabaseDescriptor object that is called for each databases provider by the
|
||||
* driver
|
||||
*/
|
||||
public abstract class DatabaseDriver<DESCRIPTOR extends DatabaseDescriptor> {
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
public DatabaseDriver(final Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
public abstract List<DESCRIPTOR> getDatabases();
|
||||
|
||||
public abstract List<String> getTableNames(DESCRIPTOR databaseDescriptor);
|
||||
|
||||
public abstract DatabaseGetTableDataResponse getTableData(
|
||||
DESCRIPTOR databaseDescriptor,
|
||||
String table,
|
||||
String order,
|
||||
boolean reverse,
|
||||
int start,
|
||||
int count);
|
||||
|
||||
public abstract DatabaseGetTableStructureResponse getTableStructure(
|
||||
DESCRIPTOR databaseDescriptor, String table);
|
||||
|
||||
public abstract DatabaseExecuteSqlResponse executeSQL(
|
||||
DESCRIPTOR databaseDescriptor, String query);
|
||||
|
||||
public static class DatabaseGetTableDataResponse {
|
||||
|
||||
public final List<String> columns;
|
||||
public final List<List<Object>> values;
|
||||
public final Integer start;
|
||||
public final Integer count;
|
||||
public final Long total;
|
||||
|
||||
public DatabaseGetTableDataResponse(
|
||||
final List<String> columns,
|
||||
final List<List<Object>> values,
|
||||
int start,
|
||||
int count,
|
||||
long total) {
|
||||
this.columns = columns;
|
||||
this.values = values;
|
||||
this.start = start;
|
||||
this.count = count;
|
||||
this.total = total;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DatabaseGetTableStructureResponse {
|
||||
|
||||
public final List<String> structureColumns;
|
||||
public final List<List<Object>> structureValues;
|
||||
public final List<String> indexesColumns;
|
||||
public final List<List<Object>> indexesValues;
|
||||
public final String definition;
|
||||
|
||||
public DatabaseGetTableStructureResponse(
|
||||
final List<String> structureColumns,
|
||||
final List<List<Object>> structureValues,
|
||||
final List<String> indexesColumns,
|
||||
final List<List<Object>> indexesValues,
|
||||
String definition) {
|
||||
this.structureColumns = structureColumns;
|
||||
this.structureValues = structureValues;
|
||||
this.indexesColumns = indexesColumns;
|
||||
this.indexesValues = indexesValues;
|
||||
this.definition = definition;
|
||||
}
|
||||
}
|
||||
|
||||
public static class DatabaseExecuteSqlResponse {
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@StringDef({TYPE_SELECT, TYPE_INSERT, TYPE_UPDATE_DELETE, TYPE_RAW})
|
||||
public @interface Type {}
|
||||
|
||||
public static final String TYPE_SELECT = "select";
|
||||
public static final String TYPE_INSERT = "insert";
|
||||
public static final String TYPE_UPDATE_DELETE = "update_delete";
|
||||
public static final String TYPE_RAW = "raw";
|
||||
|
||||
public final @Type String type;
|
||||
|
||||
// Select
|
||||
public final List<String> columns;
|
||||
public final List<List<Object>> values;
|
||||
|
||||
// insert
|
||||
public final Long insertedId;
|
||||
|
||||
// update/delete
|
||||
public final Integer affectedCount;
|
||||
|
||||
private DatabaseExecuteSqlResponse(
|
||||
final @Type String type,
|
||||
final List<String> columns,
|
||||
final List<List<Object>> values,
|
||||
Long insertedId,
|
||||
Integer affectedCount) {
|
||||
this.type = type;
|
||||
this.columns = columns;
|
||||
this.values = values;
|
||||
this.insertedId = insertedId;
|
||||
this.affectedCount = affectedCount;
|
||||
}
|
||||
|
||||
public static DatabaseExecuteSqlResponse successfulSelect(
|
||||
List<String> columns, List<List<Object>> values) {
|
||||
return new DatabaseExecuteSqlResponse(TYPE_SELECT, columns, values, null, null);
|
||||
}
|
||||
|
||||
public static DatabaseExecuteSqlResponse successfulInsert(long insertedId) {
|
||||
return new DatabaseExecuteSqlResponse(TYPE_INSERT, null, null, insertedId, null);
|
||||
}
|
||||
|
||||
public static DatabaseExecuteSqlResponse successfulUpdateDelete(int affectedRows) {
|
||||
return new DatabaseExecuteSqlResponse(TYPE_UPDATE_DELETE, null, null, null, affectedRows);
|
||||
}
|
||||
|
||||
public static DatabaseExecuteSqlResponse successfulRawQuery() {
|
||||
return new DatabaseExecuteSqlResponse(TYPE_RAW, null, null, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.databases;
|
||||
|
||||
public class DatabasesErrorCodes {
|
||||
|
||||
public static final int ERROR_INVALID_REQUEST = 1;
|
||||
public static final String ERROR_INVALID_REQUEST_MESSAGE = "The request received was invalid";
|
||||
public static final int ERROR_DATABASE_INVALID = 2;
|
||||
public static final String ERROR_DATABASE_INVALID_MESSAGE = "Could not access database";
|
||||
public static final int ERROR_SQL_EXECUTION_EXCEPTION = 3;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.databases;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.core.FlipperConnection;
|
||||
import com.facebook.flipper.core.FlipperPlugin;
|
||||
import com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class DatabasesFlipperPlugin implements FlipperPlugin {
|
||||
|
||||
private static final String ID = "Databases";
|
||||
|
||||
private final DatabasesManager databasesManager;
|
||||
|
||||
public DatabasesFlipperPlugin(Context context) {
|
||||
this(new SqliteDatabaseDriver(context));
|
||||
}
|
||||
|
||||
public DatabasesFlipperPlugin(DatabaseDriver databaseDriver) {
|
||||
this(Collections.singletonList(databaseDriver));
|
||||
}
|
||||
|
||||
public DatabasesFlipperPlugin(List<DatabaseDriver> databaseDriverList) {
|
||||
databasesManager = new DatabasesManager(databaseDriverList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect(FlipperConnection connection) {
|
||||
databasesManager.setConnection(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect() {
|
||||
databasesManager.setConnection(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean runInBackground() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.databases;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import com.facebook.flipper.core.FlipperArray;
|
||||
import com.facebook.flipper.core.FlipperConnection;
|
||||
import com.facebook.flipper.core.FlipperObject;
|
||||
import com.facebook.flipper.core.FlipperReceiver;
|
||||
import com.facebook.flipper.core.FlipperResponder;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver.DatabaseExecuteSqlResponse;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver.DatabaseGetTableDataResponse;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver.DatabaseGetTableStructureResponse;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DatabasesManager {
|
||||
|
||||
private static final String DATABASE_LIST_COMMAND = "databaseList";
|
||||
private static final String GET_TABLE_DATA_COMMAND = "getTableData";
|
||||
private static final String GET_TABLE_STRUCTURE_COMMAND = "getTableStructure";
|
||||
private static final String EXECUTE_COMMAND = "execute";
|
||||
|
||||
private final List<DatabaseDriver> mDatabaseDriverList;
|
||||
private final SparseArray<DatabaseDescriptorHolder> mDatabaseDescriptorHolderSparseArray;
|
||||
private final Set<DatabaseDescriptorHolder> mDatabaseDescriptorHolderSet;
|
||||
|
||||
private FlipperConnection mConnection;
|
||||
|
||||
public DatabasesManager(List<DatabaseDriver> databaseDriverList) {
|
||||
this.mDatabaseDriverList = databaseDriverList;
|
||||
this.mDatabaseDescriptorHolderSparseArray = new SparseArray<>();
|
||||
this.mDatabaseDescriptorHolderSet =
|
||||
new TreeSet<>(
|
||||
new Comparator<DatabaseDescriptorHolder>() {
|
||||
@Override
|
||||
public int compare(DatabaseDescriptorHolder o1, DatabaseDescriptorHolder o2) {
|
||||
return o1.databaseDescriptor.name().compareTo(o2.databaseDescriptor.name());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setConnection(@Nullable FlipperConnection connection) {
|
||||
this.mConnection = connection;
|
||||
if (connection != null) {
|
||||
listenForCommands(connection);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return mConnection != null;
|
||||
}
|
||||
|
||||
private void listenForCommands(FlipperConnection connection) {
|
||||
connection.receive(
|
||||
DATABASE_LIST_COMMAND,
|
||||
new FlipperReceiver() {
|
||||
@Override
|
||||
public void onReceive(FlipperObject params, FlipperResponder responder) {
|
||||
int databaseId = 1;
|
||||
mDatabaseDescriptorHolderSparseArray.clear();
|
||||
mDatabaseDescriptorHolderSet.clear();
|
||||
for (DatabaseDriver<?> databaseDriver : mDatabaseDriverList) {
|
||||
List<? extends DatabaseDescriptor> databaseDescriptorList =
|
||||
databaseDriver.getDatabases();
|
||||
for (DatabaseDescriptor databaseDescriptor : databaseDescriptorList) {
|
||||
int id = databaseId++;
|
||||
DatabaseDescriptorHolder databaseDescriptorHolder =
|
||||
new DatabaseDescriptorHolder(id, databaseDriver, databaseDescriptor);
|
||||
mDatabaseDescriptorHolderSparseArray.put(id, databaseDescriptorHolder);
|
||||
mDatabaseDescriptorHolderSet.add(databaseDescriptorHolder);
|
||||
}
|
||||
}
|
||||
FlipperArray result =
|
||||
ObjectMapper.databaseListToFlipperArray(mDatabaseDescriptorHolderSet);
|
||||
responder.success(result);
|
||||
}
|
||||
});
|
||||
connection.receive(
|
||||
GET_TABLE_DATA_COMMAND,
|
||||
new FlipperReceiver() {
|
||||
@Override
|
||||
public void onReceive(FlipperObject params, FlipperResponder responder) {
|
||||
GetTableDataRequest getTableDataRequest =
|
||||
ObjectMapper.flipperObjectToGetTableDataRequest(params);
|
||||
if (getTableDataRequest == null) {
|
||||
responder.error(
|
||||
ObjectMapper.toErrorFlipperObject(
|
||||
DatabasesErrorCodes.ERROR_INVALID_REQUEST,
|
||||
DatabasesErrorCodes.ERROR_INVALID_REQUEST_MESSAGE));
|
||||
} else {
|
||||
DatabaseDescriptorHolder databaseDescriptorHolder =
|
||||
mDatabaseDescriptorHolderSparseArray.get(getTableDataRequest.databaseId);
|
||||
if (databaseDescriptorHolder == null) {
|
||||
responder.error(
|
||||
ObjectMapper.toErrorFlipperObject(
|
||||
DatabasesErrorCodes.ERROR_DATABASE_INVALID,
|
||||
DatabasesErrorCodes.ERROR_DATABASE_INVALID_MESSAGE));
|
||||
} else {
|
||||
try {
|
||||
DatabaseGetTableDataResponse databaseGetTableDataResponse =
|
||||
databaseDescriptorHolder.databaseDriver.getTableData(
|
||||
databaseDescriptorHolder.databaseDescriptor,
|
||||
getTableDataRequest.table,
|
||||
getTableDataRequest.order,
|
||||
getTableDataRequest.reverse,
|
||||
getTableDataRequest.start,
|
||||
getTableDataRequest.count);
|
||||
responder.success(
|
||||
ObjectMapper.databaseGetTableDataReponseToFlipperObject(
|
||||
databaseGetTableDataResponse));
|
||||
} catch (Exception e) {
|
||||
responder.error(
|
||||
ObjectMapper.toErrorFlipperObject(
|
||||
DatabasesErrorCodes.ERROR_SQL_EXECUTION_EXCEPTION, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
connection.receive(
|
||||
GET_TABLE_STRUCTURE_COMMAND,
|
||||
new FlipperReceiver() {
|
||||
@Override
|
||||
public void onReceive(FlipperObject params, FlipperResponder responder) {
|
||||
GetTableStructureRequest getTableStructureRequest =
|
||||
ObjectMapper.flipperObjectToGetTableStructureRequest(params);
|
||||
if (getTableStructureRequest == null) {
|
||||
responder.error(
|
||||
ObjectMapper.toErrorFlipperObject(
|
||||
DatabasesErrorCodes.ERROR_INVALID_REQUEST,
|
||||
DatabasesErrorCodes.ERROR_INVALID_REQUEST_MESSAGE));
|
||||
} else {
|
||||
DatabaseDescriptorHolder databaseDescriptorHolder =
|
||||
mDatabaseDescriptorHolderSparseArray.get(getTableStructureRequest.databaseId);
|
||||
if (databaseDescriptorHolder == null) {
|
||||
responder.error(
|
||||
ObjectMapper.toErrorFlipperObject(
|
||||
DatabasesErrorCodes.ERROR_DATABASE_INVALID,
|
||||
DatabasesErrorCodes.ERROR_DATABASE_INVALID_MESSAGE));
|
||||
} else {
|
||||
try {
|
||||
DatabaseGetTableStructureResponse databaseGetTableStructureResponse =
|
||||
databaseDescriptorHolder.databaseDriver.getTableStructure(
|
||||
databaseDescriptorHolder.databaseDescriptor,
|
||||
getTableStructureRequest.table);
|
||||
responder.success(
|
||||
ObjectMapper.databaseGetTableStructureReponseToFlipperObject(
|
||||
databaseGetTableStructureResponse));
|
||||
} catch (Exception e) {
|
||||
responder.error(
|
||||
ObjectMapper.toErrorFlipperObject(
|
||||
DatabasesErrorCodes.ERROR_SQL_EXECUTION_EXCEPTION, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
connection.receive(
|
||||
EXECUTE_COMMAND,
|
||||
new FlipperReceiver() {
|
||||
@Override
|
||||
public void onReceive(FlipperObject params, FlipperResponder responder) {
|
||||
ExecuteSqlRequest executeSqlRequest =
|
||||
ObjectMapper.flipperObjectToExecuteSqlRequest(params);
|
||||
if (executeSqlRequest == null) {
|
||||
responder.error(
|
||||
ObjectMapper.toErrorFlipperObject(
|
||||
DatabasesErrorCodes.ERROR_INVALID_REQUEST,
|
||||
DatabasesErrorCodes.ERROR_INVALID_REQUEST_MESSAGE));
|
||||
} else {
|
||||
DatabaseDescriptorHolder databaseDescriptorHolder =
|
||||
mDatabaseDescriptorHolderSparseArray.get(executeSqlRequest.databaseId);
|
||||
if (databaseDescriptorHolder == null) {
|
||||
responder.error(
|
||||
ObjectMapper.toErrorFlipperObject(
|
||||
DatabasesErrorCodes.ERROR_DATABASE_INVALID,
|
||||
DatabasesErrorCodes.ERROR_DATABASE_INVALID_MESSAGE));
|
||||
} else {
|
||||
try {
|
||||
DatabaseExecuteSqlResponse databaseExecuteSqlResponse =
|
||||
databaseDescriptorHolder.databaseDriver.executeSQL(
|
||||
databaseDescriptorHolder.databaseDescriptor, executeSqlRequest.value);
|
||||
responder.success(
|
||||
ObjectMapper.databaseExecuteSqlResponseToFlipperObject(
|
||||
databaseExecuteSqlResponse));
|
||||
} catch (Exception e) {
|
||||
responder.error(
|
||||
ObjectMapper.toErrorFlipperObject(
|
||||
DatabasesErrorCodes.ERROR_SQL_EXECUTION_EXCEPTION, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static class DatabaseDescriptorHolder {
|
||||
|
||||
public final int id;
|
||||
public final DatabaseDriver databaseDriver;
|
||||
public final DatabaseDescriptor databaseDescriptor;
|
||||
|
||||
public DatabaseDescriptorHolder(
|
||||
int id, DatabaseDriver databaseDriver, DatabaseDescriptor databaseDescriptor) {
|
||||
this.id = id;
|
||||
this.databaseDriver = databaseDriver;
|
||||
this.databaseDescriptor = databaseDescriptor;
|
||||
}
|
||||
}
|
||||
|
||||
static class ExecuteSqlRequest {
|
||||
|
||||
public final int databaseId;
|
||||
public final String value;
|
||||
|
||||
ExecuteSqlRequest(int databaseId, String value) {
|
||||
this.databaseId = databaseId;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
static class GetTableDataRequest {
|
||||
|
||||
public final int databaseId;
|
||||
public final String table;
|
||||
public final String order;
|
||||
public final boolean reverse;
|
||||
public final int start;
|
||||
public final int count;
|
||||
|
||||
GetTableDataRequest(
|
||||
int databaseId, String table, String order, boolean reverse, int start, int count) {
|
||||
this.databaseId = databaseId;
|
||||
this.table = table;
|
||||
this.order = order;
|
||||
this.reverse = reverse;
|
||||
this.start = start;
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
static class GetTableStructureRequest {
|
||||
|
||||
public final int databaseId;
|
||||
public final String table;
|
||||
|
||||
GetTableStructureRequest(int databaseId, String table) {
|
||||
this.databaseId = databaseId;
|
||||
this.table = table;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.databases;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import com.facebook.flipper.core.FlipperArray;
|
||||
import com.facebook.flipper.core.FlipperArray.Builder;
|
||||
import com.facebook.flipper.core.FlipperObject;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver.DatabaseExecuteSqlResponse;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver.DatabaseGetTableDataResponse;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver.DatabaseGetTableStructureResponse;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesManager.DatabaseDescriptorHolder;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesManager.ExecuteSqlRequest;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesManager.GetTableDataRequest;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesManager.GetTableStructureRequest;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ObjectMapper {
|
||||
|
||||
private static final int MAX_BLOB_LENGTH = 512;
|
||||
private static final String UNKNOWN_BLOB_LABEL = "{blob}";
|
||||
|
||||
public static FlipperArray databaseListToFlipperArray(
|
||||
Collection<DatabaseDescriptorHolder> databaseDescriptorHolderList) {
|
||||
FlipperArray.Builder builder = new FlipperArray.Builder();
|
||||
|
||||
for (DatabaseDescriptorHolder databaseDescriptorHolder : databaseDescriptorHolderList) {
|
||||
|
||||
List<String> tableNameList =
|
||||
databaseDescriptorHolder.databaseDriver.getTableNames(
|
||||
databaseDescriptorHolder.databaseDescriptor);
|
||||
Collections.sort(tableNameList);
|
||||
FlipperArray.Builder tableBuilder = new Builder();
|
||||
for (String tablename : tableNameList) {
|
||||
tableBuilder.put(tablename);
|
||||
}
|
||||
|
||||
builder.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("id", databaseDescriptorHolder.id)
|
||||
.put("name", databaseDescriptorHolder.databaseDescriptor.name())
|
||||
.put("tables", tableBuilder.build())
|
||||
.build());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static GetTableDataRequest flipperObjectToGetTableDataRequest(FlipperObject params) {
|
||||
int databaseId = params.getInt("databaseId");
|
||||
String table = params.getString("table");
|
||||
String order = params.getString("order");
|
||||
boolean reverse = params.getBoolean("reverse");
|
||||
int start = params.getInt("start");
|
||||
int count = params.getInt("count");
|
||||
if (databaseId <= 0 || TextUtils.isEmpty(table)) {
|
||||
return null;
|
||||
}
|
||||
return new GetTableDataRequest(databaseId, table, order, reverse, start, count);
|
||||
}
|
||||
|
||||
public static GetTableStructureRequest flipperObjectToGetTableStructureRequest(
|
||||
FlipperObject params) {
|
||||
int databaseId = params.getInt("databaseId");
|
||||
String table = params.getString("table");
|
||||
if (databaseId <= 0 || TextUtils.isEmpty(table)) {
|
||||
return null;
|
||||
}
|
||||
return new GetTableStructureRequest(databaseId, table);
|
||||
}
|
||||
|
||||
public static ExecuteSqlRequest flipperObjectToExecuteSqlRequest(FlipperObject params) {
|
||||
int databaseId = params.getInt("databaseId");
|
||||
String value = params.getString("value");
|
||||
if (databaseId <= 0 || TextUtils.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
return new ExecuteSqlRequest(databaseId, value);
|
||||
}
|
||||
|
||||
public static FlipperObject databaseGetTableDataReponseToFlipperObject(
|
||||
DatabaseGetTableDataResponse databaseGetTableDataResponse) {
|
||||
|
||||
FlipperArray.Builder columnBuilder = new FlipperArray.Builder();
|
||||
for (String columnName : databaseGetTableDataResponse.columns) {
|
||||
columnBuilder.put(columnName);
|
||||
}
|
||||
|
||||
FlipperArray.Builder rowBuilder = new FlipperArray.Builder();
|
||||
for (List<Object> row : databaseGetTableDataResponse.values) {
|
||||
FlipperArray.Builder valueBuilder = new FlipperArray.Builder();
|
||||
for (Object item : row) {
|
||||
valueBuilder.put(objectAndTypeToFlipperObject(item));
|
||||
}
|
||||
rowBuilder.put(valueBuilder.build());
|
||||
}
|
||||
|
||||
return new FlipperObject.Builder()
|
||||
.put("columns", columnBuilder.build())
|
||||
.put("values", rowBuilder.build())
|
||||
.put("start", databaseGetTableDataResponse.start)
|
||||
.put("count", databaseGetTableDataResponse.count)
|
||||
.put("total", databaseGetTableDataResponse.total)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static FlipperObject databaseGetTableStructureReponseToFlipperObject(
|
||||
DatabaseGetTableStructureResponse databaseGetTableStructureResponse) {
|
||||
|
||||
FlipperArray.Builder structureColumnBuilder = new FlipperArray.Builder();
|
||||
for (String columnName : databaseGetTableStructureResponse.structureColumns) {
|
||||
structureColumnBuilder.put(columnName);
|
||||
}
|
||||
|
||||
FlipperArray.Builder indexesColumnBuilder = new FlipperArray.Builder();
|
||||
for (String columnName : databaseGetTableStructureResponse.indexesColumns) {
|
||||
indexesColumnBuilder.put(columnName);
|
||||
}
|
||||
|
||||
FlipperArray.Builder structureValuesBuilder = new FlipperArray.Builder();
|
||||
for (List<Object> row : databaseGetTableStructureResponse.structureValues) {
|
||||
FlipperArray.Builder valueBuilder = new FlipperArray.Builder();
|
||||
for (Object item : row) {
|
||||
valueBuilder.put(objectAndTypeToFlipperObject(item));
|
||||
}
|
||||
structureValuesBuilder.put(valueBuilder.build());
|
||||
}
|
||||
|
||||
FlipperArray.Builder indexesValuesBuilder = new FlipperArray.Builder();
|
||||
for (List<Object> row : databaseGetTableStructureResponse.indexesValues) {
|
||||
FlipperArray.Builder valueBuilder = new FlipperArray.Builder();
|
||||
for (Object item : row) {
|
||||
valueBuilder.put(objectAndTypeToFlipperObject(item));
|
||||
}
|
||||
indexesValuesBuilder.put(valueBuilder.build());
|
||||
}
|
||||
|
||||
return new FlipperObject.Builder()
|
||||
.put("structureColumns", structureColumnBuilder.build())
|
||||
.put("structureValues", structureValuesBuilder.build())
|
||||
.put("indexesColumns", indexesColumnBuilder.build())
|
||||
.put("indexesValues", indexesValuesBuilder.build())
|
||||
.put("definition", databaseGetTableStructureResponse.definition)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static FlipperObject databaseExecuteSqlResponseToFlipperObject(
|
||||
DatabaseExecuteSqlResponse databaseExecuteSqlResponse) {
|
||||
|
||||
FlipperArray.Builder columnBuilder = new FlipperArray.Builder();
|
||||
for (String columnName : databaseExecuteSqlResponse.columns) {
|
||||
columnBuilder.put(columnName);
|
||||
}
|
||||
|
||||
FlipperArray.Builder rowBuilder = new FlipperArray.Builder();
|
||||
for (List<Object> row : databaseExecuteSqlResponse.values) {
|
||||
FlipperArray.Builder valueBuilder = new FlipperArray.Builder();
|
||||
for (Object item : row) {
|
||||
valueBuilder.put(objectAndTypeToFlipperObject(item));
|
||||
}
|
||||
rowBuilder.put(valueBuilder.build());
|
||||
}
|
||||
|
||||
return new FlipperObject.Builder()
|
||||
.put("type", databaseExecuteSqlResponse.type)
|
||||
.put("columns", columnBuilder.build())
|
||||
.put("values", rowBuilder.build())
|
||||
.put("insertedId", databaseExecuteSqlResponse.insertedId)
|
||||
.put("affectedCount", databaseExecuteSqlResponse.affectedCount)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static FlipperObject objectAndTypeToFlipperObject(Object object) {
|
||||
if (object == null) {
|
||||
return new FlipperObject.Builder().put("type", "null").build();
|
||||
} else if (object instanceof Long) {
|
||||
return new FlipperObject.Builder().put("type", "integer").put("value", object).build();
|
||||
} else if (object instanceof Double) {
|
||||
return new FlipperObject.Builder().put("type", "float").put("value", object).build();
|
||||
} else if (object instanceof String) {
|
||||
return new FlipperObject.Builder().put("type", "string").put("value", object).build();
|
||||
} else if (object instanceof byte[]) {
|
||||
return new FlipperObject.Builder()
|
||||
.put("type", "blob")
|
||||
.put("value", blobToString((byte[]) object))
|
||||
.build();
|
||||
} else if (object instanceof Boolean) {
|
||||
return new FlipperObject.Builder().put("type", "boolean").put("value", object).build();
|
||||
} else {
|
||||
throw new IllegalArgumentException("type of Object is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private static String blobToString(byte[] blob) {
|
||||
if (blob.length <= MAX_BLOB_LENGTH) {
|
||||
if (fastIsAscii(blob)) {
|
||||
try {
|
||||
return new String(blob, "US-ASCII");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Fall through...
|
||||
}
|
||||
}
|
||||
}
|
||||
return UNKNOWN_BLOB_LABEL;
|
||||
}
|
||||
|
||||
private static boolean fastIsAscii(byte[] blob) {
|
||||
for (byte b : blob) {
|
||||
if ((b & ~0x7f) != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static FlipperObject toErrorFlipperObject(int code, String message) {
|
||||
return new FlipperObject.Builder().put("code", code).put("message", message).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.databases.impl;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import java.io.File;
|
||||
|
||||
public interface SqliteDatabaseConnectionProvider {
|
||||
|
||||
SQLiteDatabase openDatabase(File databaseFile) throws SQLiteException;
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.databases.impl;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver;
|
||||
import com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver.SqliteDatabaseDescriptor;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class SqliteDatabaseDriver extends DatabaseDriver<SqliteDatabaseDescriptor> {
|
||||
|
||||
private static final String SCHEMA_TABLE = "sqlite_master";
|
||||
private static final String[] UNINTERESTING_FILENAME_SUFFIXES =
|
||||
new String[] {"-journal", "-shm", "-uid", "-wal"};
|
||||
|
||||
private final SqliteDatabaseProvider sqliteDatabaseProvider;
|
||||
private final SqliteDatabaseConnectionProvider sqliteDatabaseConnectionProvider;
|
||||
|
||||
public SqliteDatabaseDriver(final Context context) {
|
||||
this(
|
||||
context,
|
||||
new SqliteDatabaseProvider() {
|
||||
@Override
|
||||
public List<File> getDatabaseFiles() {
|
||||
List<File> databaseFiles = new ArrayList<>();
|
||||
for (String databaseName : context.databaseList()) {
|
||||
databaseFiles.add(context.getDatabasePath(databaseName));
|
||||
}
|
||||
return databaseFiles;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public SqliteDatabaseDriver(
|
||||
final Context context, final SqliteDatabaseProvider sqliteDatabaseProvider) {
|
||||
this(
|
||||
context,
|
||||
sqliteDatabaseProvider,
|
||||
new SqliteDatabaseConnectionProvider() {
|
||||
@Override
|
||||
public SQLiteDatabase openDatabase(File databaseFile) throws SQLiteException {
|
||||
int flags = SQLiteDatabase.OPEN_READWRITE;
|
||||
return SQLiteDatabase.openDatabase(databaseFile.getAbsolutePath(), null, flags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public SqliteDatabaseDriver(
|
||||
final Context context,
|
||||
final SqliteDatabaseProvider sqliteDatabaseProvider,
|
||||
final SqliteDatabaseConnectionProvider sqliteDatabaseConnectionProvider) {
|
||||
super(context);
|
||||
this.sqliteDatabaseProvider = sqliteDatabaseProvider;
|
||||
this.sqliteDatabaseConnectionProvider = sqliteDatabaseConnectionProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SqliteDatabaseDescriptor> getDatabases() {
|
||||
ArrayList<SqliteDatabaseDescriptor> databases = new ArrayList<>();
|
||||
List<File> potentialDatabaseFiles = sqliteDatabaseProvider.getDatabaseFiles();
|
||||
Collections.sort(potentialDatabaseFiles);
|
||||
Iterable<File> tidiedList = tidyDatabaseList(potentialDatabaseFiles);
|
||||
for (File database : tidiedList) {
|
||||
databases.add(new SqliteDatabaseDescriptor(database));
|
||||
}
|
||||
return databases;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTableNames(SqliteDatabaseDescriptor databaseDescriptor) {
|
||||
SQLiteDatabase database =
|
||||
sqliteDatabaseConnectionProvider.openDatabase(databaseDescriptor.file);
|
||||
try {
|
||||
Cursor cursor =
|
||||
database.rawQuery(
|
||||
"SELECT name FROM " + SCHEMA_TABLE + " WHERE type IN (?, ?)",
|
||||
new String[] {"table", "view"});
|
||||
try {
|
||||
List<String> tableNames = new ArrayList<>();
|
||||
while (cursor.moveToNext()) {
|
||||
tableNames.add(cursor.getString(0));
|
||||
}
|
||||
return tableNames;
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseExecuteSqlResponse executeSQL(
|
||||
SqliteDatabaseDescriptor databaseDescriptor, String query) {
|
||||
SQLiteDatabase database =
|
||||
sqliteDatabaseConnectionProvider.openDatabase(databaseDescriptor.file);
|
||||
try {
|
||||
String firstWordUpperCase = getFirstWord(query).toUpperCase();
|
||||
switch (firstWordUpperCase) {
|
||||
case "UPDATE":
|
||||
case "DELETE":
|
||||
return executeUpdateDelete(database, query);
|
||||
case "INSERT":
|
||||
return executeInsert(database, query);
|
||||
case "SELECT":
|
||||
case "PRAGMA":
|
||||
case "EXPLAIN":
|
||||
return executeSelect(database, query);
|
||||
default:
|
||||
return executeRawQuery(database, query);
|
||||
}
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseGetTableDataResponse getTableData(
|
||||
SqliteDatabaseDescriptor databaseDescriptor,
|
||||
String table,
|
||||
@Nullable String order,
|
||||
boolean reverse,
|
||||
int start,
|
||||
int count) {
|
||||
SQLiteDatabase database =
|
||||
sqliteDatabaseConnectionProvider.openDatabase(databaseDescriptor.file);
|
||||
try {
|
||||
String orderBy = order != null ? order + (reverse ? " DESC" : " ASC") : null;
|
||||
String limitBy = start + ", " + count;
|
||||
Cursor cursor = database.query(table, null, null, null, null, null, orderBy, limitBy);
|
||||
long total = DatabaseUtils.queryNumEntries(database, table);
|
||||
try {
|
||||
String[] columnNames = cursor.getColumnNames();
|
||||
List<List<Object>> rows = cursorToList(cursor);
|
||||
return new DatabaseGetTableDataResponse(
|
||||
Arrays.asList(columnNames), rows, start, rows.size(), total);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseGetTableStructureResponse getTableStructure(
|
||||
SqliteDatabaseDescriptor databaseDescriptor, String table) {
|
||||
SQLiteDatabase database =
|
||||
sqliteDatabaseConnectionProvider.openDatabase(databaseDescriptor.file);
|
||||
try {
|
||||
Cursor structureCursor = database.rawQuery("PRAGMA table_info(" + table + ")", null);
|
||||
Cursor foreignKeysCursor = database.rawQuery("PRAGMA foreign_key_list(" + table + ")", null);
|
||||
Cursor indexesCursor = database.rawQuery("PRAGMA index_list(" + table + ")", null);
|
||||
Cursor definitionCursor =
|
||||
database.rawQuery(
|
||||
"SELECT sql FROM " + SCHEMA_TABLE + " WHERE name=?", new String[] {table});
|
||||
try {
|
||||
// Structure & foreign keys
|
||||
|
||||
List<String> structureColumns =
|
||||
Arrays.asList(
|
||||
"column_name", "data_type", "nullable", "default", "primary_key", "foreign_key");
|
||||
List<List<Object>> structureValues = new ArrayList<>();
|
||||
Map<String, String> foreignKeyValues = new HashMap<>();
|
||||
|
||||
while (foreignKeysCursor.moveToNext()) {
|
||||
foreignKeyValues.put(
|
||||
foreignKeysCursor.getString(foreignKeysCursor.getColumnIndex("from")),
|
||||
foreignKeysCursor.getString(foreignKeysCursor.getColumnIndex("table"))
|
||||
+ "("
|
||||
+ foreignKeysCursor.getString(foreignKeysCursor.getColumnIndex("to"))
|
||||
+ ")");
|
||||
}
|
||||
|
||||
while (structureCursor.moveToNext()) {
|
||||
|
||||
String columnName = structureCursor.getString(structureCursor.getColumnIndex("name"));
|
||||
String foreignKey =
|
||||
foreignKeyValues.containsKey(columnName) ? foreignKeyValues.get(columnName) : null;
|
||||
|
||||
structureValues.add(
|
||||
Arrays.asList(
|
||||
columnName,
|
||||
structureCursor.getString(structureCursor.getColumnIndex("type")),
|
||||
structureCursor.getInt(structureCursor.getColumnIndex("notnull"))
|
||||
== 0, // true if Nullable, false otherwise
|
||||
getObjectFromColumnIndex(
|
||||
structureCursor, structureCursor.getColumnIndex("dflt_value")),
|
||||
structureCursor.getInt(structureCursor.getColumnIndex("pk")) == 1,
|
||||
foreignKey));
|
||||
}
|
||||
|
||||
// Indexes
|
||||
|
||||
List<String> indexesColumns = Arrays.asList("index_name", "unique", "indexed_column_name");
|
||||
List<List<Object>> indexesValues = new ArrayList<>();
|
||||
|
||||
while (indexesCursor.moveToNext()) {
|
||||
List<String> indexedColumnNames = new ArrayList<>();
|
||||
String indexName = indexesCursor.getString(indexesCursor.getColumnIndex("name"));
|
||||
Cursor indexInfoCursor = database.rawQuery("PRAGMA index_info(" + indexName + ")", null);
|
||||
try {
|
||||
while (indexInfoCursor.moveToNext()) {
|
||||
indexedColumnNames.add(
|
||||
indexInfoCursor.getString(indexInfoCursor.getColumnIndex("name")));
|
||||
}
|
||||
indexesValues.add(
|
||||
Arrays.<Object>asList(
|
||||
indexName,
|
||||
indexesCursor.getInt(indexesCursor.getColumnIndex("unique")) == 1,
|
||||
TextUtils.join(",", indexedColumnNames)));
|
||||
} finally {
|
||||
indexInfoCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Definition
|
||||
|
||||
definitionCursor.moveToFirst();
|
||||
String definition = definitionCursor.getString(definitionCursor.getColumnIndex("sql"));
|
||||
|
||||
return new DatabaseGetTableStructureResponse(
|
||||
structureColumns, structureValues, indexesColumns, indexesValues, definition);
|
||||
} finally {
|
||||
structureCursor.close();
|
||||
foreignKeysCursor.close();
|
||||
indexesCursor.close();
|
||||
}
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<File> tidyDatabaseList(List<File> databaseFiles) {
|
||||
Set<File> originalAsSet = new HashSet<>(databaseFiles);
|
||||
List<File> tidiedList = new ArrayList<>();
|
||||
for (File databaseFile : databaseFiles) {
|
||||
String databaseFilename = databaseFile.getPath();
|
||||
String sansSuffix = removeSuffix(databaseFilename, UNINTERESTING_FILENAME_SUFFIXES);
|
||||
if (sansSuffix.equals(databaseFilename) || !originalAsSet.contains(new File(sansSuffix))) {
|
||||
tidiedList.add(databaseFile);
|
||||
}
|
||||
}
|
||||
return tidiedList;
|
||||
}
|
||||
|
||||
private static String removeSuffix(String str, String[] suffixesToRemove) {
|
||||
for (String suffix : suffixesToRemove) {
|
||||
if (str.endsWith(suffix)) {
|
||||
return str.substring(0, str.length() - suffix.length());
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
private static String getFirstWord(String s) {
|
||||
s = s.trim();
|
||||
int firstSpace = s.indexOf(' ');
|
||||
return firstSpace >= 0 ? s.substring(0, firstSpace) : s;
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeUpdateDelete(
|
||||
SQLiteDatabase database, String query) {
|
||||
SQLiteStatement statement = database.compileStatement(query);
|
||||
int count = statement.executeUpdateDelete();
|
||||
return DatabaseExecuteSqlResponse.successfulUpdateDelete(count);
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeInsert(SQLiteDatabase database, String query) {
|
||||
SQLiteStatement statement = database.compileStatement(query);
|
||||
long insertedId = statement.executeInsert();
|
||||
return DatabaseExecuteSqlResponse.successfulInsert(insertedId);
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeSelect(SQLiteDatabase database, String query) {
|
||||
Cursor cursor = database.rawQuery(query, null);
|
||||
try {
|
||||
String[] columnNames = cursor.getColumnNames();
|
||||
List<List<Object>> rows = cursorToList(cursor);
|
||||
return DatabaseExecuteSqlResponse.successfulSelect(Arrays.asList(columnNames), rows);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static DatabaseExecuteSqlResponse executeRawQuery(SQLiteDatabase database, String query) {
|
||||
database.execSQL(query);
|
||||
return DatabaseExecuteSqlResponse.successfulRawQuery();
|
||||
}
|
||||
|
||||
private static List<List<Object>> cursorToList(Cursor cursor) {
|
||||
List<List<Object>> rows = new ArrayList<>();
|
||||
final int numColumns = cursor.getColumnCount();
|
||||
while (cursor.moveToNext()) {
|
||||
List<Object> values = new ArrayList<>();
|
||||
for (int column = 0; column < numColumns; column++) {
|
||||
values.add(getObjectFromColumnIndex(cursor, column));
|
||||
}
|
||||
rows.add(values);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static Object getObjectFromColumnIndex(Cursor cursor, int column) {
|
||||
switch (cursor.getType(column)) {
|
||||
case Cursor.FIELD_TYPE_NULL:
|
||||
return null;
|
||||
case Cursor.FIELD_TYPE_INTEGER:
|
||||
return cursor.getLong(column);
|
||||
case Cursor.FIELD_TYPE_FLOAT:
|
||||
return cursor.getDouble(column);
|
||||
case Cursor.FIELD_TYPE_BLOB:
|
||||
return cursor.getBlob(column);
|
||||
case Cursor.FIELD_TYPE_STRING:
|
||||
default:
|
||||
return cursor.getString(column);
|
||||
}
|
||||
}
|
||||
|
||||
static class SqliteDatabaseDescriptor implements DatabaseDescriptor {
|
||||
|
||||
public final File file;
|
||||
|
||||
public SqliteDatabaseDescriptor(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return file.getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.databases.impl;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
public interface SqliteDatabaseProvider {
|
||||
|
||||
List<File> getDatabaseFiles();
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.databases;
|
||||
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import com.facebook.flipper.core.FlipperArray;
|
||||
import com.facebook.flipper.core.FlipperObject;
|
||||
import com.facebook.flipper.plugins.databases.DatabaseDriver.DatabaseExecuteSqlResponse;
|
||||
import com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver;
|
||||
import com.facebook.flipper.plugins.databases.impl.SqliteDatabaseProvider;
|
||||
import com.facebook.flipper.testing.FlipperConnectionMock;
|
||||
import com.facebook.flipper.testing.FlipperResponderMock;
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class DatabasesFlipperPluginTest {
|
||||
|
||||
FlipperConnectionMock connectionMock;
|
||||
FlipperResponderMock responderMock;
|
||||
|
||||
DatabaseHelper databaseHelper1, databaseHelper2;
|
||||
DatabasesFlipperPlugin plugin;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
connectionMock = new FlipperConnectionMock();
|
||||
responderMock = new FlipperResponderMock();
|
||||
|
||||
databaseHelper1 =
|
||||
new DatabaseHelper(ApplicationProvider.getApplicationContext(), "database1.db");
|
||||
databaseHelper1
|
||||
.getWritableDatabase()
|
||||
.execSQL("INSERT INTO first_table (column1, column2) VALUES('a','b')");
|
||||
databaseHelper2 =
|
||||
new DatabaseHelper(ApplicationProvider.getApplicationContext(), "database2.db");
|
||||
databaseHelper2
|
||||
.getWritableDatabase()
|
||||
.execSQL("INSERT INTO first_table (column1, column2) VALUES('a','b')");
|
||||
plugin =
|
||||
new DatabasesFlipperPlugin(
|
||||
new SqliteDatabaseDriver(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
new SqliteDatabaseProvider() {
|
||||
@Override
|
||||
public List<File> getDatabaseFiles() {
|
||||
return Arrays.asList(
|
||||
ApplicationProvider.getApplicationContext()
|
||||
.getDatabasePath(databaseHelper1.getDatabaseName()),
|
||||
ApplicationProvider.getApplicationContext()
|
||||
.getDatabasePath(databaseHelper2.getDatabaseName()));
|
||||
}
|
||||
}));
|
||||
|
||||
plugin.onConnect(connectionMock);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
databaseHelper1.close();
|
||||
ApplicationProvider.getApplicationContext().deleteDatabase(databaseHelper1.getDatabaseName());
|
||||
ApplicationProvider.getApplicationContext().deleteDatabase(databaseHelper2.getDatabaseName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandDatabaseList() throws Exception {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
connectionMock.receivers.get("databaseList").onReceive(null, responderMock);
|
||||
|
||||
// Assert
|
||||
assertThat(
|
||||
responderMock.successes,
|
||||
hasItem(
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("id", 1)
|
||||
.put("name", databaseHelper1.getDatabaseName())
|
||||
.put(
|
||||
"tables",
|
||||
new FlipperArray.Builder()
|
||||
.put("android_metadata")
|
||||
.put("first_table")
|
||||
.put("second_table")
|
||||
.put("sqlite_sequence")))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("id", 2)
|
||||
.put("name", databaseHelper2.getDatabaseName())
|
||||
.put(
|
||||
"tables",
|
||||
new FlipperArray.Builder()
|
||||
.put("android_metadata")
|
||||
.put("first_table")
|
||||
.put("second_table")
|
||||
.put("sqlite_sequence")))
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandGetTableDataInvalidParams() throws Exception {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
connectionMock
|
||||
.receivers
|
||||
.get("getTableData")
|
||||
.onReceive(
|
||||
new FlipperObject.Builder()
|
||||
.put("databaseId", -1) // Wrong id
|
||||
.put("table", "") // invalid, can't be empty
|
||||
.build(),
|
||||
responderMock);
|
||||
|
||||
// Assert
|
||||
assertThat(
|
||||
responderMock.errors,
|
||||
hasItem(
|
||||
new FlipperObject.Builder()
|
||||
.put("code", DatabasesErrorCodes.ERROR_INVALID_REQUEST)
|
||||
.put("message", DatabasesErrorCodes.ERROR_INVALID_REQUEST_MESSAGE)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandGetTableDataInvalidDatabase() throws Exception {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
connectionMock
|
||||
.receivers
|
||||
.get("getTableData")
|
||||
.onReceive(
|
||||
new FlipperObject.Builder().put("databaseId", 10).put("table", "first_table").build(),
|
||||
responderMock);
|
||||
|
||||
// Assert
|
||||
assertThat(
|
||||
responderMock.errors,
|
||||
hasItem(
|
||||
new FlipperObject.Builder()
|
||||
.put("code", DatabasesErrorCodes.ERROR_DATABASE_INVALID)
|
||||
.put("message", DatabasesErrorCodes.ERROR_DATABASE_INVALID_MESSAGE)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandGetTableData() throws Exception {
|
||||
// Arrange
|
||||
connectionMock.receivers.get("databaseList").onReceive(null, responderMock); // Load data
|
||||
SQLiteDatabase db = databaseHelper1.getWritableDatabase();
|
||||
db.execSQL("INSERT INTO first_table (column1, column2) VALUES('c','d')");
|
||||
db.execSQL("INSERT INTO first_table (column1, column2) VALUES('e','f')");
|
||||
db.execSQL("INSERT INTO first_table (column1, column2) VALUES('g','h')");
|
||||
db.close();
|
||||
|
||||
// Act
|
||||
connectionMock
|
||||
.receivers
|
||||
.get("getTableData")
|
||||
.onReceive(
|
||||
new FlipperObject.Builder()
|
||||
.put("databaseId", 1)
|
||||
.put("table", "first_table")
|
||||
.put("order", "column2")
|
||||
.put("reverse", true)
|
||||
.put("start", 1)
|
||||
.put("count", 2)
|
||||
.build(),
|
||||
responderMock);
|
||||
|
||||
// Assert
|
||||
assertThat(
|
||||
responderMock.successes,
|
||||
hasItem(
|
||||
new FlipperObject.Builder()
|
||||
.put(
|
||||
"columns",
|
||||
new FlipperArray.Builder().put("_id").put("column1").put("column2").build())
|
||||
.put(
|
||||
"values",
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "integer")
|
||||
.put("value", 3))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "e"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "f"))
|
||||
.build())
|
||||
.put(
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "integer")
|
||||
.put("value", 2))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "c"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "d"))
|
||||
.build())
|
||||
.build())
|
||||
.put("start", 1)
|
||||
.put("count", 2)
|
||||
.put("total", 4)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandGetTableStructure() throws Exception {
|
||||
// Arrange
|
||||
connectionMock.receivers.get("databaseList").onReceive(null, responderMock); // Load data
|
||||
SQLiteDatabase db = databaseHelper1.getWritableDatabase();
|
||||
db.execSQL("CREATE UNIQUE INDEX index_name ON first_table(column1, column2)");
|
||||
db.close();
|
||||
|
||||
// Act
|
||||
connectionMock
|
||||
.receivers
|
||||
.get("getTableStructure")
|
||||
.onReceive(
|
||||
new FlipperObject.Builder().put("databaseId", 1).put("table", "first_table").build(),
|
||||
responderMock);
|
||||
|
||||
// Assert
|
||||
assertThat(
|
||||
responderMock.successes,
|
||||
hasItem(
|
||||
new FlipperObject.Builder()
|
||||
.put(
|
||||
"structureColumns",
|
||||
new FlipperArray.Builder()
|
||||
.put("column_name")
|
||||
.put("data_type")
|
||||
.put("nullable")
|
||||
.put("default")
|
||||
.put("primary_key")
|
||||
.put("foreign_key")
|
||||
.build())
|
||||
.put(
|
||||
"structureValues",
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "_id"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "INTEGER"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "boolean")
|
||||
.put("value", true))
|
||||
.put(new FlipperObject.Builder().put("type", "null"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "boolean")
|
||||
.put("value", true))
|
||||
.put(new FlipperObject.Builder().put("type", "null"))
|
||||
.build())
|
||||
.put(
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "column1"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "TEXT"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "boolean")
|
||||
.put("value", true))
|
||||
.put(new FlipperObject.Builder().put("type", "null"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "boolean")
|
||||
.put("value", false))
|
||||
.put(new FlipperObject.Builder().put("type", "null"))
|
||||
.build())
|
||||
.put(
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "column2"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "TEXT"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "boolean")
|
||||
.put("value", true))
|
||||
.put(new FlipperObject.Builder().put("type", "null"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "boolean")
|
||||
.put("value", false))
|
||||
.put(new FlipperObject.Builder().put("type", "null"))
|
||||
.build())
|
||||
.build())
|
||||
.put(
|
||||
"indexesColumns",
|
||||
new FlipperArray.Builder()
|
||||
.put("index_name")
|
||||
.put("unique")
|
||||
.put("indexed_column_name")
|
||||
.build())
|
||||
.put(
|
||||
"indexesValues",
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "index_name"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "boolean")
|
||||
.put("value", true))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "column1,column2"))
|
||||
.build())
|
||||
.build())
|
||||
.put(
|
||||
"definition",
|
||||
"CREATE TABLE first_table (_id INTEGER PRIMARY KEY AUTOINCREMENT,column1 TEXT,column2 TEXT)")
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandExecuteInvalidParams() throws Exception {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
connectionMock
|
||||
.receivers
|
||||
.get("execute")
|
||||
.onReceive(
|
||||
new FlipperObject.Builder()
|
||||
.put("databaseId", 1)
|
||||
.put("value", "") // invalid, can't be empty
|
||||
.build(),
|
||||
responderMock);
|
||||
|
||||
// Assert
|
||||
assertThat(
|
||||
responderMock.errors,
|
||||
hasItem(
|
||||
new FlipperObject.Builder()
|
||||
.put("code", DatabasesErrorCodes.ERROR_INVALID_REQUEST)
|
||||
.put("message", DatabasesErrorCodes.ERROR_INVALID_REQUEST_MESSAGE)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandExecuteInvalidDatabase() throws Exception {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
connectionMock
|
||||
.receivers
|
||||
.get("execute")
|
||||
.onReceive(
|
||||
new FlipperObject.Builder().put("databaseId", 10).put("value", "SELECT...").build(),
|
||||
responderMock);
|
||||
|
||||
// Assert
|
||||
assertThat(
|
||||
responderMock.errors,
|
||||
hasItem(
|
||||
new FlipperObject.Builder()
|
||||
.put("code", DatabasesErrorCodes.ERROR_DATABASE_INVALID)
|
||||
.put("message", DatabasesErrorCodes.ERROR_DATABASE_INVALID_MESSAGE)
|
||||
.build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCommandExecute() throws Exception {
|
||||
// Arrange
|
||||
connectionMock.receivers.get("databaseList").onReceive(null, responderMock); // Load data
|
||||
|
||||
// Act
|
||||
connectionMock
|
||||
.receivers
|
||||
.get("execute")
|
||||
.onReceive(
|
||||
new FlipperObject.Builder()
|
||||
.put("databaseId", 1)
|
||||
.put("value", "SELECT column1,column2 FROM first_table")
|
||||
.build(),
|
||||
responderMock);
|
||||
|
||||
// Assert
|
||||
assertThat(
|
||||
responderMock.successes,
|
||||
hasItem(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", DatabaseExecuteSqlResponse.TYPE_SELECT)
|
||||
.put("columns", new FlipperArray.Builder().put("column1").put("column2").build())
|
||||
.put(
|
||||
"values",
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "a"))
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("type", "string")
|
||||
.put("value", "b"))
|
||||
.build())
|
||||
.build())
|
||||
.build()));
|
||||
}
|
||||
|
||||
public static class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
// If you change the database schema, you must increment the database version.
|
||||
public static final int DATABASE_VERSION = 1;
|
||||
private static final String SQL_CREATE_FIRST_TABLE =
|
||||
"CREATE TABLE first_table ("
|
||||
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "column1 TEXT,"
|
||||
+ "column2 TEXT)";
|
||||
private static final String SQL_CREATE_SECOND_TABLE =
|
||||
"CREATE TABLE second_table ("
|
||||
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
+ "column1 TEXT,"
|
||||
+ "column2 TEXT)";
|
||||
|
||||
public DatabaseHelper(Context context, String databaseName) {
|
||||
super(context, databaseName, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(SQL_CREATE_FIRST_TABLE);
|
||||
db.execSQL(SQL_CREATE_SECOND_TABLE);
|
||||
}
|
||||
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// This database is only a cache for online data, so its upgrade policy is
|
||||
// to simply to discard the data and start over
|
||||
db.execSQL("DROP TABLE IF EXISTS first_table");
|
||||
db.execSQL("DROP TABLE IF EXISTS second_table");
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
onUpgrade(db, oldVersion, newVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ exports[`Empty app state matches snapshot 1`] = `
|
||||
className="css-1ayt83l"
|
||||
>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
@@ -40,7 +40,7 @@ exports[`Empty app state matches snapshot 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
@@ -96,7 +96,7 @@ exports[`Empty app state matches snapshot 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
|
||||
@@ -22,7 +22,7 @@ exports[`TitleBar is rendered 1`] = `
|
||||
className="css-1ayt83l"
|
||||
>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
@@ -37,7 +37,7 @@ exports[`TitleBar is rendered 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
@@ -93,7 +93,7 @@ exports[`TitleBar is rendered 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
|
||||
45
src/plugins/databases/ButtonNavigation.js
Normal file
45
src/plugins/databases/ButtonNavigation.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
import {Button, ButtonGroup, Glyph, colors} from 'flipper';
|
||||
|
||||
export default function ButtonNavigation(props: {|
|
||||
/** Back button is enabled */
|
||||
canGoBack: boolean,
|
||||
/** Forwards button is enabled */
|
||||
canGoForward: boolean,
|
||||
/** Callback when back button is clicked */
|
||||
onBack: () => void,
|
||||
/** Callback when forwards button is clicked */
|
||||
onForward: () => void,
|
||||
|}) {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button disabled={!props.canGoBack} onClick={props.onBack}>
|
||||
<Glyph
|
||||
name="chevron-left"
|
||||
size={16}
|
||||
color={
|
||||
props.canGoBack
|
||||
? colors.macOSTitleBarIconActive
|
||||
: colors.macOSTitleBarIconBlur
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<Button disabled={!props.canGoForward} onClick={props.onForward}>
|
||||
<Glyph
|
||||
name="chevron-right"
|
||||
size={16}
|
||||
color={
|
||||
props.canGoForward
|
||||
? colors.macOSTitleBarIconActive
|
||||
: colors.macOSTitleBarIconBlur
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
70
src/plugins/databases/ClientProtocol.js
Normal file
70
src/plugins/databases/ClientProtocol.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {PluginClient} from '../../plugin';
|
||||
import type {Value} from '../../ui/components/table/TypeBasedValueRenderer';
|
||||
|
||||
type ClientCall<Params, Response> = Params => Promise<Response>;
|
||||
|
||||
type DatabaseListRequest = {};
|
||||
|
||||
type DatabaseListResponse = Array<{
|
||||
id: number,
|
||||
name: string,
|
||||
tables: Array<string>,
|
||||
}>;
|
||||
|
||||
type QueryTableRequest = {
|
||||
databaseId: number,
|
||||
table: string,
|
||||
order?: string,
|
||||
reverse: boolean,
|
||||
start: number,
|
||||
count: number,
|
||||
};
|
||||
|
||||
type QueryTableResponse = {
|
||||
columns: Array<string>,
|
||||
values: Array<Array<Value>>,
|
||||
start: number,
|
||||
count: number,
|
||||
total: number,
|
||||
};
|
||||
|
||||
type GetTableStructureRequest = {
|
||||
databaseId: number,
|
||||
table: string,
|
||||
};
|
||||
|
||||
type GetTableStructureResponse = {
|
||||
structureColumns: Array<string>,
|
||||
structureValues: Array<Array<Value>>,
|
||||
indexesColumns: Array<string>,
|
||||
indexesValues: Array<Array<Value>>,
|
||||
definition: string,
|
||||
};
|
||||
|
||||
export class DatabaseClient {
|
||||
client: PluginClient;
|
||||
|
||||
constructor(pluginClient: PluginClient) {
|
||||
this.client = pluginClient;
|
||||
}
|
||||
|
||||
getDatabases: ClientCall<
|
||||
DatabaseListRequest,
|
||||
DatabaseListResponse,
|
||||
> = params => this.client.call('databaseList', {});
|
||||
|
||||
getTableData: ClientCall<QueryTableRequest, QueryTableResponse> = params =>
|
||||
this.client.call('getTableData', params);
|
||||
|
||||
getTableStructure: ClientCall<
|
||||
GetTableStructureRequest,
|
||||
GetTableStructureResponse,
|
||||
> = params => this.client.call('getTableStructure', params);
|
||||
}
|
||||
744
src/plugins/databases/index.js
Normal file
744
src/plugins/databases/index.js
Normal file
@@ -0,0 +1,744 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {
|
||||
styled,
|
||||
Toolbar,
|
||||
Select,
|
||||
FlexColumn,
|
||||
FlexRow,
|
||||
ManagedTable,
|
||||
Text,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Input,
|
||||
} from 'flipper';
|
||||
import {Component} from 'react';
|
||||
import type {
|
||||
TableBodyRow,
|
||||
TableRowSortOrder,
|
||||
} from '../../ui/components/table/types';
|
||||
import {FlipperPlugin} from 'flipper';
|
||||
import {DatabaseClient} from './ClientProtocol';
|
||||
import {renderValue} from 'flipper';
|
||||
import type {Value} from 'flipper';
|
||||
import ButtonNavigation from './ButtonNavigation';
|
||||
import _ from 'lodash';
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const BoldSpan = styled('Span')({
|
||||
fontSize: 12,
|
||||
color: '#90949c',
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
});
|
||||
|
||||
type DatabasesPluginState = {|
|
||||
selectedDatabase: number,
|
||||
selectedDatabaseTable: ?string,
|
||||
pageRowNumber: number,
|
||||
databases: Array<DatabaseEntry>,
|
||||
viewMode: 'data' | 'structure',
|
||||
error: ?null,
|
||||
currentPage: ?Page,
|
||||
currentStructure: ?Structure,
|
||||
currentSort: ?TableRowSortOrder,
|
||||
|};
|
||||
|
||||
type Page = {
|
||||
databaseId: number,
|
||||
table: string,
|
||||
columns: Array<string>,
|
||||
rows: Array<TableBodyRow>,
|
||||
start: number,
|
||||
count: number,
|
||||
total: number,
|
||||
};
|
||||
|
||||
type Structure = {|
|
||||
databaseId: number,
|
||||
table: string,
|
||||
columns: Array<string>,
|
||||
rows: Array<TableBodyRow>,
|
||||
indexesColumns: Array<string>,
|
||||
indexesValues: Array<TableBodyRow>,
|
||||
|};
|
||||
|
||||
type Actions =
|
||||
| SelectDatabaseEvent
|
||||
| SelectDatabaseTableEvent
|
||||
| UpdateDatabasesEvent
|
||||
| UpdateViewModeEvent
|
||||
| UpdatePageEvent
|
||||
| UpdateStructureEvent
|
||||
| NextPageEvent
|
||||
| PreviousPageEvent
|
||||
| RefreshEvent
|
||||
| SortByChangedEvent
|
||||
| GoToRowEvent;
|
||||
|
||||
type DatabaseEntry = {
|
||||
id: number,
|
||||
name: string,
|
||||
tables: Array<string>,
|
||||
};
|
||||
|
||||
type UpdateDatabasesEvent = {|
|
||||
databases: Array<{name: string, id: number, tables: Array<string>}>,
|
||||
type: 'UpdateDatabases',
|
||||
|};
|
||||
|
||||
type SelectDatabaseEvent = {|
|
||||
type: 'UpdateSelectedDatabase',
|
||||
database: number,
|
||||
|};
|
||||
|
||||
type SelectDatabaseTableEvent = {|
|
||||
type: 'UpdateSelectedDatabaseTable',
|
||||
table: string,
|
||||
|};
|
||||
|
||||
type UpdateViewModeEvent = {|
|
||||
type: 'UpdateViewMode',
|
||||
viewMode: 'data' | 'structure',
|
||||
|};
|
||||
|
||||
type UpdatePageEvent = {|
|
||||
type: 'UpdatePage',
|
||||
databaseId: number,
|
||||
table: string,
|
||||
columns: Array<string>,
|
||||
values: Array<Array<any>>,
|
||||
start: number,
|
||||
count: number,
|
||||
total: number,
|
||||
|};
|
||||
|
||||
type UpdateStructureEvent = {|
|
||||
type: 'UpdateStructure',
|
||||
databaseId: number,
|
||||
table: string,
|
||||
columns: Array<string>,
|
||||
rows: Array<Array<any>>,
|
||||
indexesColumns: Array<string>,
|
||||
indexesValues: Array<Array<any>>,
|
||||
|};
|
||||
|
||||
type NextPageEvent = {
|
||||
type: 'NextPage',
|
||||
};
|
||||
|
||||
type PreviousPageEvent = {
|
||||
type: 'PreviousPage',
|
||||
};
|
||||
|
||||
type RefreshEvent = {
|
||||
type: 'Refresh',
|
||||
};
|
||||
|
||||
type SortByChangedEvent = {
|
||||
type: 'SortByChanged',
|
||||
sortOrder: TableRowSortOrder,
|
||||
};
|
||||
|
||||
type GoToRowEvent = {
|
||||
type: 'GoToRow',
|
||||
row: number,
|
||||
};
|
||||
|
||||
function transformRow(
|
||||
columns: Array<string>,
|
||||
row: Array<Value>,
|
||||
index: number,
|
||||
): TableBodyRow {
|
||||
const transformedColumns = {};
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
transformedColumns[columns[i]] = {value: renderValue(row[i])};
|
||||
}
|
||||
return {key: String(index), columns: transformedColumns};
|
||||
}
|
||||
|
||||
function renderTable(page: ?Page, component: DatabasesPlugin) {
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ManagedTable
|
||||
tableKey={`databases-${page.databaseId}-${page.table}`}
|
||||
floating={false}
|
||||
columnOrder={page.columns.map(name => ({
|
||||
key: name,
|
||||
visible: true,
|
||||
}))}
|
||||
columns={page.columns.reduce((acc, val) => {
|
||||
acc[val] = {value: val, resizable: true, sortable: true};
|
||||
return acc;
|
||||
}, {})}
|
||||
zebra={true}
|
||||
rows={page.rows}
|
||||
horizontallyScrollable={true}
|
||||
onSort={(sortOrder: TableRowSortOrder) => {
|
||||
component.dispatchAction({
|
||||
type: 'SortByChanged',
|
||||
sortOrder,
|
||||
});
|
||||
}}
|
||||
initialSortOrder={component.state.currentSort}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDatabaseColumns(structure: ?Structure) {
|
||||
if (!structure) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FlexRow grow={true}>
|
||||
<ManagedTable
|
||||
floating={false}
|
||||
columnOrder={structure.columns.map(name => ({
|
||||
key: name,
|
||||
visible: true,
|
||||
}))}
|
||||
columns={structure.columns.reduce((acc, val) => {
|
||||
acc[val] = {value: val, resizable: true};
|
||||
return acc;
|
||||
}, {})}
|
||||
zebra={true}
|
||||
rows={structure.rows || []}
|
||||
horizontallyScrollable={true}
|
||||
/>
|
||||
</FlexRow>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDatabaseIndexes(structure: ?Structure) {
|
||||
if (!structure) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FlexRow grow={true}>
|
||||
<ManagedTable
|
||||
floating={false}
|
||||
columnOrder={structure.indexesColumns.map(name => ({
|
||||
key: name,
|
||||
visible: true,
|
||||
}))}
|
||||
columns={structure.indexesColumns.reduce((acc, val) => {
|
||||
acc[val] = {value: val, resizable: true};
|
||||
return acc;
|
||||
}, {})}
|
||||
zebra={true}
|
||||
rows={structure.indexesValues || []}
|
||||
horizontallyScrollable={true}
|
||||
/>
|
||||
</FlexRow>
|
||||
);
|
||||
}
|
||||
|
||||
type PageInfoProps = {
|
||||
currentRow: number,
|
||||
count: number,
|
||||
totalRows: number,
|
||||
onChange: (currentRow: number, count: number) => void,
|
||||
};
|
||||
|
||||
class PageInfo extends Component<
|
||||
PageInfoProps,
|
||||
{isOpen: boolean, inputValue: string},
|
||||
> {
|
||||
constructor(props: PageInfoProps) {
|
||||
super(props);
|
||||
this.state = {isOpen: false, inputValue: String(props.currentRow)};
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
this.setState({isOpen: true});
|
||||
}
|
||||
|
||||
onInputChanged(e) {
|
||||
this.setState({inputValue: e.target.value});
|
||||
}
|
||||
|
||||
onSubmit(e: SyntheticKeyboardEvent<>) {
|
||||
if (e.key === 'Enter') {
|
||||
const rowNumber = parseInt(this.state.inputValue);
|
||||
console.log(rowNumber);
|
||||
this.props.onChange(rowNumber - 1, this.props.count);
|
||||
this.setState({isOpen: false});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FlexRow grow={true} alignItems={'center'}>
|
||||
<div style={{flex: 1}} />
|
||||
<Text>
|
||||
{this.props.count === this.props.totalRows
|
||||
? `${this.props.count} `
|
||||
: `${this.props.currentRow + 1}-${this.props.currentRow +
|
||||
this.props.count} `}
|
||||
of {this.props.totalRows} rows
|
||||
</Text>
|
||||
<div style={{flex: 1}} />
|
||||
{this.state.isOpen ? (
|
||||
<Input
|
||||
tabIndex={1}
|
||||
placeholder={this.props.currentRow + 1}
|
||||
onChange={this.onInputChanged.bind(this)}
|
||||
onKeyDown={this.onSubmit.bind(this)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
style={{textAlign: 'center'}}
|
||||
onClick={this.onOpen.bind(this)}>
|
||||
Go To Row
|
||||
</Button>
|
||||
)}
|
||||
</FlexRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class DatabasesPlugin extends FlipperPlugin<
|
||||
DatabasesPluginState,
|
||||
Actions,
|
||||
> {
|
||||
databaseClient: DatabaseClient;
|
||||
|
||||
state: DatabasesPluginState = {
|
||||
selectedDatabase: 0,
|
||||
selectedDatabaseTable: null,
|
||||
pageRowNumber: 0,
|
||||
databases: [],
|
||||
viewMode: 'data',
|
||||
error: null,
|
||||
currentPage: null,
|
||||
currentStructure: null,
|
||||
currentSort: null,
|
||||
};
|
||||
|
||||
reducers = [
|
||||
[
|
||||
'UpdateDatabases',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
results: UpdateDatabasesEvent,
|
||||
): DatabasesPluginState => {
|
||||
const updates = results.databases;
|
||||
const databases = updates;
|
||||
const selectedDatabase =
|
||||
state.selectedDatabase || Object.values(databases)[0]
|
||||
? // $FlowFixMe
|
||||
Object.values(databases)[0].id
|
||||
: 0;
|
||||
const selectedTable = databases[selectedDatabase - 1].tables[0];
|
||||
const sameTableSelected =
|
||||
selectedDatabase === state.selectedDatabase &&
|
||||
selectedTable === state.selectedDatabaseTable;
|
||||
return {
|
||||
...state,
|
||||
databases,
|
||||
selectedDatabase: selectedDatabase,
|
||||
selectedDatabaseTable: selectedTable,
|
||||
pageRowNumber: 0,
|
||||
currentPage: sameTableSelected ? state.currentPage : null,
|
||||
currentStructure: null,
|
||||
currentSort: sameTableSelected ? state.currentSort : null,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'UpdateSelectedDatabase',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
event: SelectDatabaseEvent,
|
||||
): DatabasesPluginState => {
|
||||
return {
|
||||
...state,
|
||||
selectedDatabase: event.database,
|
||||
selectedDatabaseTable:
|
||||
state.databases[event.database - 1].tables[0] || null,
|
||||
pageRowNumber: 0,
|
||||
currentPage: null,
|
||||
currentStructure: null,
|
||||
currentSort: null,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'UpdateSelectedDatabaseTable',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
event: SelectDatabaseTableEvent,
|
||||
): DatabasesPluginState => {
|
||||
return {
|
||||
...state,
|
||||
selectedDatabaseTable: event.table,
|
||||
pageRowNumber: 0,
|
||||
currentPage: null,
|
||||
currentStructure: null,
|
||||
currentSort: null,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'UpdateViewMode',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
event: UpdateViewModeEvent,
|
||||
): DatabasesPluginState => {
|
||||
return {
|
||||
...state,
|
||||
viewMode: event.viewMode,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'UpdatePage',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
event: UpdatePageEvent,
|
||||
): DatabasesPluginState => {
|
||||
return {
|
||||
...state,
|
||||
currentPage: {
|
||||
rows: event.values.map((row: Array<Value>, index: number) =>
|
||||
transformRow(event.columns, row, index),
|
||||
),
|
||||
...event,
|
||||
},
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'UpdateStructure',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
event: UpdateStructureEvent,
|
||||
): DatabasesPluginState => {
|
||||
return {
|
||||
...state,
|
||||
currentStructure: {
|
||||
databaseId: event.databaseId,
|
||||
table: event.table,
|
||||
columns: event.columns,
|
||||
rows: event.rows.map((row: Array<Value>, index: number) =>
|
||||
transformRow(event.columns, row, index),
|
||||
),
|
||||
indexesColumns: event.indexesColumns,
|
||||
indexesValues: event.indexesValues.map(
|
||||
(row: Array<Value>, index: number) =>
|
||||
transformRow(event.columns, row, index),
|
||||
),
|
||||
},
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'NextPage',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
event: UpdatePageEvent,
|
||||
): DatabasesPluginState => {
|
||||
return {
|
||||
...state,
|
||||
pageRowNumber: state.pageRowNumber + PAGE_SIZE,
|
||||
currentPage: null,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'PreviousPage',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
event: UpdatePageEvent,
|
||||
): DatabasesPluginState => {
|
||||
return {
|
||||
...state,
|
||||
pageRowNumber: Math.max(state.pageRowNumber - PAGE_SIZE, 0),
|
||||
currentPage: null,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'GoToRow',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
event: GoToRowEvent,
|
||||
): DatabasesPluginState => {
|
||||
if (!state.currentPage) {
|
||||
return state;
|
||||
}
|
||||
const destinationRow =
|
||||
event.row < 0
|
||||
? 0
|
||||
: event.row >= state.currentPage.total - PAGE_SIZE
|
||||
? Math.max(state.currentPage.total - PAGE_SIZE, 0)
|
||||
: event.row;
|
||||
return {
|
||||
...state,
|
||||
pageRowNumber: destinationRow,
|
||||
currentPage: null,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'Refresh',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
event: RefreshEvent,
|
||||
): DatabasesPluginState => {
|
||||
return {
|
||||
...state,
|
||||
currentPage: null,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'UpdateViewMode',
|
||||
(
|
||||
state: DatabasesPluginState,
|
||||
event: UpdateViewModeEvent,
|
||||
): DatabasesPluginState => {
|
||||
return {
|
||||
...state,
|
||||
viewMode: event.viewMode,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'SortByChanged',
|
||||
(state: DatabasesPluginState, event: SortByChangedEvent) => {
|
||||
return {
|
||||
...state,
|
||||
currentSort: event.sortOrder,
|
||||
pageRowNumber: 0,
|
||||
currentPage: null,
|
||||
};
|
||||
},
|
||||
],
|
||||
].reduce((acc, val) => {
|
||||
const name = val[0];
|
||||
const f = val[1];
|
||||
|
||||
acc[name] = (previousState, event) => {
|
||||
const newState = f(previousState, event);
|
||||
this.onStateChanged(previousState, newState);
|
||||
return newState;
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
onStateChanged(
|
||||
previousState: DatabasesPluginState,
|
||||
newState: DatabasesPluginState,
|
||||
) {
|
||||
const databaseId = newState.selectedDatabase;
|
||||
const table = newState.selectedDatabaseTable;
|
||||
if (
|
||||
newState.viewMode === 'data' &&
|
||||
newState.currentPage === null &&
|
||||
databaseId &&
|
||||
table
|
||||
) {
|
||||
this.databaseClient
|
||||
.getTableData({
|
||||
count: PAGE_SIZE,
|
||||
databaseId: newState.selectedDatabase,
|
||||
order: newState.currentSort?.key,
|
||||
reverse: (newState.currentSort?.direction || 'up') === 'down',
|
||||
table: table,
|
||||
start: newState.pageRowNumber,
|
||||
})
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
this.dispatchAction({
|
||||
type: 'UpdatePage',
|
||||
databaseId: databaseId,
|
||||
table: table,
|
||||
columns: data.columns,
|
||||
values: data.values,
|
||||
start: data.start,
|
||||
count: data.count,
|
||||
total: data.total,
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.setState({error: e});
|
||||
});
|
||||
}
|
||||
if (
|
||||
newState.viewMode === 'structure' &&
|
||||
newState.currentStructure === null &&
|
||||
databaseId &&
|
||||
table
|
||||
) {
|
||||
this.databaseClient
|
||||
.getTableStructure({
|
||||
databaseId: databaseId,
|
||||
table: table,
|
||||
})
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
this.dispatchAction({
|
||||
type: 'UpdateStructure',
|
||||
databaseId: databaseId,
|
||||
table: table,
|
||||
columns: data.structureColumns,
|
||||
rows: data.structureValues,
|
||||
indexesColumns: data.indexesColumns,
|
||||
indexesValues: data.indexesValues,
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.setState({error: e});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.databaseClient = new DatabaseClient(this.client);
|
||||
this.databaseClient.getDatabases({}).then(databases => {
|
||||
console.log(databases);
|
||||
this.dispatchAction({
|
||||
type: 'UpdateDatabases',
|
||||
databases,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onDataClicked = () => {
|
||||
this.dispatchAction({type: 'UpdateViewMode', viewMode: 'data'});
|
||||
};
|
||||
|
||||
onStructureClicked = () => {
|
||||
this.dispatchAction({type: 'UpdateViewMode', viewMode: 'structure'});
|
||||
};
|
||||
|
||||
onRefreshClicked = () => {
|
||||
this.dispatchAction({type: 'Refresh'});
|
||||
};
|
||||
|
||||
onDatabaseSelected = (selected: string) => {
|
||||
const dbId = this.state.databases.find(x => x.name === selected)?.id || 0;
|
||||
this.dispatchAction({
|
||||
database: dbId,
|
||||
type: 'UpdateSelectedDatabase',
|
||||
});
|
||||
};
|
||||
|
||||
onDatabaseTableSelected = (selected: string) => {
|
||||
this.dispatchAction({
|
||||
table: selected,
|
||||
type: 'UpdateSelectedDatabaseTable',
|
||||
});
|
||||
};
|
||||
|
||||
onNextPageClicked = () => {
|
||||
this.dispatchAction({type: 'NextPage'});
|
||||
};
|
||||
|
||||
onPreviousPageClicked = () => {
|
||||
this.dispatchAction({type: 'PreviousPage'});
|
||||
};
|
||||
|
||||
onGoToRow = (row: number, count: number) => {
|
||||
this.dispatchAction({type: 'GoToRow', row: row});
|
||||
};
|
||||
|
||||
renderStructure() {
|
||||
return [
|
||||
renderDatabaseColumns(this.state.currentStructure),
|
||||
renderDatabaseIndexes(this.state.currentStructure),
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const tableOptions =
|
||||
(this.state.selectedDatabase &&
|
||||
this.state.databases[this.state.selectedDatabase - 1] &&
|
||||
this.state.databases[this.state.selectedDatabase - 1].tables.reduce(
|
||||
(options, tableName) => ({...options, [tableName]: tableName}),
|
||||
{},
|
||||
)) ||
|
||||
{};
|
||||
|
||||
return (
|
||||
<FlexColumn style={{flex: 1}}>
|
||||
<Toolbar position="top" style={{paddingLeft: 8}}>
|
||||
<BoldSpan style={{marginRight: 16}}>Database</BoldSpan>
|
||||
<Select
|
||||
options={this.state.databases
|
||||
.map(x => x.name)
|
||||
.reduce((obj, item) => {
|
||||
obj[item] = item;
|
||||
return obj;
|
||||
}, {})}
|
||||
selected={String(this.state.selectedDatabase)}
|
||||
onChange={this.onDatabaseSelected}
|
||||
/>
|
||||
<BoldSpan style={{marginLeft: 16, marginRight: 16}}>Table</BoldSpan>
|
||||
<Select
|
||||
options={tableOptions}
|
||||
selected={this.state.selectedDatabaseTable}
|
||||
onChange={this.onDatabaseTableSelected}
|
||||
/>
|
||||
<div grow={true} />
|
||||
<Button onClick={this.onRefreshClicked}>Refresh</Button>
|
||||
<Button style={{marginLeft: 'auto', display: 'none'}}>
|
||||
Execute SQL
|
||||
</Button>
|
||||
</Toolbar>
|
||||
<FlexColumn grow={true}>
|
||||
{this.state.viewMode === 'data'
|
||||
? renderTable(this.state.currentPage, this)
|
||||
: this.renderStructure()}
|
||||
</FlexColumn>
|
||||
<Toolbar position="bottom" style={{paddingLeft: 8}}>
|
||||
<FlexRow grow={true}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
icon={'data-table'}
|
||||
onClick={this.onDataClicked}
|
||||
selected={this.state.viewMode === 'data'}>
|
||||
Data
|
||||
</Button>
|
||||
<Button
|
||||
icon={'gears-two'}
|
||||
onClick={this.onStructureClicked}
|
||||
selected={this.state.viewMode === 'structure'}>
|
||||
Structure
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{this.state.viewMode === 'data' && this.state.currentPage ? (
|
||||
<PageInfo
|
||||
currentRow={this.state.currentPage.start}
|
||||
count={this.state.currentPage.count}
|
||||
totalRows={this.state.currentPage.total}
|
||||
onChange={this.onGoToRow}
|
||||
/>
|
||||
) : null}
|
||||
{this.state.viewMode === 'data' && this.state.currentPage ? (
|
||||
<ButtonNavigation
|
||||
canGoBack={this.state.currentPage.start > 0}
|
||||
canGoForward={
|
||||
this.state.currentPage.start + this.state.currentPage.count <
|
||||
this.state.currentPage.total
|
||||
}
|
||||
onBack={this.onPreviousPageClicked}
|
||||
onForward={this.onNextPageClicked}
|
||||
/>
|
||||
) : null}
|
||||
</FlexRow>
|
||||
</Toolbar>
|
||||
{this.state.error && JSON.stringify(this.state.error)}
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
}
|
||||
15
src/plugins/databases/package.json
Normal file
15
src/plugins/databases/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Databases",
|
||||
"version": "1.0.0",
|
||||
"title": "Databases",
|
||||
"icon": "internet",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.11"
|
||||
},
|
||||
"bugs": {
|
||||
"email": "oncall+flipper@xmail.facebook.com",
|
||||
"url": "https://fb.workplace.com/groups/230455004101832/"
|
||||
}
|
||||
}
|
||||
8
src/plugins/databases/yarn.lock
Normal file
8
src/plugins/databases/yarn.lock
Normal file
@@ -0,0 +1,8 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
lodash@^4.17.11:
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||
@@ -127,7 +127,9 @@ const StyledButton = styled('div')(props => ({
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
'&:active': {
|
||||
'&:active': props.disabled
|
||||
? null
|
||||
: {
|
||||
borderColor: colors.macOSTitleBarButtonBorder,
|
||||
borderBottomColor: colors.macOSTitleBarButtonBorderBottom,
|
||||
background: `linear-gradient(to bottom, ${
|
||||
|
||||
@@ -116,6 +116,7 @@ export type ManagedTableProps = {|
|
||||
* Allows to create context menu items for rows.
|
||||
*/
|
||||
buildContextMenuItems?: () => MenuTemplate,
|
||||
initialSortOrder?: ?TableRowSortOrder,
|
||||
/**
|
||||
* Callback when sorting changes.
|
||||
*/
|
||||
|
||||
@@ -171,7 +171,8 @@ class TableHeadColumn extends PureComponent<{
|
||||
<TableHeaderColumnInteractive
|
||||
grow={true}
|
||||
resizable={RIGHT_RESIZABLE}
|
||||
onResize={this.onResize}>
|
||||
onResize={this.onResize}
|
||||
minWidth={20}>
|
||||
{children}
|
||||
</TableHeaderColumnInteractive>
|
||||
);
|
||||
|
||||
68
src/ui/components/table/TypeBasedValueRenderer.js
Normal file
68
src/ui/components/table/TypeBasedValueRenderer.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
import {default as styled} from 'react-emotion';
|
||||
import {colors} from '../colors';
|
||||
import {default as Text} from '../Text';
|
||||
|
||||
export type Value =
|
||||
| {
|
||||
type: 'string',
|
||||
value: string,
|
||||
}
|
||||
| {
|
||||
type: 'boolean',
|
||||
value: boolean,
|
||||
}
|
||||
| {
|
||||
type: 'integer' | 'float' | 'double' | 'number',
|
||||
value: number,
|
||||
}
|
||||
| {
|
||||
type: 'null',
|
||||
};
|
||||
|
||||
const NonWrappingText = styled(Text)({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
const BooleanValue = styled(NonWrappingText)(props => ({
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: props.active ? colors.green : colors.red,
|
||||
marginRight: 5,
|
||||
marginTop: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
export function renderValue(val: Value) {
|
||||
switch (val.type) {
|
||||
case 'boolean':
|
||||
return (
|
||||
<BooleanValue code={true} active={val.value}>
|
||||
{val.value.toString()}
|
||||
</BooleanValue>
|
||||
);
|
||||
case 'string':
|
||||
return <NonWrappingText>{val.value}</NonWrappingText>;
|
||||
case 'integer':
|
||||
case 'float':
|
||||
case 'double':
|
||||
case 'number':
|
||||
return <NonWrappingText>{val.value}</NonWrappingText>;
|
||||
case 'null':
|
||||
return <NonWrappingText>NULL</NonWrappingText>;
|
||||
default:
|
||||
return <NonWrappingText>{val.value}</NonWrappingText>;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export {default as LoadingIndicator} from './components/LoadingIndicator.js';
|
||||
//
|
||||
export {default as Popover} from './components/Popover.js';
|
||||
|
||||
//
|
||||
// tables
|
||||
export type {
|
||||
TableColumns,
|
||||
TableRows,
|
||||
@@ -39,6 +39,8 @@ export type {
|
||||
} from './components/table/types.js';
|
||||
export {default as ManagedTable} from './components/table/ManagedTable.js';
|
||||
export type {ManagedTableProps} from './components/table/ManagedTable.js';
|
||||
export type {Value} from './components/table/TypeBasedValueRenderer.js';
|
||||
export {renderValue} from './components/table/TypeBasedValueRenderer.js';
|
||||
|
||||
//
|
||||
export type {
|
||||
|
||||
Reference in New Issue
Block a user