mojo/syncbase: Implementing ListApps and ListDatabases.

Since in Mojo Syncbase, there is no naming involved,
instead of using the namespace client library to
Glob (Like the Go client library does) we instead synthesize
a call GlobChildren__ directly.

MultiPart: 2/2
Change-Id: I86bc4eb24d5fa022adc5b8a3677e403e32a05a7e
diff --git a/lib/gen/dart-gen/mojom/lib/mojo/syncbase.mojom.dart b/lib/gen/dart-gen/mojom/lib/mojo/syncbase.mojom.dart
index 216e846..83cec07 100644
--- a/lib/gen/dart-gen/mojom/lib/mojo/syncbase.mojom.dart
+++ b/lib/gen/dart-gen/mojom/lib/mojo/syncbase.mojom.dart
@@ -1776,6 +1776,153 @@
 }
 
 
+class SyncbaseServiceListAppsParams extends bindings.Struct {
+  static const List<bindings.StructDataHeader> kVersions = const [
+    const bindings.StructDataHeader(8, 0)
+  ];
+
+  SyncbaseServiceListAppsParams() : super(kVersions.last.size);
+
+  static SyncbaseServiceListAppsParams deserialize(bindings.Message message) {
+    var decoder = new bindings.Decoder(message);
+    var result = decode(decoder);
+    decoder.excessHandles.forEach((h) => h.close());
+    return result;
+  }
+
+  static SyncbaseServiceListAppsParams decode(bindings.Decoder decoder0) {
+    if (decoder0 == null) {
+      return null;
+    }
+    SyncbaseServiceListAppsParams result = new SyncbaseServiceListAppsParams();
+
+    var mainDataHeader = decoder0.decodeStructDataHeader();
+    if (mainDataHeader.version <= kVersions.last.version) {
+      // Scan in reverse order to optimize for more recent versions.
+      for (int i = kVersions.length - 1; i >= 0; --i) {
+        if (mainDataHeader.version >= kVersions[i].version) {
+          if (mainDataHeader.size == kVersions[i].size) {
+            // Found a match.
+            break;
+          }
+          throw new bindings.MojoCodecError(
+              'Header size doesn\'t correspond to known version size.');
+        }
+      }
+    } else if (mainDataHeader.size < kVersions.last.size) {
+      throw new bindings.MojoCodecError(
+        'Message newer than the last known version cannot be shorter than '
+        'required by the last known version.');
+    }
+    return result;
+  }
+
+  void encode(bindings.Encoder encoder) {
+    encoder.getStructEncoderAtOffset(kVersions.last);
+  }
+
+  String toString() {
+    return "SyncbaseServiceListAppsParams("")";
+  }
+
+  Map toJson() {
+    Map map = new Map();
+    return map;
+  }
+}
+
+
+class SyncbaseServiceListAppsResponseParams extends bindings.Struct {
+  static const List<bindings.StructDataHeader> kVersions = const [
+    const bindings.StructDataHeader(24, 0)
+  ];
+  Error err = null;
+  List<String> apps = null;
+
+  SyncbaseServiceListAppsResponseParams() : super(kVersions.last.size);
+
+  static SyncbaseServiceListAppsResponseParams deserialize(bindings.Message message) {
+    var decoder = new bindings.Decoder(message);
+    var result = decode(decoder);
+    decoder.excessHandles.forEach((h) => h.close());
+    return result;
+  }
+
+  static SyncbaseServiceListAppsResponseParams decode(bindings.Decoder decoder0) {
+    if (decoder0 == null) {
+      return null;
+    }
+    SyncbaseServiceListAppsResponseParams result = new SyncbaseServiceListAppsResponseParams();
+
+    var mainDataHeader = decoder0.decodeStructDataHeader();
+    if (mainDataHeader.version <= kVersions.last.version) {
+      // Scan in reverse order to optimize for more recent versions.
+      for (int i = kVersions.length - 1; i >= 0; --i) {
+        if (mainDataHeader.version >= kVersions[i].version) {
+          if (mainDataHeader.size == kVersions[i].size) {
+            // Found a match.
+            break;
+          }
+          throw new bindings.MojoCodecError(
+              'Header size doesn\'t correspond to known version size.');
+        }
+      }
+    } else if (mainDataHeader.size < kVersions.last.size) {
+      throw new bindings.MojoCodecError(
+        'Message newer than the last known version cannot be shorter than '
+        'required by the last known version.');
+    }
+    if (mainDataHeader.version >= 0) {
+      
+      var decoder1 = decoder0.decodePointer(8, false);
+      result.err = Error.decode(decoder1);
+    }
+    if (mainDataHeader.version >= 0) {
+      
+      var decoder1 = decoder0.decodePointer(16, false);
+      {
+        var si1 = decoder1.decodeDataHeaderForPointerArray(bindings.kUnspecifiedArrayLength);
+        result.apps = new List<String>(si1.numElements);
+        for (int i1 = 0; i1 < si1.numElements; ++i1) {
+          
+          result.apps[i1] = decoder1.decodeString(bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, false);
+        }
+      }
+    }
+    return result;
+  }
+
+  void encode(bindings.Encoder encoder) {
+    var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
+    
+    encoder0.encodeStruct(err, 8, false);
+    
+    if (apps == null) {
+      encoder0.encodeNullPointer(16, false);
+    } else {
+      var encoder1 = encoder0.encodePointerArray(apps.length, 16, bindings.kUnspecifiedArrayLength);
+      for (int i0 = 0; i0 < apps.length; ++i0) {
+        
+        encoder1.encodeString(apps[i0], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i0, false);
+      }
+    }
+  }
+
+  String toString() {
+    return "SyncbaseServiceListAppsResponseParams("
+           "err: $err" ", "
+           "apps: $apps" ")";
+  }
+
+  Map toJson() {
+    Map map = new Map();
+    map["err"] = err;
+    map["apps"] = apps;
+    return map;
+  }
+}
+
+
 class SyncbaseAppCreateParams extends bindings.Struct {
   static const List<bindings.StructDataHeader> kVersions = const [
     const bindings.StructDataHeader(24, 0)
@@ -2338,6 +2485,162 @@
 }
 
 
+class SyncbaseAppListDatabasesParams extends bindings.Struct {
+  static const List<bindings.StructDataHeader> kVersions = const [
+    const bindings.StructDataHeader(16, 0)
+  ];
+  String name = null;
+
+  SyncbaseAppListDatabasesParams() : super(kVersions.last.size);
+
+  static SyncbaseAppListDatabasesParams deserialize(bindings.Message message) {
+    var decoder = new bindings.Decoder(message);
+    var result = decode(decoder);
+    decoder.excessHandles.forEach((h) => h.close());
+    return result;
+  }
+
+  static SyncbaseAppListDatabasesParams decode(bindings.Decoder decoder0) {
+    if (decoder0 == null) {
+      return null;
+    }
+    SyncbaseAppListDatabasesParams result = new SyncbaseAppListDatabasesParams();
+
+    var mainDataHeader = decoder0.decodeStructDataHeader();
+    if (mainDataHeader.version <= kVersions.last.version) {
+      // Scan in reverse order to optimize for more recent versions.
+      for (int i = kVersions.length - 1; i >= 0; --i) {
+        if (mainDataHeader.version >= kVersions[i].version) {
+          if (mainDataHeader.size == kVersions[i].size) {
+            // Found a match.
+            break;
+          }
+          throw new bindings.MojoCodecError(
+              'Header size doesn\'t correspond to known version size.');
+        }
+      }
+    } else if (mainDataHeader.size < kVersions.last.size) {
+      throw new bindings.MojoCodecError(
+        'Message newer than the last known version cannot be shorter than '
+        'required by the last known version.');
+    }
+    if (mainDataHeader.version >= 0) {
+      
+      result.name = decoder0.decodeString(8, false);
+    }
+    return result;
+  }
+
+  void encode(bindings.Encoder encoder) {
+    var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
+    
+    encoder0.encodeString(name, 8, false);
+  }
+
+  String toString() {
+    return "SyncbaseAppListDatabasesParams("
+           "name: $name" ")";
+  }
+
+  Map toJson() {
+    Map map = new Map();
+    map["name"] = name;
+    return map;
+  }
+}
+
+
+class SyncbaseAppListDatabasesResponseParams extends bindings.Struct {
+  static const List<bindings.StructDataHeader> kVersions = const [
+    const bindings.StructDataHeader(24, 0)
+  ];
+  Error err = null;
+  List<String> databases = null;
+
+  SyncbaseAppListDatabasesResponseParams() : super(kVersions.last.size);
+
+  static SyncbaseAppListDatabasesResponseParams deserialize(bindings.Message message) {
+    var decoder = new bindings.Decoder(message);
+    var result = decode(decoder);
+    decoder.excessHandles.forEach((h) => h.close());
+    return result;
+  }
+
+  static SyncbaseAppListDatabasesResponseParams decode(bindings.Decoder decoder0) {
+    if (decoder0 == null) {
+      return null;
+    }
+    SyncbaseAppListDatabasesResponseParams result = new SyncbaseAppListDatabasesResponseParams();
+
+    var mainDataHeader = decoder0.decodeStructDataHeader();
+    if (mainDataHeader.version <= kVersions.last.version) {
+      // Scan in reverse order to optimize for more recent versions.
+      for (int i = kVersions.length - 1; i >= 0; --i) {
+        if (mainDataHeader.version >= kVersions[i].version) {
+          if (mainDataHeader.size == kVersions[i].size) {
+            // Found a match.
+            break;
+          }
+          throw new bindings.MojoCodecError(
+              'Header size doesn\'t correspond to known version size.');
+        }
+      }
+    } else if (mainDataHeader.size < kVersions.last.size) {
+      throw new bindings.MojoCodecError(
+        'Message newer than the last known version cannot be shorter than '
+        'required by the last known version.');
+    }
+    if (mainDataHeader.version >= 0) {
+      
+      var decoder1 = decoder0.decodePointer(8, false);
+      result.err = Error.decode(decoder1);
+    }
+    if (mainDataHeader.version >= 0) {
+      
+      var decoder1 = decoder0.decodePointer(16, false);
+      {
+        var si1 = decoder1.decodeDataHeaderForPointerArray(bindings.kUnspecifiedArrayLength);
+        result.databases = new List<String>(si1.numElements);
+        for (int i1 = 0; i1 < si1.numElements; ++i1) {
+          
+          result.databases[i1] = decoder1.decodeString(bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i1, false);
+        }
+      }
+    }
+    return result;
+  }
+
+  void encode(bindings.Encoder encoder) {
+    var encoder0 = encoder.getStructEncoderAtOffset(kVersions.last);
+    
+    encoder0.encodeStruct(err, 8, false);
+    
+    if (databases == null) {
+      encoder0.encodeNullPointer(16, false);
+    } else {
+      var encoder1 = encoder0.encodePointerArray(databases.length, 16, bindings.kUnspecifiedArrayLength);
+      for (int i0 = 0; i0 < databases.length; ++i0) {
+        
+        encoder1.encodeString(databases[i0], bindings.ArrayDataHeader.kHeaderSize + bindings.kPointerSize * i0, false);
+      }
+    }
+  }
+
+  String toString() {
+    return "SyncbaseAppListDatabasesResponseParams("
+           "err: $err" ", "
+           "databases: $databases" ")";
+  }
+
+  Map toJson() {
+    Map map = new Map();
+    map["err"] = err;
+    map["databases"] = databases;
+    return map;
+  }
+}
+
+
 class SyncbaseAppSetPermissionsParams extends bindings.Struct {
   static const List<bindings.StructDataHeader> kVersions = const [
     const bindings.StructDataHeader(32, 0)
@@ -8290,46 +8593,48 @@
 
 const int kSyncbase_serviceGetPermissions_name = 0;
 const int kSyncbase_serviceSetPermissions_name = 1;
-const int kSyncbase_appCreate_name = 2;
-const int kSyncbase_appDestroy_name = 3;
-const int kSyncbase_appExists_name = 4;
-const int kSyncbase_appGetPermissions_name = 5;
-const int kSyncbase_appSetPermissions_name = 6;
-const int kSyncbase_dbCreate_name = 7;
-const int kSyncbase_dbDestroy_name = 8;
-const int kSyncbase_dbExists_name = 9;
-const int kSyncbase_dbExec_name = 10;
-const int kSyncbase_dbBeginBatch_name = 11;
-const int kSyncbase_dbCommit_name = 12;
-const int kSyncbase_dbAbort_name = 13;
-const int kSyncbase_dbGetPermissions_name = 14;
-const int kSyncbase_dbSetPermissions_name = 15;
-const int kSyncbase_dbWatchGlob_name = 16;
-const int kSyncbase_dbGetResumeMarker_name = 17;
-const int kSyncbase_dbListTables_name = 18;
-const int kSyncbase_dbGetSyncgroupNames_name = 19;
-const int kSyncbase_dbCreateSyncgroup_name = 20;
-const int kSyncbase_dbJoinSyncgroup_name = 21;
-const int kSyncbase_dbLeaveSyncgroup_name = 22;
-const int kSyncbase_dbDestroySyncgroup_name = 23;
-const int kSyncbase_dbEjectFromSyncgroup_name = 24;
-const int kSyncbase_dbGetSyncgroupSpec_name = 25;
-const int kSyncbase_dbSetSyncgroupSpec_name = 26;
-const int kSyncbase_dbGetSyncgroupMembers_name = 27;
-const int kSyncbase_tableCreate_name = 28;
-const int kSyncbase_tableDestroy_name = 29;
-const int kSyncbase_tableExists_name = 30;
-const int kSyncbase_tableGetPermissions_name = 31;
-const int kSyncbase_tableSetPermissions_name = 32;
-const int kSyncbase_tableDeleteRange_name = 33;
-const int kSyncbase_tableScan_name = 34;
-const int kSyncbase_tableGetPrefixPermissions_name = 35;
-const int kSyncbase_tableSetPrefixPermissions_name = 36;
-const int kSyncbase_tableDeletePrefixPermissions_name = 37;
-const int kSyncbase_rowExists_name = 38;
-const int kSyncbase_rowGet_name = 39;
-const int kSyncbase_rowPut_name = 40;
-const int kSyncbase_rowDelete_name = 41;
+const int kSyncbase_serviceListApps_name = 2;
+const int kSyncbase_appCreate_name = 3;
+const int kSyncbase_appDestroy_name = 4;
+const int kSyncbase_appExists_name = 5;
+const int kSyncbase_appGetPermissions_name = 6;
+const int kSyncbase_appListDatabases_name = 7;
+const int kSyncbase_appSetPermissions_name = 8;
+const int kSyncbase_dbCreate_name = 9;
+const int kSyncbase_dbDestroy_name = 10;
+const int kSyncbase_dbExists_name = 11;
+const int kSyncbase_dbExec_name = 12;
+const int kSyncbase_dbBeginBatch_name = 13;
+const int kSyncbase_dbCommit_name = 14;
+const int kSyncbase_dbAbort_name = 15;
+const int kSyncbase_dbGetPermissions_name = 16;
+const int kSyncbase_dbSetPermissions_name = 17;
+const int kSyncbase_dbWatchGlob_name = 18;
+const int kSyncbase_dbGetResumeMarker_name = 19;
+const int kSyncbase_dbListTables_name = 20;
+const int kSyncbase_dbGetSyncgroupNames_name = 21;
+const int kSyncbase_dbCreateSyncgroup_name = 22;
+const int kSyncbase_dbJoinSyncgroup_name = 23;
+const int kSyncbase_dbLeaveSyncgroup_name = 24;
+const int kSyncbase_dbDestroySyncgroup_name = 25;
+const int kSyncbase_dbEjectFromSyncgroup_name = 26;
+const int kSyncbase_dbGetSyncgroupSpec_name = 27;
+const int kSyncbase_dbSetSyncgroupSpec_name = 28;
+const int kSyncbase_dbGetSyncgroupMembers_name = 29;
+const int kSyncbase_tableCreate_name = 30;
+const int kSyncbase_tableDestroy_name = 31;
+const int kSyncbase_tableExists_name = 32;
+const int kSyncbase_tableGetPermissions_name = 33;
+const int kSyncbase_tableSetPermissions_name = 34;
+const int kSyncbase_tableDeleteRange_name = 35;
+const int kSyncbase_tableScan_name = 36;
+const int kSyncbase_tableGetPrefixPermissions_name = 37;
+const int kSyncbase_tableSetPrefixPermissions_name = 38;
+const int kSyncbase_tableDeletePrefixPermissions_name = 39;
+const int kSyncbase_rowExists_name = 40;
+const int kSyncbase_rowGet_name = 41;
+const int kSyncbase_rowPut_name = 42;
+const int kSyncbase_rowDelete_name = 43;
 
 const String SyncbaseName =
       'mojo::Syncbase';
@@ -8337,10 +8642,12 @@
 abstract class Syncbase {
   dynamic serviceGetPermissions([Function responseFactory = null]);
   dynamic serviceSetPermissions(Perms perms,String version,[Function responseFactory = null]);
+  dynamic serviceListApps([Function responseFactory = null]);
   dynamic appCreate(String name,Perms perms,[Function responseFactory = null]);
   dynamic appDestroy(String name,[Function responseFactory = null]);
   dynamic appExists(String name,[Function responseFactory = null]);
   dynamic appGetPermissions(String name,[Function responseFactory = null]);
+  dynamic appListDatabases(String name,[Function responseFactory = null]);
   dynamic appSetPermissions(String name,Perms perms,String version,[Function responseFactory = null]);
   dynamic dbCreate(String name,Perms perms,[Function responseFactory = null]);
   dynamic dbDestroy(String name,[Function responseFactory = null]);
@@ -8428,6 +8735,20 @@
         assert(!c.isCompleted);
         c.complete(r);
         break;
+      case kSyncbase_serviceListApps_name:
+        var r = SyncbaseServiceListAppsResponseParams.deserialize(
+            message.payload);
+        if (!message.header.hasRequestId) {
+          throw 'Expected a message with a valid request Id.';
+        }
+        Completer c = completerMap[message.header.requestId];
+        if (c == null) {
+          throw 'Message had unknown request Id: ${message.header.requestId}';
+        }
+        completerMap.remove(message.header.requestId);
+        assert(!c.isCompleted);
+        c.complete(r);
+        break;
       case kSyncbase_appCreate_name:
         var r = SyncbaseAppCreateResponseParams.deserialize(
             message.payload);
@@ -8484,6 +8805,20 @@
         assert(!c.isCompleted);
         c.complete(r);
         break;
+      case kSyncbase_appListDatabases_name:
+        var r = SyncbaseAppListDatabasesResponseParams.deserialize(
+            message.payload);
+        if (!message.header.hasRequestId) {
+          throw 'Expected a message with a valid request Id.';
+        }
+        Completer c = completerMap[message.header.requestId];
+        if (c == null) {
+          throw 'Message had unknown request Id: ${message.header.requestId}';
+        }
+        completerMap.remove(message.header.requestId);
+        assert(!c.isCompleted);
+        c.complete(r);
+        break;
       case kSyncbase_appSetPermissions_name:
         var r = SyncbaseAppSetPermissionsResponseParams.deserialize(
             message.payload);
@@ -9025,6 +9360,15 @@
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
+    dynamic serviceListApps([Function responseFactory = null]) {
+      assert(_proxyImpl.isBound);
+      var params = new SyncbaseServiceListAppsParams();
+      return _proxyImpl.sendMessageWithRequestId(
+          params,
+          kSyncbase_serviceListApps_name,
+          -1,
+          bindings.MessageHeader.kMessageExpectsResponse);
+    }
     dynamic appCreate(String name,Perms perms,[Function responseFactory = null]) {
       assert(_proxyImpl.isBound);
       var params = new SyncbaseAppCreateParams();
@@ -9066,6 +9410,16 @@
           -1,
           bindings.MessageHeader.kMessageExpectsResponse);
     }
+    dynamic appListDatabases(String name,[Function responseFactory = null]) {
+      assert(_proxyImpl.isBound);
+      var params = new SyncbaseAppListDatabasesParams();
+      params.name = name;
+      return _proxyImpl.sendMessageWithRequestId(
+          params,
+          kSyncbase_appListDatabases_name,
+          -1,
+          bindings.MessageHeader.kMessageExpectsResponse);
+    }
     dynamic appSetPermissions(String name,Perms perms,String version,[Function responseFactory = null]) {
       assert(_proxyImpl.isBound);
       var params = new SyncbaseAppSetPermissionsParams();
@@ -9552,6 +9906,12 @@
     result.err = err;
     return result;
   }
+  SyncbaseServiceListAppsResponseParams _SyncbaseServiceListAppsResponseParamsFactory(Error err, List<String> apps) {
+    var result = new SyncbaseServiceListAppsResponseParams();
+    result.err = err;
+    result.apps = apps;
+    return result;
+  }
   SyncbaseAppCreateResponseParams _SyncbaseAppCreateResponseParamsFactory(Error err) {
     var result = new SyncbaseAppCreateResponseParams();
     result.err = err;
@@ -9575,6 +9935,12 @@
     result.version = version;
     return result;
   }
+  SyncbaseAppListDatabasesResponseParams _SyncbaseAppListDatabasesResponseParamsFactory(Error err, List<String> databases) {
+    var result = new SyncbaseAppListDatabasesResponseParams();
+    result.err = err;
+    result.databases = databases;
+    return result;
+  }
   SyncbaseAppSetPermissionsResponseParams _SyncbaseAppSetPermissionsResponseParamsFactory(Error err) {
     var result = new SyncbaseAppSetPermissionsResponseParams();
     result.err = err;
@@ -9824,6 +10190,28 @@
               bindings.MessageHeader.kMessageIsResponse);
         }
         break;
+      case kSyncbase_serviceListApps_name:
+        var params = SyncbaseServiceListAppsParams.deserialize(
+            message.payload);
+        var response = _impl.serviceListApps(_SyncbaseServiceListAppsResponseParamsFactory);
+        if (response is Future) {
+          return response.then((response) {
+            if (response != null) {
+              return buildResponseWithId(
+                  response,
+                  kSyncbase_serviceListApps_name,
+                  message.header.requestId,
+                  bindings.MessageHeader.kMessageIsResponse);
+            }
+          });
+        } else if (response != null) {
+          return buildResponseWithId(
+              response,
+              kSyncbase_serviceListApps_name,
+              message.header.requestId,
+              bindings.MessageHeader.kMessageIsResponse);
+        }
+        break;
       case kSyncbase_appCreate_name:
         var params = SyncbaseAppCreateParams.deserialize(
             message.payload);
@@ -9912,6 +10300,28 @@
               bindings.MessageHeader.kMessageIsResponse);
         }
         break;
