diff --git a/packages/bigtable/package.json b/packages/bigtable/package.json index e1d9ded8aa6..0576b56c545 100644 --- a/packages/bigtable/package.json +++ b/packages/bigtable/package.json @@ -50,13 +50,13 @@ "bigtable" ], "dependencies": { - "@google-cloud/common": "^0.1.0", "arrify": "^1.0.0", "concat-stream": "^1.5.0", "create-error-class": "^2.0.1", "dot-prop": "^2.4.0", "extend": "^3.0.0", - "google-proto-files": "^0.2.1", + "@google-cloud/common": "^0.3.0", + "google-proto-files": "^0.7.0", "is": "^3.0.1", "lodash.flatten": "^4.2.0", "node-int64": "^0.4.0", diff --git a/packages/bigtable/src/cluster.js b/packages/bigtable/src/cluster.js new file mode 100644 index 00000000000..ddd7595b94a --- /dev/null +++ b/packages/bigtable/src/cluster.js @@ -0,0 +1,275 @@ +/*! + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module bigtable/instance + */ + +'use strict'; + +var common = require('@google-cloud/common'); +var format = require('string-format-obj'); +var is = require('is'); +var util = require('util'); + +/** + * Create a cluster object to interact with your cluster. + * + * @constructor + * @alias module:bigtable/cluster + * + * @param {string} name - Name of the cluster. + * + * @example + * var instance = bigtable.instance('my-instance'); + * var cluster = instance.cluster('my-cluster'); + */ +function Cluster(instance, name) { + var id = name; + + if (id.indexOf('/') === -1) { + id = instance.id + '/clusters/' + name; + } + + var methods = { + + /** + * Create a cluster. + * + * @param {object} options - See {module:bigtable/instance#createCluster} + * + * @example + * cluster.create(function(err, cluster, operation, apiResponse) { + * if (err) { + * // Error handling omitted. + * } + * + * operation + * .on('error', console.error) + * .on('complete', function() { + * // The cluster was created successfully. + * }); + * }); + */ + create: true, + + /** + * Delete the cluster. + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * cluster.delete(function(err, apiResponse) {}); + */ + delete: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'deleteCluster' + }, + reqOpts: { + name: id + } + }, + + /** + * Check if a cluster exists. + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {boolean} callback.exists - Whether the cluster exists or not. + * + * @example + * cluster.exists(function(err, exists) {}); + */ + exists: true, + + /** + * Get a cluster if it exists. + * + * @example + * cluster.get(function(err, cluster, apiResponse) { + * // The `cluster` data has been populated. + * }); + */ + get: true, + + /** + * Get the cluster metadata. + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.metadata - The metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * cluster.getMetadata(function(err, metadata, apiResponse) {}); + */ + getMetadata: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'getCluster' + }, + reqOpts: { + name: id + } + } + }; + + var config = { + parent: instance, + id: id, + methods: methods, + createMethod: function(_, options, callback) { + instance.createCluster(name, options, callback); + } + }; + + common.GrpcServiceObject.call(this, config); +} + +util.inherits(Cluster, common.GrpcServiceObject); + +/** + * Formats zone location. + * + * @private + * + * @param {string} project - The project. + * @param {string} location - The zone location. + * @return {string} + * + * @example + * Cluster.getLocation_('my-project', 'us-central1-b'); + * // 'projects/my-project/locations/us-central1-b' + */ +Cluster.getLocation_ = function(project, location) { + if (location.indexOf('/') > -1) { + return location; + } + + return format('projects/{project}/locations/{location}', { + project: project, + location: location + }); +}; + +/** + * Maps the storage type to the proper integer. + * + * @private + * + * @param {string} type - The storage type (hdd, ssd). + * @return {number} + * + * @example + * Cluster.getStorageType_('ssd'); + * // 1 + */ +Cluster.getStorageType_ = function(type) { + var storageTypes = { + unspecified: 0, + ssd: 1, + hdd: 2 + }; + + if (is.string(type)) { + type = type.toLowerCase(); + } + + return storageTypes[type] || storageTypes.unspecified; +}; + +/** + * Set the cluster metadata. + * + * See {module:bigtable/instance#createCluster} for a detailed explanation of + * the arguments. + * + * @param {object} metadata - Metadata object. + * @param {string} metadata.location - The cluster location. + * @param {number} metadata.nodes - Number of nodes allocated to the cluster. + * @param {string} metadata.storage - The cluster storage type. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {Operation} callback.operation - An operation object that can be used + * to check the status of the request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * var callback = function(err, operation, apiResponse) { + * if (err) { + * // Error handling omitted. + * } + * + * operation + * .on('error', console.error) + * .on('complete', function() { + * // The cluster was updated successfully. + * }); + * }; + * + * cluster.setMetadata({ + * location: 'us-central1-b', + * nodes: 3, + * storage: 'ssd' + * }, callback); + */ +Cluster.prototype.setMetadata = function(options, callback) { + var protoOpts = { + service: 'BigtableInstanceAdmin', + method: 'updateCluster' + }; + + var reqOpts = { + name: this.id + }; + + var bigtable = this.parent.parent; + + if (options.location) { + reqOpts.location = Cluster.getLocation_( + bigtable.projectId, + options.location + ); + } + + if (options.nodes) { + reqOpts.serveNodes = options.nodes; + } + + if (options.storage) { + reqOpts.defaultStorageType = Cluster.getStorageType_(options.storage); + } + + this.request(protoOpts, reqOpts, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var operation = bigtable.operation(resp.name); + operation.metadata = resp; + + callback(null, operation, resp); + }); +}; + +module.exports = Cluster; diff --git a/packages/bigtable/src/family.js b/packages/bigtable/src/family.js index c83c27e26fd..15e37aa2467 100644 --- a/packages/bigtable/src/family.js +++ b/packages/bigtable/src/family.js @@ -22,7 +22,6 @@ var common = require('@google-cloud/common'); var createErrorClass = require('create-error-class'); -var is = require('is'); var util = require('util'); /** @@ -40,11 +39,13 @@ var FamilyError = createErrorClass('FamilyError', function(name) { * @alias module:bigtable/family * * @example - * var table = bigtable.table('prezzy'); + * var instance = bigtable.instance('my-instance'); + * var table = instance.table('prezzy'); * var family = table.family('follows'); */ function Family(table, name) { var id = Family.formatName_(table.id, name); + this.familyName = name.split('/').pop(); var methods = { @@ -73,11 +74,15 @@ function Family(table, name) { */ delete: { protoOpts: { - service: 'BigtableTableService', - method: 'deleteColumnFamily' + service: 'BigtableTableAdmin', + method: 'modifyColumnFamilies' }, reqOpts: { - name: id + name: table.id, + modifications: [{ + id: this.familyName, + drop: true + }] } }, @@ -256,8 +261,7 @@ Family.prototype.getMetadata = function(callback) { * @resource [Garbage Collection Proto Docs]{@link https://github.com/googleapis/googleapis/blob/3592a7339da5a31a3565870989beb86e9235476e/google/bigtable/admin/table/v1/bigtable_table_data.proto#L59} * * @param {object} metadata - Metadata object. - * @param {object|string=} metadata.rule - Garbage collection rule. - * @param {string=} metadata.name - The updated column family name. + * @param {object=} metadata.rule - Garbage collection rule. * @param {function} callback - The callback function. * @param {?error} callback.err - An error returned while making this * request. @@ -265,33 +269,43 @@ Family.prototype.getMetadata = function(callback) { * * @example * family.setMetadata({ - * name: 'updated-name', - * rule: 'version() > 3 || (age() > 3d && version() > 1)' + * rule: { + * versions: 2, + * union: true + * } * }, function(err, apiResponse) {}); */ Family.prototype.setMetadata = function(metadata, callback) { + var self = this; + var grpcOpts = { - service: 'BigtableTableService', - method: 'updateColumnFamily' + service: 'BigtableTableAdmin', + method: 'modifyColumnFamilies' }; - var reqOpts = { - name: this.id + var mod = { + id: this.familyName, + update: {} }; if (metadata.rule) { - if (is.string(metadata.rule)) { - reqOpts.gcExpression = metadata.rule; - } else if (is.object(metadata.rule)) { - reqOpts.gcRule = Family.formatRule_(metadata.rule); - } + mod.update.gcRule = Family.formatRule_(metadata.rule); } - if (metadata.name) { - reqOpts.name = Family.formatName_(this.parent.id, metadata.name); - } + var reqOpts = { + name: this.parent.id, + modifications: [mod] + }; - this.request(grpcOpts, reqOpts, callback); + this.request(grpcOpts, reqOpts, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp.columnFamilies[self.familyName]; + callback(null, self.metadata, resp); + }); }; module.exports = Family; diff --git a/packages/bigtable/src/filter.js b/packages/bigtable/src/filter.js index 08340bbf6f3..e5a511d57f8 100644 --- a/packages/bigtable/src/filter.js +++ b/packages/bigtable/src/filter.js @@ -170,7 +170,7 @@ Filter.createRange = function(start, end, key) { function createBound(boundName, boundData, key) { var isInclusive = boundData.inclusive !== false; - var boundKey = boundName + key + (isInclusive ? 'Inclusive' : 'Exclusive'); + var boundKey = boundName + key + (isInclusive ? 'Closed' : 'Open'); var bound = {}; bound[boundKey] = Mutation.convertToBytes(boundData.value || boundData); diff --git a/packages/bigtable/src/index.js b/packages/bigtable/src/index.js index 276fc0e0f35..943bdd12bc7 100644 --- a/packages/bigtable/src/index.js +++ b/packages/bigtable/src/index.js @@ -20,24 +20,24 @@ 'use strict'; +var arrify = require('arrify'); var common = require('@google-cloud/common'); var extend = require('extend'); -var format = require('string-format-obj'); var googleProtoFiles = require('google-proto-files'); var is = require('is'); var util = require('util'); /** - * @type {module:bigtable/family} * @private + * @type {module:bigtable/instance} */ -var Family = require('./family.js'); +var Instance = require('./instance.js'); /** - * @type {module:bigtable/table} * @private + * @type {module:bigtable/cluster} */ -var Table = require('./table.js'); +var Cluster = require('./cluster.js'); var PKG = require('../package.json'); @@ -48,31 +48,49 @@ var PKG = require('../package.json'); * @resource [Creating a Cloud Bigtable Cluster]{@link https://cloud.google.com/bigtable/docs/creating-compute-instance} * @resource [Google Cloud Bigtable Concepts Overview]{@link https://cloud.google.com/bigtable/docs/concepts} * - * @throws {error} If a cluster is not provided. - * @throws {error} If a zone is not provided. - * * @param {object=} options - [Configuration object](#/docs). - * @param {string} options.cluster - The cluster name that hosts your tables. - * @param {string|module:compute/zone} options.zone - The zone in which your - * cluster resides. * + * @example * //- - * //

Creating a Cluster

+ * //

Creating a Compute Instance

* // - * // Before you create your table, you first need to create a Bigtable Cluster - * // for the table to be served from. This can be done from either the - * // Google Cloud Platform Console or the `gcloud` cli tool. Please refer to - * // the + * // Before you create your table, you first need to create a Compute Instance + * // for the table to be served from. + * //- + * var callback = function(err, instance, operation) { + * operation + * .on('error', console.log) + * .on('complete', function() { + * // `instance` is your newly created Instance object. + * }); + * }; + * + * var instance = bigtable.instance('my-instance'); + * + * instance.create({ + * clusters: [ + * { + * name: 'my-cluster', + * location: 'us-central1-b', + * nodes: 3 + * } + * ] + * }, callback); + * + * //- + * // This can also be done from either the Google Cloud Platform Console or the + * // `gcloud` cli tool. Please refer to the + * // * // official Bigtable documentation for more information. * //- * * //- * //

Creating Tables

* // - * // After creating your cluster and enabling the Bigtable APIs, you are now - * // ready to create your table with {module:bigtable#createTable}. + * // After creating your instance and enabling the Bigtable APIs, you are now + * // ready to create your table with {module:bigtable/instance#createTable}. * //- - * bigtable.createTable('prezzy', function(err, table) { + * instance.createTable('prezzy', function(err, table) { * // `table` is your newly created Table object. * }); * @@ -85,13 +103,23 @@ var PKG = require('../package.json'); * // * // We can create a column family with {module:bigtable/table#createFamily}. * //- - * var table = bigtable.table('prezzy'); + * var table = instance.table('prezzy'); * * table.createFamily('follows', function(err, family) { * // `family` is your newly created Family object. * }); * * //- + * // It is also possible to create your column families when creating a new + * // table. + * //- + * var options = { + * families: ['follows'] + * }; + * + * instance.createTable('prezzy', options, function(err, table) {}); + * + * //- * //

Creating Rows

* // * // New rows can be created within your table using @@ -173,7 +201,6 @@ var PKG = require('../package.json'); * // with all previous versions of the data. So your `row.data` object could * // resemble the following. * //- - * console.log(row.data); * // { * // follows: { * // wmckinley: [ @@ -262,267 +289,262 @@ function Bigtable(options) { return new Bigtable(options); } - if (!options.cluster) { - throw new Error('A cluster must be provided to interact with Bigtable.'); - } - - if (!options.zone) { - throw new Error('A zone must be provided to interact with Bigtable.'); - } - - options = extend({}, options, { - zone: options.zone.name || options.zone - }); - - this.clusterName = format( - 'projects/{projectId}/zones/{zone}/clusters/{cluster}', - options - ); + var adminBaseUrl = 'bigtableadmin.googleapis.com'; var config = { baseUrl: 'bigtable.googleapis.com', service: 'bigtable', - apiVersion: 'v1', + apiVersion: 'v2', protoServices: { - BigtableService: googleProtoFiles.bigtable.v1, - BigtableTableService: { - path: googleProtoFiles.bigtable.admin, - service: 'bigtable.admin.table' + Bigtable: googleProtoFiles.bigtable.v2, + BigtableTableAdmin: { + baseUrl: adminBaseUrl, + path: googleProtoFiles.bigtable.admin.v2.table, + service: 'bigtable.admin' + }, + BigtableInstanceAdmin: { + baseUrl: adminBaseUrl, + path: googleProtoFiles.bigtable.admin.v2.instance, + service: 'bigtable.admin' + }, + Operations: { + baseUrl: adminBaseUrl, + path: googleProtoFiles('longrunning/operations.proto'), + service: 'longrunning', + apiVersion: 'v1' } }, scopes: [ 'https://www.googleapis.com/auth/bigtable.admin', - 'https://www.googleapis.com/auth/bigtable.data' + 'https://www.googleapis.com/auth/bigtable.data', + 'https://www.googleapis.com/auth/cloud-platform' ], userAgent: PKG.name + '/' + PKG.version }; common.GrpcService.call(this, config, options); + + this.projectName = 'projects/' + this.projectId; } util.inherits(Bigtable, common.GrpcService); /** - * Formats the full table name into a user friendly version. + * Create a Compute instance. * - * @private - * - * @param {string} name - The formatted Table name. - * @return {string} + * @resource [Creating a Compute Instance]{@link https://cloud.google.com/bigtable/docs/creating-compute-instance} * - * @example - * Bigtable.formatTableName_('projects/p/zones/z/clusters/c/tables/my-table'); - * // => 'my-table' - */ -Bigtable.formatTableName_ = function(name) { - if (name.indexOf('/') === -1) { - return name; - } - - var parts = name.split('/'); - return parts[parts.length - 1]; -}; - -/** - * Create a table on your Bigtable cluster. - * - * @resource [Designing Your Schema]{@link https://cloud.google.com/bigtable/docs/schema-design} - * @resource [Splitting Keys]{@link https://cloud.google.com/bigtable/docs/managing-tables#splits} - * - * @throws {error} If a name is not provided. - * - * @param {string} name - The name of the table. - * @param {object=} options - Table creation options. - * @param {object|string[]} options.families - Column families to be created - * within the table. - * @param {string} options.operation - Operation used for table that has already - * been queued to be created. - * @param {string[]} options.splits - Initial - * [split keys](https://cloud.google.com/bigtable/docs/managing-tables#splits). + * @param {string} name - The unique name of the instance. + * @param {object=} options - Instance creation options. + * @param {object[]} options.clusters - The clusters to be created within the + * instance. + * @param {string} options.displayName - The descriptive name for this instance + * as it appears in UIs. * @param {function} callback - The callback function. * @param {?error} callback.err - An error returned while making this request. - * @param {module:bigtable/table} callback.table - The newly created table. + * @param {module:bigtable/instance} callback.instance - The newly created + * instance. + * @param {Operation} callback.operation - An operation object that can be used + * to check the status of the request. * @param {object} callback.apiResponse - The full API response. * * @example - * var callback = function(err, table, apiResponse) { - * // `table` is a Table object. - * }; - * - * bigtable.createTable('prezzy', callback); + * var callback = function(err, instance, operation, apiResponse) { + * if (err) { + * // Error handling omitted. + * } * - * //- - * // Optionally specify column families to be created within the table. - * //- - * var options = { - * families: ['follows'] + * operation + * .on('error', console.log) + * .on('complete', function() { + * // The instance was created successfully. + * }); * }; * - * bigtable.createTable('prezzy', options, callback); - * - * //- - * // You can also specify garbage collection rules for your column families. - * // See {module:bigtable/table#createFamily} for more information about - * // column families and garbage collection rules. - * //- * var options = { - * families: [ + * displayName: 'my-sweet-instance', + * clusters: [ * { - * name: 'follows', - * rule: { - * age: { - * seconds: 0, - * nanos: 5000 - * }, - * versions: 3, - * union: true - * } + * name: 'my-sweet-cluster', + * nodes: 3, + * location: 'us-central1-b', + * storage: 'ssd' * } * ] * }; * - * bigtable.createTable('prezzy', options, callback); - * - * //- - * // Pre-split the table based on the row key to spread the load across - * // multiple Cloud Bigtable nodes. - * //- - * var options = { - * splits: ['10', '20'] - * }; - * - * bigtable.createTable('prezzy', options, callback); + * bigtable.createInstance('my-instance', options, callback); */ -Bigtable.prototype.createTable = function(name, options, callback) { +Bigtable.prototype.createInstance = function(name, options, callback) { var self = this; - options = options || {}; - if (is.function(options)) { callback = options; options = {}; } - if (!name) { - throw new Error('A name is required to create a table.'); - } - var protoOpts = { - service: 'BigtableTableService', - method: 'createTable' + service: 'BigtableInstanceAdmin', + method: 'createInstance' }; var reqOpts = { - name: this.clusterName, - tableId: name, - table: { - // The granularity at which timestamps are stored in the table. - // Currently only milliseconds is supported, so it's not configurable. - granularity: 0 + parent: this.projectName, + instanceId: name, + instance: { + displayName: options.displayName || name } }; - if (options.operation) { - reqOpts.table.currentOperation = options.operation; - } - - if (options.splits) { - reqOpts.initialSplitKeys = options.splits; - } - - if (options.families) { - var columnFamilies = options.families.reduce(function(families, family) { - if (is.string(family)) { - family = { - name: family - }; - } - - var columnFamily = families[family.name] = {}; + reqOpts.clusters = arrify(options.clusters) + .reduce(function(clusters, cluster) { + clusters[cluster.name] = { + location: Cluster.getLocation_(self.projectId, cluster.location), + serveNodes: cluster.nodes, + defaultStorageType: Cluster.getStorageType_(cluster.storage) + }; - if (is.string(family.rule)) { - columnFamily.gcExpression = family.rule; - } else if (is.object(family.rule)) { - columnFamily.gcRule = Family.formatRule_(family.rule); - } - - return families; + return clusters; }, {}); - reqOpts.table.columnFamilies = columnFamilies; - } - this.request(protoOpts, reqOpts, function(err, resp) { if (err) { - callback(err, null, resp); + callback(err, null, null, resp); return; } - var table = self.table(resp.name); - table.metadata = resp; + var instance = self.instance(name); + var operation = self.operation(resp.name); + operation.metadata = resp; - callback(null, table, resp); + callback(null, instance, operation, resp); }); }; /** - * Get Table objects for all the tables in your Bigtable cluster. - * + * Get Instance objects for all of your Compute instances. + * + * @param {object} query - Query object. + * @param {boolean} query.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number} query.maxApiCalls - Maximum number of API calls to make. + * @param {number} query.maxResults - Maximum number of results to return. + * @param {string} query.pageToken - Token returned from a previous call, to + * request the next page of results. * @param {function} callback - The callback function. - * @param {?error} callback.err - An error returned while making this request. - * @param {module:bigtable/table[]} callback.tables - List of all Tables. + * @param {?error} callback.error - An error returned while making this request. + * @param {module:bigtable/instance[]} callback.instances - List of all + * instances. + * @param {object} callback.nextQuery - If present, query with this object to + * check for more results. * @param {object} callback.apiResponse - The full API response. * * @example - * bigtable.getTables(function(err, tables) { + * bigtable.getInstances(function(err, instances) { * if (!err) { - * // `tables` is an array of Table objects. + * // `instances` is an array of Instance objects. * } * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to false. + * //- + * var callback = function(err, instances, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * bigtable.getInstances(nextQuery, calback); + * } + * }; + * + * bigtable.getInstances({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the instances from your project as a readable object stream. + * //- + * bigtable.getInstances() + * .on('error', console.error) + * .on('data', function(instance) { + * // `instance` is an Instance object. + * }) + * .on('end', function() { + * // All instances retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * bigtable.getInstances() + * .on('data', function(instance) { + * this.end(); + * }); */ -Bigtable.prototype.getTables = function(callback) { +Bigtable.prototype.getInstances = function(query, callback) { var self = this; + if (is.function(query)) { + callback = query; + query = {}; + } + var protoOpts = { - service: 'BigtableTableService', - method: 'listTables' + service: 'BigtableInstanceAdmin', + method: 'listInstances' }; - var reqOpts = { - name: this.clusterName - }; + var reqOpts = extend({}, query, { + parent: this.projectName + }); this.request(protoOpts, reqOpts, function(err, resp) { if (err) { - callback(err, null, resp); + callback(err, null, null, resp); return; } - var tables = resp.tables.map(function(metadata) { - var name = Bigtable.formatTableName_(metadata.name); - var table = self.table(name); - - table.metadata = metadata; - return table; + var instances = resp.instances.map(function(instanceData) { + var instance = self.instance(instanceData.name); + instance.metadata = instanceData; + return instance; }); - callback(null, tables, resp); + var nextQuery = null; + if (resp.nextPageToken) { + nextQuery = extend({}, query, { pageToken: resp.nextPageToken }); + } + + callback(null, instances, nextQuery, resp); }); }; /** - * Get a reference to a Bigtable table. + * Get a reference to a Compute instance. * - * @param {string} name - The name of the table. - * @return {module:bigtable/table} + * @param {string} name - The name of the instance. + * @return {module:bigtable/instance} + */ +Bigtable.prototype.instance = function(name) { + return new Instance(this, name); +}; + +/** + * Get a reference to an Operation. * - * @example - * var table = bigtable.table('presidents'); + * @param {string} name - The name of the instance. + * @return {Operation} */ -Bigtable.prototype.table = function(name) { - return new Table(this, name); +Bigtable.prototype.operation = function(name) { + return new common.GrpcOperation(this, name); }; -Bigtable.Table = Table; +/*! Developer Documentation + * + * These methods can be used with either a callback or as a readable object + * stream. `streamRouter` is used to add this dual behavior. + */ +common.streamRouter.extend(Bigtable, ['getInstances']); module.exports = Bigtable; diff --git a/packages/bigtable/src/instance.js b/packages/bigtable/src/instance.js new file mode 100644 index 00000000000..579ba608296 --- /dev/null +++ b/packages/bigtable/src/instance.js @@ -0,0 +1,667 @@ +/*! + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module bigtable/instance + */ + +'use strict'; + +var common = require('@google-cloud/common'); +var extend = require('extend'); +var is = require('is'); +var util = require('util'); + +/** + * @private + * @type {module:bigtable/cluster} + */ +var Cluster = require('./cluster.js'); + +/** + * @private + * @type {module:bigtable/family} + */ +var Family = require('./family.js'); + +/** + * @private + * @type {module:bigtable/table} + */ +var Table = require('./table.js'); + +/** + * Create an Instance object to interact with a Compute instance. + * + * @constructor + * @alias module:bigtable/instance + * + * @param {string} name - Name of the instance. + * + * @example + * var instance = bigtable.instance('my-instance'); + */ +function Instance(bigtable, name) { + var id = name; + + if (id.indexOf('/') === -1) { + id = bigtable.projectName + '/instances/' + name; + } + + var methods = { + + /** + * Create an instance. + * + * @param {object=} options - See {module:bigtable#createInstance}. + * + * @example + * instance.create(function(err, instance, operation, apiResponse) { + * if (err) { + * // Error handling omitted. + * } + * + * operation + * .on('error', console.error) + * .on('complete', function() { + * // The instance was created successfully. + * }); + * }); + */ + create: true, + + /** + * Delete the instance. + * + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * instance.delete(function(err, apiResponse) {}); + */ + delete: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'deleteInstance' + }, + reqOpts: { + name: id + } + }, + + /** + * Check if an instance exists. + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {boolean} callback.exists - Whether the instance exists or not. + * + * @example + * instance.exists(function(err, exists) {}); + */ + exists: true, + + /** + * Get an instance if it exists. + * + * @example + * instance.get(function(err, instance, apiResponse) { + * // The `instance` data has been populated. + * }); + */ + get: true, + + /** + * Get the instance metadata. + * + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.metadata - The metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * instance.getMetadata(function(err, metadata, apiResponse) {}); + */ + getMetadata: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'getInstance' + }, + reqOpts: { + name: id + } + }, + + /** + * Set the instance metadata. + * + * @param {object} metadata - Metadata object. + * @param {string} metadata.displayName - The descriptive name for this + * instance as it appears in UIs. It can be changed at any time, but + * should be kept globally unique to avoid confusion. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * instance.setMetadata({ + * displayName: 'updated-name' + * }, function(err, apiResponse) {}); + */ + setMetadata: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'updateInstance' + }, + reqOpts: { + name: id + } + } + }; + + var config = { + parent: bigtable, + id: id, + methods: methods, + createMethod: function(_, options, callback) { + bigtable.createInstance(name, options, callback); + } + }; + + common.GrpcServiceObject.call(this, config); +} + +util.inherits(Instance, common.GrpcServiceObject); + +/** + * Create a cluster. + * + * @param {string} name - The name to be used when referring to the new + * cluster within its instance. + * @param {object=} options - Cluster creation options. + * @param {string} options.location - The location where this cluster's nodes + * and storage reside. For best performance clients should be located as + * as close as possible to this cluster. Currently only zones are + * supported. + * @param {number} options.nodes - The number of nodes allocated to this + * cluster. More nodes enable higher throughput and more consistent + * performance. + * @param {string} options.storage - The type of storage used by this cluster + * to serve its parent instance's tables. Options are 'hdd' or 'ssd'. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:bigtable/cluster} callback.cluster - The newly created + * cluster. + * @param {Operation} callback.operation - An operation object that can be used + * to check the status of the request. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * var callback = function(err, cluster, operation, apiResponse) { + * if (err) { + * // Error handling omitted. + * } + * + * operation + * .on('error', console.log) + * .on('complete', function() { + * // The cluster was created successfully. + * }); + * }; + * + * var options = { + * location: 'us-central1-b', + * nodes: 3, + * storage: 'ssd' + * }; + * + * instance.createCluster('my-cluster', options, callback); + */ +Instance.prototype.createCluster = function(name, options, callback) { + var self = this; + + if (is.function(options)) { + callback = options; + options = {}; + } + + var protoOpts = { + service: 'BigtableInstanceAdmin', + method: 'createCluster' + }; + + var reqOpts = { + parent: this.id, + clusterId: name + }; + + if (!is.empty(options)) { + reqOpts.cluster = {}; + } + + if (options.location) { + reqOpts.cluster.location = Cluster.getLocation_( + this.parent.projectName, + options.location + ); + } + + if (options.nodes) { + reqOpts.cluster.serveNodes = options.nodes; + } + + if (options.storage) { + var storageType = Cluster.getStorageType_(options.storage); + reqOpts.cluster.defaultStorageType = storageType; + } + + this.request(protoOpts, reqOpts, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var bigtable = self.parent; + + var cluster = self.cluster(name); + var operation = bigtable.operation(resp.name); + operation.metadata = resp; + + callback(null, cluster, operation, resp); + }); +}; + +/** + * Create a table on your Bigtable instance. + * + * @resource [Designing Your Schema]{@link https://cloud.google.com/bigtable/docs/schema-design} + * @resource [Splitting Keys]{@link https://cloud.google.com/bigtable/docs/managing-tables#splits} + * + * @throws {error} If a name is not provided. + * + * @param {string} name - The name of the table. + * @param {object=} options - Table creation options. + * @param {object|string[]} options.families - Column families to be created + * within the table. + * @param {string[]} options.splits - Initial + * [split keys](https://cloud.google.com/bigtable/docs/managing-tables#splits). + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:bigtable/table} callback.table - The newly created table. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * var callback = function(err, table, apiResponse) { + * // `table` is a Table object. + * }; + * + * instance.createTable('prezzy', callback); + * + * //- + * // Optionally specify column families to be created within the table. + * //- + * var options = { + * families: ['follows'] + * }; + * + * instance.createTable('prezzy', options, callback); + * + * //- + * // You can also specify garbage collection rules for your column families. + * // See {module:bigtable/table#createFamily} for more information about + * // column families and garbage collection rules. + * //- + * var options = { + * families: [ + * { + * name: 'follows', + * rule: { + * age: { + * seconds: 0, + * nanos: 5000 + * }, + * versions: 3, + * union: true + * } + * } + * ] + * }; + * + * instance.createTable('prezzy', options, callback); + * + * //- + * // Pre-split the table based on the row key to spread the load across + * // multiple Cloud Bigtable nodes. + * //- + * var options = { + * splits: ['10', '20'] + * }; + * + * instance.createTable('prezzy', options, callback); + */ +Instance.prototype.createTable = function(name, options, callback) { + var self = this; + + if (!name) { + throw new Error('A name is required to create a table.'); + } + + options = options || {}; + + if (is.function(options)) { + callback = options; + options = {}; + } + + var protoOpts = { + service: 'BigtableTableAdmin', + method: 'createTable' + }; + + var reqOpts = { + parent: this.id, + tableId: name, + table: { + // The granularity at which timestamps are stored in the table. + // Currently only milliseconds is supported, so it's not configurable. + granularity: 0 + } + }; + + if (options.splits) { + reqOpts.initialSplits = options.splits.map(function(key) { + return { + key: key + }; + }); + } + + if (options.families) { + var columnFamilies = options.families.reduce(function(families, family) { + if (is.string(family)) { + family = { + name: family + }; + } + + var columnFamily = families[family.name] = {}; + + if (family.rule) { + columnFamily.gcRule = Family.formatRule_(family.rule); + } + + return families; + }, {}); + + reqOpts.table.columnFamilies = columnFamilies; + } + + this.request(protoOpts, reqOpts, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var table = self.table(resp.name); + table.metadata = resp; + + callback(null, table, resp); + }); +}; + +/** + * Get a reference to a Bigtable Cluster. + * + * @param {string} name - The name of the cluster. + * @return {module:bigtable/cluster} + */ +Instance.prototype.cluster = function(name) { + return new Cluster(this, name); +}; + +/** + * Get Cluster objects for all of your clusters. + * + * @param {object=} query - Query object. + * @param {boolean} query.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number} query.maxApiCalls - Maximum number of API calls to make. + * @param {number} query.maxResults - Maximum number of results to return. + * @param {string} query.pageToken - Token returned from a previous call, to + * request the next page of results. + * @param {function} callback - The callback function. + * @param {?error} callback.error - An error returned while making this request. + * @param {module:bigtable/cluster[]} callback.clusters - List of all + * Clusters. + * @param {object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * instance.getClusters(function(err, clusters) { + * if (!err) { + * // `clusters` is an array of Cluster objects. + * } + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to false. + * //- + * var callback = function(err, clusters, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * instance.getClusters(nextQuery, calback); + * } + * }; + * + * instance.getClusters({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the clusters from your project as a readable object stream. + * //- + * instance.getClusters() + * .on('error', console.error) + * .on('data', function(cluster) { + * // `cluster` is a Cluster object. + * }) + * .on('end', function() { + * // All clusters retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * instance.getClusters() + * .on('data', function(cluster) { + * this.end(); + * }); + */ +Instance.prototype.getClusters = function(query, callback) { + var self = this; + + if (is.function(query)) { + callback = query; + query = {}; + } + + var protoOpts = { + service: 'BigtableInstanceAdmin', + method: 'listClusters' + }; + + var reqOpts = extend({}, query, { + parent: this.id + }); + + this.request(protoOpts, reqOpts, function(err, resp) { + if (err) { + callback(err, null, null, resp); + return; + } + + var clusters = resp.clusters.map(function(clusterObj) { + var cluster = self.cluster(clusterObj.name); + cluster.metadata = clusterObj; + return cluster; + }); + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, query, { + pageToken: resp.nextPageToken + }); + } + + callback(null, clusters, nextQuery, resp); + }); +}; + +/** + * Get Table objects for all the tables in your Compute instance. + * + * @param {object=} query - Query object. + * @param {boolean} query.autoPaginate - Have pagination handled automatically. + * Default: true. + * @param {number} query.maxApiCalls - Maximum number of API calls to make. + * @param {number} query.maxResults - Maximum number of items to return. + * @param {string} query.pageToken - A previously-returned page token + * representing part of a larger set of results to view. + * @param {string} query.view - View over the table's fields. Possible options + * are 'name', 'schema' or 'full'. Default: 'name'. + * @param {function} callback - The callback function. + * @param {?error} callback.err - An error returned while making this request. + * @param {module:bigtable/table[]} callback.tables - List of all Tables. + * @param {object} callback.nextQuery - If present, query with this object to + * check for more results. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * instance.getTables(function(err, tables) { + * if (!err) { + * // `tables` is an array of Table objects. + * } + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to false. + * //- + * var callback = function(err, tables, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * instance.getTables(nextQuery, calback); + * } + * }; + * + * instance.getTables({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the tables from your project as a readable object stream. + * //- + * instance.getTables() + * .on('error', console.error) + * .on('data', function(table) { + * // table is a Table object. + * }) + * .on('end', function() { + * // All tables retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * instance.getTables() + * .on('data', function(table) { + * this.end(); + * }); + */ +Instance.prototype.getTables = function(query, callback) { + var self = this; + + if (is.function(query)) { + callback = query; + query = {}; + } + + var protoOpts = { + service: 'BigtableTableAdmin', + method: 'listTables' + }; + + var reqOpts = extend({}, query, { + parent: this.id, + view: Table.VIEWS[query.view || 'unspecified'] + }); + + this.request(protoOpts, reqOpts, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + var tables = resp.tables.map(function(metadata) { + var name = metadata.name.split('/').pop(); + var table = self.table(name); + + table.metadata = metadata; + return table; + }); + + var nextQuery = null; + if (resp.nextPageToken) { + nextQuery = extend({}, query, { + pageToken: resp.nextPageToken + }); + } + + callback(null, tables, nextQuery, resp); + }); +}; + +/** + * Get a reference to a Bigtable table. + * + * @param {string} name - The name of the table. + * @return {module:bigtable/table} + * + * @example + * var table = instance.table('presidents'); + */ +Instance.prototype.table = function(name) { + return new Table(this, name); +}; + +/*! Developer Documentation + * + * These methods can be used with either a callback or as a readable object + * stream. `streamRouter` is used to add this dual behavior. + */ +common.streamRouter.extend(Instance, ['getClusters', 'getTables']); + +module.exports = Instance; diff --git a/packages/bigtable/src/mutation.js b/packages/bigtable/src/mutation.js index 5ca993cc5fe..a829dc4eff6 100644 --- a/packages/bigtable/src/mutation.js +++ b/packages/bigtable/src/mutation.js @@ -80,6 +80,10 @@ Mutation.convertFromBytes = function(bytes) { * @return {buffer} */ Mutation.convertToBytes = function(data) { + if (data instanceof Buffer) { + return data; + } + if (is.number(data)) { return new Int64(data).toBuffer(); } @@ -152,7 +156,7 @@ Mutation.encodeSetCell = function(data) { Object.keys(family).forEach(function(cellName) { var cell = family[cellName]; - if (!is.object(cell)) { + if (!is.object(cell) || cell instanceof Buffer) { cell = { value: cell }; diff --git a/packages/bigtable/src/row.js b/packages/bigtable/src/row.js index 44d78ddf0f9..64dad9c3ce4 100644 --- a/packages/bigtable/src/row.js +++ b/packages/bigtable/src/row.js @@ -60,7 +60,8 @@ var RowError = createErrorClass('RowError', function(row) { * @alias module:bigtable/row * * @example - * var table = bigtable.table('prezzy'); + * var instance = bigtable.instance('my-instance'); + * var table = instance.table('prezzy'); * var row = table.row('gwashington'); */ function Row(table, key) { @@ -108,44 +109,9 @@ util.inherits(Row, common.GrpcServiceObject); * @private * * @param {chunk[]} chunks - The list of chunks. + * @param {object=} options - Formatting options. * * @example - * var chunks = [ - * { - * rowContents: { - * name: 'follows', - * columns: [ - * { - * qualifier: 'gwashington', - * cells: [ - * { - * value: 1 - * } - * ] - * } - * ] - * } - * }, { - * resetRow: true - * }, { - * rowContents: { - * name: 'follows', - * columns: [ - * { - * qualifier: 'gwashington', - * cells: [ - * { - * value: 2 - * } - * ] - * } - * ] - * } - * }, { - * commitRow: true - * } - * ]; - * * Row.formatChunks_(chunks); * // { * // follows: { @@ -157,26 +123,67 @@ util.inherits(Row, common.GrpcServiceObject); * // } * // } */ -Row.formatChunks_ = function(chunks) { - var families = []; - var chunkList = []; +Row.formatChunks_ = function(chunks, options) { + var rows = []; + var familyName; + var qualifierName; + + options = options || {}; + + chunks.reduce(function(row, chunk) { + var family; + var qualifier; - chunks.forEach(function(chunk) { - if (chunk.resetRow) { - chunkList = []; + row.data = row.data || {}; + + if (chunk.rowKey) { + row.key = Mutation.convertFromBytes(chunk.rowKey); + } + + if (chunk.familyName) { + familyName = chunk.familyName.value; + } + + if (familyName) { + family = row.data[familyName] = row.data[familyName] || {}; } - if (chunk.rowContents) { - chunkList.push(chunk.rowContents); + if (chunk.qualifier) { + qualifierName = Mutation.convertFromBytes(chunk.qualifier.value); + } + + if (family && qualifierName) { + qualifier = family[qualifierName] = family[qualifierName] || []; + } + + if (qualifier && chunk.value) { + var value = chunk.value; + + if (options.decode !== false) { + value = Mutation.convertFromBytes(value); + } + + qualifier.push({ + value: value, + labels: chunk.labels, + timestamp: chunk.timestampMicros, + size: chunk.valueSize + }); } if (chunk.commitRow) { - families = families.concat(chunkList); - chunkList = []; + rows.push(row); + } + + if (chunk.commitRow || chunk.resetRow) { + familyName = qualifierName = null; + return {}; } - }); - return Row.formatFamilies_(families); + return row; + }, {}); + + return rows; }; /** @@ -185,6 +192,7 @@ Row.formatChunks_ = function(chunks) { * @private * * @param {object[]} families - The row families. + * @param {object=} options - Formatting options. * * @example * var families = [ @@ -214,9 +222,11 @@ Row.formatChunks_ = function(chunks) { * // } * // } */ -Row.formatFamilies_ = function(families) { +Row.formatFamilies_ = function(families, options) { var data = {}; + options = options || {}; + families.forEach(function(family) { var familyData = data[family.name] = {}; @@ -224,8 +234,14 @@ Row.formatFamilies_ = function(families) { var qualifier = Mutation.convertFromBytes(column.qualifier); familyData[qualifier] = column.cells.map(function(cell) { + var value = cell.value; + + if (options.decode !== false) { + value = Mutation.convertFromBytes(value); + } + return { - value: Mutation.convertFromBytes(cell.value), + value: value, timestamp: cell.timestampMicros, labels: cell.labels }; @@ -355,7 +371,7 @@ Row.prototype.createRules = function(rules, callback) { }); var grpcOpts = { - service: 'BigtableService', + service: 'Bigtable', method: 'readModifyWriteRow' }; @@ -421,7 +437,7 @@ Row.prototype.createRules = function(rules, callback) { */ Row.prototype.filter = function(filter, onMatch, onNoMatch, callback) { var grpcOpts = { - service: 'BigtableService', + service: 'Bigtable', method: 'checkAndMutateRow' }; @@ -520,6 +536,9 @@ Row.prototype.deleteCells = function(columns, callback) { * Get the row data. See {module:bigtable/table#getRows}. * * @param {string[]=} columns - List of specific columns to retrieve. + * @param {object} options - Configuration object. + * @param {boolean} options.decode - If set to `false` it will not decode Buffer + * values returned from Bigtable. Default: true. * @param {function} callback - The callback function. * @param {?error} callback.err - An error returned while making this * request. @@ -547,14 +566,20 @@ Row.prototype.deleteCells = function(columns, callback) { * 'follows:alincoln' * ], callback); */ -Row.prototype.get = function(columns, callback) { +Row.prototype.get = function(columns, options, callback) { var self = this; - if (is.function(columns)) { - callback = columns; + if (!is.array(columns)) { + callback = options; + options = columns; columns = []; } + if (is.function(options)) { + callback = options; + options = {}; + } + var filter; columns = arrify(columns); @@ -581,10 +606,10 @@ Row.prototype.get = function(columns, callback) { } } - var reqOpts = { - key: this.id, + var reqOpts = extend({}, options, { + keys: [this.id], filter: filter - }; + }); this.parent.getRows(reqOpts, function(err, rows, apiResponse) { if (err) { @@ -612,6 +637,9 @@ Row.prototype.get = function(columns, callback) { /** * Get the row's metadata. * + * @param {object=} options - Configuration object. + * @param {boolean} options.decode - If set to `false` it will not decode Buffer + * values returned from Bigtable. Default: true. * @param {function} callback - The callback function. * @param {?error} callback.err - An error returned while making this * request. @@ -621,8 +649,13 @@ Row.prototype.get = function(columns, callback) { * @example * row.getMetadata(function(err, metadata, apiResponse) {}); */ -Row.prototype.getMetadata = function(callback) { - this.get(function(err, row, resp) { +Row.prototype.getMetadata = function(options, callback) { + if (is.function(options)) { + callback = options; + options = {}; + } + + this.get(options, function(err, row, resp) { if (err) { callback(err, null, resp); return; @@ -674,16 +707,16 @@ Row.prototype.increment = function(column, value, callback) { increment: value }; - this.createRules(reqOpts, function(err, apiResponse) { + this.createRules(reqOpts, function(err, resp) { if (err) { - callback(err, null, apiResponse); + callback(err, null, resp); return; } - var data = Row.formatFamilies_(apiResponse.families); + var data = Row.formatFamilies_(resp.row.families); var value = dotProp.get(data, column.replace(':', '.'))[0].value; - callback(null, value, apiResponse); + callback(null, value, resp); }); }; @@ -693,6 +726,8 @@ Row.prototype.increment = function(column, value, callback) { * @param {string|object} key - Either a column name or an entry * object to be inserted into the row. See {module:bigtable/table#insert}. * @param {*=} value - This can be omitted if using entry object. + * @param {object=} options - Configuration options. See + * {module:bigtable/table#mutate}. * @param {function} callback - The callback function. * @param {?error} callback.err - An error returned while making this * request. diff --git a/packages/bigtable/src/table.js b/packages/bigtable/src/table.js index e68a373e238..5df17d1e6a6 100644 --- a/packages/bigtable/src/table.js +++ b/packages/bigtable/src/table.js @@ -23,6 +23,7 @@ var arrify = require('arrify'); var common = require('@google-cloud/common'); var concat = require('concat-stream'); +var events = require('events'); var flatten = require('lodash.flatten'); var is = require('is'); var propAssign = require('prop-assign'); @@ -63,17 +64,18 @@ var Row = require('./row.js'); * @param {string} name - Name of the table. * * @example - * var table = bigtable.table('prezzy'); + * var instance = bigtable.instance('my-instance'); + * var table = instance.table('prezzy'); */ -function Table(bigtable, name) { - var id = Table.formatName_(bigtable.clusterName, name); +function Table(instance, name) { + var id = Table.formatName_(instance.id, name); var methods = { /** * Create a table. * - * @param {object=} options - See {module:bigtable#createTable}. + * @param {object=} options - See {module:bigtable/instance#createTable}. * * @example * table.create(function(err, table, apiResponse) { @@ -97,7 +99,7 @@ function Table(bigtable, name) { */ delete: { protoOpts: { - service: 'BigtableTableService', + service: 'BigtableTableAdmin', method: 'deleteTable' }, reqOpts: { @@ -129,43 +131,23 @@ function Table(bigtable, name) { * @param {options=} options - Configuration object. * @param {boolean} options.autoCreate - Automatically create the object if * it does not exist. Default: `false` + * @param {string} options.view - The view to be applied to the table + * fields. See {module:bigtable/table#getMetadata}. * * @example * table.get(function(err, table, apiResponse) { * // The `table` data has been populated. * }); */ - get: true, - - /** - * Get the table's metadata. - * - * @param {function=} callback - The callback function. - * @param {?error} callback.err - An error returned while making this - * request. - * @param {object} callback.metadata - The table's metadata. - * @param {object} callback.apiResponse - The full API response. - * - * @example - * table.getMetadata(function(err, metadata, apiResponse) {}); - */ - getMetadata: { - protoOpts: { - service: 'BigtableTableService', - method: 'getTable' - }, - reqOpts: { - name: id - } - } + get: true }; var config = { - parent: bigtable, + parent: instance, id: id, methods: methods, createMethod: function(_, options, callback) { - bigtable.createTable(name, options, callback); + instance.createTable(name, options, callback); } }; @@ -174,78 +156,57 @@ function Table(bigtable, name) { util.inherits(Table, common.GrpcServiceObject); +/** + * The view to be applied to the returned table's fields. + * Defaults to schema if unspecified. + * + * @private + */ +Table.VIEWS = { + unspecified: 0, + name: 1, + schema: 2, + full: 4 +}; + /** * Formats the table name to include the Bigtable cluster. * * @private * - * @param {string} clusterName - The formatted cluster name. + * @param {string} instanceName - The formatted instance name. * @param {string} name - The table name. * * @example * Table.formatName_( - * 'projects/my-project/zones/my-zone/clusters/my-cluster', + * 'projects/my-project/zones/my-zone/instances/my-instance', * 'my-table' * ); - * // 'projects/my-project/zones/my-zone/clusters/my-cluster/tables/my-table' + * // 'projects/my-project/zones/my-zone/instances/my-instance/tables/my-table' */ -Table.formatName_ = function(clusterName, name) { +Table.formatName_ = function(instanceName, name) { if (name.indexOf('/') > -1) { return name; } - return clusterName + '/tables/' + name; -}; - -/** - * Formats a row range into the desired proto format. - * - * @private - * - * @param {object} range - The range object. - * @param {string} range.start - The lower bound for the range. - * @param {string} range.end - The upper bound for the range. - * @return {object} - * - * @example - * Table.formatRowRange_({ - * start: 'gwashington', - * end: 'alincoln' - * }); - * // { - * // startKey: new Buffer('gwashington'), - * // endKey: new Buffer('alincoln') - * // } - */ -Table.formatRowRange_ = function(range) { - var rowRange = {}; - - if (range.start) { - rowRange.startKey = Mutation.convertToBytes(range.start); - } - - if (range.end) { - rowRange.endKey = Mutation.convertToBytes(range.end); - } - - return rowRange; + return instanceName + '/tables/' + name; }; /** * Create a column family. * - * Optionally you can send garbage collection rules and expressions when - * creating a family. Garbage collection executes opportunistically in the - * background, so it's possible for reads to return a cell even if it - * matches the active expression for its family. + * Optionally you can send garbage collection rules and when creating a family. + * Garbage collection executes opportunistically in the background, so it's + * possible for reads to return a cell even if it matches the active expression + * for its family. * * @resource [Garbage Collection Proto Docs]{@link https://github.com/googleapis/googleapis/blob/master/google/bigtable/admin/table/v1/bigtable_table_data.proto#L59} * * @throws {error} If a name is not provided. * * @param {string} name - The name of column family. - * @param {string|object=} rule - Garbage collection rule. - * @param {object=} rule.age - Delete cells in a column older than the given + * @param {object=} rule - Garbage collection rule. + * @param {object} rule.age - Delete cells in a column older than the given * age. Values must be at least 1 millisecond. * @param {number} rule.versions - Maximum number of versions to delete cells * in a column, except for the most recent. @@ -271,13 +232,6 @@ Table.formatRowRange_ = function(range) { * }; * * table.createFamily('follows', rule, callback); - * - * //- - * // Alternatively you can send a garbage collection expression. - * //- - * var expression = 'version() > 3 || (age() > 3d && version() > 1)'; - * - * table.createFamily('follows', expression, callback); */ Table.prototype.createFamily = function(name, rule, callback) { var self = this; @@ -292,25 +246,24 @@ Table.prototype.createFamily = function(name, rule, callback) { } var grpcOpts = { - service: 'BigtableTableService', - method: 'createColumnFamily' + service: 'BigtableTableAdmin', + method: 'modifyColumnFamilies' }; - var reqOpts = { - name: this.id, - columnFamilyId: name + var mod = { + id: name, + create: {} }; - if (is.string(rule)) { - reqOpts.columnFamily = { - gcExpression: rule - }; - } else if (is.object(rule)) { - reqOpts.columnFamily = { - gcRule: Family.formatRule_(rule) - }; + if (rule) { + mod.create.gcRule = Family.formatRule_(rule); } + var reqOpts = { + name: this.id, + modifications: [mod] + }; + this.request(grpcOpts, reqOpts, function(err, resp) { if (err) { callback(err, null, resp); @@ -360,12 +313,12 @@ Table.prototype.deleteRows = function(options, callback) { } var grpcOpts = { - service: 'BigtableTableService', - method: 'bulkDeleteRows' + service: 'BigtableTableAdmin', + method: 'dropRowRange' }; var reqOpts = { - tableName: this.id + name: this.id }; if (options.prefix) { @@ -428,11 +381,55 @@ Table.prototype.getFamilies = function(callback) { }); }; +/** + * Get the table's metadata. + * + * @param {object=} options - Table request options. + * @param {string} options.view - The view to be applied to the table fields. + * @param {function=} callback - The callback function. + * @param {?error} callback.err - An error returned while making this + * request. + * @param {object} callback.metadata - The table's metadata. + * @param {object} callback.apiResponse - The full API response. + * + * @example + * table.getMetadata(function(err, metadata, apiResponse) {}); + */ +Table.prototype.getMetadata = function(options, callback) { + var self = this; + + if (is.function(options)) { + callback = options; + options = {}; + } + + var protoOpts = { + service: 'BigtableTableAdmin', + method: 'getTable' + }; + + var reqOpts = { + name: this.id, + view: Table.VIEWS[options.view || 'unspecified'] + }; + + this.request(protoOpts, reqOpts, function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } + + self.metadata = resp; + callback(null, self.metadata, resp); + }); +}; + /** * Get Row objects for the rows currently in your table. * * @param {options=} options - Configuration object. - * @param {string} options.key - An individual row key. + * @param {boolean} options.decode - If set to `false` it will not decode Buffer + * values returned from Bigtable. Default: true. * @param {string[]} options.keys - A list of row keys. * @param {string} options.start - Start value for key range. * @param {string} options.end - End value for key range. @@ -461,13 +458,6 @@ Table.prototype.getFamilies = function(callback) { * table.getRows(callback); * * //- - * // Specify a single row to be returned. - * //- - * table.getRows({ - * key: 'alincoln' - * }, callback); - * - * //- * // Specify arbitrary keys for a non-contiguous set of rows. * // The total size of the keys must remain under 1MB, after encoding. * //- @@ -557,9 +547,10 @@ Table.prototype.getRows = function(options, callback) { } options = options || {}; + options.ranges = options.ranges || []; var grpcOpts = { - service: 'BigtableService', + service: 'Bigtable', method: 'readRows' }; @@ -568,19 +559,24 @@ Table.prototype.getRows = function(options, callback) { objectMode: true }; - if (options.key) { - reqOpts.rowKey = Mutation.convertToBytes(options.key); - } else if (options.start || options.end) { - reqOpts.rowRange = Table.formatRowRange_(options); - } else if (options.keys || options.ranges) { - reqOpts.rowSet = {}; + if (options.start || options.end) { + options.ranges.push({ + start: options.start, + end: options.end + }); + } + + if (options.keys || options.ranges.length) { + reqOpts.rows = {}; if (options.keys) { - reqOpts.rowSet.rowKeys = options.keys.map(Mutation.convertToBytes); + reqOpts.rows.rowKeys = options.keys.map(Mutation.convertToBytes); } - if (options.ranges) { - reqOpts.rowSet.rowRanges = options.ranges.map(Table.formatRowRange_); + if (options.ranges.length) { + reqOpts.rows.rowRanges = options.ranges.map(function(range) { + return Filter.createRange(range.start, range.end, 'key'); + }); } } @@ -588,21 +584,26 @@ Table.prototype.getRows = function(options, callback) { reqOpts.filter = Filter.parse(options.filter); } - if (options.interleave) { - reqOpts.allowRowInterleaving = options.interleave; - } - if (options.limit) { reqOpts.numRowsLimit = options.limit; } var stream = pumpify.obj([ this.requestStream(grpcOpts, reqOpts), - through.obj(function(rowData, enc, next) { - var row = self.row(Mutation.convertFromBytes(rowData.rowKey)); + through.obj(function(data, enc, next) { + var throughStream = this; + var rows = Row.formatChunks_(data.chunks, { + decode: options.decode + }); - row.data = Row.formatChunks_(rowData.chunks); - next(null, row); + rows.forEach(function(rowData) { + var row = self.row(rowData.key); + + row.data = rowData.data; + throughStream.push(row); + }); + + next(); }) ]); @@ -624,9 +625,32 @@ Table.prototype.getRows = function(options, callback) { * See {module:bigtable/table#mutate}. * @param {function} callback - The callback function. * @param {?error} callback.err - An error returned while making this request. - * @param {object} callback.apiResponse - The full API response. + * @param {object[]} callback.insertErrors - A status object for each failed + * insert. * * @example + * var callback = function(err, insertErrors) { + * if (err) { + * // Error handling omitted. + * } + * + * // insertErrors = [ + * // { + * // code: 500, + * // message: 'Internal Server Error', + * // entry: { + * // key: 'gwashington', + * // data: { + * // follows: { + * // jadams: 1 + * // } + * // } + * // } + * // }, + * // ... + * // ] + * }; + * * var entries = [ * { * key: 'alincoln', @@ -638,7 +662,7 @@ Table.prototype.getRows = function(options, callback) { * } * ]; * - * table.insert(entries, function(err, apiResponse) {}); + * table.insert(entries, callback); * * //- * // By default whenever you insert new data, the server will capture a @@ -659,7 +683,18 @@ Table.prototype.getRows = function(options, callback) { * } * ]; * - * table.insert(entries, function(err, apiResponse) {}); + * table.insert(entries, callback); + * + * //- + * // If you don't provide a callback, an EventEmitter is returned. Listen for + * // the error event to catch API and insert errors, and complete for when + * // the API request has completed. + * //- + * table.insert(entries) + * .on('error', console.error) + * .on('complete', function() { + * // All requested inserts have been processed. + * }); */ Table.prototype.insert = function(entries, callback) { entries = arrify(entries).map(propAssign('method', Mutation.methods.INSERT)); @@ -676,16 +711,34 @@ Table.prototype.insert = function(entries, callback) { * deleted. * @param {function} callback - The callback function. * @param {?error} callback.err - An error returned while making this request. - * @param {object} callback.apiResponse - The full API response. + * @param {object[]} callback.mutationErrors - A status object for each failed + * mutation. * * @example * //- * // Insert entities. See {module:bigtable/table#insert} * //- - * var callback = function(err, apiResponse) { - * if (!err) { - * // Mutations were successful. + * var callback = function(err, mutationErrors) { + * if (err) { + * // Error handling omitted. * } + * + * // mutationErrors = [ + * // { + * // code: 500, + * // message: 'Internal Server Error', + * // entry: { + * // method: 'insert', + * // key: 'gwashington', + * // data: { + * // follows: { + * // jadams: 1 + * // } + * // } + * // } + * // }, + * // ... + * // ] * }; * * var entries = [ @@ -752,21 +805,77 @@ Table.prototype.insert = function(entries, callback) { * ]; * * table.mutate(entries, callback); + * + * //- + * // If you don't provide a callback, an EventEmitter is returned. Listen for + * // the error event to catch API and mutation errors, and complete for when + * // the API request has completed. + * //- + * table.mutate(entries) + * .on('error', console.error) + * .on('complete', function() { + * // All requested mutations have been processed. + * }); */ Table.prototype.mutate = function(entries, callback) { - entries = flatten(arrify(entries)).map(Mutation.parse); + entries = flatten(arrify(entries)); var grpcOpts = { - service: 'BigtableService', + service: 'Bigtable', method: 'mutateRows' }; var reqOpts = { + objectMode: true, tableName: this.id, - entries: entries + entries: entries.map(Mutation.parse) }; - this.request(grpcOpts, reqOpts, callback); + var isCallbackMode = is.function(callback); + var emitter = null; + + if (!isCallbackMode) { + emitter = new events.EventEmitter(); + } + + var stream = pumpify.obj([ + this.requestStream(grpcOpts, reqOpts), + through.obj(function(data, enc, next) { + var throughStream = this; + + data.entries.forEach(function(entry) { + // mutation was successful, no need to notify the user + if (entry.status.code === 0) { + return; + } + + var status = common.GrpcService.decorateStatus_(entry.status); + status.entry = entries[entry.index]; + + + if (!isCallbackMode) { + emitter.emit('error', status); + return; + } + + throughStream.push(status); + }); + + next(); + }) + ]); + + if (!isCallbackMode) { + stream.on('error', emitter.emit.bind(emitter, 'error')); + stream.on('finish', emitter.emit.bind(emitter, 'complete')); + return emitter; + } + + stream + .on('error', callback) + .pipe(concat(function(mutationErrors) { + callback(null, mutationErrors); + })); }; /** @@ -828,7 +937,7 @@ Table.prototype.row = function(key) { */ Table.prototype.sampleRowKeys = function(callback) { var grpcOpts = { - service: 'BigtableService', + service: 'Bigtable', method: 'sampleRowKeys' }; diff --git a/packages/bigtable/system-test/bigtable.js b/packages/bigtable/system-test/bigtable.js index fbb9421d871..819b2514b07 100644 --- a/packages/bigtable/system-test/bigtable.js +++ b/packages/bigtable/system-test/bigtable.js @@ -18,70 +18,230 @@ var assert = require('assert'); var async = require('async'); -var exec = require('methmeth'); -var extend = require('extend'); var uuid = require('node-uuid'); -var Bigtable = require('../'); var env = require('../../../system-test/env.js'); +var Bigtable = require('../'); +var Instance = require('../src/instance.js'); +var Cluster = require('../src/cluster.js'); +var Table = require('../src/table.js'); var Family = require('../src/family.js'); var Row = require('../src/row.js'); -var Table = require('../src/table.js'); -var clusterName = process.env.GCLOUD_TESTS_BIGTABLE_CLUSTER; -var zoneName = process.env.GCLOUD_TESTS_BIGTABLE_ZONE; - -var isTestable = clusterName && zoneName; - -function generateTableName() { - return 'test-table-' + uuid.v4(); +function generateName(obj) { + return ['test', obj, uuid.v4()].join('-'); } -(isTestable ? describe : describe.skip)('Bigtable', function() { +describe('Bigtable', function() { var bigtable; - var TABLE_NAME = generateTableName(); + var INSTANCE_NAME = 'test-bigtable-instance'; + var INSTANCE; + + var TABLE_NAME = generateName('table'); var TABLE; + var CLUSTER_NAME = 'test-bigtable-cluster'; + before(function(done) { - bigtable = new Bigtable(extend({ - cluster: clusterName, - zone: zoneName - }, env)); + bigtable = new Bigtable(env); + + INSTANCE = bigtable.instance(INSTANCE_NAME); - TABLE = bigtable.table(TABLE_NAME); + var options = { + clusters: [{ + name: CLUSTER_NAME, + location: 'us-central1-b', + nodes: 3 + }] + }; - bigtable.getTables(function(err, tables) { + INSTANCE.create(options, function(err, instance, operation) { if (err) { done(err); return; } - async.each(tables, exec('delete'), function(err) { - if (err) { - done(err); - return; - } + operation + .on('error', done) + .on('complete', function() { + TABLE = INSTANCE.table(TABLE_NAME); + + TABLE.create({ + families: ['follows', 'traits'] + }, done); + }); + }); + }); + + after(function(done) { + INSTANCE.delete(done); + }); + + describe('instances', function() { + it('should get a list of instances', function(done) { + bigtable.getInstances(function(err, instances) { + assert.ifError(err); + assert(instances.length > 0); + done(); + }); + }); + + it('should get a list of instances in stream mode', function(done) { + var instances = []; + + bigtable.getInstances() + .on('error', done) + .on('data', function(instance) { + assert(instance instanceof Instance); + instances.push(instance); + }) + .on('end', function() { + assert(instances.length > 0); + done(); + }); + }); + + it('should check if an instance exists', function(done) { + INSTANCE.exists(function(err, exists) { + assert.ifError(err); + assert.strictEqual(exists, true); + done(); + }); + }); + + it('should check if an instance does not exist', function(done) { + var instance = bigtable.instance('fake-instance'); - TABLE.create(done); + instance.exists(function(err, exists) { + assert.ifError(err); + assert.strictEqual(exists, false); + done(); + }); + }); + + it('should get a single instance', function(done) { + var instance = bigtable.instance(INSTANCE_NAME); + + instance.get(done); + }); + + it('should update an instance', function(done) { + var metadata = { + displayName: 'metadata-test' + }; + + INSTANCE.setMetadata(metadata, function(err) { + assert.ifError(err); + + INSTANCE.getMetadata(function(err, metadata_) { + assert.ifError(err); + assert.strictEqual(metadata.displayName, metadata_.displayName); + done(); + }); }); }); }); - after(function() { - TABLE.delete(); + describe('clusters', function() { + var CLUSTER; + + beforeEach(function() { + CLUSTER = INSTANCE.cluster(CLUSTER_NAME); + }); + + it('should retrieve a list of clusters', function(done) { + INSTANCE.getClusters(function(err, clusters) { + assert.ifError(err); + assert(clusters[0] instanceof Cluster); + done(); + }); + }); + + it('should retrieve a list of clusters in stream mode', function(done) { + var clusters = []; + + INSTANCE.getClusters() + .on('error', done) + .on('data', function(cluster) { + assert(cluster instanceof Cluster); + clusters.push(cluster); + }) + .on('end', function() { + assert(clusters.length > 0); + done(); + }); + }); + + it('should check if a cluster exists', function(done) { + CLUSTER.exists(function(err, exists) { + assert.ifError(err); + assert.strictEqual(exists, true); + done(); + }); + }); + + it('should check if a cluster does not exist', function(done) { + var cluster = INSTANCE.cluster('fake-cluster'); + + cluster.exists(function(err, exists) { + assert.ifError(err); + assert.strictEqual(exists, false); + done(); + }); + }); + + it('should get a cluster', function(done) { + CLUSTER.get(done); + }); + + it('should update a cluster', function(done) { + var metadata = { + nodes: 4 + }; + + CLUSTER.setMetadata(metadata, function(err, operation) { + assert.ifError(err); + + operation + .on('error', done) + .on('complete', function() { + CLUSTER.getMetadata(function(err, _metadata) { + assert.ifError(err); + assert.strictEqual(metadata.nodes, _metadata.serveNodes); + done(); + }); + }); + }); + }); + }); describe('tables', function() { it('should retrieve a list of tables', function(done) { - bigtable.getTables(function(err, tables) { + INSTANCE.getTables(function(err, tables) { assert.ifError(err); assert(tables[0] instanceof Table); done(); }); }); + it('should retrieve a list of tables in stream mode', function(done) { + var tables = []; + + INSTANCE.getTables() + .on('error', done) + .on('data', function(table) { + assert(table instanceof Table); + tables.push(table); + }) + .on('end', function() { + assert(tables.length > 0); + done(); + }); + }); + it('should check if a table exists', function(done) { TABLE.exists(function(err, exists) { assert.ifError(err); @@ -91,7 +251,7 @@ function generateTableName() { }); it('should check if a table does not exist', function(done) { - var table = bigtable.table('should-not-exist'); + var table = INSTANCE.table('should-not-exist'); table.exists(function(err, exists) { assert.ifError(err); @@ -101,7 +261,7 @@ function generateTableName() { }); it('should get a table', function(done) { - var table = bigtable.table(TABLE_NAME); + var table = INSTANCE.table(TABLE_NAME); table.get(function(err, table_) { assert.ifError(err); @@ -111,7 +271,7 @@ function generateTableName() { }); it('should delete a table', function(done) { - var table = bigtable.table(generateTableName()); + var table = INSTANCE.table(generateName('table')); async.series([ table.create.bind(table), @@ -127,12 +287,12 @@ function generateTableName() { }); it('should create a table with column family data', function(done) { - var name = generateTableName(); + var name = generateName('table'); var options = { families: ['test'] }; - bigtable.createTable(name, options, function(err, table) { + INSTANCE.createTable(name, options, function(err, table) { assert.ifError(err); assert(table.metadata.columnFamilies.test); done(); @@ -153,7 +313,7 @@ function generateTableName() { it('should get a list of families', function(done) { TABLE.getFamilies(function(err, families) { assert.ifError(err); - assert.strictEqual(families.length, 1); + assert.strictEqual(families.length, 3); assert(families[0] instanceof Family); assert.strictEqual(families[0].name, FAMILY.name); done(); @@ -206,13 +366,12 @@ function generateTableName() { union: true }; - FAMILY.setMetadata({ rule: rule }, function(err, metadata_) { + FAMILY.setMetadata({ rule: rule }, function(err, metadata) { assert.ifError(err); + var maxAge = metadata.gcRule.maxAge; - var maxAge_ = metadata_.gcRule.maxAge; - - assert.equal(maxAge_.seconds, rule.age.seconds); - assert.strictEqual(maxAge_.nanas, rule.age.nanas); + assert.equal(maxAge.seconds, rule.age.seconds); + assert.strictEqual(maxAge.nanas, rule.age.nanas); done(); }); }); @@ -225,12 +384,6 @@ function generateTableName() { describe('rows', function() { - before(function(done) { - async.each(['follows', 'traits'], function(family, callback) { - TABLE.createFamily(family, callback); - }, done); - }); - describe('inserting data', function() { it('should insert rows', function(done) { @@ -259,8 +412,9 @@ function generateTableName() { } }]; - TABLE.insert(rows, function(err) { + TABLE.insert(rows, function(err, insertErrors) { assert.ifError(err); + assert.strictEqual(insertErrors.length, 0); done(); }); }); @@ -323,15 +477,15 @@ function generateTableName() { append: '-wood' }; - row.createRules(rule, function(err) { + row.save('traits:teeth', 'shiny', function(err) { assert.ifError(err); - row.save('traits:teeth', 'shiny', function(err) { + row.createRules(rule, function(err) { assert.ifError(err); row.get(['traits:teeth'], function(err, data) { assert.ifError(err); - assert(data.traits.teeth[0].value, 'shiny-wood'); + assert.strictEqual(data.traits.teeth[0].value, 'shiny-wood'); done(); }); }); @@ -345,12 +499,12 @@ function generateTableName() { value: 'alincoln' }; - var batch = [{ + var mutations = [{ method: 'delete', - data: ['follows:lincoln'] + data: ['follows:alincoln'] }]; - row.filter(filter, null, batch, function(err, matched) { + row.filter(filter, mutations, function(err, matched) { assert.ifError(err); assert(matched); done(); @@ -405,6 +559,29 @@ function generateTableName() { }); }); + it('should not decode the values', function(done) { + var row = TABLE.row('alincoln'); + var options = { + decode: false + }; + + row.get(options, function(err) { + assert.ifError(err); + + var presidents = Object.keys(row.data.follows); + + assert(presidents.length > 0); + + presidents.forEach(function(prez) { + var follower = row.data.follows[prez]; + + assert.strictEqual(follower[0].value, 'AAAAAAAAAAE='); + }); + + done(); + }); + }); + it('should get sample row keys', function(done) { TABLE.sampleRowKeys(function(err, keys) { assert.ifError(err); diff --git a/packages/bigtable/test/cluster.js b/packages/bigtable/test/cluster.js new file mode 100644 index 00000000000..0dc7c04d623 --- /dev/null +++ b/packages/bigtable/test/cluster.js @@ -0,0 +1,300 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var format = require('string-format-obj'); +var proxyquire = require('proxyquire'); +var util = require('util'); + +var GrpcServiceObject = require('@google-cloud/common').GrpcServiceObject; + +function FakeGrpcServiceObject() { + this.calledWith_ = arguments; + GrpcServiceObject.apply(this, arguments); +} + +util.inherits(FakeGrpcServiceObject, GrpcServiceObject); + +describe('Bigtable/Cluster', function() { + var CLUSTER_NAME = 'my-cluster'; + var PROJECT_ID = 'grape-spaceship-123'; + + var INSTANCE = { + id: 'projects/p/instances/i', + parent: { projectId: PROJECT_ID } + }; + + var CLUSTER_ID = format('{instance}/clusters/{cluster}', { + instance: INSTANCE.id, + cluster: CLUSTER_NAME + }); + + var Cluster; + var cluster; + + before(function() { + Cluster = proxyquire('../src/cluster.js', { + '@google-cloud/common': { + GrpcServiceObject: FakeGrpcServiceObject + } + }); + }); + + beforeEach(function() { + cluster = new Cluster(INSTANCE, CLUSTER_NAME); + }); + + describe('instantiation', function() { + it('should inherit from GrpcServiceObject', function() { + assert(cluster instanceof FakeGrpcServiceObject); + + var config = cluster.calledWith_[0]; + + assert.strictEqual(config.parent, INSTANCE); + assert.strictEqual(config.id, CLUSTER_ID); + + assert.deepEqual(config.methods, { + create: true, + delete: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'deleteCluster' + }, + reqOpts: { + name: CLUSTER_ID + } + }, + exists: true, + get: true, + getMetadata: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'getCluster' + }, + reqOpts: { + name: CLUSTER_ID + } + } + }); + }); + + it('should Instance#createCluster to create the cluster', function(done) { + var config = cluster.calledWith_[0]; + var fakeOptions = {}; + + INSTANCE.createCluster = function(name, options, callback) { + assert.strictEqual(name, CLUSTER_NAME); + assert.strictEqual(options, fakeOptions); + callback(); + }; + + config.createMethod(null, fakeOptions, done); + }); + + it('should leave full cluster names unaltered', function() { + var fakeName = 'a/b/c/d'; + var cluster = new Cluster(INSTANCE, fakeName); + var config = cluster.calledWith_[0]; + + assert.strictEqual(config.id, fakeName); + + assert.deepEqual(config.methods, { + create: true, + delete: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'deleteCluster' + }, + reqOpts: { + name: fakeName + } + }, + exists: true, + get: true, + getMetadata: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'getCluster' + }, + reqOpts: { + name: fakeName + } + } + }); + }); + }); + + describe('getLocation_', function() { + var LOCATION = 'us-centralb-1'; + + it('should format the location name', function() { + var expected = format('projects/{project}/locations/{location}', { + project: PROJECT_ID, + location: LOCATION + }); + + var formatted = Cluster.getLocation_(PROJECT_ID, LOCATION); + assert.strictEqual(formatted, expected); + }); + + it('should not re-format a complete location', function() { + var complete = format('projects/p/locations/{location}', { + location: LOCATION + }); + + var formatted = Cluster.getLocation_(PROJECT_ID, complete); + assert.strictEqual(formatted, complete); + }); + }); + + describe('getStorageType_', function() { + var types = { + unspecified: 0, + ssd: 1, + hdd: 2 + }; + + it('should default to unspecified', function() { + assert.strictEqual(Cluster.getStorageType_(), types.unspecified); + }); + + it('should lowercase a type', function() { + assert.strictEqual(Cluster.getStorageType_('SSD'), types.ssd); + }); + + Object.keys(types).forEach(function(type) { + it('should get the storage type for "' + type + '"', function() { + assert.strictEqual(Cluster.getStorageType_(type), types[type]); + }); + }); + }); + + describe('setMetadata', function() { + it('should provide the proper request options', function(done) { + cluster.request = function(grpcOpts, reqOpts) { + assert.deepEqual(grpcOpts, { + service: 'BigtableInstanceAdmin', + method: 'updateCluster' + }); + + assert.strictEqual(reqOpts.name, CLUSTER_ID); + done(); + }; + + cluster.setMetadata({}, assert.ifError); + }); + + it('should respect the location option', function(done) { + var options = { + location: 'us-centralb-1' + }; + + var getLocation = Cluster.getLocation_; + var fakeLocation = 'a/b/c/d'; + + Cluster.getLocation_ = function(project, location) { + assert.strictEqual(project, PROJECT_ID); + assert.strictEqual(location, options.location); + return fakeLocation; + }; + + cluster.request = function(grpcOpts, reqOpts) { + assert.strictEqual(reqOpts.location, fakeLocation); + Cluster.getLocation_ = getLocation; + done(); + }; + + cluster.setMetadata(options, assert.ifError); + }); + + it('should respect the nodes option', function(done) { + var options = { + nodes: 3 + }; + + cluster.request = function(grpcOpts, reqOpts) { + assert.strictEqual(reqOpts.serveNodes, options.nodes); + done(); + }; + + cluster.setMetadata(options, assert.ifError); + }); + + it('should respect the storage option', function(done) { + var options = { + storage: 'ssd' + }; + + var getStorageType = Cluster.getStorageType_; + var fakeStorageType = 'a'; + + Cluster.getStorageType_ = function(storage) { + assert.strictEqual(storage, options.storage); + return fakeStorageType; + }; + + cluster.request = function(grpcOpts, reqOpts) { + assert.strictEqual(reqOpts.defaultStorageType, fakeStorageType); + Cluster.getStorageType_ = getStorageType; + done(); + }; + + cluster.setMetadata(options, assert.ifError); + }); + + it('should return an error to the callback', function(done) { + var error = new Error('err'); + var response = {}; + + cluster.request = function(grpcOpts, reqOpts, callback) { + callback(error, response); + }; + + cluster.setMetadata({}, function(err, operation, apiResponse) { + assert.strictEqual(err, error); + assert.strictEqual(operation, null); + assert.strictEqual(apiResponse, response); + done(); + }); + }); + + it('should return an operation to the callback', function(done) { + var response = { + name: 'my-operation' + }; + var fakeOperation = {}; + + cluster.request = function(grpcOpts, reqOpts, callback) { + callback(null, response); + }; + + INSTANCE.parent.operation = function(name) { + assert.strictEqual(name, response.name); + return fakeOperation; + }; + + cluster.setMetadata({}, function(err, operation, apiResponse) { + assert.ifError(err); + assert.strictEqual(operation, fakeOperation); + assert.strictEqual(operation.metadata, response); + assert.strictEqual(apiResponse, response); + done(); + }); + }); + }); +}); diff --git a/packages/bigtable/test/family.js b/packages/bigtable/test/family.js index b8ad445c479..89489ac2301 100644 --- a/packages/bigtable/test/family.js +++ b/packages/bigtable/test/family.js @@ -74,15 +74,20 @@ describe('Bigtable/Family', function() { get: true, delete: { protoOpts: { - service: 'BigtableTableService', - method: 'deleteColumnFamily' + service: 'BigtableTableAdmin', + method: 'modifyColumnFamilies' }, reqOpts: { - name: FAMILY_ID + name: TABLE.id, + modifications: [{ + drop: true, + id: FAMILY_NAME + }] } } }); assert.strictEqual(typeof config.createMethod, 'function'); + assert.strictEqual(family.familyName, FAMILY_NAME); }); it('should call Table#createFamily for the create method', function(done) { @@ -96,6 +101,11 @@ describe('Bigtable/Family', function() { family.create(fakeOptions, done); }); + + it('should extract the family name', function() { + var family = new Family(TABLE, FAMILY_ID); + assert.strictEqual(family.familyName, FAMILY_NAME); + }); }); describe('formatName_', function() { @@ -229,30 +239,21 @@ describe('Bigtable/Family', function() { describe('setMetadata', function() { it('should provide the proper request options', function(done) { - family.request = function(protoOpts, reqOpts, callback) { + family.request = function(protoOpts, reqOpts) { assert.deepEqual(protoOpts, { - service: 'BigtableTableService', - method: 'updateColumnFamily' + service: 'BigtableTableAdmin', + method: 'modifyColumnFamilies' }); - assert.strictEqual(reqOpts.name, FAMILY_ID); - callback(); - }; - - family.setMetadata({}, done); - }); - - it('should respect the gc expression option', function(done) { - var metadata = { - rule: 'a b c' - }; - - family.request = function(p, reqOpts) { - assert.strictEqual(reqOpts.gcExpression, metadata.rule); + assert.strictEqual(reqOpts.name, TABLE.id); + assert.deepEqual(reqOpts.modifications, [{ + id: FAMILY_NAME, + update: {} + }]); done(); }; - family.setMetadata(metadata, assert.ifError); + family.setMetadata({}, assert.ifError); }); it('should respect the gc rule option', function(done) { @@ -276,7 +277,15 @@ describe('Bigtable/Family', function() { }; family.request = function(p, reqOpts) { - assert.strictEqual(reqOpts.gcRule, formattedRule); + assert.deepEqual(reqOpts, { + name: TABLE.id, + modifications: [{ + id: family.familyName, + update: { + gcRule: formattedRule + } + }] + }); Family.formatRule_ = formatRule; done(); }; @@ -284,27 +293,41 @@ describe('Bigtable/Family', function() { family.setMetadata(metadata, assert.ifError); }); - it('should respect the updated name option', function(done) { - var formatName = Family.formatName_; - var fakeName = 'a/b/c'; + it('should return an error to the callback', function(done) { + var error = new Error('err'); + var response = {}; - var metadata = { - name: 'new-name' + family.request = function(protoOpts, reqOpts, callback) { + callback(error, response); }; - Family.formatName_ = function(parent, newName) { - assert.strictEqual(parent, TABLE.id); - assert.strictEqual(newName, metadata.name); - return fakeName; + family.setMetadata({}, function(err, metadata, apiResponse) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse, response); + done(); + }); + }); + + it('should update the metadata property', function(done) { + var fakeMetadata = {}; + var response = { + columnFamilies: { + 'family-test': fakeMetadata + } }; - family.request = function(p, reqOpts) { - assert.strictEqual(reqOpts.name, fakeName); - Family.formatName_ = formatName; - done(); + family.request = function(protoOpts, reqOpts, callback) { + callback(null, response); }; - family.setMetadata(metadata, assert.ifError); + family.setMetadata({}, function(err, metadata, apiResponse) { + assert.ifError(err); + assert.strictEqual(metadata, fakeMetadata); + assert.strictEqual(family.metadata, fakeMetadata); + assert.strictEqual(apiResponse, response); + done(); + }); }); }); diff --git a/packages/bigtable/test/filter.js b/packages/bigtable/test/filter.js index 717b840ae9a..c41d02bc462 100644 --- a/packages/bigtable/test/filter.js +++ b/packages/bigtable/test/filter.js @@ -80,9 +80,11 @@ describe('Bigtable/Filter', function() { }); it('should throw an error for unknown types', function() { + var errorReg = /Can\'t convert to RegExp String from unknown type\./; + assert.throws(function() { Filter.convertToRegExpString(true); - }, /Can\'t convert to RegExp String from unknown type\./); + }, errorReg); }); }); @@ -95,8 +97,8 @@ describe('Bigtable/Filter', function() { var range = Filter.createRange(start, end, key); assert.deepEqual(range, { - startKeyInclusive: start, - endKeyInclusive: end + startKeyClosed: start, + endKeyClosed: end }); }); @@ -108,7 +110,7 @@ describe('Bigtable/Filter', function() { assert(FakeMutation.convertToBytes.calledWithExactly(start)); assert.deepEqual(range, { - startKeyInclusive: start + startKeyClosed: start }); }); @@ -120,7 +122,7 @@ describe('Bigtable/Filter', function() { assert(FakeMutation.convertToBytes.calledWithExactly(end)); assert.deepEqual(range, { - endKeyInclusive: end + endKeyClosed: end }); }); @@ -140,8 +142,8 @@ describe('Bigtable/Filter', function() { var range = Filter.createRange(start, end, key); assert.deepEqual(range, { - startKeyExclusive: start.value, - endKeyExclusive: end.value + startKeyOpen: start.value, + endKeyOpen: end.value }); }); }); diff --git a/packages/bigtable/test/index.js b/packages/bigtable/test/index.js index 82af59c9d85..6b719f21525 100644 --- a/packages/bigtable/test/index.js +++ b/packages/bigtable/test/index.js @@ -18,43 +18,39 @@ var assert = require('assert'); var extend = require('extend'); -var format = require('string-format-obj'); var googleProtoFiles = require('google-proto-files'); var nodeutil = require('util'); var proxyquire = require('proxyquire'); var sinon = require('sinon').sandbox.create(); -var GrpcService = require('@google-cloud/common').GrpcService; +var common = require('@google-cloud/common'); +var Cluster = require('../src/cluster.js'); +var Instance = require('../src/instance.js'); var PKG = require('../package.json'); -var Table = require('../src/table.js'); -var util = require('@google-cloud/common').util; -var fakeUtil = extend({}, util); - -function FakeGrpcService() { - this.calledWith_ = arguments; - GrpcService.apply(this, arguments); -} - -nodeutil.inherits(FakeGrpcService, GrpcService); - -function FakeTable() { - this.calledWith_ = arguments; - Table.apply(this, arguments); +var fakeUtil = extend({}, common.util); +var fakeStreamRouter = { + extend: function() { + this.calledWith_ = arguments; + } +}; + +function createFake(Class) { + function Fake() { + this.calledWith_ = arguments; + Class.apply(this, arguments); + } + nodeutil.inherits(Fake, Class); + return Fake; } -function FakeFamily() {} +var FakeGrpcService = createFake(common.GrpcService); +var FakeCluster = createFake(Cluster); +var FakeInstance = createFake(Instance); +var FakeGrpcOperation = createFake(function() {}); describe('Bigtable', function() { var PROJECT_ID = 'test-project'; - var ZONE = 'test-zone'; - var CLUSTER = 'test-cluster'; - - var CLUSTER_NAME = format('projects/{p}/zones/{z}/clusters/{c}', { - p: PROJECT_ID, - z: ZONE, - c: CLUSTER - }); var Bigtable; var bigtable; @@ -63,10 +59,12 @@ describe('Bigtable', function() { Bigtable = proxyquire('../', { '@google-cloud/common': { GrpcService: FakeGrpcService, + GrpcOperation: FakeGrpcOperation, + streamRouter: fakeStreamRouter, util: fakeUtil }, - './family.js': FakeFamily, - './table.js': FakeTable + './cluster.js': FakeCluster, + './instance.js': FakeInstance }); }); @@ -75,21 +73,22 @@ describe('Bigtable', function() { }); beforeEach(function() { - bigtable = new Bigtable({ - projectId: PROJECT_ID, - zone: ZONE, - cluster: CLUSTER - }); + bigtable = new Bigtable({ projectId: PROJECT_ID }); }); describe('instantiation', function() { + it('should streamify the correct methods', function() { + var args = fakeStreamRouter.calledWith_; + + assert.strictEqual(args[0], Bigtable); + assert.deepEqual(args[1], ['getInstances']); + }); + it('should normalize the arguments', function() { var normalizeArguments = fakeUtil.normalizeArguments; var normalizeArgumentsCalled = false; var fakeOptions = { - projectId: PROJECT_ID, - zone: ZONE, - cluster: CLUSTER + projectId: PROJECT_ID }; var fakeContext = {}; @@ -106,325 +105,315 @@ describe('Bigtable', function() { fakeUtil.normalizeArguments = normalizeArguments; }); - it('should throw if a cluster is not provided', function() { - assert.throws(function() { - new Bigtable({}); - }, /A cluster must be provided to interact with Bigtable\./); - }); - - it('should throw if a zone is not provided', function() { - assert.throws(function() { - new Bigtable({ - cluster: CLUSTER - }); - }, /A zone must be provided to interact with Bigtable\./); - }); - - it('should leave the original options unaltered', function() { - var fakeOptions = { - a: 'a', - b: 'b', - c: 'c', - cluster: CLUSTER, - zone: ZONE - }; - - var bigtable = new Bigtable(fakeOptions); - var options = bigtable.calledWith_[1]; - - for (var option in fakeOptions) { - assert.strictEqual(fakeOptions[option], options[option]); - } - - assert.notStrictEqual(fakeOptions, options); - }); - - it('should localize the cluster name', function() { - assert.strictEqual(bigtable.clusterName, CLUSTER_NAME); - }); - it('should inherit from GrpcService', function() { - assert(bigtable instanceof GrpcService); + assert(bigtable instanceof FakeGrpcService); var calledWith = bigtable.calledWith_[0]; assert.strictEqual(calledWith.baseUrl, 'bigtable.googleapis.com'); assert.strictEqual(calledWith.service, 'bigtable'); - assert.strictEqual(calledWith.apiVersion, 'v1'); + assert.strictEqual(calledWith.apiVersion, 'v2'); assert.deepEqual(calledWith.protoServices, { - BigtableService: googleProtoFiles.bigtable.v1, - BigtableTableService: { - path: googleProtoFiles.bigtable.admin, - service: 'bigtable.admin.table' + Bigtable: googleProtoFiles('bigtable/v2/bigtable.proto'), + BigtableTableAdmin: { + baseUrl: 'bigtableadmin.googleapis.com', + path: googleProtoFiles( + 'bigtable/admin/v2/bigtable_table_admin.proto'), + service: 'bigtable.admin' + }, + BigtableInstanceAdmin: { + baseUrl: 'bigtableadmin.googleapis.com', + path: googleProtoFiles( + 'bigtable/admin/v2/bigtable_instance_admin.proto' + ), + service: 'bigtable.admin' + }, + Operations: { + baseUrl: 'bigtableadmin.googleapis.com', + path: googleProtoFiles('longrunning/operations.proto'), + service: 'longrunning', + apiVersion: 'v1' } }); assert.deepEqual(calledWith.scopes, [ 'https://www.googleapis.com/auth/bigtable.admin', - 'https://www.googleapis.com/auth/bigtable.data' + 'https://www.googleapis.com/auth/bigtable.data', + 'https://www.googleapis.com/auth/cloud-platform' ]); - assert.strictEqual(calledWith.userAgent, PKG.name + '/' + PKG.version); - }); - }); - describe('formatTableName_', function() { - it('should return the last section of a formatted table name', function() { - var fakeTableName = 'projects/p/zones/z/clusters/c/tables/my-table'; - var formatted = Bigtable.formatTableName_(fakeTableName); - - assert.strictEqual(formatted, 'my-table'); + assert.strictEqual(calledWith.userAgent, PKG.name + '/' + PKG.version); }); - it('should do nothing if the table is name is not formatted', function() { - var fakeTableName = 'my-table'; - var formatted = Bigtable.formatTableName_(fakeTableName); - - assert.strictEqual(formatted, fakeTableName); + it('should set the projectName', function() { + assert.strictEqual(bigtable.projectName, 'projects/' + PROJECT_ID); }); }); - describe('createTable', function() { - var TABLE_ID = 'my-table'; - - it('should throw if a name is not provided', function() { - assert.throws(function() { - bigtable.createTable(); - }, /A name is required to create a table\./); - }); + describe('createInstance', function() { + var INSTANCE_NAME = 'my-instance'; it('should provide the proper request options', function(done) { bigtable.request = function(protoOpts, reqOpts) { assert.deepEqual(protoOpts, { - service: 'BigtableTableService', - method: 'createTable' + service: 'BigtableInstanceAdmin', + method: 'createInstance' }); - assert.strictEqual(reqOpts.name, CLUSTER_NAME); - assert.strictEqual(reqOpts.tableId, TABLE_ID); - assert.deepEqual(reqOpts.table, { - granularity: 0 - }); + assert.strictEqual(reqOpts.parent, bigtable.projectName); + assert.strictEqual(reqOpts.instanceId, INSTANCE_NAME); + assert.strictEqual(reqOpts.instance.displayName, INSTANCE_NAME); done(); }; - bigtable.createTable(TABLE_ID, assert.ifError); + bigtable.createInstance(INSTANCE_NAME, assert.ifError); }); - it('should set the current operation', function(done) { + it('should respect the displayName option', function(done) { var options = { - operation: 'abc' + displayName: 'robocop' }; bigtable.request = function(protoOpts, reqOpts) { - assert.strictEqual(reqOpts.table.currentOperation, options.operation); + assert.strictEqual(reqOpts.instance.displayName, options.displayName); done(); }; - bigtable.createTable(TABLE_ID, options, assert.ifError); + bigtable.createInstance(INSTANCE_NAME, options, assert.ifError); }); - it('should set the initial split keys', function(done) { - var options = { - splits: ['a', 'b'] + it('should respect the clusters option', function(done) { + var cluster = { + name: 'my-cluster', + location: 'us-central1-b', + nodes: 3, + storage: 'ssd' }; - bigtable.request = function(protoOpts, reqOpts) { - assert.strictEqual(reqOpts.initialSplitKeys, options.splits); - done(); + var options = { + clusters: [cluster] }; - bigtable.createTable(TABLE_ID, options, assert.ifError); - }); - - describe('creating column families', function() { - it('should accept a family name', function(done) { - var options = { - families: ['a', 'b'] - }; - - bigtable.request = function(protoOpts, reqOpts) { - assert.deepEqual(reqOpts.table.columnFamilies, { - a: {}, - b: {} - }); + var fakeLocation = 'a/b/c/d'; + FakeCluster.getLocation_ = function(project, location) { + assert.strictEqual(project, PROJECT_ID); + assert.strictEqual(location, cluster.location); + return fakeLocation; + }; - done(); - }; + var fakeStorage = 20; + FakeCluster.getStorageType_ = function(storage) { + assert.strictEqual(storage, cluster.storage); + return fakeStorage; + }; - bigtable.createTable(TABLE_ID, options, assert.ifError); - }); + bigtable.request = function(protoOpts, reqOpts) { + assert.deepEqual(reqOpts.clusters, { + 'my-cluster': { + location: fakeLocation, + serveNodes: cluster.nodes, + defaultStorageType: fakeStorage + } + }); - it('should accept a garbage collection expression', function(done) { - var options = { - families: [ - { - name: 'c', - rule: 'd' - } - ] - }; - - bigtable.request = function(protoOpts, reqOpts) { - assert.deepEqual(reqOpts.table.columnFamilies, { - c: { - gcExpression: 'd' - } - }); - done(); - }; - - bigtable.createTable(TABLE_ID, options, assert.ifError); - }); + done(); + }; - it('should accept a garbage collection object', function(done) { - var options = { - families: [ - { - name: 'e', - rule: {} - } - ] - }; - - var fakeRule = { a: 'b' }; - - FakeFamily.formatRule_ = function(rule) { - assert.strictEqual(rule, options.families[0].rule); - return fakeRule; - }; - - bigtable.request = function(protoOpts, reqOpts) { - assert.deepEqual(reqOpts.table.columnFamilies, { - e: { - gcRule: fakeRule - } - }); - done(); - }; - - bigtable.createTable(TABLE_ID, options, assert.ifError); - }); + bigtable.createInstance(INSTANCE_NAME, options, assert.ifError); }); it('should return an error to the callback', function(done) { - var err = new Error('err'); + var error = new Error('err'); var response = {}; bigtable.request = function(protoOpts, reqOpts, callback) { - callback(err, response); + callback(error, response); }; - bigtable.createTable(TABLE_ID, function(err_, table, apiResponse) { - assert.strictEqual(err, err_); - assert.strictEqual(table, null); + var callback = function(err, instance, operation, apiResponse) { + assert.strictEqual(err, error); + assert.strictEqual(instance, null); + assert.strictEqual(operation, null); assert.strictEqual(apiResponse, response); done(); - }); + }; + + bigtable.createInstance(INSTANCE_NAME, callback); }); - it('should return a Table object', function(done) { + it('should pass an operation and instance to the callback', function(done) { var response = { - name: TABLE_ID + name: 'my-operation' }; - var fakeTable = {}; + var fakeInstance = {}; + bigtable.instance = function(name) { + assert.strictEqual(name, INSTANCE_NAME); + return fakeInstance; + }; - var tableSpy = sinon.stub(bigtable, 'table', function() { - return fakeTable; - }); + var fakeOperation = {}; + bigtable.operation = function(name) { + assert.strictEqual(name, response.name); + return fakeOperation; + }; - bigtable.request = function(p, r, callback) { + bigtable.request = function(protoOpts, reqOpts, callback) { callback(null, response); }; - bigtable.createTable(TABLE_ID, function(err, table, apiResponse) { + var callback = function(err, instance, operation, apiResponse) { assert.ifError(err); - assert.strictEqual(table, fakeTable); - assert(tableSpy.calledWithExactly(response.name)); - assert.strictEqual(table.metadata, response); - assert.strictEqual(response, apiResponse); + assert.strictEqual(instance, fakeInstance); + assert.strictEqual(operation, fakeOperation); + assert.strictEqual(operation.metadata, response); + assert.strictEqual(apiResponse, response); done(); - }); + }; + + bigtable.createInstance(INSTANCE_NAME, callback); }); }); - describe('getTables', function() { + describe('getInstances', function() { it('should provide the proper request options', function(done) { - bigtable.request = function(protoOpts, reqOpts) { - assert.deepEqual(protoOpts, { - service: 'BigtableTableService', - method: 'listTables' + bigtable.request = function(grpcOpts, reqOpts) { + assert.deepEqual(grpcOpts, { + service: 'BigtableInstanceAdmin', + method: 'listInstances' + }); + + assert.strictEqual(reqOpts.parent, bigtable.projectName); + done(); + }; + + bigtable.getInstances(assert.ifError); + }); + + it('should copy all query options', function(done) { + var fakeOptions = { + a: 'a', + b: 'b' + }; + + bigtable.request = function(grpcOpts, reqOpts) { + Object.keys(fakeOptions).forEach(function(key) { + assert.strictEqual(reqOpts[key], fakeOptions[key]); }); - assert.strictEqual(reqOpts.name, CLUSTER_NAME); + + assert.notStrictEqual(reqOpts, fakeOptions); done(); }; - bigtable.getTables(assert.ifError); + bigtable.getInstances(fakeOptions, assert.ifError); }); it('should return an error to the callback', function(done) { - var err = new Error('err'); + var error = new Error('err'); var response = {}; - bigtable.request = function(p, r, callback) { - callback(err, response); + bigtable.request = function(grpcOpts, reqOpts, callback) { + callback(error, response); }; - bigtable.getTables(function(err_, tables, apiResponse) { - assert.strictEqual(err, err_); - assert.strictEqual(tables, null); + bigtable.getInstances(function(err, instances, nextQuery, apiResponse) { + assert.strictEqual(err, error); + assert.strictEqual(instances, null); + assert.strictEqual(nextQuery, null); assert.strictEqual(apiResponse, response); done(); }); }); - it('should return a list of Table objects', function(done) { - var fakeFormattedName = 'abcd'; + it('should return an array of instance objects', function(done) { var response = { - tables: [{ - name: 'projects/p/zones/z/clusters/c/tables/my-table' + instances: [{ + name: 'a' + }, { + name: 'b' }] }; - var fakeTable = {}; - bigtable.request = function(p, r, callback) { + var fakeInstances = [ + {}, + {} + ]; + + bigtable.request = function(grpcOpts, reqOpts, callback) { callback(null, response); }; - var tableSpy = sinon.stub(bigtable, 'table', function() { - return fakeTable; - }); + var instanceCount = 0; - var formatSpy = sinon.stub(Bigtable, 'formatTableName_', function() { - return fakeFormattedName; - }); + bigtable.instance = function(name) { + assert.strictEqual(name, response.instances[instanceCount].name); + return fakeInstances[instanceCount++]; + }; - bigtable.getTables(function(err, tables, apiResponse) { + bigtable.getInstances(function(err, instances, nextQuery, apiResponse) { assert.ifError(err); + assert.strictEqual(instances[0], fakeInstances[0]); + assert.strictEqual(instances[0].metadata, response.instances[0]); + assert.strictEqual(instances[1], fakeInstances[1]); + assert.strictEqual(instances[1].metadata, response.instances[1]); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResponse, response); + done(); + }); + }); - var table = tables[0]; + it('should provide a nextQuery object', function(done) { + var response = { + instances: [], + nextPageToken: 'a' + }; + + var options = { + a: 'b' + }; + + bigtable.request = function(grpcOpts, reqOpts, callback) { + callback(null, response); + }; - assert.strictEqual(table, fakeTable); - assert(formatSpy.calledWithExactly(response.tables[0].name)); - assert(tableSpy.calledWithExactly(fakeFormattedName)); - assert.strictEqual(table.metadata, response.tables[0]); - assert.strictEqual(response, apiResponse); + var callback = function(err, instances, nextQuery) { + var expectedQuery = extend({}, options, { + pageToken: response.nextPageToken + }); + + assert.ifError(err); + assert.deepEqual(nextQuery, expectedQuery); done(); - }); + }; + + bigtable.getInstances(options, callback); + }); + }); + + describe('instance', function() { + var INSTANCE_NAME = 'my-instance'; + + it('should return an Instance object', function() { + var instance = bigtable.instance(INSTANCE_NAME); + var args = instance.calledWith_; + + assert(instance instanceof FakeInstance); + assert.strictEqual(args[0], bigtable); + assert.strictEqual(args[1], INSTANCE_NAME); }); }); - describe('table', function() { - var TABLE_ID = 'table-id'; + describe('operation', function() { + var OPERATION_NAME = 'my-operation'; - it('should return a table instance', function() { - var table = bigtable.table(TABLE_ID); - var args = table.calledWith_; + it('should return a GrpcOperation object', function() { + var operation = bigtable.operation(OPERATION_NAME); + var args = operation.calledWith_; - assert(table instanceof FakeTable); + assert(operation instanceof FakeGrpcOperation); assert.strictEqual(args[0], bigtable); - assert.strictEqual(args[1], TABLE_ID); + assert.strictEqual(args[1], OPERATION_NAME); }); }); diff --git a/packages/bigtable/test/instance.js b/packages/bigtable/test/instance.js new file mode 100644 index 00000000000..5aa689d929f --- /dev/null +++ b/packages/bigtable/test/instance.js @@ -0,0 +1,696 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var proxyquire = require('proxyquire'); +var util = require('util'); +var format = require('string-format-obj'); +var extend = require('extend'); + +var GrpcServiceObject = require('@google-cloud/common').GrpcServiceObject; +var Cluster = require('../src/cluster.js'); +var Family = require('../src/family.js'); +var Table = require('../src/table.js'); + +var fakeStreamRouter = { + extend: function() { + this.calledWith_ = arguments; + } +}; + +function createFake(Class) { + function Fake() { + this.calledWith_ = arguments; + Class.apply(this, arguments); + } + + util.inherits(Fake, Class); + return Fake; +} + +var FakeGrpcServiceObject = createFake(GrpcServiceObject); +var FakeCluster = createFake(Cluster); +var FakeFamily = createFake(Family); +var FakeTable = createFake(Table); + +describe('Bigtable/Instance', function() { + var INSTANCE_NAME = 'my-instance'; + var BIGTABLE = { projectName: 'projects/my-project' }; + + var INSTANCE_ID = format('{project}/instances/{instance}', { + project: BIGTABLE.projectName, + instance: INSTANCE_NAME + }); + + var CLUSTER_NAME = 'my-cluster'; + + var Instance; + var instance; + + before(function() { + Instance = proxyquire('../src/instance.js', { + '@google-cloud/common': { + GrpcServiceObject: FakeGrpcServiceObject, + streamRouter: fakeStreamRouter + }, + './cluster.js': FakeCluster, + './family.js': FakeFamily, + './table.js': FakeTable + }); + }); + + beforeEach(function() { + instance = new Instance(BIGTABLE, INSTANCE_NAME); + }); + + describe('instantiation', function() { + it('should streamify the correct methods', function() { + var args = fakeStreamRouter.calledWith_; + + assert.strictEqual(args[0], Instance); + assert.deepEqual(args[1], ['getClusters', 'getTables']); + }); + + it('should inherit from GrpcServiceObject', function() { + var config = instance.calledWith_[0]; + + assert(instance instanceof FakeGrpcServiceObject); + assert.strictEqual(config.parent, BIGTABLE); + assert.strictEqual(config.id, INSTANCE_ID); + + assert.deepEqual(config.methods, { + create: true, + delete: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'deleteInstance' + }, + reqOpts: { + name: INSTANCE_ID + } + }, + exists: true, + get: true, + getMetadata: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'getInstance' + }, + reqOpts: { + name: INSTANCE_ID + } + }, + setMetadata: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'updateInstance' + }, + reqOpts: { + name: INSTANCE_ID + } + } + }); + }); + + it('should Bigtable#createInstance to create the table', function(done) { + var fakeOptions = {}; + var config = instance.calledWith_[0]; + + BIGTABLE.createInstance = function(name, options, callback) { + assert.strictEqual(name, INSTANCE_NAME); + assert.strictEqual(options, fakeOptions); + callback(); + }; + + config.createMethod(null, fakeOptions, done); + }); + + it('should not alter full instance ids', function() { + var fakeId = 'a/b/c/d'; + var instance = new Instance(BIGTABLE, fakeId); + var config = instance.calledWith_[0]; + + assert.strictEqual(config.id, fakeId); + + assert.deepEqual(config.methods, { + create: true, + delete: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'deleteInstance' + }, + reqOpts: { + name: fakeId + } + }, + exists: true, + get: true, + getMetadata: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'getInstance' + }, + reqOpts: { + name: fakeId + } + }, + setMetadata: { + protoOpts: { + service: 'BigtableInstanceAdmin', + method: 'updateInstance' + }, + reqOpts: { + name: fakeId + } + } + }); + }); + }); + + describe('createCluster', function() { + it('should provide the proper request options', function(done) { + instance.request = function(grpcOpts, reqOpts) { + assert.deepEqual(grpcOpts, { + service: 'BigtableInstanceAdmin', + method: 'createCluster' + }); + + assert.strictEqual(reqOpts.parent, INSTANCE_ID); + assert.strictEqual(reqOpts.clusterId, CLUSTER_NAME); + done(); + }; + + instance.createCluster(CLUSTER_NAME, assert.ifError); + }); + + it('should respect the location option', function(done) { + var options = { + location: 'us-central1-b' + }; + + var fakeLocation = 'a/b/c/d'; + + FakeCluster.getLocation_ = function(project, location) { + assert.strictEqual(project, BIGTABLE.projectName); + assert.strictEqual(location, options.location); + return fakeLocation; + }; + + instance.request = function(grpcOpts, reqOpts) { + assert.strictEqual(reqOpts.cluster.location, fakeLocation); + done(); + }; + + instance.createCluster(CLUSTER_NAME, options, assert.ifError); + }); + + it('should respect the nodes option', function(done) { + var options = { + nodes: 3 + }; + + instance.request = function(grpcOpts, reqOpts) { + assert.strictEqual(reqOpts.cluster.serveNodes, options.nodes); + done(); + }; + + instance.createCluster(CLUSTER_NAME, options, assert.ifError); + }); + + it('should respect the storage option', function(done) { + var options = { + storage: 'ssd' + }; + + var fakeStorageType = 2; + + FakeCluster.getStorageType_ = function(type) { + assert.strictEqual(type, options.storage); + return fakeStorageType; + }; + + instance.request = function(grpcOpts, reqOpts) { + assert.strictEqual(reqOpts.cluster.defaultStorageType, fakeStorageType); + done(); + }; + + instance.createCluster(CLUSTER_NAME, options, assert.ifError); + }); + + it('should return an error to the callback', function(done) { + var error = new Error('err'); + var response = {}; + + instance.request = function(grpcOpts, reqOpts, callback) { + callback(error, response); + }; + + var callback = function(err, cluster, operation, apiResponse) { + assert.strictEqual(err, error); + assert.strictEqual(cluster, null); + assert.strictEqual(operation, null); + assert.strictEqual(apiResponse, response); + done(); + }; + + instance.createCluster(CLUSTER_NAME, callback); + }); + + it('should return a cluster and operation object', function(done) { + var fakeCluster = {}; + var fakeOperation = {}; + + var response = { + name: 'my-operation' + }; + + instance.request = function(grpcOpts, reqOpts, callback) { + callback(null, response); + }; + + BIGTABLE.operation = function(name) { + assert.strictEqual(name, response.name); + return fakeOperation; + }; + + instance.cluster = function(name) { + assert.strictEqual(name, CLUSTER_NAME); + return fakeCluster; + }; + + var callback = function(err, cluster, operation, apiResponse) { + assert.strictEqual(err, null); + assert.strictEqual(cluster, fakeCluster); + assert.strictEqual(operation, fakeOperation); + assert.strictEqual(operation.metadata, response); + assert.strictEqual(apiResponse, response); + done(); + }; + + instance.createCluster(CLUSTER_NAME, callback); + }); + }); + + describe('createTable', function() { + var TABLE_ID = 'my-table'; + + it('should throw if a name is not provided', function() { + assert.throws(function() { + instance.createTable(); + }, /A name is required to create a table\./); + }); + + it('should provide the proper request options', function(done) { + instance.request = function(protoOpts, reqOpts) { + assert.deepEqual(protoOpts, { + service: 'BigtableTableAdmin', + method: 'createTable' + }); + + assert.strictEqual(reqOpts.parent, INSTANCE_ID); + assert.strictEqual(reqOpts.tableId, TABLE_ID); + assert.deepEqual(reqOpts.table, { + granularity: 0 + }); + done(); + }; + + instance.createTable(TABLE_ID, assert.ifError); + }); + + it('should set the initial split keys', function(done) { + var options = { + splits: ['a', 'b'] + }; + + var expectedSplits = [ + { key: 'a' }, + { key: 'b' } + ]; + + instance.request = function(protoOpts, reqOpts) { + assert.deepEqual(reqOpts.initialSplits, expectedSplits); + done(); + }; + + instance.createTable(TABLE_ID, options, assert.ifError); + }); + + describe('creating column families', function() { + it('should accept a family name', function(done) { + var options = { + families: ['a', 'b'] + }; + + instance.request = function(protoOpts, reqOpts) { + assert.deepEqual(reqOpts.table.columnFamilies, { + a: {}, + b: {} + }); + + done(); + }; + + instance.createTable(TABLE_ID, options, assert.ifError); + }); + + it('should accept a garbage collection object', function(done) { + var options = { + families: [ + { + name: 'e', + rule: {} + } + ] + }; + + var fakeRule = { a: 'b' }; + + FakeFamily.formatRule_ = function(rule) { + assert.strictEqual(rule, options.families[0].rule); + return fakeRule; + }; + + instance.request = function(protoOpts, reqOpts) { + assert.deepEqual(reqOpts.table.columnFamilies, { + e: { + gcRule: fakeRule + } + }); + done(); + }; + + instance.createTable(TABLE_ID, options, assert.ifError); + }); + }); + + it('should return an error to the callback', function(done) { + var err = new Error('err'); + var response = {}; + + instance.request = function(protoOpts, reqOpts, callback) { + callback(err, response); + }; + + instance.createTable(TABLE_ID, function(err_, table, apiResponse) { + assert.strictEqual(err, err_); + assert.strictEqual(table, null); + assert.strictEqual(apiResponse, response); + done(); + }); + }); + + it('should return a Table object', function(done) { + var response = { + name: TABLE_ID + }; + + var fakeTable = {}; + + instance.table = function(name) { + assert.strictEqual(name, response.name); + return fakeTable; + }; + + instance.request = function(p, r, callback) { + callback(null, response); + }; + + instance.createTable(TABLE_ID, function(err, table, apiResponse) { + assert.ifError(err); + assert.strictEqual(table, fakeTable); + assert.strictEqual(table.metadata, response); + assert.strictEqual(response, apiResponse); + done(); + }); + }); + }); + + describe('cluster', function() { + it('should return a Cluster object', function() { + var cluster = instance.cluster(CLUSTER_NAME); + + assert(cluster instanceof FakeCluster); + + var args = cluster.calledWith_; + + assert.strictEqual(args[0], instance); + assert.strictEqual(args[1], CLUSTER_NAME); + }); + }); + + describe('getClusters', function() { + it('should provide the proper request options', function(done) { + instance.request = function(grpcOpts, reqOpts) { + assert.deepEqual(grpcOpts, { + service: 'BigtableInstanceAdmin', + method: 'listClusters' + }); + + assert.strictEqual(reqOpts.parent, INSTANCE_ID); + done(); + }; + + instance.getClusters(assert.ifError); + }); + + it('should copy all query options', function(done) { + var fakeOptions = { + a: 'a', + b: 'b' + }; + + instance.request = function(grpcOpts, reqOpts) { + Object.keys(fakeOptions).forEach(function(key) { + assert.strictEqual(reqOpts[key], fakeOptions[key]); + }); + + assert.notStrictEqual(reqOpts, fakeOptions); + done(); + }; + + instance.getClusters(fakeOptions, assert.ifError); + }); + + it('should return an error to the callback', function(done) { + var error = new Error('err'); + var response = {}; + + instance.request = function(grpcOpts, reqOpts, callback) { + callback(error, response); + }; + + instance.getClusters(function(err, clusters, nextQuery, apiResponse) { + assert.strictEqual(err, error); + assert.strictEqual(clusters, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResponse, response); + done(); + }); + }); + + it('should return an array of cluster objects', function(done) { + var response = { + clusters: [{ + name: 'a' + }, { + name: 'b' + }] + }; + + var fakeClusters = [ + {}, + {} + ]; + + instance.request = function(grpcOpts, reqOpts, callback) { + callback(null, response); + }; + + var clusterCount = 0; + + instance.cluster = function(name) { + assert.strictEqual(name, response.clusters[clusterCount].name); + return fakeClusters[clusterCount++]; + }; + + instance.getClusters(function(err, clusters, nextQuery, apiResponse) { + assert.ifError(err); + assert.strictEqual(clusters[0], fakeClusters[0]); + assert.strictEqual(clusters[0].metadata, response.clusters[0]); + assert.strictEqual(clusters[1], fakeClusters[1]); + assert.strictEqual(clusters[1].metadata, response.clusters[1]); + assert.strictEqual(nextQuery, null); + assert.strictEqual(apiResponse, response); + done(); + }); + }); + + it('should provide a nextQuery object', function(done) { + var response = { + clusters: [], + nextPageToken: 'a' + }; + + var options = { + a: 'b' + }; + + instance.request = function(grpcOpts, reqOpts, callback) { + callback(null, response); + }; + + instance.getClusters(options, function(err, clusters, nextQuery) { + var expectedQuery = extend({}, options, { + pageToken: response.nextPageToken + }); + + assert.ifError(err); + assert.deepEqual(nextQuery, expectedQuery); + done(); + }); + }); + }); + + describe('getTables', function() { + var views = FakeTable.VIEWS = { + unspecified: 0, + name: 1, + schema: 2, + full: 4 + }; + + it('should provide the proper request options', function(done) { + instance.request = function(protoOpts, reqOpts) { + assert.deepEqual(protoOpts, { + service: 'BigtableTableAdmin', + method: 'listTables' + }); + assert.strictEqual(reqOpts.parent, INSTANCE_ID); + assert.strictEqual(reqOpts.view, views.unspecified); + done(); + }; + + instance.getTables(assert.ifError); + }); + + Object.keys(views).forEach(function(view) { + it('should set the "' + view + '" view', function(done) { + var options = { + view: view + }; + + instance.request = function(protoOpts, reqOpts) { + assert.strictEqual(reqOpts.view, views[view]); + done(); + }; + + instance.getTables(options, assert.ifError); + }); + }); + + it('should return an error to the callback', function(done) { + var err = new Error('err'); + var response = {}; + + instance.request = function(p, r, callback) { + callback(err, response); + }; + + instance.getTables(function(err_, tables, apiResponse) { + assert.strictEqual(err, err_); + assert.strictEqual(tables, null); + assert.strictEqual(apiResponse, response); + done(); + }); + }); + + it('should return a list of Table objects', function(done) { + var tableName = 'projects/p/zones/z/clusters/c/tables/my-table'; + var fakeFormattedName = 'my-table'; + var fakeTable = {}; + + var response = { + tables: [{ + name: tableName + }] + }; + + instance.request = function(p, r, callback) { + callback(null, response); + }; + + instance.table = function(name) { + assert.strictEqual(name, fakeFormattedName); + return fakeTable; + }; + + instance.getTables(function(err, tables, nextQuery, apiResponse) { + assert.ifError(err); + + var table = tables[0]; + + assert.strictEqual(table, fakeTable); + assert.strictEqual(table.metadata, response.tables[0]); + assert.strictEqual(nextQuery, null); + assert.strictEqual(response, apiResponse); + done(); + }); + }); + + it('should create a nextQuery object', function(done) { + var response = { + tables: [], + nextPageToken: 'a' + }; + + var options = { + a: 'b' + }; + + instance.request = function(protoOpts, reqOpts, callback) { + callback(null, response); + }; + + instance.getTables(options, function(err, tables, nextQuery) { + assert.ifError(err); + + var expectedQuery = extend({}, options, { + pageToken: response.nextPageToken + }); + + assert.deepEqual(nextQuery, expectedQuery); + done(); + }); + }); + }); + + describe('table', function() { + var TABLE_ID = 'table-id'; + + it('should return a table instance', function() { + var table = instance.table(TABLE_ID); + var args = table.calledWith_; + + assert(table instanceof FakeTable); + assert.strictEqual(args[0], instance); + assert.strictEqual(args[1], TABLE_ID); + }); + }); + +}); diff --git a/packages/bigtable/test/mutation.js b/packages/bigtable/test/mutation.js index 36024b0961b..eff4369192b 100644 --- a/packages/bigtable/test/mutation.js +++ b/packages/bigtable/test/mutation.js @@ -28,13 +28,13 @@ describe('Bigtable/Mutation', function() { }); describe('instantiation', function() { - it('should localize all the mutation properties', function() { - var fakeData = { - key: 'a', - method: 'b', - data: 'c' - }; + var fakeData = { + key: 'a', + method: 'b', + data: 'c' + }; + it('should localize all the mutation properties', function() { var mutation = new Mutation(fakeData); assert.strictEqual(mutation.key, fakeData.key); @@ -62,6 +62,13 @@ describe('Bigtable/Mutation', function() { }); describe('convertToBytes', function() { + it('should not re-wrap buffers', function() { + var buf = new Buffer('hello'); + var encoded = Mutation.convertToBytes(buf); + + assert.strictEqual(buf, encoded); + }); + it('should pack numbers into int64 values', function() { var num = 10; var encoded = Mutation.convertToBytes(num); @@ -175,6 +182,29 @@ describe('Bigtable/Mutation', function() { assert.strictEqual(convertCalls.length, 2); assert.deepEqual(convertCalls, ['gwashington', 1]); }); + + it('should accept buffers', function() { + var val = new Buffer('hello'); + var fakeMutation = { + follows: { + gwashington: val + } + }; + + var cells = Mutation.encodeSetCell(fakeMutation); + + assert.deepEqual(cells, [{ + setCell: { + familyName: 'follows', + columnQualifier: 'gwashington', + timestampMicros: -1, + value: val + } + }]); + + assert.strictEqual(convertCalls.length, 2); + assert.deepEqual(convertCalls, ['gwashington', val]); + }); }); describe('encodeDelete', function() { @@ -376,15 +406,17 @@ describe('Bigtable/Mutation', function() { data: [] }; + var mutation = new Mutation(data); + Mutation.encodeSetCell = function(_data) { assert.strictEqual(_data, data.data); return fakeEncoded; }; - var mutation = new Mutation(data).toProto(); + var mutationProto = mutation.toProto(); - assert.strictEqual(mutation.mutations, fakeEncoded); - assert.strictEqual(mutation.rowKey, data.key); + assert.strictEqual(mutationProto.mutations, fakeEncoded); + assert.strictEqual(mutationProto.rowKey, data.key); assert.strictEqual(convertCalls[0], data.key); }); diff --git a/packages/bigtable/test/row.js b/packages/bigtable/test/row.js index 49c507be125..0c4e05c3d80 100644 --- a/packages/bigtable/test/row.js +++ b/packages/bigtable/test/row.js @@ -110,50 +110,265 @@ describe('Bigtable/Row', function() { }); describe('formatChunks_', function() { - var formatFamiliesSpy; + var convert; beforeEach(function() { - formatFamiliesSpy = sinon.stub(Row, 'formatFamilies_'); + convert = FakeMutation.convertFromBytes; + FakeMutation.convertFromBytes = sinon.spy(function(val) { + return val.replace('unconverted', 'converted'); + }); + }); + + afterEach(function() { + FakeMutation.convertFromBytes = convert; + }); + + it('should format the chunks', function() { + var timestamp = Date.now(); + var chunks = [{ + rowKey: 'unconvertedKey', + familyName: { + value: 'familyName' + }, + qualifier: { + value: 'unconvertedQualifier' + }, + value: 'unconvertedValue', + labels: ['label'], + timestampMicros: timestamp, + valueSize: 0, + commitRow: false, + resetRow: false + }, { + commitRow: true + }]; + + var rows = Row.formatChunks_(chunks); + + assert.deepEqual(rows, [{ + key: 'convertedKey', + data: { + familyName: { + convertedQualifier: [{ + value: 'convertedValue', + labels: ['label'], + timestamp: timestamp, + size: 0 + }] + } + } + }]); }); - it('should not include chunks without commitRow', function() { + it('should inherit the row key', function() { var chunks = [{ - rowConents: {} + rowKey: 'unconvertedKey' + }, { + rowKey: null, + familyName: { + value: 'familyName' + }, + commitRow: true + }, { + rowKey: 'unconvertedKey2' + }, { + rowKey: null, + familyName: { + value: 'familyName2' + }, + commitRow: true }]; - var fakeFamilies = []; - formatFamiliesSpy.returns(fakeFamilies); + var rows = Row.formatChunks_(chunks); - var formatted = Row.formatChunks_(chunks); + assert.deepEqual(rows, [{ + key: 'convertedKey', + data: { + familyName: {} + } + }, { + key: 'convertedKey2', + data: { + familyName2: {} + } + }]); + }); + + it('should inherit the family name', function() { + var chunks = [{ + rowKey: 'unconvertedKey', + familyName: { + value: 'familyName' + } + }, { + qualifier: { + value: 'unconvertedQualifier' + } + }, { + qualifier: { + value: 'unconvertedQualifier2' + } + }, { + commitRow: true + }]; - assert.strictEqual(formatted, fakeFamilies); - assert.strictEqual(formatFamiliesSpy.callCount, 1); - assert.deepEqual(formatFamiliesSpy.getCall(0).args[0], []); + var rows = Row.formatChunks_(chunks); + + assert.deepEqual(rows, [{ + key: 'convertedKey', + data: { + familyName: { + convertedQualifier: [], + convertedQualifier2: [] + } + } + }]); }); - it('should ignore any chunks previous to a resetRow', function() { - var badData = {}; - var goodData = {}; - var fakeFamilies = [goodData, {}]; + it('should inherit the qualifier', function() { + var timestamp1 = 123; + var timestamp2 = 345; var chunks = [{ - rowContents: badData, + rowKey: 'unconvertedKey', + familyName: { + value: 'familyName' + }, + qualifier: { + value: 'unconvertedQualifier' + } }, { - resetRow: true + value: 'unconvertedValue', + labels: ['label'], + timestampMicros: timestamp1, + valueSize: 0 }, { - rowContents: goodData + value: 'unconvertedValue2', + labels: ['label2'], + timestampMicros: timestamp2, + valueSize: 2 }, { commitRow: true }]; - formatFamiliesSpy.returns(fakeFamilies); + var rows = Row.formatChunks_(chunks); + + assert.deepEqual(rows, [{ + key: 'convertedKey', + data: { + familyName: { + convertedQualifier: [{ + value: 'convertedValue', + labels: ['label'], + timestamp: timestamp1, + size: 0 + }, { + value: 'convertedValue2', + labels: ['label2'], + timestamp: timestamp2, + size: 2 + }] + } + } + }]); + }); + + it('should not decode values when applicable', function() { + var timestamp1 = 123; + var timestamp2 = 345; + + var chunks = [{ + rowKey: 'unconvertedKey', + familyName: { + value: 'familyName' + }, + qualifier: { + value: 'unconvertedQualifier' + } + }, { + value: 'unconvertedValue', + labels: ['label'], + timestampMicros: timestamp1, + valueSize: 0 + }, { + value: 'unconvertedValue2', + labels: ['label2'], + timestampMicros: timestamp2, + valueSize: 2 + }, { + commitRow: true + }]; - var formatted = Row.formatChunks_(chunks); + var rows = Row.formatChunks_(chunks, { + decode: false + }); - assert.strictEqual(formatted, fakeFamilies); - assert.strictEqual(formatted.indexOf(badData), -1); - assert.strictEqual(formatFamiliesSpy.callCount, 1); - assert.deepEqual(formatFamiliesSpy.getCall(0).args[0], [goodData]); + assert.deepEqual(rows, [{ + key: 'convertedKey', + data: { + familyName: { + convertedQualifier: [{ + value: 'unconvertedValue', + labels: ['label'], + timestamp: timestamp1, + size: 0 + }, { + value: 'unconvertedValue2', + labels: ['label2'], + timestamp: timestamp2, + size: 2 + }] + } + } + }]); + }); + + it('should discard old data when reset row is found', function() { + var chunks = [{ + rowKey: 'unconvertedKey', + familyName: { + value: 'familyName' + }, + qualifier: { + value: 'unconvertedQualifier' + }, + value: 'unconvertedValue', + labels: ['label'], + valueSize: 0, + timestampMicros: 123 + }, { + resetRow: true + }, { + rowKey: 'unconvertedKey2', + familyName: { + value: 'familyName2' + }, + qualifier: { + value: 'unconvertedQualifier2' + }, + value: 'unconvertedValue2', + labels: ['label2'], + valueSize: 2, + timestampMicros: 345 + }, { + commitRow: true + }]; + + var rows = Row.formatChunks_(chunks); + + assert.deepEqual(rows, [{ + key: 'convertedKey2', + data: { + familyName2: { + convertedQualifier2: [{ + value: 'convertedValue2', + labels: ['label2'], + size: 2, + timestamp: 345 + }] + } + } + }]); }); }); @@ -191,6 +406,18 @@ describe('Bigtable/Row', function() { assert.strictEqual(convertStpy.getCall(0).args[0], 'test-column'); assert.strictEqual(convertStpy.getCall(1).args[0], 'test-value'); }); + + it('should optionally not decode the value', function() { + var formatted = Row.formatFamilies_(families, { + decode: false + }); + + assert.deepEqual(formatted, formattedRowData); + + var convertStpy = FakeMutation.convertFromBytes; + assert.strictEqual(convertStpy.callCount, 1); + assert.strictEqual(convertStpy.getCall(0).args[0], 'test-column'); + }); }); describe('create', function() { @@ -219,6 +446,20 @@ describe('Bigtable/Row', function() { row.create(data, assert.ifError); }); + it('should accept options when inserting data', function(done) { + var data = { + a: 'a', + b: 'b' + }; + + row.parent.mutate = function(entry) { + assert.strictEqual(entry.data, data); + done(); + }; + + row.create(data, assert.ifError); + }); + it('should return an error to the callback', function(done) { var err = new Error('err'); var response = {}; @@ -267,7 +508,7 @@ describe('Bigtable/Row', function() { it('should read/modify/write rules', function(done) { row.request = function(grpcOpts, reqOpts, callback) { assert.deepEqual(grpcOpts, { - service: 'BigtableService', + service: 'Bigtable', method: 'readModifyWriteRow' }); @@ -345,7 +586,7 @@ describe('Bigtable/Row', function() { row.request = function(grpcOpts, reqOpts) { assert.deepEqual(grpcOpts, { - service: 'BigtableService', + service: 'Bigtable', method: 'checkAndMutateRow' }); @@ -456,7 +697,7 @@ describe('Bigtable/Row', function() { describe('get', function() { it('should provide the proper request options', function(done) { row.parent.getRows = function(reqOpts) { - assert.strictEqual(reqOpts.key, ROW_ID); + assert.strictEqual(reqOpts.keys[0], ROW_ID); assert.strictEqual(reqOpts.filter, undefined); assert.strictEqual(FakeMutation.parseColumnName.callCount, 0); done(); @@ -539,6 +780,44 @@ describe('Bigtable/Row', function() { row.get(keys, assert.ifError); }); + it('should respect the options object', function(done) { + var keys = [ + 'a' + ]; + + var options = { + decode: false + }; + + var expectedFilter = [{ + family: 'a' + }]; + + row.parent.getRows = function(reqOpts) { + assert.deepEqual(reqOpts.filter, expectedFilter); + assert.strictEqual(FakeMutation.parseColumnName.callCount, 1); + assert(FakeMutation.parseColumnName.calledWith(keys[0])); + assert.strictEqual(reqOpts.decode, options.decode); + done(); + }; + + row.get(keys, options, assert.ifError); + }); + + it('should accept options without keys', function(done) { + var options = { + decode: false + }; + + row.parent.getRows = function(reqOpts) { + assert.strictEqual(reqOpts.decode, options.decode); + assert(!reqOpts.filter); + done(); + }; + + row.get(options, assert.ifError); + }); + it('should return an error to the callback', function(done) { var error = new Error('err'); var response = {}; @@ -626,7 +905,7 @@ describe('Bigtable/Row', function() { var error = new Error('err'); var response = {}; - row.get = function(callback) { + row.get = function(options, callback) { callback(error, null, response); }; @@ -640,19 +919,42 @@ describe('Bigtable/Row', function() { it('should return metadata to the callback', function(done) { var response = {}; - var metadata = { + var fakeMetadata = { a: 'a', b: 'b' }; - row.get = function(callback) { - row.metadata = metadata; + row.get = function(options, callback) { callback(null, row, response); }; + row.metadata = fakeMetadata; + row.getMetadata(function(err, metadata, apiResponse) { assert.ifError(err); - assert.strictEqual(metadata, metadata); + assert.strictEqual(metadata, fakeMetadata); + assert.strictEqual(response, apiResponse); + done(); + }); + }); + + it('should accept an options object', function(done) { + var response = {}; + var fakeMetadata = {}; + var fakeOptions = { + decode: false + }; + + row.get = function(options, callback) { + assert.strictEqual(options, fakeOptions); + callback(null, row, response); + }; + + row.metadata = fakeMetadata; + + row.getMetadata(fakeOptions, function(err, metadata, apiResponse) { + assert.ifError(err); + assert.strictEqual(metadata, fakeMetadata); assert.strictEqual(response, apiResponse); done(); }); @@ -714,18 +1016,19 @@ describe('Bigtable/Row', function() { it('should pass back the updated value to the callback', function(done) { var fakeValue = 10; var response = { - key: 'fakeKey', - families: [{ - name: 'a', - columns: [{ - qualifier: 'b', - cells: [{ - timestampMicros: Date.now(), - value: fakeValue, - labels: [] + row: { + families: [{ + name: 'a', + columns: [{ + qualifier: 'b', + cells: [{ + timestampMicros: Date.now(), + value: fakeValue, + labels: [] + }] }] }] - }] + } }; row.createRules = function(r, callback) { @@ -737,14 +1040,14 @@ describe('Bigtable/Row', function() { assert.strictEqual(value, fakeValue); assert.strictEqual(apiResponse, response); assert.strictEqual(formatFamiliesSpy.callCount, 1); - assert(formatFamiliesSpy.calledWithExactly(response.families)); + assert(formatFamiliesSpy.calledWithExactly(response.row.families)); done(); }); }); }); describe('save', function() { - it('should insert a key value pair', function(done) { + describe('key value pair', function() { var key = 'a:b'; var value = 'c'; @@ -754,37 +1057,45 @@ describe('Bigtable/Row', function() { } }; - var parseSpy = Mutation.parseColumnName = sinon.spy(function() { - return { - family: 'd', - qualifier: 'e' - }; + var parseSpy; + + beforeEach(function() { + parseSpy = Mutation.parseColumnName = sinon.spy(function() { + return { + family: 'd', + qualifier: 'e' + }; + }); }); - row.parent.mutate = function(entry, callback) { - assert.strictEqual(entry.key, ROW_ID); - assert.deepEqual(entry.data, expectedData); - assert.strictEqual(entry.method, FakeMutation.methods.INSERT); - assert(parseSpy.calledWithExactly(key)); - callback(); - }; + it('should insert a key value pair', function(done) { + row.parent.mutate = function(entry, callback) { + assert.strictEqual(entry.key, ROW_ID); + assert.deepEqual(entry.data, expectedData); + assert.strictEqual(entry.method, FakeMutation.methods.INSERT); + assert(parseSpy.calledWithExactly(key)); + callback(); + }; - row.save(key, value, done); + row.save(key, value, done); + }); }); - it('should insert an object', function(done) { + describe('object mode', function() { var data = { a: { b: 'c' } }; - row.parent.mutate = function(entry) { - assert.strictEqual(entry.data, data); - done(); - }; + it('should insert an object', function(done) { + row.parent.mutate = function(entry) { + assert.strictEqual(entry.data, data); + done(); + }; - row.save(data, assert.ifError); + row.save(data, assert.ifError); + }); }); }); diff --git a/packages/bigtable/test/table.js b/packages/bigtable/test/table.js index 27986f2424b..b159450f560 100644 --- a/packages/bigtable/test/table.js +++ b/packages/bigtable/test/table.js @@ -17,6 +17,7 @@ 'use strict'; var assert = require('assert'); +var events = require('events'); var nodeutil = require('util'); var proxyquire = require('proxyquire'); var pumpify = require('pumpify'); @@ -24,40 +25,34 @@ var sinon = require('sinon').sandbox.create(); var Stream = require('stream').PassThrough; var through = require('through2'); +var common = require('@google-cloud/common'); var Family = require('../src/family.js'); -var GrpcServiceObject = require('@google-cloud/common').GrpcServiceObject; var Mutation = require('../src/mutation.js'); var Row = require('../src/row.js'); -function FakeGrpcServiceObject() { - this.calledWith_ = arguments; - GrpcServiceObject.apply(this, arguments); +function createFake(Class) { + function Fake() { + this.calledWith_ = arguments; + Class.apply(this, arguments); + } + nodeutil.inherits(Fake, Class); + return Fake; } -nodeutil.inherits(FakeGrpcServiceObject, GrpcServiceObject); - -function FakeFamily() { - this.calledWith_ = arguments; - Family.apply(this, arguments); -} +var FakeGrpcService = createFake(common.GrpcService); +var FakeGrpcServiceObject = createFake(common.GrpcServiceObject); +var FakeFamily = createFake(Family); FakeFamily.formatRule_ = sinon.spy(function(rule) { return rule; }); -nodeutil.inherits(FakeFamily, Family); - -function FakeRow() { - this.calledWith_ = arguments; - Row.apply(this, arguments); -} +var FakeRow = createFake(Row); FakeRow.formatChunks_ = sinon.spy(function(chunks) { return chunks; }); -nodeutil.inherits(FakeRow, Row); - var FakeMutation = { methods: Mutation.methods, convertToBytes: sinon.spy(function(value) { @@ -79,10 +74,11 @@ var FakeFilter = { describe('Bigtable/Table', function() { var TABLE_ID = 'my-table'; - var BIGTABLE = { - clusterName: 'a/b/c/d' + var INSTANCE = { + id: 'a/b/c/d' }; - var TABLE_NAME = BIGTABLE.clusterName + '/tables/' + TABLE_ID; + + var TABLE_NAME = INSTANCE.id + '/tables/' + TABLE_ID; var Table; var table; @@ -90,6 +86,7 @@ describe('Bigtable/Table', function() { before(function() { Table = proxyquire('../src/table.js', { '@google-cloud/common': { + GrpcService: FakeGrpcService, GrpcServiceObject: FakeGrpcServiceObject }, './family.js': FakeFamily, @@ -101,7 +98,7 @@ describe('Bigtable/Table', function() { }); beforeEach(function() { - table = new Table(BIGTABLE, TABLE_ID); + table = new Table(INSTANCE, TABLE_ID); }); afterEach(function() { @@ -124,18 +121,18 @@ describe('Bigtable/Table', function() { return FAKE_TABLE_NAME; }); - var table = new Table(BIGTABLE, TABLE_ID); + var table = new Table(INSTANCE, TABLE_ID); var config = table.calledWith_[0]; assert(table instanceof FakeGrpcServiceObject); - assert.strictEqual(config.parent, BIGTABLE); + assert.strictEqual(config.parent, INSTANCE); assert.strictEqual(config.id, FAKE_TABLE_NAME); assert.deepEqual(config.methods, { create: true, delete: { protoOpts: { - service: 'BigtableTableService', + service: 'BigtableTableAdmin', method: 'deleteTable' }, reqOpts: { @@ -143,25 +140,16 @@ describe('Bigtable/Table', function() { } }, exists: true, - get: true, - getMetadata: { - protoOpts: { - service: 'BigtableTableService', - method: 'getTable' - }, - reqOpts: { - name: FAKE_TABLE_NAME - } - } + get: true }); - assert(Table.formatName_.calledWith(BIGTABLE.clusterName, TABLE_ID)); + assert(Table.formatName_.calledWith(INSTANCE.id, TABLE_ID)); }); - it('should use Bigtable#createTable to create the table', function(done) { + it('should use Instance#createTable to create the table', function(done) { var fakeOptions = {}; - BIGTABLE.createTable = function(name, options, callback) { + INSTANCE.createTable = function(name, options, callback) { assert.strictEqual(name, TABLE_ID); assert.strictEqual(options, fakeOptions); callback(); @@ -171,49 +159,31 @@ describe('Bigtable/Table', function() { }); }); + describe('VIEWS', function() { + var views = { + unspecified: 0, + name: 1, + schema: 2, + full: 4 + }; + + it('should export the table views', function() { + assert.deepEqual(views, Table.VIEWS); + }); + }); + describe('formatName_', function() { it('should format the table name to include the cluster name', function() { - var tableName = Table.formatName_(BIGTABLE.clusterName, TABLE_ID); + var tableName = Table.formatName_(INSTANCE.id, TABLE_ID); assert.strictEqual(tableName, TABLE_NAME); }); it('should not re-format the table name', function() { - var tableName = Table.formatName_(BIGTABLE.clusterName, TABLE_NAME); + var tableName = Table.formatName_(INSTANCE.id, TABLE_NAME); assert.strictEqual(tableName, TABLE_NAME); }); }); - describe('formatRowRange_', function() { - it('should create a row range object', function() { - var fakeRange = { - start: 'a', - end: 'b' - }; - var convertedFakeRange = { - start: 'c', - end: 'd' - }; - - var spy = FakeMutation.convertToBytes = sinon.spy(function(value) { - if (value === fakeRange.start) { - return convertedFakeRange.start; - } - return convertedFakeRange.end; - }); - - var range = Table.formatRowRange_(fakeRange); - - assert.deepEqual(range, { - startKey: convertedFakeRange.start, - endKey: convertedFakeRange.end - }); - - assert.strictEqual(spy.callCount, 2); - assert.strictEqual(spy.getCall(0).args[0], 'a'); - assert.strictEqual(spy.getCall(1).args[0], 'b'); - }); - }); - describe('createFamily', function() { var COLUMN_ID = 'my-column'; @@ -226,27 +196,20 @@ describe('Bigtable/Table', function() { it('should provide the proper request options', function(done) { table.request = function(grpcOpts, reqOpts) { assert.deepEqual(grpcOpts, { - service: 'BigtableTableService', - method: 'createColumnFamily' + service: 'BigtableTableAdmin', + method: 'modifyColumnFamilies' }); assert.strictEqual(reqOpts.name, TABLE_NAME); - assert.strictEqual(reqOpts.columnFamilyId, COLUMN_ID); - done(); - }; + assert.deepEqual(reqOpts.modifications, [{ + id: COLUMN_ID, + create: {} + }]); - table.createFamily(COLUMN_ID, assert.ifError); - }); - - it('should respect the gc expression option', function(done) { - var expression = 'a && b'; - - table.request = function(g, reqOpts) { - assert.strictEqual(reqOpts.columnFamily.gcExpression, expression); done(); }; - table.createFamily(COLUMN_ID, expression, assert.ifError); + table.createFamily(COLUMN_ID, assert.ifError); }); it('should respect the gc rule option', function(done) { @@ -264,7 +227,9 @@ describe('Bigtable/Table', function() { }); table.request = function(g, reqOpts) { - assert.strictEqual(reqOpts.columnFamily.gcRule, convertedRule); + var modification = reqOpts.modifications[0]; + + assert.strictEqual(modification.create.gcRule, convertedRule); assert.strictEqual(spy.callCount, 1); assert.strictEqual(spy.getCall(0).args[0], rule); done(); @@ -318,11 +283,11 @@ describe('Bigtable/Table', function() { it('should provide the proper request options', function(done) { table.request = function(grpcOpts, reqOpts, callback) { assert.deepEqual(grpcOpts, { - service: 'BigtableTableService', - method: 'bulkDeleteRows' + service: 'BigtableTableAdmin', + method: 'dropRowRange' }); - assert.strictEqual(reqOpts.tableName, TABLE_NAME); + assert.strictEqual(reqOpts.name, TABLE_NAME); callback(); }; @@ -429,25 +394,94 @@ describe('Bigtable/Table', function() { }); }); + describe('getMetadata', function() { + var views = { + unspecified: 0, + name: 1, + schema: 2, + full: 4 + }; + beforeEach(function() { + Table.VIEWS = views; + }); + + it('should provide the proper request options', function(done) { + table.request = function(grpcOpts, reqOpts) { + assert.deepEqual(grpcOpts, { + service: 'BigtableTableAdmin', + method: 'getTable' + }); + + assert.strictEqual(reqOpts.name, table.id); + assert.strictEqual(reqOpts.view, views.unspecified); + done(); + }; + + table.getMetadata(assert.ifError); + }); + + Object.keys(views).forEach(function(view) { + it('should set the "' + view + '" view', function(done) { + var options = { + view: view + }; + + table.request = function(grpcOpts, reqOpts) { + assert.strictEqual(reqOpts.view, views[view]); + done(); + }; + + table.getMetadata(options, assert.ifError); + }); + }); + + it('should return an error to the callback', function(done) { + var error = new Error('err'); + var response = {}; + + table.request = function(grpcOpts, reqOpts, callback) { + callback(error, response); + }; + + table.getMetadata(function(err, metadata, apiResponse) { + assert.strictEqual(err, error); + assert.strictEqual(metadata, null); + assert.strictEqual(apiResponse, response); + done(); + }); + }); + + it('should update the metadata', function(done) { + var response = {}; + + table.request = function(grpcOpts, reqOpts, callback) { + callback(null, response); + }; + + table.getMetadata(function(err, metadata, apiResponse) { + assert.ifError(err); + assert.strictEqual(metadata, response); + assert.strictEqual(apiResponse, response); + assert.strictEqual(table.metadata, response); + done(); + }); + }); + }); + describe('getRows', function() { describe('options', function() { var pumpSpy; - var formatSpy; beforeEach(function() { pumpSpy = sinon.stub(pumpify, 'obj', function() { return through.obj(); }); - - formatSpy = sinon.stub(Table, 'formatRowRange_', function(value) { - return value; - }); }); it('should provide the proper request options', function(done) { table.requestStream = function(grpcOpts, reqOpts) { assert.deepEqual(grpcOpts, { - service: 'BigtableService', + service: 'Bigtable', method: 'readRows' }); @@ -459,26 +493,6 @@ describe('Bigtable/Table', function() { table.getRows(); }); - it('should retrieve an individual row', function(done) { - var options = { - key: 'gwashington' - }; - var fakeKey = 'a'; - - var convertSpy = FakeMutation.convertToBytes = sinon.spy(function() { - return fakeKey; - }); - - table.requestStream = function(g, reqOpts) { - assert.strictEqual(reqOpts.rowKey, fakeKey); - assert.strictEqual(convertSpy.callCount, 1); - assert.strictEqual(convertSpy.getCall(0).args[0], options.key); - done(); - }; - - table.getRows(options); - }); - it('should retrieve a range of rows', function(done) { var options = { start: 'gwashington', @@ -490,14 +504,18 @@ describe('Bigtable/Table', function() { end: 'b' }; - var formatSpy = Table.formatRowRange_ = sinon.spy(function() { + var formatSpy = FakeFilter.createRange = sinon.spy(function() { return fakeRange; }); table.requestStream = function(g, reqOpts) { - assert.strictEqual(reqOpts.rowRange, fakeRange); + assert.deepEqual(reqOpts.rows.rowRanges[0], fakeRange); assert.strictEqual(formatSpy.callCount, 1); - assert.strictEqual(formatSpy.getCall(0).args[0], options); + assert.deepEqual(formatSpy.getCall(0).args, [ + options.start, + options.end, + 'key' + ]); done(); }; @@ -522,7 +540,7 @@ describe('Bigtable/Table', function() { }); table.requestStream = function(g, reqOpts) { - assert.deepEqual(reqOpts.rowSet.rowKeys, convertedKeys); + assert.deepEqual(reqOpts.rows.rowKeys, convertedKeys); assert.strictEqual(convertSpy.callCount, 2); assert.strictEqual(convertSpy.getCall(0).args[0], options.keys[0]); assert.strictEqual(convertSpy.getCall(1).args[0], options.keys[1]); @@ -551,16 +569,23 @@ describe('Bigtable/Table', function() { end: 'h' }]; - var formatSpy = Table.formatRowRange_ = sinon.spy(function(range) { - var rangeIndex = options.ranges.indexOf(range); - return fakeRanges[rangeIndex]; + var formatSpy = FakeFilter.createRange = sinon.spy(function() { + return fakeRanges[formatSpy.callCount - 1]; }); table.requestStream = function(g, reqOpts) { - assert.deepEqual(reqOpts.rowSet.rowRanges, fakeRanges); + assert.deepEqual(reqOpts.rows.rowRanges, fakeRanges); assert.strictEqual(formatSpy.callCount, 2); - assert.strictEqual(formatSpy.getCall(0).args[0], options.ranges[0]); - assert.strictEqual(formatSpy.getCall(1).args[0], options.ranges[1]); + assert.deepEqual(formatSpy.getCall(0).args, [ + options.ranges[0].start, + options.ranges[0].end, + 'key' + ]); + assert.deepEqual(formatSpy.getCall(1).args, [ + options.ranges[1].start, + options.ranges[1].end, + 'key' + ]); done(); }; @@ -588,19 +613,6 @@ describe('Bigtable/Table', function() { table.getRows(options); }); - it('should allow row interleaving', function(done) { - var options = { - interleave: true - }; - - table.requestStream = function(g, reqOpts) { - assert.strictEqual(reqOpts.allowRowInterleaving, options.interleave); - done(); - }; - - table.getRows(options); - }); - it('should allow setting a row limit', function(done) { var options = { limit: 10 @@ -616,20 +628,21 @@ describe('Bigtable/Table', function() { }); describe('success', function() { - var fakeRows = [{ - rowKey: 'a', - chunks: [] - }, { - rowKey: 'b', - chunks: [] - }]; - var convertedKeys = [ - 'c', - 'd' - ]; - var formattedChunks = [ - [{ a: 'a' }], - [{ b: 'b' }] + var fakeChunks = { + chunks: [{ + rowKey: 'a', + }, { + commitRow: true + }, { + rowKey: 'b', + }, { + commitRow: true + }] + }; + + var formattedRows = [ + { key: 'c', data: {} }, + { key: 'd', data: {} } ]; beforeEach(function() { @@ -637,18 +650,8 @@ describe('Bigtable/Table', function() { return {}; }); - FakeMutation.convertFromBytes = sinon.spy(function(value) { - if (value === fakeRows[0].rowKey) { - return convertedKeys[0]; - } - return convertedKeys[1]; - }); - - FakeRow.formatChunks_ = sinon.spy(function(value) { - if (value === fakeRows[0].chunks) { - return formattedChunks[0]; - } - return formattedChunks[1]; + FakeRow.formatChunks_ = sinon.spy(function() { + return formattedRows; }); table.requestStream = function() { @@ -657,10 +660,7 @@ describe('Bigtable/Table', function() { }); setImmediate(function() { - fakeRows.forEach(function(data) { - stream.push(data); - }); - + stream.push(fakeChunks); stream.push(null); }); @@ -668,6 +668,19 @@ describe('Bigtable/Table', function() { }; }); + it('should pass the decode option', function(done) { + var options = { + decode: false + }; + + table.getRows(options, function(err) { + assert.ifError(err); + var formatArgs = FakeRow.formatChunks_.getCall(0).args[1]; + assert.strictEqual(formatArgs.decode, options.decode); + done(); + }); + }); + it('should stream Row objects', function(done) { var rows = []; @@ -680,18 +693,16 @@ describe('Bigtable/Table', function() { var rowSpy = table.row; var formatSpy = FakeRow.formatChunks_; - assert.strictEqual(rows.length, fakeRows.length); - assert.strictEqual(rowSpy.callCount, fakeRows.length); + assert.strictEqual(rows.length, formattedRows.length); + assert.strictEqual(rowSpy.callCount, formattedRows.length); - assert.strictEqual(rowSpy.getCall(0).args[0], convertedKeys[0]); - assert.strictEqual(rows[0].data, formattedChunks[0]); - assert.strictEqual( - formatSpy.getCall(0).args[0], fakeRows[0].chunks); + assert.strictEqual(formatSpy.getCall(0).args[0], fakeChunks.chunks); - assert.strictEqual(rowSpy.getCall(1).args[0], convertedKeys[1]); - assert.strictEqual(rows[1].data, formattedChunks[1]); - assert.strictEqual( - formatSpy.getCall(1).args[0], fakeRows[1].chunks); + assert.strictEqual(rowSpy.getCall(0).args[0], formattedRows[0].key); + assert.strictEqual(rows[0].data, formattedRows[0].data); + + assert.strictEqual(rowSpy.getCall(1).args[0], formattedRows[1].key); + assert.strictEqual(rows[1].data, formattedRows[1].data); done(); }); @@ -704,18 +715,16 @@ describe('Bigtable/Table', function() { var rowSpy = table.row; var formatSpy = FakeRow.formatChunks_; - assert.strictEqual(rows.length, fakeRows.length); - assert.strictEqual(rowSpy.callCount, fakeRows.length); + assert.strictEqual(rows.length, formattedRows.length); + assert.strictEqual(rowSpy.callCount, formattedRows.length); + + assert.strictEqual(formatSpy.getCall(0).args[0], fakeChunks.chunks); - assert.strictEqual(rowSpy.getCall(0).args[0], convertedKeys[0]); - assert.strictEqual(rows[0].data, formattedChunks[0]); - assert.strictEqual( - formatSpy.getCall(0).args[0], fakeRows[0].chunks); + assert.strictEqual(rowSpy.getCall(0).args[0], formattedRows[0].key); + assert.strictEqual(rows[0].data, formattedRows[0].data); - assert.strictEqual(rowSpy.getCall(1).args[0], convertedKeys[1]); - assert.strictEqual(rows[1].data, formattedChunks[1]); - assert.strictEqual( - formatSpy.getCall(1).args[0], fakeRows[1].chunks); + assert.strictEqual(rowSpy.getCall(1).args[0], formattedRows[1].key); + assert.strictEqual(rows[1].data, formattedRows[1].data); done(); }); @@ -786,21 +795,37 @@ describe('Bigtable/Table', function() { table.insert(fakeEntries, done); }); + + it('should return the mutate stream', function() { + var fakeStream = {}; + + table.mutate = function() { + return fakeStream; + }; + + var stream = table.insert([]); + assert.strictEqual(stream, fakeStream); + }); }); describe('mutate', function() { - it('should provide the proper request options', function(done) { - var entries = [{}, {}]; - var fakeEntries = [{}, {}]; + var entries = [{}, {}]; + var fakeEntries = [{}, {}]; + var parseSpy; - var parseSpy = FakeMutation.parse = sinon.spy(function(value) { + beforeEach(function() { + parseSpy = FakeMutation.parse = sinon.spy(function(value) { var entryIndex = entries.indexOf(value); return fakeEntries[entryIndex]; }); + }); - table.request = function(grpcOpts, reqOpts, callback) { + it('should provide the proper request options', function(done) { + var stream = through.obj(); + + table.requestStream = function(grpcOpts, reqOpts) { assert.deepEqual(grpcOpts, { - service: 'BigtableService', + service: 'Bigtable', method: 'mutateRows' }); @@ -810,10 +835,165 @@ describe('Bigtable/Table', function() { assert.strictEqual(parseSpy.callCount, 2); assert.strictEqual(parseSpy.getCall(0).args[0], entries[0]); assert.strictEqual(parseSpy.getCall(1).args[0], entries[1]); - callback(); + + setImmediate(done); + + return stream; }; - table.mutate(entries, done); + table.mutate(entries, assert.ifError); + }); + + describe('error', function() { + describe('API errors', function() { + var error = new Error('err'); + + beforeEach(function() { + table.requestStream = function() { + var stream = new Stream({ + objectMode: true + }); + + setImmediate(function() { + stream.emit('error', error); + }); + + return stream; + }; + }); + + it('should return the error to the callback', function(done) { + table.mutate(entries, function(err) { + assert.strictEqual(err, error); + done(); + }); + }); + + it('should emit the error via error event', function(done) { + table.mutate(entries).on('error', function(err) { + assert.strictEqual(err, error); + done(); + }); + }); + }); + + describe('mutation errors', function() { + var fakeStatuses = [{ + index: 0, + status: { + code: 1 + } + }, { + index: 1, + status: { + code: 1 + } + }]; + + var parsedStatuses = [{}, {}]; + + beforeEach(function() { + table.requestStream = function() { + var stream = through.obj(); + + stream.push({ entries: fakeStatuses }); + + setImmediate(function() { + stream.end(); + }); + + return stream; + }; + + var statusCount = 0; + FakeGrpcService.decorateStatus_ = function(status) { + assert.strictEqual(status, fakeStatuses[statusCount].status); + return parsedStatuses[statusCount++]; + }; + }); + + it('should populate the mutationErrors array', function(done) { + table.mutate(entries, function(err, mutationErrors) { + assert.ifError(err); + assert.strictEqual(mutationErrors[0], parsedStatuses[0]); + assert.strictEqual(mutationErrors[0].entry, entries[0]); + assert.strictEqual(mutationErrors[1], parsedStatuses[1]); + assert.strictEqual(mutationErrors[1].entry, entries[1]); + done(); + }); + }); + + it('should emit a mutation error as an error event', function(done) { + var mutationErrors = []; + var emitter = table.mutate(entries); + + assert(emitter instanceof events.EventEmitter); + + emitter + .on('error', function(err) { + mutationErrors.push(err); + }) + .on('complete', function() { + assert.strictEqual(mutationErrors[0], parsedStatuses[0]); + assert.strictEqual(mutationErrors[0].entry, entries[0]); + assert.strictEqual(mutationErrors[1], parsedStatuses[1]); + assert.strictEqual(mutationErrors[1].entry, entries[1]); + done(); + }); + }); + }); + }); + + describe('success', function() { + var fakeStatuses = [{ + index: 0, + status: { + code: 0 + } + }, { + index: 1, + status: { + code: 0 + } + }]; + + beforeEach(function() { + table.requestStream = function() { + var stream = through.obj(); + + stream.push({ entries: fakeStatuses }); + + setImmediate(function() { + stream.end(); + }); + + return stream; + }; + + FakeGrpcServiceObject.decorateStatus_ = function() { + throw new Error('Should not be called'); + }; + }); + + it('should return an empty array to the callback', function(done) { + table.mutate(entries, function(err, mutateErrors) { + assert.ifError(err); + assert.strictEqual(mutateErrors.length, 0); + done(); + }); + }); + + it('should emit the appropriate stream events', function(done) { + var emitter = table.mutate(entries); + + assert(emitter instanceof events.EventEmitter); + + emitter + .on('error', done) // should not be emitted + .on('complete', function() { + done(); + }); + }); }); }); @@ -839,7 +1019,7 @@ describe('Bigtable/Table', function() { it('should provide the proper request options', function(done) { table.requestStream = function(grpcOpts, reqOpts) { assert.deepEqual(grpcOpts, { - service: 'BigtableService', + service: 'Bigtable', method: 'sampleRowKeys' });