diff --git a/docs/components/docs/docs.js b/docs/components/docs/docs.js index 186892bffd0..f179ba847cf 100644 --- a/docs/components/docs/docs.js +++ b/docs/components/docs/docs.js @@ -181,6 +181,9 @@ angular function getMixIns($sce, $q, $http, version, baseUrl) { return function(data) { + var classMethodNames = data.map(function(method) { + return method.name; + }); var methodWithMixIns = data.filter(function(method) { return method.mixes.length > 0; })[0]; @@ -188,17 +191,18 @@ angular return data; } return $q - .all(getMixInMethods(methodWithMixIns)) + .all(getMixInMethods(classMethodNames, methodWithMixIns)) .then(combineMixInMethods(data)); }; - function getMixInMethods(method) { + function getMixInMethods(classMethodNames, method) { return method.mixes.map(function (module) { module = module.string.trim().replace('module:', ''); return $http.get(baseUrl + '/' + module + '.json') .then(filterDocJson($sce, version)) .then(function(mixInData) { return mixInData.filter(function(method) { - return !method.constructor; + return !method.constructor && + classMethodNames.indexOf(method.name) === -1; }); }); }); diff --git a/lib/datastore/dataset.js b/lib/datastore/dataset.js index 1ff57351869..0eda205da68 100644 --- a/lib/datastore/dataset.js +++ b/lib/datastore/dataset.js @@ -99,8 +99,8 @@ function Dataset(options) { scopes: SCOPES }); - this.projectId = options.projectId; this.namespace = options.namespace; + this.projectId = options.projectId; } nodeutil.inherits(Dataset, DatastoreRequest); @@ -142,6 +142,7 @@ Dataset.prototype.key = function(options) { namespace: this.namespace, path: util.arrayize(options) }; + return new entity.Key(options); }; @@ -163,6 +164,7 @@ Dataset.prototype.createQuery = function(namespace, kinds) { kinds = util.arrayize(namespace); namespace = this.namespace; } + return new Query(namespace, util.arrayize(kinds)); }; @@ -191,12 +193,14 @@ Dataset.prototype.createQuery = function(namespace, kinds) { */ Dataset.prototype.runInTransaction = function(fn, callback) { var newTransaction = this.createTransaction_(); + newTransaction.begin(function(err) { if (err) { callback(err); return; } - fn(newTransaction, newTransaction.finalize.bind(newTransaction, callback)); + + fn(newTransaction, newTransaction.commit.bind(newTransaction, callback)); }); }; diff --git a/lib/datastore/entity.js b/lib/datastore/entity.js index 7f77958bf13..f5b14fe7fe1 100644 --- a/lib/datastore/entity.js +++ b/lib/datastore/entity.js @@ -81,7 +81,8 @@ function Key(options) { }, path: { enumerable: true, - value: options.path + value: options.path, + writable: true } }); } diff --git a/lib/datastore/request.js b/lib/datastore/request.js index 30155f60e71..f38fe1e1b4a 100644 --- a/lib/datastore/request.js +++ b/lib/datastore/request.js @@ -105,41 +105,52 @@ function DatastoreRequest() {} * ], function(err, entities) {}); */ DatastoreRequest.prototype.get = function(keys, callback) { + var that = this; + var isMultipleRequest = Array.isArray(keys); keys = isMultipleRequest ? keys : [keys]; + callback = callback || util.noop; + var req = { key: keys.map(entity.keyToKeyProto) }; + this.makeReq_('lookup', req, function(err, resp) { if (err) { callback(err); return; } + var found = entity.formatArray(resp.found); + if (isMultipleRequest && resp.deferred && resp.deferred.length) { // There may be more results. Call `.get` again, and append the results. - this.get( + that.get( resp.deferred.map(entity.keyFromKeyProto), function(err, entities) { if (err) { callback(err); return; } + if (resp) { found = (found || []).concat(entities); } + callback(null, found); }); + return; } + callback(null, isMultipleRequest ? found : found[0]); - }.bind(this)); + }); }; /** - * 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). If a key is incomplete, its + * associated object is inserted and the original Key object is updated to + * contain the generated ID. * * This method will determine the correct Datastore method to execute (`upsert`, * `insert`, `update`, and `insertAutoId`) by using the key(s) provided. For @@ -163,24 +174,26 @@ DatastoreRequest.prototype.get = function(keys, callback) { * * @example * //- - * // Where you see `transaction`, assume this is the context that's relevant to - * // your use, whether that be a Dataset or a Transaction object. + * // Save a single entity. + * // + * // Notice that we are providing an incomplete key. After saving, the original + * // Key object used to save will be updated to contain the path with its + * // generated ID. * //- + * var key = dataset.key('Company'); * - * // Save a single entity. - * transaction.save({ - * key: dataset.key('Company'), + * dataset.save({ + * key: key, * data: { * rating: '10' * } - * }, function(err, key) { - * // Because we gave an incomplete key as an argument, `key` will be - * // populated with the complete, generated key. - * }); + * }, function(err) {}); * + * //- * // 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({ + * //- + * dataset.save({ * key: dataset.key('Company'), * data: [ * { @@ -189,90 +202,106 @@ DatastoreRequest.prototype.get = function(keys, callback) { * excludeFromIndexes: false * } * ] - * }, function(err, key) {}); + * }, function(err) {}); * + * //- * // Save multiple entities at once. - * transaction.save([ + * //- + * var companyKey = dataset.key(['Company', 123]); + * var productKey = dataset.key(['Product', 'Computer']); + * + * dataset.save([ * { - * key: dataset.key(['Company', 123]), + * key: companyKey, * data: { * HQ: 'Dallas, TX' * } * }, * { - * key: dataset.key(['Product', 'Computer']), + * key: productKey, * data: { * vendor: 'Dell' * } * } - * ], function(err, keys) {}); + * ], function(err) {}); */ DatastoreRequest.prototype.save = function(entities, callback) { var isMultipleRequest = Array.isArray(entities); entities = isMultipleRequest ? entities : [entities]; + var insertIndexes = []; - var keys = entities.map(function(entityObject) { - return entityObject.key; - }); + var req = { mutation: entities.reduce(function(acc, entityObject, index) { 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); } else { insertIndexes.push(index); acc.insert_auto_id.push(ent); } + return acc; - }.bind(this), { upsert: [], insert_auto_id: [] }) + }, { + upsert: [], + insert_auto_id: [] + }) }; - this.makeReq_('commit', req, function(err, resp) { + + if (this.id) { + this.requests_.push(req); + this.requestCallbacks_.push(onCommit); + return; + } else { + this.makeReq_('commit', req, onCommit); + } + + function onCommit(err, resp) { if (err || !resp) { callback(err); return; } - if (this.id) { - this.isFinalized = true; - } + var autoInserted = (resp.mutation_result.insert_auto_id_key || []); autoInserted.forEach(function(key, index) { - keys[insertIndexes[index]] = entity.keyFromKeyProto(key); + var path = entity.keyFromKeyProto(key).path; + entities[insertIndexes[index]].key.path = path; }); - callback(null, isMultipleRequest ? keys : keys[0]); - }.bind(this)); + + callback(null); + } }; /** - * Delete all entities identified with the specified key(s) in the current - * transaction. + * Delete all entities identified with the specified key(s). * * @param {Key|Key[]} key - Datastore key object(s). * @param {function} callback - The callback function. * * @example - * //- - * // Where you see `transaction`, assume this is the context that's relevant to - * // your use, whether that be a Dataset or a Transaction object. - * //- - * * // Delete a single entity. - * transaction.delete(dataset.key(['Company', 123]), function(err) {}); + * dataset.delete(dataset.key(['Company', 123]), function(err) {}); * * // Delete multiple entities at once. - * transaction.delete([ + * dataset.delete([ * dataset.key(['Company', 123]), * dataset.key(['Product', 'Computer']) * ], function(err) {}); @@ -280,18 +309,21 @@ DatastoreRequest.prototype.save = function(entities, callback) { DatastoreRequest.prototype.delete = function(keys, callback) { var isMultipleRequest = Array.isArray(keys); keys = isMultipleRequest ? keys : [keys]; + callback = callback || util.noop; + var req = { mutation: { delete: keys.map(entity.keyToKeyProto) } }; - this.makeReq_('commit', req, function(err) { - if (!err && this.id) { - this.isFinalized = true; - } - callback.apply(null, util.toArray(arguments)); - }.bind(this)); + + if (this.id) { + this.requests_.push(req); + return; + } + + this.makeReq_('commit', req, callback); }; /** @@ -434,22 +466,24 @@ DatastoreRequest.prototype.allocateIds = function(incompleteKey, n, callback) { if (entity.isKeyComplete(incompleteKey)) { throw new Error('An incomplete key should be provided.'); } + var incompleteKeys = []; for (var i = 0; i < n; i++) { incompleteKeys.push(entity.keyToKeyProto(incompleteKey)); } + var req = { key: incompleteKeys }; + this.makeReq_('allocateIds', req, function(err, resp) { if (err) { callback(err); return; } - var keys = []; - (resp.key || []).forEach(function(k) { - keys.push(entity.keyFromKeyProto(k)); - }); + + var keys = (resp.key || []).map(entity.keyFromKeyProto); + callback(null, keys); }); }; @@ -478,6 +512,7 @@ DatastoreRequest.prototype.makeReq_ = function(method, body, callback) { callback = body; body = {}; } + callback = callback || util.noop; // Set properties to indicate if we're in a transaction or not. @@ -514,6 +549,7 @@ DatastoreRequest.prototype.makeReq_ = function(method, body, callback) { callback(err); return; } + var remoteStream = https.request(authorizedReqOpts, function(resp) { var buffer = new Buffer(''); resp.on('data', function(chunk) { diff --git a/lib/datastore/transaction.js b/lib/datastore/transaction.js index b387bdd84a2..8647f74a9d0 100644 --- a/lib/datastore/transaction.js +++ b/lib/datastore/transaction.js @@ -34,6 +34,7 @@ var util = require('../common/util.js'); */ var DatastoreRequest = require('./request.js'); +var extend = require('extend'); /*! Developer Documentation * @@ -59,10 +60,18 @@ var DatastoreRequest = require('./request.js'); * }); */ function Transaction(dataset, projectId) { - this.makeAuthorizedRequest_ = dataset.makeAuthorizedRequest_; this.id = null; - this.isFinalized = false; + this.makeAuthorizedRequest_ = dataset.makeAuthorizedRequest_; this.projectId = projectId; + + // A queue for entity modifications made during the transaction. + this.modifiedEntities_ = []; + + // Queue the callbacks that process the API responses. + this.requestCallbacks_ = []; + + // Queue the requests to make when we send the transactional commit. + this.requests_ = []; } nodeutil.inherits(Transaction, DatastoreRequest); @@ -87,15 +96,20 @@ nodeutil.inherits(Transaction, DatastoreRequest); * }); */ Transaction.prototype.begin = function(callback) { + var that = this; + callback = callback || util.noop; + this.makeReq_('beginTransaction', function(err, resp) { if (err) { callback(err); return; } - this.id = resp.transaction; + + that.id = resp.transaction; + callback(null); - }.bind(this)); + }); }; /** @@ -113,11 +127,15 @@ Transaction.prototype.begin = function(callback) { * }); */ Transaction.prototype.rollback = function(callback) { + var that = this; + callback = callback || util.noop; + this.makeReq_('rollback', function(err) { - this.isFinalized = true; + that.skipCommit = true; + callback(err || null); - }.bind(this)); + }); }; /** @@ -135,37 +153,221 @@ Transaction.prototype.rollback = function(callback) { * }); */ Transaction.prototype.commit = function(callback) { + var that = this; + callback = callback || util.noop; - this.makeReq_('commit', function(err) { + + if (this.skipCommit) { + setImmediate(callback); + return; + } + + var keys = {}; + + this.modifiedEntities_ + + // Reverse the order of the queue to respect the "last queued request wins" + // behavior. + .reverse() + + // Limit the operations we're going to send through to only the most + // recently queued operations. E.g., if a user tries to save with the same + // key they just asked to be deleted, the delete request will be ignored, + // giving preference to the save operation. + .filter(function(modifiedEntity) { + var key = JSON.stringify(modifiedEntity.entity.key); + + if (!keys[key]) { + keys[key] = true; + return true; + } + }) + + // Group entities together by action (delete or save). + .sort(function(a, b) { + return a.method > b.method ? 1 : a.method < b.method ? -1 : 0; + }) + + // Group arguments together so that we only make one call to each method. + // This is important for `DatastoreRequest.save`, especially, as that method + // handles assigning auto-generated IDs to the original keys passed in. When + // we eventually execute the `save` method's API callback, having all the + // keys together is necessary to maintain order. + .reduce(function(acc, entityObject) { + var lastEntityObject = acc[acc.length - 1]; + var sameMethod = lastEntityObject && + entityObject.method === lastEntityObject.method; + + if (!lastEntityObject || !sameMethod) { + acc.push(entityObject); + } else { + lastEntityObject.args = lastEntityObject.args.concat(entityObject.args); + } + + return acc; + }, []) + + // Call each of the mutational methods (DatastoreRequest[save,delete]) to + // build up a `req` array on this instance. This will also build up a + // `callbacks` array, that is the same callback that would run if we were + // using `save` and `delete` outside of a transaction, to process the + // response from the API. + .forEach(function(modifiedEntity) { + var method = modifiedEntity.method; + var args = modifiedEntity.args.reverse(); + + DatastoreRequest.prototype[method].call(that, args, util.noop); + }); + + // Take the `req` array built previously, and merge them into one request to + // send as the final transactional commit. + var req = this.requests_.reduce(function(acc, req) { + return extend(true, acc, req); + }, {}); + + this.makeReq_('commit', req, function(err, resp) { if (err) { callback(err); return; } - this.isFinalized = true; + + // The `callbacks` array was built previously. These are the callbacks that + // handle the API response normally when using the DatastoreRequest.save and + // .delete methods. + that.requestCallbacks_.forEach(function(cb) { + cb(null, resp); + }); + callback(null); - }.bind(this)); + }); }; +/*! Developer Documentation + * + * Below, we override two methods that we inherit from DatastoreRequest: + * `delete` and `save`. This is done because: + * + * A) the documentation needs to be different for a transactional save, and + * B) we build up a "modifiedEntities_" array on this object, used to build + * the final commit request with. + */ /** - * Commit a transaction if it's not finalized yet. + * Delete all entities identified with the specified key(s) in the current + * transaction. * - * @param {function} callback - The callback function. + * @param {Key|Key[]} key - Datastore key object(s). * * @example - * transaction.begin(function(err) { - * transaction.finalize(function(err) { - * if (err) { - * // Transaction could not be finalized. + * // Delete a single entity. + * transaction.delete(dataset.key(['Company', 123]), function(err) {}); + * + * // Delete multiple entities at once. + * transaction.delete([ + * dataset.key(['Company', 123]), + * dataset.key(['Product', 'Computer']) + * ], function(err) {}); + */ +Transaction.prototype.delete = function(entities) { + var that = this; + + util.arrayize(entities).forEach(function(ent) { + that.modifiedEntities_.push({ + entity: { + key: ent + }, + method: 'delete', + args: [ent] + }); + }); +}; + +/** + * Insert or update the specified object(s) in the current transaction. If a key + * is incomplete, its associated object is inserted and the original Key object + * is updated to contain the generated ID. + * + * This method will determine the correct Datastore method to execute (`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 create a new entity and have its ID automatically assigned. If you + * provide a complete key, the entity will be updated with the data specified. + * + * 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|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`. + * + * @example + * //- + * // Save a single entity. + * // + * // Notice that we are providing an incomplete key. After the transaction is + * // committed, the Key object held by the `key` variable will be populated + * // with a path containing its generated ID. + * //- + * var key = dataset.key('Company'); + * + * transaction.save({ + * key: key, + * data: { + * rating: '10' + * } + * }); + * + * //- + * // 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: key, + * data: [ + * { + * name: 'rating', + * value: '10', + * excludeFromIndexes: false * } - * }); + * ] * }); + * + * //- + * // Save multiple entities at once. + * //- + * var companyKey = dataset.key(['Company', 123]); + * var productKey = dataset.key(['Product', 'Computer']); + * + * transaction.save([ + * { + * key: companyKey, + * data: { + * HQ: 'Dallas, TX' + * } + * }, + * { + * key: productKey, + * data: { + * vendor: 'Dell' + * } + * } + * ]); */ -Transaction.prototype.finalize = function(callback) { - if (!this.isFinalized) { - this.commit(callback); - return; - } - setImmediate(callback); +Transaction.prototype.save = function(entities) { + var that = this; + + util.arrayize(entities).forEach(function(ent) { + that.modifiedEntities_.push({ + entity: { + key: ent.key + }, + method: 'save', + args: [ent] + }); + }); }; /** diff --git a/regression/datastore.js b/regression/datastore.js index ee0c4d46c6b..d64b849a8cd 100644 --- a/regression/datastore.js +++ b/regression/datastore.js @@ -21,6 +21,7 @@ var env = require('./env.js'); var assert = require('assert'); +var async = require('async'); var datastore = require('../lib/datastore'); var ds = datastore.dataset(env); var entity = require('../lib/datastore/entity.js'); @@ -48,76 +49,72 @@ describe('datastore', function() { it('should save/get/delete with a key name', function(done) { var postKey = ds.key(['Post', 'post1']); - ds.save({ key: postKey, data: post }, function(err, key) { + ds.save({ key: postKey, data: post }, function(err) { assert.ifError(err); - assert.equal(key.path[1], 'post1'); - ds.get(key, function(err, entity) { + + ds.get(postKey, function(err, entity) { assert.ifError(err); + assert.deepEqual(entity.data, post); - ds.delete(key, function(err) { - assert.ifError(err); - done(); - }); + + ds.delete(postKey, done); }); }); }); it('should save/get/delete with a numeric key id', function(done) { var postKey = ds.key(['Post', 123456789]); - ds.save({ - key: postKey, - data: post - }, function(err, key) { + + ds.save({ key: postKey, data: post }, function(err) { assert.ifError(err); - assert.equal(key.path[1], 123456789); - ds.get(key, function(err, entity) { + + ds.get(postKey, function(err, entity) { assert.ifError(err); + assert.deepEqual(entity.data, post); - ds.delete(key, function(err) { - assert.ifError(err); - done(); - }); + + ds.delete(postKey, done); }); }); }); it('should save/get/delete a buffer', function(done) { + var postKey = ds.key('Post'); var data = { buf: new Buffer('010100000000000000000059400000000000006940', 'hex') }; - ds.save({ - key: ds.key('Post'), - data: data - }, function (err, key) { + + ds.save({ key: postKey, data: data }, function (err) { assert.ifError(err); - var assignedId = key.path[1]; + + var assignedId = postKey.path[1]; assert(assignedId); - ds.get(key, function (err, entity) { + + ds.get(postKey, function(err, entity) { assert.ifError(err); + assert.deepEqual(entity.data, data); - ds.delete(ds.key(['Post', assignedId]), function(err) { - assert.ifError(err); - done(); - }); + + ds.delete(ds.key(['Post', assignedId]), done); }); }); }); it('should save/get/delete with a generated key id', function(done) { - ds.save({ - key: ds.key('Post'), - data: post - }, function(err, key) { + var postKey = ds.key('Post'); + + ds.save({ key: postKey, data: post }, function(err) { assert.ifError(err); - var assignedId = key.path[1]; - assert(assignedId); - ds.get(ds.key(['Post', assignedId]), function(err, entity) { + + // The key's path should now be complete. + assert(postKey.path[1]); + + ds.get(postKey, function(err, entity) { assert.ifError(err); + assert.deepEqual(entity.data, post); - ds.delete(ds.key(['Post', assignedId]), function(err) { - assert.ifError(err); - done(); - }); + + ds.delete(postKey, done); }); }); }); @@ -132,30 +129,31 @@ describe('datastore', function() { wordCount: 450, rating: 4.5, }; - var key = ds.key('Post'); + var key1 = ds.key('Post'); + var key2 = ds.key('Post'); + ds.save([ - { key: key, data: post }, - { key: key, data: post2 } - ], function(err, keys) { + { key: key1, data: post }, + { key: key2, data: post2 } + ], function(err) { assert.ifError(err); - assert.equal(keys.length,2); - var firstKey = ds.key(['Post', keys[0].path[1]]); - var secondKey = ds.key(['Post', keys[1].path[1]]); + + var firstKey = ds.key(['Post', key1.path[1]]); + var secondKey = ds.key(['Post', key2.path[1]]); + ds.get([firstKey, secondKey], function(err, entities) { assert.ifError(err); + assert.equal(entities.length, 2); - ds.delete([firstKey, secondKey], function(err) { - assert.ifError(err); - done(); - }); + + ds.delete([firstKey, secondKey], done); }); }); }); }); - it('should be able to save keys as a part of entity and query by key', - function(done) { + it('should save keys as a part of entity and query by key', function(done) { var personKey = ds.key(['Person', 'name']); ds.save({ key: personKey, @@ -177,7 +175,6 @@ describe('datastore', function() { }); describe('querying the datastore', function() { - var ancestor = ds.key(['Book', 'GoT']); var keys = [ @@ -428,35 +425,110 @@ describe('datastore', function() { }); describe('transactions', function() { - it('should run in a transaction', function(done) { var key = ds.key(['Company', 'Google']); var obj = { url: 'www.google.com' }; + ds.runInTransaction(function(t, tDone) { - t.get(key, function(err, entity) { + t.get(key, function(err) { assert.ifError(err); - if (entity) { - tDone(); - return; - } else { - t.save({ key: key, data: obj }, function(err) { + + t.save({ key: key, data: obj }); + tDone(); + }); + }, function(err) { + assert.ifError(err); + + ds.get(key, function(err, entity) { + assert.ifError(err); + + assert.deepEqual(entity.data, obj); + + ds.delete(key, done); + }); + }); + }); + + it('should commit all saves and deletes at the end', function(done) { + var deleteKey = ds.key(['Company', 'Subway']); + var key = ds.key(['Company', 'Google']); + var incompleteKey = ds.key('Company'); + + ds.runInTransaction(function(t, tDone) { + t.delete(deleteKey); + + t.save([ + { + key: key, + data: { rating: 10 } + }, + { + key: incompleteKey, + data: { rating: 100 } + } + ]); + + tDone(); + }, function(err) { + assert.ifError(err); + + // Incomplete key should have been given an ID. + assert.equal(incompleteKey.path.length, 2); + + async.parallel([ + // The key queued for deletion should have been deleted. + function(done) { + ds.get(deleteKey, function(err, entity) { + assert.ifError(err); + assert.equal(typeof entity, 'undefined'); + done(); + }); + }, + + // Data should have been updated on the key. + function(done) { + ds.get(key, function(err, entity) { assert.ifError(err); - tDone(); - return; + assert.equal(entity.data.rating, 10); + done(); }); } - }); + ], done); + }); + }); + + it('should use the last modification to a key', function(done) { + var incompleteKey = ds.key('Company'); + var key = ds.key(['Company', 'Google']); + + ds.runInTransaction(function(t, tDone) { + t.save([ + { + key: key, + data: { rating: 10 } + }, + { + key: incompleteKey, + data: { rating: 100 } + } + ]); + + t.delete(key); + + tDone(); }, function(err) { assert.ifError(err); + + // Should not return a result. ds.get(key, function(err, entity) { assert.ifError(err); - assert.deepEqual(entity.data, obj); - ds.delete(entity.key, function(err) { - assert.ifError(err); - done(); - }); + assert.strictEqual(entity, undefined); + + // Incomplete key should have been given an id. + assert.equal(incompleteKey.path.length, 2); + done(); }); }); }); diff --git a/test/datastore/dataset.js b/test/datastore/dataset.js index 24f53f24c44..e3102c073a3 100644 --- a/test/datastore/dataset.js +++ b/test/datastore/dataset.js @@ -85,7 +85,7 @@ describe('Dataset', function() { begin: function(callback) { callback(); }, - finalize: util.noop + commit: util.noop }; ds.createTransaction_ = function() { return transaction; @@ -102,7 +102,7 @@ describe('Dataset', function() { begin: function(callback) { callback(); }, - finalize: function() { + commit: function() { done(); } }; diff --git a/test/datastore/request.js b/test/datastore/request.js index 24677aff71f..4226d51a108 100644 --- a/test/datastore/request.js +++ b/test/datastore/request.js @@ -149,6 +149,43 @@ describe('Request', function() { request.save({ key: key, data: {} }, done); }); + it('should set the ID on incomplete key objects', function(done) { + var key = new entity.Key({ namespace: 'ns', path: ['Company'] }); + var id = 50714372; + + var mockCommitResponse = { + mutation_result: { + insert_auto_id_key: [ + { + partition_id: { + dataset_id: 's~project-id', + namespace: 'ns' + }, + path_element: [ + { + kind: 'Company', + id: id, + name: null + } + ] + } + ] + } + }; + + request.makeReq_ = function(method, req, callback) { + callback(null, mockCommitResponse); + }; + + request.save({ key: key, data: {} }, function(err) { + assert.ifError(err); + + assert.equal(key.path[1], id); + + done(); + }); + }); + it('should save with keys', function(done) { request.makeReq_ = function(method, req, callback) { assert.equal(method, 'commit'); @@ -164,61 +201,50 @@ describe('Request', function() { ], done); }); + it('should not set an indexed value by default', function(done) { + request.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, undefined); + done(); + }; + request.save({ + key: key, + data: [{ name: 'name', value: 'value' }] + }, assert.ifError); + }); + + it('should allow setting the indexed value of property', function(done) { + request.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); + done(); + }; + request.save({ + key: key, + data: [{ name: 'name', value: 'value', excludeFromIndexes: true }] + }, assert.ifError); + }); + describe('transactions', function() { beforeEach(function() { // Trigger transaction mode. request.id = 'transaction-id'; + request.requestCallbacks_ = []; + request.requests_ = []; }); - it('should mark transaction as finalized', function(done) { - assert.strictEqual(request.isFinalized, undefined); - request.makeReq_ = function(method, req, callback) { - callback(null, { mutation_result: {} }); - }; - request.save({ key: key, data: {} }, function(err) { - assert.ifError(err); - assert.strictEqual(request.isFinalized, true); - done(); - }); - }); - - it('should not mark as finalized if an error occurred', function(done) { - assert.strictEqual(request.isFinalized, undefined); - request.makeReq_ = function(method, req, callback) { - callback(new Error('Error.')); - }; - request.save({ key: key, data: {} }, function() { - assert.strictEqual(request.isFinalized, undefined); - done(); - }); - }); - - it('should not set an indexed value by default', function(done) { - request.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, undefined); - done(); - }; + it('should queue request & callback', function() { request.save({ key: key, data: [{ name: 'name', value: 'value' }] - }, assert.ifError); - }); + }); - it('should allow setting the indexed value of property', function(done) { - request.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); - done(); - }; - request.save({ - key: key, - data: [{ name: 'name', value: 'value', excludeFromIndexes: true }] - }, assert.ifError); + assert.equal(typeof request.requestCallbacks_[0], 'function'); + assert.equal(typeof request.requests_[0], 'object'); }); }); }); @@ -246,29 +272,13 @@ describe('Request', function() { beforeEach(function() { // Trigger transaction mode. request.id = 'transaction-id'; + request.requests_ = []; }); - it('should mark transaction as finalized', function(done) { - assert.strictEqual(request.isFinalized, undefined); - request.makeReq_ = function(method, req, callback) { - callback(null, { mutation_result: {} }); - }; - request.delete(key, function(err) { - assert.ifError(err); - assert.strictEqual(request.isFinalized, true); - done(); - }); - }); + it('should queue request', function() { + request.delete(key); - it('should not mark as finalized if an error occurred', function(done) { - assert.strictEqual(request.isFinalized, undefined); - request.makeReq_ = function(method, req, callback) { - callback(new Error('Error.')); - }; - request.delete(key, function() { - assert.strictEqual(request.isFinalized, undefined); - done(); - }); + assert.equal(typeof request.requests_[0].mutation.delete, 'object'); }); }); }); diff --git a/test/datastore/transaction.js b/test/datastore/transaction.js index cde3a33eaa0..76ef2bad16b 100644 --- a/test/datastore/transaction.js +++ b/test/datastore/transaction.js @@ -19,12 +19,49 @@ 'use strict'; var assert = require('assert'); +var entity = require('../../lib/datastore/entity.js'); +var extend = require('extend'); var Transaction = require('../../lib/datastore/transaction.js'); +var util = require('../../lib/common/util.js'); + +var DatastoreRequest_Override = { + delete: util.noop, + save: util.noop +}; + +var FakeDatastoreRequest = { + prototype: { + delete: function() { + var args = [].slice.apply(arguments); + var results = DatastoreRequest_Override.delete.apply(null, args); + DatastoreRequest_Override.delete = util.noop; + return results; + }, + + save: function() { + var args = [].slice.apply(arguments); + var results = DatastoreRequest_Override.save.apply(null, args); + DatastoreRequest_Override.save = util.noop; + return results; + } + } +}; + +var Transaction = require('sandboxed-module') + .require('../../lib/datastore/transaction.js', { + requires: { + './request.js': FakeDatastoreRequest + } + }); describe('Transaction', function() { var transaction; var TRANSACTION_ID = 'transaction-id'; + function key(path) { + return new entity.Key({ path: util.arrayize(path) }); + } + beforeEach(function() { transaction = new Transaction({ authorizeReq_: function(req, callback) { @@ -93,24 +130,24 @@ describe('Transaction', function() { }); }); - it('should mark as finalized', function(done) { + it('should set skipCommit', function(done) { transaction.makeReq_ = function(method, req, callback) { callback = callback || req; callback(); }; transaction.rollback(function() { - assert.strictEqual(transaction.isFinalized, true); + assert.strictEqual(transaction.skipCommit, true); done(); }); }); - it('should mark as finalized when rollback errors', function(done) { + it('should set skipCommit when rollback errors', function(done) { transaction.makeReq_ = function(method, req, callback) { callback = callback || req; callback(new Error('Error.')); }; transaction.rollback(function() { - assert.strictEqual(transaction.isFinalized, true); + assert.strictEqual(transaction.skipCommit, true); done(); }); }); @@ -141,41 +178,146 @@ describe('Transaction', function() { }); }); - it('should mark as finalized', function(done) { - transaction.makeReq_ = function(method, req, callback) { - callback = callback || req; - callback(); + it('should group mutations & execute original methods', function() { + var deleteArg1 = key(['Product', 123]); + var deleteArg2 = key(['Product', 234]); + + var saveArg1 = { key: key(['Product', 345]), data: '' }; + var saveArg2 = { key: key(['Product', 456]), data: '' }; + + // Queue saves & deletes in varying order. + transaction.delete(deleteArg1); + transaction.save(saveArg1); + transaction.delete(deleteArg2); + transaction.save(saveArg2); + + var args = []; + + var deleteCalled = 0; + DatastoreRequest_Override.delete = function() { + args.push(arguments[0]); + deleteCalled++; }; - transaction.commit(function() { - assert.strictEqual(transaction.isFinalized, true); - done(); - }); + + var saveCalled = 0; + DatastoreRequest_Override.save = function() { + args.push(arguments[0]); + saveCalled++; + }; + + transaction.makeReq_ = util.noop; + + transaction.commit(); + + assert.equal(deleteCalled, 1); + assert.equal(saveCalled, 1); + + assert.equal(args.length, 2); + assert.deepEqual(args, [ + [deleteArg1, deleteArg2], + [saveArg1, saveArg2] + ]); }); - it('should not mark as finalized if commit errors', function(done) { - transaction.makeReq_ = function(method, req, callback) { - callback = callback || req; - callback(new Error('Error.')); + it('should honor ordering of mutations (last wins)', function() { + // The delete should be ignored. + transaction.delete(key(['Product', 123])); + transaction.save({ key: key(['Product', 123]), data: '' }); + + var deleteCalled = 0; + DatastoreRequest_Override.delete = function() { + deleteCalled++; }; - transaction.commit(function() { - assert.strictEqual(transaction.isFinalized, false); - done(); - }); + + var saveCalled = 0; + DatastoreRequest_Override.save = function() { + saveCalled++; + }; + + transaction.makeReq_ = util.noop; + + transaction.commit(); + assert.equal(deleteCalled, 0); + assert.equal(saveCalled, 1); }); - }); - describe('finalize', function() { - it('should be committed if not finalized', function(done) { - transaction.isFinalized = false; - transaction.commit = function () { + it('should send the built request object', function(done) { + transaction.requests_ = [ + { a: 'b', c: 'd' }, + { e: 'f', g: 'h' } + ]; + + transaction.makeReq_ = function(method, req) { + var req1 = transaction.requests_[0]; + var req2 = transaction.requests_[1]; + assert.deepEqual(req, extend(req1, req2)); done(); }; - transaction.finalize(); + + transaction.commit(); }); - it('should execute callback if already finalized', function(done) { - transaction.isFinalized = true; - transaction.finalize(done); + it('should execute the queued callbacks', function() { + var cb1Called = false; + var cb2Called = false; + + transaction.requestCallbacks_ = [ + function() { cb1Called = true; }, + function() { cb2Called = true; } + ]; + + transaction.makeReq_ = function(method, req, cb) { + cb(); + }; + + transaction.commit(); + + assert(cb1Called); + assert(cb2Called); + }); + }); + + describe('delete', function() { + it('should push entities into a queue', function() { + var keys = [ + key('Product', 123), + key('Product', 234), + key('Product', 345) + ]; + + transaction.delete(keys); + + assert.equal(transaction.modifiedEntities_.length, keys.length); + + transaction.modifiedEntities_.forEach(function (queuedEntity) { + assert.equal(queuedEntity.method, 'delete'); + assert(keys.indexOf(queuedEntity.entity.key) > -1); + assert.deepEqual(queuedEntity.args, [queuedEntity.entity.key]); + }); + }); + }); + + describe('save', function() { + it('should push entities into a queue', function() { + var entities = [ + { key: key('Product', 123), data: 123 }, + { key: key('Product', 234), data: 234 }, + { key: key('Product', 345), data: 345 } + ]; + + transaction.save(entities); + + assert.equal(transaction.modifiedEntities_.length, entities.length); + + transaction.modifiedEntities_.forEach(function (queuedEntity) { + assert.equal(queuedEntity.method, 'save'); + + var match = entities.filter(function(ent) { + return ent.key === queuedEntity.entity.key; + })[0]; + + assert.deepEqual(queuedEntity.args, [match]); + }); }); }); });