+      case kSyncbase_appListDatabases_name:
+        var params = SyncbaseAppListDatabasesParams.deserialize(
+            message.payload);
+        var response = _impl.appListDatabases(params.name,_SyncbaseAppListDatabasesResponseParamsFactory);
+        if (response is Future) {
+          return response.then((response) {
+            if (response != null) {
+              return buildResponseWithId(
+                  response,
+                  kSyncbase_appListDatabases_name,
+                  message.header.requestId,
+                  bindings.MessageHeader.kMessageIsResponse);
+            }
+          });
+        } else if (response != null) {
+          return buildResponseWithId(
+              response,
+              kSyncbase_appListDatabases_name,
+              message.header.requestId,
+              bindings.MessageHeader.kMessageIsResponse);
+        }
+        break;
       case kSyncbase_appSetPermissions_name:
         var params = SyncbaseAppSetPermissionsParams.deserialize(
             message.payload);
diff --git a/lib/src/app.dart b/lib/src/app.dart
index 6c27195..b3f3876 100644
--- a/lib/src/app.dart
+++ b/lib/src/app.dart
@@ -41,6 +41,15 @@
     return v.perms;
   }
 
+  // TODO(aghassemi): Maybe add an abstract class for SyncbaseDatabase so we can
+  // return either NoSqlDatabase or SqlDatabase (when both exist).
+  Future<List<SyncbaseNoSqlDatabase>> listDatabases() async {
+    var v = await _ctx.syncbase.appListDatabases(fullName);
+    if (isError(v.err)) throw v.err;
+
+    return v.databases.map((dbName) => this.noSqlDatabase(dbName)).toList();
+  }
+
   Future setPermissions(mojom.Perms perms, String version) async {
     var v = await _ctx.syncbase.appSetPermissions(fullName, perms, version);
     if (isError(v.err)) throw v.err;
diff --git a/lib/src/nosql/database.dart b/lib/src/nosql/database.dart
index 2577feb..b4e4581 100644
--- a/lib/src/nosql/database.dart
+++ b/lib/src/nosql/database.dart
@@ -4,7 +4,6 @@
 
 part of syncbase_client;
 
-// TODO(sadovsky): Add listTables method.
 class SyncbaseNoSqlDatabase extends NamedResource {
   SyncbaseNoSqlDatabase._internal(
       _ctx, _parentFullName, relativeName, batchSuffix)
diff --git a/lib/syncbase_client.dart b/lib/syncbase_client.dart
index e259b51..2a354da 100644
--- a/lib/syncbase_client.dart
+++ b/lib/syncbase_client.dart
@@ -74,6 +74,13 @@
     return v.perms;
   }
 
+  Future<List<SyncbaseApp>> listApps() async {
+    var v = await _ctx.syncbase.serviceListApps();
+    if (isError(v.err)) throw v.err;
+
+    return v.apps.map((appName) => this.app(appName)).toList();
+  }
+
   Future setPermissions(mojom.Perms perms, String version) async {
     var v = await _ctx.syncbase.serviceSetPermissions(perms, version);
     if (isError(v.err)) throw v.err;
diff --git a/mojom/syncbase.mojom b/mojom/syncbase.mojom
index ff137e5..f40ee5c 100644
--- a/mojom/syncbase.mojom
+++ b/mojom/syncbase.mojom
@@ -96,7 +96,6 @@
 };
 
 // TODO(sadovsky): Add schema version to all RPCs. See v.io/c/13734.
-// TODO(aghassemi): Add ListApps and ListDatabases methods. See v.io/i/794.
 // All 'name' params are service-relative object names.
 
 // Error handling modeled after:
@@ -107,6 +106,7 @@
 
   ServiceGetPermissions() => (Error err, Perms perms, string version);
   ServiceSetPermissions(Perms perms, string version) => (Error err);
+  ServiceListApps() => (Error err, array<string> apps);
 
   ////////////////////////////////////////
   // App
@@ -115,6 +115,7 @@
   AppDestroy(string name) => (Error err);
   AppExists(string name) => (Error err, bool exists);
   AppGetPermissions(string name) => (Error err, Perms perms, string version);
+  AppListDatabases(string name) => (Error err, array<string> databases);
   AppSetPermissions(string name, Perms perms, string version) => (Error err);
 
   ////////////////////////////////////////
diff --git a/test/integration/syncbase_app_test.dart b/test/integration/syncbase_app_test.dart
index 33f3d8c..6777ec5 100644
--- a/test/integration/syncbase_app_test.dart
+++ b/test/integration/syncbase_app_test.dart
@@ -29,5 +29,39 @@
     expect(await app.exists(), equals(false));
   });
 
