syncbase.js support for syncgroups

Change-Id: I520eb340424115000f8aec1483e53041b7047842
diff --git a/src/nosql/database.js b/src/nosql/database.js
index 236eb77..1329135 100644
--- a/src/nosql/database.js
+++ b/src/nosql/database.js
@@ -8,6 +8,7 @@
 
 var BatchDatabase = require('./batch-database');
 var nosqlVdl = require('../gen-vdl/v.io/syncbase/v23/services/syncbase/nosql');
+var SyncGroup = require('./syncgroup');
 var Table = require('./table');
 var util = require('../util');
 
@@ -198,3 +199,21 @@
     return cb(null, new BatchDatabase(db));
   });
 };
+
+/**
+ * Gets a handle to the SyncGroup with the given name.
+ *
+ * @param {string} name SyncGroup name.
+ */
+Database.prototype.syncGroup = function(name) {
+  return new SyncGroup(this, name);
+};
+
+/**
+ * Gets the global names of all SyncGroups attached to this database.
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {function} cb Callback.
+ */
+Database.prototype.getSyncGroupNames = function(ctx, cb) {
+  this._wire(ctx).getSyncGroupNames(ctx, cb);
+};
diff --git a/src/nosql/index.js b/src/nosql/index.js
index c0fe714..c589058 100644
--- a/src/nosql/index.js
+++ b/src/nosql/index.js
@@ -17,5 +17,7 @@
   BatchOptions: vdl.BatchOptions,
   ReadOnlyBatchError: vdl.ReadOnlyBatchError,
   rowrange: rowrange,
-  runInBatch: runInBatch
+  runInBatch: runInBatch,
+  SyncGroupMemberInfo: vdl.SyncGroupMemberInfo,
+  SyncGroupSpec: vdl.SyncGroupSpec
 };
