row.js

"use strict";
/*!
 * 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.
 */
Object.defineProperty(exports, "__esModule", { value: true });
const promisify_1 = require("@google-cloud/promisify");
const arrify = require("arrify");
const dotProp = require('dot-prop');
const is = require("is");
const filter_1 = require("./filter");
const mutation_1 = require("./mutation");
/**
 * @private
 */
class RowError extends Error {
    constructor(row) {
        super();
        this.name = 'RowError';
        this.message = `Unknown row: ${row}.`;
        this.code = 404;
    }
}
exports.RowError = RowError;
/**
 * Create a Row object to interact with your table rows.
 *
 * @class
 * @param {Table} table The row's parent Table instance.
 * @param {string} key The key for this row.
 *
 * @example
 * const Bigtable = require('@google-cloud/bigtable');
 * const bigtable = new Bigtable();
 * const instance = bigtable.instance('my-instance');
 * const table = instance.table('prezzy');
 * const row = table.row('gwashington');
 */
class Row {
    constructor(table, key) {
        this.bigtable = table.bigtable;
        this.table = table;
        this.id = key;
        this.data = {};
    }
    /**
     * Formats the row chunks into friendly format. Chunks contain 3 properties:
     *
     * `rowContents` The row contents, this essentially is all data pertaining
     *     to a single family.
     *
     * `commitRow` This is a boolean telling us the all previous chunks for this
     *     row are ok to consume.
     *
     * `resetRow` This is a boolean telling us that all the previous chunks are to
     *     be discarded.
     *
     * @private
     *
     * @param {chunk[]} chunks The list of chunks.
     * @param {object} [options] Formatting options.
     *
     * @example
     * Row.formatChunks_(chunks);
     * // {
     * //   follows: {
     * //     gwashington: [
     * //       {
     * //         value: 2
     * //       }
     * //     ]
     * //   }
     * // }
     */
    static formatChunks_(chunks, options) {
        const rows = [];
        let familyName;
        let qualifierName;
        options = options || {};
        chunks.reduce((row, chunk) => {
            let family;
            let qualifier;
            row.data = row.data || {};
            if (chunk.rowKey) {
                row.key = mutation_1.Mutation.convertFromBytes(chunk.rowKey, {
                    userOptions: options,
                });
            }
            if (chunk.familyName) {
                familyName = chunk.familyName.value;
            }
            if (familyName) {
                family = row.data[familyName] = row.data[familyName] || {};
            }
            if (chunk.qualifier) {
                qualifierName = mutation_1.Mutation.convertFromBytes(chunk.qualifier.value, {
                    userOptions: options,
                });
            }
            if (family && qualifierName) {
                qualifier = family[qualifierName] = family[qualifierName] || [];
            }
            if (qualifier && chunk.value) {
                qualifier.push({
                    value: mutation_1.Mutation.convertFromBytes(chunk.value, { userOptions: options }),
                    labels: chunk.labels,
                    timestamp: chunk.timestampMicros,
                    size: chunk.valueSize,
                });
            }
            if (chunk.commitRow) {
                rows.push(row);
            }
            if (chunk.commitRow || chunk.resetRow) {
                familyName = qualifierName = null;
                return {};
            }
            return row;
        }, {});
        return rows;
    }
    /**
     * Formats a rowContents object into friendly format.
     *
     * @private
     *
     * @param {object[]} families The row families.
     * @param {object} [options] Formatting options.
     *
     * @example
     * var families = [
     *   {
     *     name: 'follows',
     *     columns: [
     *       {
     *         qualifier: 'gwashington',
     *         cells: [
     *           {
     *             value: 2
     *           }
     *         ]
     *       }
     *     ]
     *   }
     * ];
     *
     * Row.formatFamilies_(families);
     * // {
     * //   follows: {
     * //     gwashington: [
     * //       {
     * //         value: 2
     * //       }
     * //     ]
     * //   }
     * // }
     */
    static formatFamilies_(families, options) {
        const data = {};
        options = options || {};
        families.forEach(family => {
            const familyData = (data[family.name] = {});
            family.columns.forEach(column => {
                const qualifier = mutation_1.Mutation.convertFromBytes(column.qualifier);
                familyData[qualifier] = column.cells.map(cell => {
                    let value = cell.value;
                    if (options.decode !== false) {
                        value = mutation_1.Mutation.convertFromBytes(value, { isPossibleNumber: true });
                    }
                    return {
                        value,
                        timestamp: cell.timestampMicros,
                        labels: cell.labels,
                    };
                });
            });
        });
        return data;
    }
    /**
     * Create a new row in your table.
     *
     * @param {object} [options] Configuration object.
     * @param {object} [options.entry] An entry. See {@link Table#insert}.
     * @param {object} [options.gaxOptions] Request configuration options, outlined
     *     here: https://googleapis.github.io/gax-nodejs/CallSettings.html.
     * @param {function} callback The callback function.
     * @param {?error} callback.err An error returned while making this
     *     request.
     * @param {Row} callback.row The newly created row object.
     * @param {object} callback.apiResponse The full API response.
     *
     * @example <caption>include:samples/document-snippets/row.js</caption>
     * region_tag:bigtable_create_row
     */
    create(options, callback) {
        if (is.function(options)) {
            callback = options;
            options = {};
        }
        const entry = {
            key: this.id,
            data: options.entry,
            method: mutation_1.Mutation.methods.INSERT,
        };
        this.data = {};
        this.table.mutate(entry, options.gaxOptions, (err, apiResponse) => {
            if (err) {
                callback(err, null, apiResponse);
                return;
            }
            callback(null, this, apiResponse);
        });
    }
    /**
     * Update a row with rules specifying how the row's contents are to be
     * transformed into writes. Rules are applied in order, meaning that earlier
     * rules will affect the results of later ones.
     *
     * @throws {error} If no rules are provided.
     *
     * @param {object|object[]} rules The rules to apply to this row.
     * @param {object} [gaxOptions] Request configuration options, outlined here:
     *     https://googleapis.github.io/gax-nodejs/CallSettings.html.
     * @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 <caption>include:samples/document-snippets/row.js</caption>
     * region_tag:bigtable_create_rules
     */
    createRules(rules, gaxOptions, callback) {
        if (is.fn(gaxOptions)) {
            callback = gaxOptions;
            gaxOptions = {};
        }
        if (!rules || rules.length === 0) {
            throw new Error('At least one rule must be provided.');
        }
        rules = arrify(rules).map(rule => {
            const column = mutation_1.Mutation.parseColumnName(rule.column);
            const ruleData = {
                familyName: column.family,
                columnQualifier: mutation_1.Mutation.convertToBytes(column.qualifier),
            };
            if (rule.append) {
                ruleData.appendValue = mutation_1.Mutation.convertToBytes(rule.append);
            }
            if (rule.increment) {
                ruleData.incrementAmount = rule.increment;
            }
            return ruleData;
        });
        const reqOpts = {
            tableName: this.table.name,
            appProfileId: this.bigtable.appProfileId,
            rowKey: mutation_1.Mutation.convertToBytes(this.id),
            rules,
        };
        this.data = {};
        this.bigtable.request({
            client: 'BigtableClient',
            method: 'readModifyWriteRow',
            reqOpts,
            gaxOpts: gaxOptions,
        }, callback);
    }
    /**
     * Deletes all cells in the row.
     *
     * @param {object} [gaxOptions] Request configuration options, outlined here:
     *     https://googleapis.github.io/gax-nodejs/CallSettings.html.
     * @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 <caption>include:samples/document-snippets/row.js</caption>
     * region_tag:bigtable_delete_all_cells
     */
    delete(gaxOptions, callback) {
        if (is.fn(gaxOptions)) {
            callback = gaxOptions;
            gaxOptions = {};
        }
        const mutation = {
            key: this.id,
            method: mutation_1.Mutation.methods.DELETE,
        };
        this.data = {};
        this.table.mutate(mutation, gaxOptions, callback);
    }
    /**
     * Delete specified cells from the row. See {@link Table#mutate}.
     *
     * @param {string[]} columns Column names for the cells to be deleted.
     * @param {object} [gaxOptions] Request configuration options, outlined here:
     *     https://googleapis.github.io/gax-nodejs/CallSettings.html.
     * @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 <caption>include:samples/document-snippets/row.js</caption>
     * region_tag:bigtable_delete_particular_cells
     */
    deleteCells(columns, gaxOptions, callback) {
        if (is.fn(gaxOptions)) {
            callback = gaxOptions;
            gaxOptions = {};
        }
        const mutation = {
            key: this.id,
            data: arrify(columns),
            method: mutation_1.Mutation.methods.DELETE,
        };
        this.data = {};
        this.table.mutate(mutation, gaxOptions, callback);
    }
    /**
     * Check if the table row exists.
     *
     * @param {object} [gaxOptions] Request configuration options, outlined here:
     *     https://googleapis.github.io/gax-nodejs/CallSettings.html.
     * @param {function} callback The callback function.
     * @param {?error} callback.err An error returned while making this
     *     request.
     * @param {boolean} callback.exists Whether the row exists or not.
     *
     * @example <caption>include:samples/document-snippets/row.js</caption>
     * region_tag:bigtable_row_exists
     */
    exists(gaxOptions, callback) {
        if (is.fn(gaxOptions)) {
            callback = gaxOptions;
            gaxOptions = {};
        }
        this.getMetadata(gaxOptions, err => {
            if (err) {
                if (err instanceof RowError) {
                    callback(null, false);
                    return;
                }
                callback(err);
                return;
            }
            callback(null, true);
        });
    }
    /**
     * Mutates a row atomically based on the output of a filter. Depending on
     * whether or not any results are yielded, either the `onMatch` or `onNoMatch`
     * callback will be executed.
     *
     * @param {Filter} filter Filter to be applied to the contents of the row.
     * @param {object} config Configuration object.
     * @param {?object[]} config.onMatch A list of entries to be ran if a match is
     *     found.
     * @param {object[]} [config.onNoMatch] A list of entries to be ran if no
     *     matches are found.
     * @param {object} [config.gaxOptions] Request configuration options, outlined
     *     here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
     * @param {function} callback The callback function.
     * @param {?error} callback.err An error returned while making this
     *     request.
     * @param {boolean} callback.matched Whether a match was found or not.
     *
     * @example <caption>include:samples/document-snippets/row.js</caption>
     * region_tag:bigtable_row_filter
     */
    filter(filter, config, callback) {
        const reqOpts = {
            tableName: this.table.name,
            appProfileId: this.bigtable.appProfileId,
            rowKey: mutation_1.Mutation.convertToBytes(this.id),
            predicateFilter: filter_1.Filter.parse(filter),
            trueMutations: createFlatMutationsList(config.onMatch),
            falseMutations: createFlatMutationsList(config.onNoMatch),
        };
        this.data = {};
        this.bigtable.request({
            client: 'BigtableClient',
            method: 'checkAndMutateRow',
            reqOpts,
            gaxOpts: config.gaxOptions,
        }, (err, apiResponse) => {
            if (err) {
                callback(err, null, apiResponse);
                return;
            }
            callback(null, apiResponse.predicateMatched, apiResponse);
        });
        function createFlatMutationsList(entries) {
            entries = arrify(entries).map(entry => mutation_1.Mutation.parse(entry).mutations);
            return entries.reduce((a, b) => a.concat(b), []);
        }
    }
    /**
     * Get the row data. See {@link Table#getRows}.
     *
     * @param {string[]} [columns] List of specific columns to retrieve.
     * @param {object} [options] Configuration object.
     * @param {boolean} [options.decode=true] If set to `false` it will not decode Buffer
     *     values returned from Bigtable.
     * @param {object} [options.gaxOptions] Request configuration options, outlined
     *     here: https://googleapis.github.io/gax-nodejs/CallSettings.html.
     * @param {function} callback The callback function.
     * @param {?error} callback.err An error returned while making this
     *     request.
     * @param {Row} callback.row The updated Row object.
     *
     * @example <caption>include:samples/document-snippets/row.js</caption>
     * region_tag:bigtable_get_row
     */
    get(columns, options, callback) {
        if (!is.array(columns)) {
            callback = options;
            options = columns;
            columns = [];
        }
        if (is.function(options)) {
            callback = options;
            options = {};
        }
        let filter;
        columns = arrify(columns);
        // if there is column filter
        if (columns.length) {
            const filters = columns.map(mutation_1.Mutation.parseColumnName).map(column => {
                const colmFilters = [{ family: column.family }];
                if (column.qualifier) {
                    colmFilters.push({ column: column.qualifier });
                }
                return colmFilters;
            });
            // if there is more then one filter, make it type inteleave filter
            if (filters.length > 1) {
                filter = [
                    {
                        interleave: filters,
                    },
                ];
            }
            else {
                filter = filters[0];
            }
        }
        // if there is also a second option.filter append to filter array
        if (options.filter) {
            filter = arrify(filter).concat(options.filter);
        }
        const getRowsOptions = Object.assign({}, options, {
            keys: [this.id],
            filter,
        });
        this.table.getRows(getRowsOptions, (err, rows) => {
            if (err) {
                callback(err);
                return;
            }
            const row = rows[0];
            if (!row) {
                err = new RowError(this.id);
                callback(err);
                return;
            }
            this.data = row.data;
            // If the user specifies column names, we'll return back the row data
            // we received. Otherwise, we'll return the row "this" in a typical
            // GrpcServiceObject#get fashion.
            callback(null, columns.length ? row.data : this);
        });
    }
    /**
     * Get the row's metadata.
     *
     * @param {object} [options] Configuration object.
     * @param {boolean} [options.decode=true] If set to `false` it will not decode
     *     Buffer values returned from Bigtable.
     * @param {object} [options.gaxOptions] Request configuration options, outlined
     *     here: https://googleapis.github.io/gax-nodejs/CallSettings.html.
     * @param {function} callback The callback function.
     * @param {?error} callback.err An error returned while making this
     *     request.
     * @param {object} callback.metadata The row's metadata.
     *
     * @example <caption>include:samples/document-snippets/row.js</caption>
     * region_tag:bigtable_get_row_meta
     */
    getMetadata(options, callback) {
        if (is.function(options)) {
            callback = options;
            options = {};
        }
        this.get(options, (err, row) => {
            if (err) {
                callback(err);
                return;
            }
            callback(null, row.metadata);
        });
    }
    /**
     * Increment a specific column within the row. If the column does not
     * exist, it is automatically initialized to 0 before being incremented.
     *
     * @param {string} column The column we are incrementing a value in.
     * @param {number} [value] The amount to increment by, defaults to 1.
     * @param {object} [gaxOptions] Request configuration options, outlined here:
     *     https://googleapis.github.io/gax-nodejs/CallSettings.html.
     * @param {function} callback The callback function.
     * @param {?error} callback.err An error returned while making this
     *     request.
     * @param {number} callback.value The updated value of the column.
     * @param {object} callback.apiResponse The full API response.
     *
     * @example <caption>include:samples/document-snippets/row.js</caption>
     * region_tag:bigtable_row_increment
     */
    increment(column, value, gaxOptions, callback) {
        // increment('column', callback)
        if (is.function(value)) {
            callback = value;
            value = 1;
            gaxOptions = {};
        }
        // increment('column', value, callback)
        if (is.function(gaxOptions)) {
            callback = gaxOptions;
            gaxOptions = {};
        }
        // increment('column', { gaxOptions }, callback)
        if (is.object(value)) {
            callback = gaxOptions;
            gaxOptions = value;
            value = 1;
        }
        const reqOpts = {
            column,
            increment: value,
        };
        this.createRules(reqOpts, gaxOptions, (err, resp) => {
            if (err) {
                callback(err, null, resp);
                return;
            }
            const data = Row.formatFamilies_(resp.row.families);
            const value = dotProp.get(data, column.replace(':', '.'))[0].value;
            callback(null, value, resp);
        });
    }
    /**
     * Update the row cells.
     *
     * @param {object} key An entry object to be inserted into the row. See
     *     {@link Table#insert}.
     * @param {object} [gaxOptions] Request configuration options, outlined here:
     *     https://googleapis.github.io/gax-nodejs/CallSettings.html.
     * @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 <caption>include:samples/document-snippets/row.js</caption>
     * region_tag:bigtable_row_save
     */
    save(entry, gaxOptions, callback) {
        if (is.fn(gaxOptions)) {
            callback = gaxOptions;
            gaxOptions = {};
        }
        const mutation = {
            key: this.id,
            data: entry,
            method: mutation_1.Mutation.methods.INSERT,
        };
        this.data = {};
        this.table.mutate(mutation, gaxOptions, callback);
    }
}
exports.Row = Row;
/*! Developer Documentation
 *
 * All async methods (except for streams) will return a Promise in the event
 * that a callback is omitted.
 */
promisify_1.promisifyAll(Row);
//# sourceMappingURL=row.js.map