+  test('listing apps', () async {
+    var appName = utils.uniqueName('app');
+    await c.app(appName).create(utils.emptyPerms());
+
+    var apps = await c.listApps();
+
+    // NOTE(aghassemi): Since the Syncbase instance is shared between all tests,
+    // we will get a lot more than just 1 app, so we simply verify that our
+    // appName is in the returned list.
+    expect(apps.length, greaterThan(0));
+    var ourApp = apps.firstWhere((e) => e.name == appName);
+    expect(ourApp.name, equals(appName));
+  });
+
+  test('listing databases', () async {
+    var appName = utils.uniqueName('app');
+    var app = c.app(appName);
+    await app.create(utils.emptyPerms());
+
+    var dbNames = [utils.uniqueName('db1'), utils.uniqueName('db2')];
+    dbNames.sort();
+
+    for (var dbName in dbNames) {
+      await app.noSqlDatabase(dbName).create(utils.emptyPerms());
+    }
+
+    var dbs = await app.listDatabases();
+    dbs.sort((d1, d2) => d1.name.compareTo(d2.name));
+    expect(dbs.length, equals(dbNames.length));
+    for (var i = 0; i < dbNames.length; i++) {
+      expect(dbs[i].name, equals(dbNames[i]));
+    }
+  });
+
   // TODO(nlacasse): Test app.get/setPermissions.
 }
diff --git a/test/integration/syncbase_database_test.dart b/test/integration/syncbase_database_test.dart
index 9318f57..850766f 100644
--- a/test/integration/syncbase_database_test.dart
+++ b/test/integration/syncbase_database_test.dart
@@ -43,11 +43,7 @@
     var db = app.noSqlDatabase(utils.uniqueName('db'));
     await db.create(utils.emptyPerms());
 
-    var tableNames = [
-      utils.uniqueName('table'),
-      utils.uniqueName('table'),
-      utils.uniqueName('table'),
-    ];
+    var tableNames = [utils.uniqueName('table1'), utils.uniqueName('table2')];
     tableNames.sort();
 
     for (var tableName in tableNames) {
@@ -57,7 +53,7 @@
     var tables = await db.listTables();
     tables.sort((t1, t2) => t1.name.compareTo(t2.name));
     expect(tables.length, equals(tableNames.length));
-    for (var i = 0; i < tables.length; i++) {
+    for (var i = 0; i < tableNames.length; i++) {
       expect(tables[i].name, equals(tableNames[i]));
     }
   });