diff --git a/src/nosql/syncgroup.js b/src/nosql/syncgroup.js
new file mode 100644
index 0000000..2fdce27
--- /dev/null
+++ b/src/nosql/syncgroup.js
@@ -0,0 +1,151 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+module.exports = SyncGroup;
+
+/**
+ * SyncGroup is the interface for a SyncGroup in the store.
+ */
+function SyncGroup(db, name) {
+  if (!(this instanceof SyncGroup)) {
+    return new SyncGroup(db, name);
+  }
+
+  /**
+   * @private
+   */
+  Object.defineProperty(this, '_db', {
+    enumerable: false,
+    value: db,
+    writable: false
+  });
+
+  /**
+   * @property name
+   * @type {string}
+   */
+  Object.defineProperty(this, 'name', {
+    enumerable: true,
+    value: name,
+    writable: false
+  });
+}
+
+/**
+ * Creates a new SyncGroup with the given spec.
+ *
+ * Requires: Client must have at least Read access on the Database; prefix ACL
+ * must exist at each SyncGroup prefix; Client must have at least Read access
+ * on each of these prefix ACLs.
+ *
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {module:syncbase.nosql.SyncGroupSpec} spec SyncGroupSpec.
+ * @param {module:syncbase.nosql.SyncGroupMemberInfo} myInfo
+ * SyncGroupMemberInfo.
+ * @param {function} cb Callback.
+ */
+SyncGroup.prototype.create = function(ctx, spec, myInfo, cb) {
+  this._db._wire(ctx).createSyncGroup(ctx, this.name, spec, myInfo, cb);
+};
+
+/**
+ * Joins a SyncGroup.
+ *
+ * Requires: Client must have at least Read access on the Database and on the
+ * SyncGroup ACL.
+ *
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {module:syncbase.nosql.SyncGroupMemberInfo} myInfo
+ * SyncGroupMemberInfo.
+ * @param {function} cb Callback.
+ */
+SyncGroup.prototype.join = function(ctx, myInfo, cb) {
+  this._db._wire(ctx).joinSyncGroup(ctx, this.name, myInfo, cb);
+};
+
+/**
+ * Leaves the SyncGroup. Previously synced data will continue to be
+ * available.
+ *
+ * Requires: Client must have at least Read access on the Database.
+ *
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {function} cb Callback.
+ */
+SyncGroup.prototype.leave = function(ctx, cb) {
+  this._db._wire(ctx).leaveSyncGroup(ctx, this.name, cb);
+};
+
+/**
+ * Destroys a SyncGroup. Previously synced data will continue to be available
+ * to all members.
+ *
+ * Requires: Client must have at least Read access on the Database, and must
+ * have Admin access on the SyncGroup ACL.
+ *
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {function} cb Callback.
+ */
+SyncGroup.prototype.destroy = function(ctx, cb) {
+  this._db._wire(ctx).destroySyncGroup(ctx, this.name, cb);
+};
+
+/**
+ * Ejects a member from the SyncGroup. The ejected member will not be able to
+ * sync further, but will retain any data it has already synced.
+ *
+ * Requires: Client must have at least Read access on the Database, and must
+ * have Admin access on the SyncGroup ACL.
+ *
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {module:syncbase.nosql.SyncGroupMemberInfo} member
+ * SyncGroupMemberInfo.
+ * @param {function} cb Callback.
+ */
+SyncGroup.prototype.eject = function(ctx, member, cb) {
+  this._db._wire(ctx).ejectFromSyncGroup(ctx, this.name, member, cb);
+};
+
+/**
+ *
+ * Gets the SyncGroup spec. version allows for atomic read-modify-write of the
+ * spec - see comment for setSpec.
+ *
+ * Requires: Client must have at least Read access on the Database and on the
+ * SyncGroup ACL.
+ *
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {function} cb Callback.
+ */
+SyncGroup.prototype.getSpec = function(ctx, cb) {
+  this._db._wire(ctx).getSyncGroupSpec(ctx, this.name, cb);
+};
+
+/**
+ * Sets the SyncGroup spec. version may be either empty or the value from a
+ * previous Get. If not empty, Set will only succeed if the current version
+ * matches the specified one.
+ *
+ * Requires: Client must have at least Read access on the Database, and must
+ * have Admin access on the SyncGroup ACL.
+ *
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {module:syncbase.nosql.SyncGroupSpec} spec SyncGroupSpec.
+ * @param {function} cb Callback.
+ */
+SyncGroup.prototype.setSpec = function(ctx, spec, cb) {
+  this._db._wire(ctx).setSyncGroupSpec(ctx, this.name, spec, cb);
+};
+
+/**
+ * Gets the info objects for members of the SyncGroup.
+ *
+ * Requires: Client must have at least Read access on the Database and on the
+ * SyncGroup ACL.
+ * @param {module:vanadium.context.Context} ctx Vanadium context.
+ * @param {function} cb Callback.
+ */
+SyncGroup.prototype.getMembers = function(ctx, cb) {
+  this._db._wire(ctx).getSyncGroupMembers(ctx, this.name, cb);
+};
diff --git a/test/integration/test-syncgroup.js b/test/integration/test-syncgroup.js
new file mode 100644
index 0000000..76cae4a
--- /dev/null
+++ b/test/integration/test-syncgroup.js
@@ -0,0 +1,247 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+var async = require('async');
+var naming = require('vanadium').naming;
+var test = require('prova');
+
+var nosql = require('../..').nosql;
+var SyncGroup = require('../../src/nosql/syncgroup');
+var verror = require('vanadium').verror;
+
+var testUtil = require('./util');
+var setupDatabase = testUtil.setupDatabase;
+var setupSyncGroup = testUtil.setupSyncGroup;
+var uniqueName = testUtil.uniqueName;
+
+// TODO(nlacasse): Where does this magic number 8 come from? It's in
+// syncgroup_test.go.
+var myInfo = new nosql.SyncGroupMemberInfo({
+  syncPriority: 8
+});
+
+test('db.syncGroup returns a SyncGroup with name', function(t) {
+  setupDatabase(t, function(err, o) {
+    if (err) {
+      return t.end(err);
+    }
+
+    var db = o.database;
+
+    var sgName = uniqueName('syncgroup');
+    var sg = db.syncGroup(sgName);
+    t.ok(sg instanceof SyncGroup, 'syncgroup is instanceof SyncGroup');
+    t.equal(sg.name, sgName, 'syncgroup has correct name');
+    o.teardown(t.end);
+  });
+});
+
+test('syncgroup.create with empty spec', function(t) {
+  setupDatabase(t, function(err, o) {
+    if (err) {
+      return t.end(err);
+    }
+
+    var db = o.database;
+    var ctx = o.ctx;
+
+    var spec = new nosql.SyncGroupSpec();
+    var name = uniqueName('syncgroup');
+
+    db.syncGroup(name).create(ctx, spec, myInfo, function(err) {
+      t.ok(err, 'should error');
+      t.ok(err instanceof verror.BadArgError, 'err is BadArgError');
+      o.teardown(t.end);
+    });
+  });
+});
+
+test('syncgroup.create with valid spec', function(t) {
+  setupDatabase(t, function(err, o) {
+    if (err) {
+      return t.end(err);
+    }
+
+    var db = o.database;
+    var ctx = o.ctx;
+
+    // TODO(nlacasse): It's not obvious that the syncgroup name needs to be
+    // appended to a syncbase service name.
+    var name = naming.join(o.service.fullName, uniqueName('syncgroup'));
+    var prefix = 't1/foo';
+
+    var spec = new nosql.SyncGroupSpec({
+      description: 'test syncgroup ' + name,
+      perms: {},
+      prefixes: [prefix]
+    });
+
+    db.syncGroup(name).create(ctx, spec, myInfo, function(err) {
+      t.error(err, 'should not error');
+      o.teardown(t.end);
+    });
+  });
+});
+
+test('creating a nested syncgroup', function(t) {
+  var perms = {};
+  var prefixes = ['t1/foo'];
+
+  setupSyncGroup(t, perms, prefixes, function(err, o) {
+    if (err) {
+      return t.end(err);
+    }
+
+    var db = o.database;
+    var ctx = o.ctx;
+
+    var prefixes2 = ['t1/foobar'];
+
+    // TODO(nlacasse): It's not obvious that the syncgroup name needs to be
+    // appended to a syncbase service name.
+    var name = naming.join(o.service.fullName, uniqueName('syncgroup'));
+
+    var spec = new nosql.SyncGroupSpec({
+      description: 'another syncgroup named ' + name,
+      perms: {},
+      prefixes: prefixes2
+    });
+
+    var sg2 = db.syncGroup(name);
+    sg2.create(ctx, spec, myInfo, function(err) {
+      t.error(err, 'should not error');
+      o.teardown(t.end);
+    });
+  });
+});
+
+test('creating a syncgroup that already exists', function(t) {
+  var perms = {};
+  var prefixes = ['t1/foo'];
+
+  setupSyncGroup(t, perms, prefixes, function(err, o) {
+    if (err) {
+      return t.end(err);
+    }
+
+    var db = o.database;
+    var ctx = o.ctx;
+
+    var name = o.syncgroup.name;
+
+    var spec = new nosql.SyncGroupSpec({
+      description: 'another syncgroup named ' + name,
+      perms: {},
+      prefixes: ['another/prefix']
+    });
+
+    var sg2 = db.syncGroup(name);
+    sg2.create(ctx, spec, myInfo, function(err) {
+      t.ok(err, 'should error');
+      t.ok(err instanceof verror.ExistError, 'err is ExistError');
+      o.teardown(t.end);
+    });
+  });
+});
+
+test('syncgroup.join succeeds if user has Read access', function(t) {
+  var perms = new Map([
+    ['Read', {
+      'in': ['...']
+    }]
+  ]);
+  var prefixes = ['t1/foo'];
+
+  setupSyncGroup(t, perms, prefixes, function(err, o) {
+    if (err) {
+      return t.end(err);
+    }
+
+    o.syncgroup.join(o.ctx, myInfo, function(err) {
+      t.error(err, 'should not error');
+      o.teardown(t.end);
+    });
+  });
+});
+
+test('syncgroup.join fails if user does not have Read access', function(t) {
+  var perms = new Map([
+    ['Read', {
+      'in': ['some/blessing/name']
+    }]
+  ]);
+  var prefixes = ['t1/foo'];
+
+  setupSyncGroup(t, perms, prefixes, function(err, o) {
+    if (err) {
+      return t.end(err);
+    }
+
+    var sg = o.syncgroup;
+    var ctx = o.ctx;
+
+    sg.join(ctx, myInfo, function(err) {
+      t.ok(err, 'should not error');
+      t.ok(err instanceof verror.NoAccessError, 'err is NoAccessError');
+      o.teardown(t.end);
+    });
+  });
+});
+
+// TODO(nlacasse): Enable this test once Syncbase server implements
+// Database.GetSyncGroupNames.
+test.skip('db.getSyncGroupNames returns the correct names', function(t) {
+  setupDatabase(t, function(err, o) {
+    if (err) {
+      return t.end(err);
+    }
+
+    var ctx = o.ctx;
+    var db = o.database;
+
+    var names = [
+      uniqueName('syncgroup'),
+      uniqueName('syncgroup'),
+      uniqueName('syncgroup')
+    ];
+
+    var fullNames = names.map(function(name) {
+      return naming.join(o.service.fullName, name);
+    });
+
+    createSyncGroups();
+
+    function createSyncGroups() {
+      async.forEach(fullNames, function(fullName, cb) {
+        var spec = new nosql.SyncGroupSpec({
+          description: 'syncgroup named ' + fullName,
+          prefixes: ['']
+        });
+
+        db.syncGroup(fullName).create(ctx, spec, myInfo, cb);
+      }, getSyncGroupNames);
+    }
+
+    function getSyncGroupNames(err) {
+      if (err) {
+        t.error(err);
+        o.teardown(t.end);
+      }
+
+      db.getSyncGroupNames(ctx, assertSyncGroupNames);
+    }
+
+    function assertSyncGroupNames(err, gotNames) {
+      if (err) {
+        t.error(err);
+        return o.teardown(t.end);
+      }
+
+      fullNames.sort();
+      gotNames.sort();
+      t.deepEqual(fullNames, gotNames, 'syncgroup names are correct');
+      o.teardown(t.end);
+    }
+  });
+});
diff --git a/test/integration/util.js b/test/integration/util.js
index 6f8d934..01a2aa4 100644
--- a/test/integration/util.js
+++ b/test/integration/util.js
@@ -10,6 +10,7 @@
   setupApp: setupApp,
   setupDatabase: setupDatabase,
   setupService: setupService,
