From 902aff90dd50d7aa099ed0cd4fbd086376368a04 Mon Sep 17 00:00:00 2001 From: Stephen Sawchuk Date: Sat, 27 Sep 2014 12:40:07 -0400 Subject: [PATCH] feat(datastore): support setting a property indexed value By default, property index values are set to `true`, without allowing the user to specify an override. Now, when a user passes in an array to `dataset.save`, they will have the option of setting `true` or `false`. Example: dataset.save({ key: dataset.key('Company'), data: [ { name: 'propertyName', value: 'any value type', excludeFromIndexes: false } ] }, function(err, keys) {}). Resolves: #208 Related: http://goo.gl/tKVvhP --- lib/datastore/dataset.js | 35 +++++++- lib/datastore/entity.js | 28 ++++--- lib/datastore/transaction.js | 45 ++++++++-- test/datastore/entity.js | 151 +++++++++++++++++++++++++++------- test/datastore/transaction.js | 112 +++++++++++++++++-------- 5 files changed, 282 insertions(+), 89 deletions(-) diff --git a/lib/datastore/dataset.js b/lib/datastore/dataset.js index 35290fc1367b..d90e7f36672a 100644 --- a/lib/datastore/dataset.js +++ b/lib/datastore/dataset.js @@ -187,15 +187,29 @@ Dataset.prototype.get = function(key, callback) { }; /** - * Insert or update the specified object(s) in the current transaction. If a - * key is incomplete, its associated object is inserted and its generated - * identifier is returned to the callback. + * Insert or update the specified object(s) in the current transaction. If a key + * is incomplete, its associated object is inserted and its generated identifier + * is returned to the callback. + * + * This method will determine the correct Datastore method to enact (`upsert`, + * `insert`, `update`, and `insertAutoId`) by using the key(s) provided. For + * example, if you provide an incomplete key (one without an ID), the request + * will automatically create a new entity and have its ID assigned. Or, if you + * provide a complete key, the data you pass with it will be used to update the + * entity. + * + * By default, all properties are indexed. To prevent a property from being + * included in *all* indexes, you must supply an entity's `data` property as an + * array. See below for an example. * * @borrows {module:datastore/transaction#save} as save * * @param {object|object[]} entities - Datastore key object(s). * @param {Key} entities.key - Datastore key object. - * @param {object} entities.data - Data to save with the provided key. + * @param {object|object[]} entities.data - Data to save with the provided key. + * If you provide an array of objects, you must use the explicit syntax: + * `name` for the name of the property and `value` for its value. You may + * also specify an `excludeFromIndexes` property, set to `true` or `false`. * @param {function} callback - The callback function. * * @example @@ -210,6 +224,19 @@ Dataset.prototype.get = function(key, callback) { * // populated with the complete, generated key. * }); * + * // To specify an `excludeFromIndexes` value for a Datastore entity, pass in + * // an array for the key's data. The above example would then look like: + * transaction.save({ + * key: dataset.key('Company'), + * data: [ + * { + * name: 'rating', + * value: '10', + * excludeFromIndexes: false + * } + * ] + * }, function(err, key) {}); + * * // Save multiple entities at once. * dataset.save([ * { diff --git a/lib/datastore/entity.js b/lib/datastore/entity.js index 931c213a40e4..e3e3c8060344 100644 --- a/lib/datastore/entity.js +++ b/lib/datastore/entity.js @@ -21,9 +21,7 @@ 'use strict'; -/** - * @type {object} - */ +/** @type {object} */ var entityMeta = {}; /** @const {regexp} Regular expression to verify a field name. */ @@ -457,6 +455,8 @@ function valueToProperty(v) { throw new Error('Unsupported field value, ' + v + ', is provided.'); } +module.exports.valueToProperty = valueToProperty; + /** * Convert an entity object to an entity protocol object. * @@ -465,19 +465,23 @@ function valueToProperty(v) { * * @example * entityToEntityProto({ - * { - * name: 'Burcu', - * legit: true - * } + * name: 'Burcu', + * legit: true * }); * // { * // key: null, - * // properties: { - * // name: { - * // stringValue: 'Burcu' + * // property: [ + * // { + * // name: 'name', + * // value: { + * // string_value: 'Burcu' + * // } * // }, - * // legit: { - * // booleanValue: true + * // { + * // name: 'legit', + * // value: { + * // boolean_value: true + * // } * // } * // } * // } diff --git a/lib/datastore/transaction.js b/lib/datastore/transaction.js index db9db825a9d2..5e31c2c76c0f 100644 --- a/lib/datastore/transaction.js +++ b/lib/datastore/transaction.js @@ -241,13 +241,23 @@ Transaction.prototype.get = function(keys, callback) { }; /** - * Insert or update the specified object(s) in the current transaction. If a - * key is incomplete, its associated object is inserted and its generated - * identifier is returned to the callback. + * Insert or update the specified object(s) in the current transaction. If a key + * is incomplete, its associated object is inserted and its generated identifier + * is returned to the callback. + * + * This method automatically handles the `upsert`, `insert`, `update`, and + * `insertAutoId` Datastore methods. + * + * By default, all properties are indexed. To prevent a property from being + * included in *all* indexes, you must supply an entity's `data` property as an + * array. See below for an example. * * @param {object|object[]} entities - Datastore key object(s). * @param {Key} entities.key - Datastore key object. - * @param {object} entities.data - Data to save with the provided key. + * @param {object|object[]} entities.data - Data to save with the provided key. + * If you provide an array of objects, you must use the explicit syntax: + * `name` for the name of the property and `value` for its value. You may + * also specify an `excludeFromIndexes` property, set to `true` or `false`. * @param {function} callback - The callback function. * * @example @@ -264,6 +274,19 @@ Transaction.prototype.get = function(keys, callback) { * // populated with the complete, generated key. * }); * + * // To specify an `excludeFromIndexes` value for a Datastore entity, pass in + * // an array for the key's data. The above example would then look like: + * transaction.save({ + * key: dataset.key('Company'), + * data: [ + * { + * name: 'rating', + * value: '10', + * excludeFromIndexes: false + * } + * ] + * }, function(err, key) {}); + * * // Save multiple entities at once. * transaction.save([ * { @@ -290,7 +313,19 @@ Transaction.prototype.save = function(entities, callback) { var req = { mode: MODE_NON_TRANSACTIONAL, mutation: entities.reduce(function(acc, entityObject, index) { - var ent = entity.entityToEntityProto(entityObject.data); + var ent = {}; + if (Array.isArray(entityObject.data)) { + ent.property = entityObject.data.map(function(data) { + data.value = entity.valueToProperty(data.value); + if (util.is(data.excludeFromIndexes, 'boolean')) { + data.value.indexed = data.excludeFromIndexes; + delete data.excludeFromIndexes; + } + return data; + }); + } else { + ent = entity.entityToEntityProto(entityObject.data); + } ent.key = entity.keyToKeyProto(entityObject.key); if (entity.isKeyComplete(entityObject.key)) { acc.upsert.push(ent); diff --git a/test/datastore/entity.js b/test/datastore/entity.js index da2e3798a290..fffed3c46063 100644 --- a/test/datastore/entity.js +++ b/test/datastore/entity.js @@ -304,39 +304,22 @@ describe('entityFromEntityProto', function() { }); describe('entityToEntityProto', function() { - it('should support bool, int, double, str, entity & list values', function() { - var now = new Date(); - var proto = entity.entityToEntityProto({ - name: 'Burcu', - desc: 'Description', - count: new entity.Int(6), - primitiveCount: 6, - legit: true, - date : now, - bytes: new Buffer('Hello'), - list: ['a', new entity.Double(54.7)], - metadata: { - key1: 'value1', - key2: 'value2' - } + it('should format an entity', function() { + var val = entity.entityToEntityProto({ + name: 'name' }); - var properties = proto.property; - assert.equal(properties[0].value.string_value, 'Burcu'); - assert.equal(properties[1].value.string_value, 'Description'); - assert.equal(properties[2].value.integer_value, 6); - assert.equal(properties[3].value.integer_value, 6); - assert.equal(properties[4].value.boolean_value, true); - assert.equal( - properties[5].value.timestamp_microseconds_value, now.getTime() * 1000); - assert.deepEqual(properties[6].value.blob_value, new Buffer('Hello')); - - var listValue = properties[7].value.list_value; - assert.equal(listValue[0].string_value, 'a'); - assert.equal(listValue[1].double_value, 54.7); - - var entityValue = properties[8].value.entity_value; - assert.equal(entityValue.property[0].value.string_value, 'value1'); - assert.equal(entityValue.property[1].value.string_value, 'value2'); + var expected = { + key: null, + property: [ + { + name: 'name', + value: { + string_value: 'name' + } + } + ] + }; + assert.deepEqual(val, expected); }); }); @@ -350,3 +333,107 @@ describe('queryToQueryProto', function() { assert.deepEqual(proto, queryFilterProto); }); }); + +describe('valueToProperty', function() { + it('should translate a boolean', function() { + var val = entity.valueToProperty(true); + assert.deepEqual(val, { + boolean_value: true + }); + }); + + it('should translate an int', function() { + var val1 = entity.valueToProperty(new entity.Int(3)); + var val2 = entity.valueToProperty(3); + var expected = { integer_value: 3 }; + assert.deepEqual(val1, expected); + assert.deepEqual(val2, expected); + }); + + it('should translate a double', function() { + var val1 = entity.valueToProperty(new entity.Double(3.1)); + var val2 = entity.valueToProperty(3.1); + var expected = { double_value: 3.1 }; + assert.deepEqual(val1, expected); + assert.deepEqual(val2, expected); + }); + + it('should translate a date', function() { + var date = new Date(); + var val = entity.valueToProperty(date); + var expected = { + timestamp_microseconds_value: date.getTime() * 1000 + }; + assert.deepEqual(val, expected); + }); + + it('should translate a string', function() { + var val = entity.valueToProperty('Hi'); + var expected = { + string_value: 'Hi' + }; + assert.deepEqual(val, expected); + }); + + it('should translate a buffer', function() { + var buffer = new Buffer('Hi'); + var val = entity.valueToProperty(buffer); + var expected = { + blob_value: buffer + }; + assert.deepEqual(val, expected); + }); + + it('should translate an array', function() { + var array = [1, '2', true]; + var val = entity.valueToProperty(array); + var expected = { + list_value: [ + { integer_value: 1 }, + { string_value: '2' }, + { boolean_value: true } + ] + }; + assert.deepEqual(val, expected); + }); + + it('should translate a Key', function() { + var key = new entity.Key({ + namespace: 'ns', + path: ['Kind', 3] + }); + var val = entity.valueToProperty(key); + var expected = { + key_value: entity.keyToKeyProto(key) + }; + assert.deepEqual(val, expected); + }); + + describe('objects', function() { + it('should translate an object', function() { + var val = entity.valueToProperty({ + name: 'value' + }); + var expected = { + entity_value: { + property: [ + { + name: 'name', + value: { + string_value: 'value', + } + } + ] + }, + indexed: false + }; + assert.deepEqual(val, expected); + }); + + it('should not translate a key-less object', function() { + assert.throws(function() { + entity.valueToProperty({}); + }, /Unsupported field value/); + }); + }); +}); diff --git a/test/datastore/transaction.js b/test/datastore/transaction.js index c273c7a1e1de..c7a4cb300467 100644 --- a/test/datastore/transaction.js +++ b/test/datastore/transaction.js @@ -20,6 +20,7 @@ var assert = require('assert'); var datastore = require('../../lib').datastore; +var Key = require('../../lib/datastore/entity').Key; describe('Transaction', function() { var ds; @@ -30,49 +31,88 @@ describe('Transaction', function() { transaction = ds.createTransaction_(null, 'test'); }); - it('should begin', function(done) { - transaction.makeReq = function(method, proto, respType, callback) { - assert.equal(method, 'beginTransaction'); - callback(null, 'some-id'); - }; - transaction.begin(done); + describe('begin', function() { + it('should begin', function(done) { + transaction.makeReq = function(method, proto, respType, callback) { + assert.equal(method, 'beginTransaction'); + callback(null, 'some-id'); + }; + transaction.begin(done); + }); }); - it('should rollback', function(done) { - transaction.id = 'some-id'; - transaction.makeReq = function(method, proto, respType, callback) { - assert.equal(method, 'rollback'); - assert.equal( - proto.transaction.toBase64(), - new Buffer('some-id').toString('base64')); - callback(); - }; - transaction.rollback(function() { - assert.equal(transaction.isFinalized, true); - done(); + describe('rollback', function() { + it('should rollback', function(done) { + transaction.id = 'some-id'; + transaction.makeReq = function(method, proto, respType, callback) { + assert.equal(method, 'rollback'); + assert.equal( + proto.transaction.toBase64(), + new Buffer('some-id').toString('base64')); + callback(); + }; + transaction.rollback(function() { + assert.equal(transaction.isFinalized, true); + done(); + }); }); }); - it('should commit', function(done) { - transaction.id = 'some-id'; - transaction.makeReq = function(method, proto, respType, callback) { - assert.equal(method, 'commit'); - assert.equal( - proto.transaction.toBase64(), - new Buffer('some-id').toString('base64')); - callback(); - }; - transaction.commit(function() { - assert.equal(transaction.isFinalized, true); - done(); + describe('commit', function() { + it('should commit', function(done) { + transaction.id = 'some-id'; + transaction.makeReq = function(method, proto, respType, callback) { + assert.equal(method, 'commit'); + assert.equal( + proto.transaction.toBase64(), + new Buffer('some-id').toString('base64')); + callback(); + }; + transaction.commit(function() { + assert.equal(transaction.isFinalized, true); + done(); + }); }); }); - it('should be committed if not rolled back', function(done) { - transaction.makeReq = function(method) { - assert.equal(method, 'commit'); - done(); - }; - transaction.finalize(); + describe('finalize', function() { + it('should be committed if not rolled back', function(done) { + transaction.makeReq = function(method) { + assert.equal(method, 'commit'); + done(); + }; + transaction.finalize(); + }); + }); + + describe('save', function() { + var key = new Key({ + namespace: null, + path: ['Kind', 1] + }); + + it('should not set an indexed value by default', function() { + transaction.makeReq = function(method, req) { + var property = req.mutation.upsert[0].property[0]; + assert.strictEqual(property.value.indexed, null); + }; + transaction.save({ + key: key, + data: [{ name: 'name', value: 'value' }] + }, assert.ifError); + }); + + it('should allow setting the indexed value of a property', function() { + transaction.makeReq = function(method, req) { + var property = req.mutation.upsert[0].property[0]; + assert.equal(property.name, 'name'); + assert.equal(property.value.string_value, 'value'); + assert.strictEqual(property.value.indexed, false); + }; + transaction.save({ + key: key, + data: [{ name: 'name', value: 'value', excludeFromIndexes: false }] + }, assert.ifError); + }); }); });