+  setupSyncGroup: setupSyncGroup,
   setupTable: setupTable,
 
   assertScanRows: assertScanRows,
@@ -27,7 +28,7 @@
 var SERVICE_NAME = require('./service-name');
 
 // Helper function to generate unique names.
-var nameCounter = 0;
+var nameCounter = Date.now();
 
 function uniqueName(prefix) {
   prefix = prefix || 'name';
@@ -103,6 +104,42 @@
   });
 }
 
+// Initializes Vanadium runtime and creats an App, Database and SyncGroup.
+function setupSyncGroup(t, perms, prefixes, cb) {
+  setupDatabase(t, function(err, o) {
+    if (err) {
+      return cb(err);
+    }
+
+    var sgName = uniqueName('syncgroup');
+    var fullSgName = vanadium.naming.join(o.service.fullName, sgName);
+
+    // TODO(nlacasse): Where does this magic number 8 come from? It's in
+    // syncgroup_test.go.
+    var myInfo = new syncbase.nosql.SyncGroupMemberInfo({
+      syncPriority: 8
+    });
+
+    var spec = new syncbase.nosql.SyncGroupSpec({
+      description: 'test syncgroup ' + fullSgName,
+      perms: perms,
+      prefixes: prefixes
+    });
+
+    var sg = o.database.syncGroup(fullSgName);
+    sg.create(o.ctx, spec, myInfo, function(err) {
+      if (err) {
+        o.rt.close(t.error);
+        return cb(err);
+      }
+
+      return cb(null, extend(o, {
+        syncgroup: sg
+      }));
+    });
+  });
+}
+
 // Initializes Vanadium runtime and creates an App, a Database and a Table.
 function setupTable(t, cb) {
   setupDatabase(t, function(err, o) {
diff --git a/test/start-syncbased.sh b/test/start-syncbased.sh
index 8fe21d6..d6c2e11 100755
--- a/test/start-syncbased.sh
+++ b/test/start-syncbased.sh
@@ -10,4 +10,4 @@
 # fix service-runner to allow flags/arguments, and then have it start syncbased
 # directly with the appropriate flags.  Then we can delete this file.
 
-syncbased -v=3 --name test/syncbased --engine memstore
+syncbased -v=1 --name test/syncbased --engine memstore