diff --git a/datastore/concepts.js b/datastore/concepts.js new file mode 100644 index 00000000000..3d0223625a6 --- /dev/null +++ b/datastore/concepts.js @@ -0,0 +1,1329 @@ +// Copyright 2015, Google, Inc. +// 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 asyncUtil = require('async'); +var gcloud = require('gcloud'); + +module.exports = { + Entity: Entity, + Index: Index, + Metadata: Metadata, + Query: Query, + Transaction: Transaction +}; + +// This mock is used in the documentation snippets. +var datastore = { + delete: function() {}, + get: function() {}, + insert: function() {}, + key: function() {}, + update: function() {}, + upsert: function() {}, + runQuery: function() {}, + save: function() {} +}; + +var keyFile = process.env.DATASTORE_KEYFILE || + process.env.GOOGLE_APPLICATION_CREDENTIALS; + +function Entity(projectId) { + var options = { + projectId: projectId + }; + + if (keyFile) { + options.keyFilename = keyFile; + } + this.datastore = gcloud.datastore(options); + + // To create the keys, we have to use this instance of Datastore. + datastore.key = this.datastore.key; + + this.incompleteKey = this.getIncompleteKey(); + this.namedKey = this.getNamedKey(); + this.keyWithParent = this.getKeyWithParent(); + this.keyWithMultiLevelParent = this.getKeyWithMultiLevelParent(); +} + +Entity.prototype.getIncompleteKey = function() { + // [START incomplete_key] + var taskKey = datastore.key('Task'); + // [END incomplete_key] + + return taskKey; +}; + +Entity.prototype.getNamedKey = function() { + // [START named_key] + var taskKey = datastore.key([ + 'Task', + 'sampletask' + ]); + // [END named_key] + + return taskKey; +}; + +Entity.prototype.getKeyWithParent = function() { + // [START key_with_parent] + var taskKey = datastore.key([ + 'TaskList', + 'default', + 'Task', + 'sampleTask' + ]); + // [END key_with_parent] + + return taskKey; +}; + +Entity.prototype.getKeyWithMultiLevelParent = function() { + // [START key_with_multilevel_parent] + var taskKey = datastore.key([ + 'User', + 'alice', + 'TaskList', + 'default', + 'Task', + 'sampleTask' + ]); + // [END key_with_multilevel_parent] + + return taskKey; +}; + +Entity.prototype.getTask = function() { + // [START basic_entity] + var task = { + type: 'Personal', + done: false, + priority: 4, + description: 'Learn Cloud Datastore' + }; + // [END basic_entity] + + return task; +}; + +Entity.prototype.testIncompleteKey = function(callback) { + this.datastore.save({ + key: this.incompleteKey, + data: {} + }, callback); +}; + +Entity.prototype.testNamedKey = function(callback) { + this.datastore.save({ + key: this.namedKey, + data: {} + }, callback); +}; + +Entity.prototype.testKeyWithParent = function(callback) { + this.datastore.save({ + key: this.keyWithParent, + data: {} + }, callback); +}; + +Entity.prototype.testKeyWithMultiLevelParent = function(callback) { + this.datastore.save({ + key: this.keyWithMultiLevelParent, + data: {} + }, callback); +}; + +Entity.prototype.testEntityWithParent = function(callback) { + var taskKey = this.keyWithParent; + + // [START entity_with_parent] + var task = { + key: taskKey, + data: { + type: 'Personal', + done: false, + priority: 4, + description: 'Learn Cloud Datastore' + } + }; + // [END entity_with_parent] + + this.datastore.save(task, callback); +}; + +Entity.prototype.testProperties = function(callback) { + // jshint camelcase:false + // [START properties] + var task = { + type: 'Personal', + created: new Date(), + done: false, + priority: 4, + percent_complete: 10.0, + description: 'Learn Cloud Datastore' + }; + // [END properties] + + this.datastore.save({ + key: this.incompleteKey, + data: task + }, callback); +}; + +Entity.prototype.testArrayValue = function(callback) { + // [START array_value] + var task = { + tags: [ + 'fun', + 'programming' + ], + collaborators: [ + 'alice', + 'bob' + ] + }; + // [END array_value] + + this.datastore.save({ + key: this.incompleteKey, + data: task + }, callback); +}; + +Entity.prototype.testBasicEntity = function(callback) { + this.datastore.save({ + key: this.getIncompleteKey(), + data: this.getTask() + }, callback); +}; + +Entity.prototype.testUpsert = function(callback) { + var taskKey = this.getIncompleteKey(); + var task = this.getTask(); + + // [START upsert] + datastore.upsert({ + key: taskKey, + data: task + }, function(err) { + if (!err) { + // Task inserted successfully. + } + }); + // [END upsert] + + this.datastore.upsert({ + key: taskKey, + data: task + }, callback); +}; + +Entity.prototype.testInsert = function(callback) { + var taskKey = this.getIncompleteKey(); + var task = this.getTask(); + + // [START insert] + datastore.insert({ + key: taskKey, + data: task + }, function(err) { + if (!err) { + // Task inserted successfully. + } + }); + // [END insert] + + this.datastore.insert({ + key: taskKey, + data: task + }, callback); +}; + +Entity.prototype.testLookup = function(callback) { + var self = this; + var taskKey = this.getIncompleteKey(); + + // jshint unused:false + // [START lookup] + datastore.get(taskKey, function(err, entity) { + if (!err) { + // Task found. + + // entity.data = { + // type: 'Personal', + // done: false, + // priority: 4, + // description: 'Learn Cloud Datastore' + // }; + } + }); + // [END lookup] + + this.datastore.insert({ + key: taskKey, + data: {} + }, function(err) { + if (err) { + callback(err); + return; + } + + self.datastore.get(taskKey, callback); + }); +}; + +Entity.prototype.testUpdate = function(callback) { + var self = this; + var taskKey = this.getIncompleteKey(); + var task = this.getTask(); + + // [START update] + datastore.update({ + key: taskKey, + data: task + }, function(err) { + if (!err) { + // Task updated successfully. + } + }); + // [END update] + + this.datastore.insert({ + key: taskKey, + data: {} + }, function(err) { + if (err) { + callback(err); + return; + } + + self.datastore.update({ + key: taskKey, + data: task + }, callback); + }); +}; + +Entity.prototype.testDelete = function(callback) { + var self = this; + var taskKey = this.getIncompleteKey(); + + // [START delete] + datastore.delete(taskKey, function(err) { + if (!err) { + // Task deleted successfully. + } + }); + // [END delete] + + this.datastore.insert({ + key: taskKey, + data: {} + }, function(err) { + if (err) { + callback(err); + return; + } + + self.datastore.delete(taskKey, callback); + }); +}; + +Entity.prototype.testBatchUpsert = function(callback) { + datastore.key = this.datastore.key; + + var taskKey1 = datastore.key(['Task', 1]); + var taskKey2 = datastore.key(['Task', 2]); + + var task1 = { + type: 'Personal', + done: false, + priority: 4, + description: 'Learn Cloud Datastore' + }; + + var task2 = { + type: 'Work', + done: false, + priority: 8, + description: 'Integrate Cloud Datastore' + }; + + // [START batch_upsert] + datastore.upsert([ + { + key: taskKey1, + data: task1 + }, + { + key: taskKey2, + data: task2 + } + ], function(err) { + if (!err) { + // Tasks inserted successfully. + } + }); + // [END batch_upsert] + + this.datastore.upsert([ + { + key: taskKey1, + data: task1 + }, + { + key: taskKey2, + data: task2 + } + ], callback); +}; + +Entity.prototype.testBatchLookup = function(callback) { + var taskKey1 = this.datastore.key(['Task', 1]); + var taskKey2 = this.datastore.key(['Task', 2]); + + // jshint unused:false + // [START batch_lookup] + datastore.get([ + taskKey1, + taskKey2 + ], function(err, tasks) { + if (!err) { + // Tasks retrieved successfully. + } + }); + // [END batch_lookup] + + this.datastore.get([ + taskKey1, + taskKey2 + ], callback); +}; + +Entity.prototype.testBatchDelete = function(callback) { + var taskKey1 = this.datastore.key(['Task', 1]); + var taskKey2 = this.datastore.key(['Task', 2]); + + // [START batch_delete] + datastore.delete([ + taskKey1, + taskKey2 + ], function(err) { + if (!err) { + // Tasks deleted successfully. + } + }); + // [END batch_delete] + + this.datastore.delete([ + taskKey1, + taskKey2 + ], callback); +}; + +function Index(projectId) { + var options = { + projectId: projectId + }; + + if (keyFile) { + options.keyFilename = keyFile; + } + this.datastore = gcloud.datastore(options); +} + +Index.prototype.testUnindexedPropertyQuery = function(callback) { + var datastore = this.datastore; + + // [START unindexed_property_query] + var query = datastore.createQuery('Task') + .filter('description =', 'A task description.'); + // [END unindexed_property_query] + + this.datastore.runQuery(query, callback); +}; + +Index.prototype.testExplodingProperties = function(callback) { + datastore.key = this.datastore.key; + + // [START exploding_properties] + var task = { + key: datastore.key('Task'), + data: { + tags: [ + 'fun', + 'programming', + 'learn' + ], + collaborators: [ + 'alice', + 'bob', + 'charlie' + ], + created: new Date() + } + }; + // [END exploding_properties] + + delete datastore.key; + + this.datastore.insert(task, callback); +}; + +function Metadata(projectId) { + var options = { + projectId: projectId + }; + + if (keyFile) { + options.keyFilename = keyFile; + } + this.datastore = gcloud.datastore(options); +} + +Metadata.prototype.testNamespaceRunQuery = function(callback) { + var self = this; + + datastore.createQuery = this.datastore.createQuery; + datastore.key = this.datastore.key; + + var startNamespace = 'Animals'; + var endNamespace = 'Zoos'; + + this.datastore.save([ + { + key: datastore.key({ + namespace: 'Animals', + path: ['Ant', 1] + }), + data: {} + } + ], function(err) { + if (err) { + callback(err); + return; + } + + // jshint unused:false + // [START namespace_run_query] + var query = datastore.createQuery('__namespace__') + .select('__key__') + .filter('__key__ >=', datastore.key(['__namespace__', startNamespace])) + .filter('__key__ <', datastore.key(['__namespace__', endNamespace])); + + datastore.runQuery(query, function(err, entities) { + if (err) { + // An error occurred while running the query. + return; + } + + var namespaces = entities.map(function(entity) { + return entity.key.path.pop(); + }); + }); + // [END namespace_run_query] + + self.datastore.runQuery(query, callback); + }); +}; + +Metadata.prototype.testKindRunQuery = function(callback) { + datastore.createQuery = this.datastore.createQuery; + + // jshint unused:false + // [START kind_run_query] + var query = datastore.createQuery('__kind__') + .select('__key__'); + + datastore.runQuery(query, function(err, entities) { + if (err) { + // An error occurred while running the query. + return; + } + + var kinds = entities.map(function(entity) { + return entity.key.path.pop(); + }); + }); + // [END kind_run_query] + + this.datastore.runQuery(query, callback); +}; + +Metadata.prototype.testPropertyRunQuery = function(callback) { + datastore.createQuery = this.datastore.createQuery; + + // [START property_run_query] + var query = datastore.createQuery('__property__') + .select('__key__'); + + datastore.runQuery(query, function(err, entities) { + if (err) { + // An error occurred while running the query. + return; + } + + var propertiesByKind = {}; + + entities.forEach(function(entity) { + var kind = entity.key.path[1]; + var propertyName = entity.key.path[3]; + + propertiesByKind[kind] = propertiesByKind[kind] || []; + propertiesByKind[kind].push(propertyName); + }); + }); + // [END property_run_query] + + this.datastore.runQuery(query, callback); +}; + +Metadata.prototype.testPropertyByKindRunQuery = function(callback) { + var datastore = this.datastore; + + // jshint camelcase:false + // [START property_by_kind_run_query] + var ancestorKey = datastore.key(['__kind__', 'Task']); + + var query = datastore.createQuery('__property__') + .hasAncestor(ancestorKey); + + datastore.runQuery(query, function(err, entities) { + if (err) { + // An error occurred while running the query. + return; + } + + var representationsByProperty = {}; + + entities.forEach(function(entity) { + var propertyName = entity.key.path.pop(); + var propertyType = entity.data.property_representation; + + representationsByProperty[propertyName] = propertyType; + }); + }); + // [END property_by_kind_run_query] + + this.datastore.runQuery(query, callback); +}; + +function Query(projectId) { + var options = { + projectId: projectId + }; + + if (keyFile) { + options.keyFilename = keyFile; + } + this.datastore = gcloud.datastore(options); + + this.basicQuery = this.getBasicQuery(); + this.projectionQuery = this.getProjectionQuery(); + this.ancestorQuery = this.getAncestorQuery(); +} + +Query.prototype.getBasicQuery = function() { + var datastore = this.datastore; + + // [START basic_query] + var query = datastore.createQuery('Task') + .filter('done =', false) + .filter('priority >=', 4) + .order('-priority'); + // [END basic_query] + + return query; +}; + +Query.prototype.getProjectionQuery = function() { + var datastore = this.datastore; + + // [START projection_query] + var query = datastore.createQuery('Task') + .select('priority') + .select('percent_complete'); + // [END projection_query] + + return query; +}; + +Query.prototype.getAncestorQuery = function() { + var datastore = this.datastore; + + // [START ancestor_query] + var ancestorKey = datastore.key(['TaskList', 'default']); + + var query = datastore.createQuery('Task') + .hasAncestor(ancestorKey); + // [END ancestor_query] + + return query; +}; + +Query.prototype.testRunQuery = function(callback) { + var query = this.basicQuery; + + // jshint unused:false + // [START run_query] + datastore.runQuery(query, function(err, tasks) { + if (!err) { + // Task entities found. + } + }); + // [END run_query] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testPropertyFilter = function(callback) { + var datastore = this.datastore; + + // [START property_filter] + var query = datastore.createQuery('Task') + .filter('done =', false); + // [END property_filter] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testCompositeFilter = function(callback) { + var datastore = this.datastore; + + // [START composite_filter] + var query = datastore.createQuery('Task') + .filter('done =', false) + .filter('priority =', 4); + // [END composite_filter] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testKeyFilter = function(callback) { + var datastore = this.datastore; + + // [START key_filter] + var query = datastore.createQuery('Task') + .filter('__key__ >', datastore.key(['Task', 'someTask'])); + // [END key_filter] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testAscendingSort = function(callback) { + var datastore = this.datastore; + + // [START ascending_sort] + var query = datastore.createQuery('Task') + .order('created'); + // [END ascending_sort] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testDescendingSort = function(callback) { + var datastore = this.datastore; + + // [START descending_sort] + var query = datastore.createQuery('Task') + .order('-created'); + // [END descending_sort] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testMultiSort = function(callback) { + var datastore = this.datastore; + + // [START multi_sort] + var query = datastore.createQuery('Task') + .order('-priority') + .order('created'); + // [END multi_sort] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testKindlessQuery = function(callback) { + var datastore = this.datastore; + var lastSeenKey = this.datastore.key(['Task', Date.now()]); + + // [START kindless_query] + var query = datastore.createQuery() + .filter('__key__ >', lastSeenKey); + // [END kindless_query] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testRunQueryProjection = function(callback) { + var self = this; + var query = this.projectionQuery; + + // Overwrite the mock to actually run the query. + datastore.runQuery = function(query, queryCallback) { + // Restore the mock. + datastore.runQuery = function() {}; + + self.datastore.runQuery(query, function(err) { + if (err) { + callback(err); + return; + } + + queryCallback.apply(null, arguments); + + if (priorities.length === 0 || percentCompletes.length === 0) { + callback(new Error('Projection lists did not build up.')); + } else { + callback(); + } + }); + }; + + // jshint unused:false, camelcase:false + // [START run_query_projection] + var priorities = []; + var percentCompletes = []; + + datastore.runQuery(query, function(err, tasks) { + if (err) { + // An error occurred while running the query. + return; + } + + tasks.forEach(function(task) { + priorities.push(task.data.priority); + percentCompletes.push(task.data.percent_complete); + }); + }); + // [END run_query_projection] +}; + +Query.prototype.testKeysOnlyQuery = function(callback) { + var datastore = this.datastore; + + // [START keys_only_query] + var query = datastore.createQuery() + .select('__key__'); + // [END keys_only_query] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testDistinctQuery = function(callback) { + var datastore = this.datastore; + + // [START distinct_query] + var query = datastore.createQuery('Task') + .groupBy(['type', 'priority']) + .order('type') + .order('priority'); + // [END distinct_query] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testDistinctOnQuery = function(callback) { + var datastore = this.datastore; + + // [START distinct_on_query] + var query = datastore.createQuery('Task') + .groupBy('type') + .order('type') + .order('priority'); + // [END distinct_on_query] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testArrayValueInequalityRange = function(callback) { + var datastore = this.datastore; + + // [START array_value_inequality_range] + var query = datastore.createQuery('Task') + .filter('tag >', 'learn') + .filter('tag <', 'math'); + // [END array_value_inequality_range] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testArrayValueEquality = function(callback) { + var datastore = this.datastore; + + // [START array_value_equality] + var query = datastore.createQuery('Task') + .filter('tag =', 'fun') + .filter('tag =', 'programming'); + // [END array_value_equality] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testInequalityRange = function(callback) { + var datastore = this.datastore; + + // [START inequality_range] + var query = datastore.createQuery('Task') + .filter('created >', new Date('1990-01-01T00:00:00z')) + .filter('created <', new Date('2000-12-31T23:59:59z')); + // [END inequality_range] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testInequalityInvalid = function(callback) { + var datastore = this.datastore; + + // [START inequality_invalid] + var query = datastore.createQuery('Task') + .filter('priority >', 3) + .filter('created >', new Date('1990-01-01T00:00:00z')); + // [END inequality_invalid] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testEqualAndInequalityRange = function(callback) { + var datastore = this.datastore; + + // [START equal_and_inequality_range] + var query = datastore.createQuery('Task') + .filter('priority =', 4) + .filter('done =', false) + .filter('created >', new Date('1990-01-01T00:00:00z')) + .filter('created <', new Date('2000-12-31T23:59:59z')); + // [END equal_and_inequality_range] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testInequalitySort = function(callback) { + var datastore = this.datastore; + + // [START inequality_sort] + var query = datastore.createQuery('Task') + .filter('priority >', 3) + .order('priority') + .order('created'); + // [END inequality_sort] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testInequalitySortInvalidNotSame = function(callback) { + var datastore = this.datastore; + + // [START inequality_sort_invalid_not_same] + var query = datastore.createQuery('Task') + .filter('priority >', 3) + .order('created'); + // [END inequality_sort_invalid_not_same] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testInequalitySortInvalidNotFirst = function(callback) { + var datastore = this.datastore; + + // [START inequality_sort_invalid_not_first] + var query = datastore.createQuery('Task') + .filter('priority >', 3) + .order('created') + .order('priority'); + // [END inequality_sort_invalid_not_first] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testLimit = function(callback) { + var datastore = this.datastore; + + // [START limit] + var query = datastore.createQuery('Task') + .limit(5); + // [END limit] + + this.datastore.runQuery(query, callback); +}; + +Query.prototype.testCursorPaging = function(callback) { + var pageSize = 1; + var pageCursor = ''; + + datastore.createQuery = this.datastore.createQuery; + + // [START cursor_paging] + // By default, gcloud-node will paginate through all of the results that match + // a query, push them into an array, then return them to your callback after + // they have all been retrieved. You must execute `.autoPaginate(false)` on + // your query to disable this behavior. + var query = datastore.createQuery('Task') + .autoPaginate(false) + .limit(pageSize) + .start(pageCursor); + + datastore.runQuery(query, function(err, results, nextQuery) { + if (err) { + // An error occurred while running the query. + return; + } + + var nextPageCursor; + + if (nextQuery) { + // If there are more results to retrieve, the start cursor is + // automatically set on `nextQuery`. To get this value directly, access + // the `startVal` property. + nextPageCursor = nextQuery.startVal; + } else { + // No more results exist. + } + }); + // [END cursor_paging] + + delete datastore.createQuery; + this.datastore.runQuery(query, function(err, results, nextQuery) { + if (err) { + callback(err); + return; + } + + if (!nextQuery || !nextQuery.startVal) { + callback(new Error('A nextQuery with a startVal is not present.')); + } else { + callback(); + } + }); +}; + +Query.prototype.testEventualConsistentQuery = function() { + // [START eventual_consistent_query] + // Read consistency cannot be specified in gcloud-node. + // [END eventual_consistent_query] +}; + +// [START transactional_update] +function transferFunds(fromKey, toKey, amount, callback) { + var error; + + datastore.runInTransaction(function(transaction, done) { + transaction.get([ + fromKey, + toKey + ], function(err, accounts) { + if (err) { + // An error occurred while getting the values. + error = err; + transaction.rollback(done); + return; + } + + accounts[0].data.balance -= amount; + accounts[1].data.balance += amount; + + transaction.save(accounts); + + done(); + }); + }, function(transactionError) { + if (transactionError || error) { + callback(transactionError || error); + } else { + // The transaction completed successfully. + callback(); + } + }); +} +// [END transactional_update] + +function Transaction(projectId) { + var options = { + projectId: projectId + }; + + if (keyFile) { + options.keyFilename = keyFile; + } + this.datastore = gcloud.datastore(options); + + this.fromKey = this.datastore.key(['Bank', 1, 'Account', 1]); + this.toKey = this.datastore.key(['Bank', 1, 'Account', 2]); + + this.originalBalance = 100; + this.amountToTransfer = 10; +} + +Transaction.prototype.restoreBankAccountBalances = function(config, callback) { + var saveArray = config.keys.map(function(key) { + return { + key: key, + data: { + balance: config.balance + } + }; + }); + + this.datastore.save(saveArray, callback); +}; + +Transaction.prototype.testTransactionalUpdate = function(callback) { + var self = this; + + var fromKey = this.fromKey; + var toKey = this.toKey; + var originalBalance = this.originalBalance; + var amountToTransfer = this.amountToTransfer; + + this.restoreBankAccountBalances({ + keys: [fromKey, toKey], + balance: originalBalance + }, function(err) { + if (err) { + callback(err); + return; + } + + // Overwrite so the real Datastore instance is used in `transferFunds`. + var datastoreMock = datastore; + datastore = self.datastore; + + transferFunds(fromKey, toKey, amountToTransfer, function(err) { + // Restore `datastore` to the mock API. + datastore = datastoreMock; + + if (err) { + callback(err); + return; + } + + self.datastore.get([ + fromKey, + toKey + ], function(err, accounts) { + if (err) { + callback(err); + return; + } + + var transactionWasSuccessful = + accounts[0].data.balance === originalBalance - amountToTransfer && + accounts[1].data.balance === originalBalance + amountToTransfer; + + if (!transactionWasSuccessful) { + callback(new Error('Accounts were not updated successfully.')); + } else { + callback(); + } + }); + }); + }); +}; + +Transaction.prototype.testTransactionalRetry = function(callback) { + // Overwrite so the real Datastore instance is used in `transferFunds`. + var datastoreMock = datastore; + datastore = this.datastore; + + var originalCallback = callback; + callback = function() { + // Restore `datastore` to the mock API. + datastore = datastoreMock; + originalCallback.apply(null, arguments); + }; + + var fromKey = this.fromKey; + var toKey = this.toKey; + + this.restoreBankAccountBalances({ + keys: [fromKey, toKey], + balance: this.originalBalance + }, function(err) { + if (err) { + callback(err); + return; + } + + // [START transactional_retry] + var async = require('async'); + + function attemptTransfer(callback) { + transferFunds(fromKey, toKey, 10, callback); + } + + async.retry(5, attemptTransfer, callback); + // [END transactional_retry] + }); +}; + +Transaction.prototype.testTransactionalGetOrCreate = function(callback) { + var taskKey = this.datastore.key(['Task', Date.now()]); + + // Overwrite so the real Datastore instance is used in `transferFunds`. + var datastoreMock = datastore; + datastore = this.datastore; + + var originalCallback = callback; + callback = function() { + // Restore `datastore` to the mock API. + datastore = datastoreMock; + originalCallback.apply(null, arguments); + }; + + // [START transactional_get_or_create] + function getOrCreate(taskKey, taskData, callback) { + var error; + + var taskEntity = { + key: taskKey, + data: taskData + }; + + datastore.runInTransaction(function(transaction, done) { + transaction.get(taskKey, function(err, task) { + if (err) { + // An error occurred while getting the values. + error = err; + transaction.rollback(done); + return; + } + + if (task) { + // The task entity already exists. + transaction.rollback(done); + } else { + // Create the task entity. + transaction.save(taskEntity); + done(); + } + }); + }, function(transactionError) { + if (transactionError || error) { + callback(transactionError || error); + } else { + // The transaction completed successfully. + callback(null, taskEntity); + } + }); + } + // [END transactional_get_or_create] + + asyncUtil.series([ + // Create: + testWithCreateBehavior, + // Then try to get it: + testWithGetBehavior + ], callback); + + function testWithCreateBehavior(callback) { + getOrCreate(taskKey, {}, function(err, task) { + if (err) { + callback(err); + return; + } + + if (!task) { + callback(new Error('Entity was not created successfully.')); + } else { + callback(); + } + }); + } + + function testWithGetBehavior(callback) { + getOrCreate(taskKey, {}, function(err, task) { + if (err) { + callback(err); + return; + } + + if (!task) { + callback(new Error('Entity was not retrieved successfully.')); + } else { + callback(); + } + }); + } +}; + +Transaction.prototype.testSingleEntityGroupReadOnly = function(callback) { + // Overwrite so the real Datastore instance is used in `transferFunds`. + var datastoreMock = datastore; + datastore = this.datastore; + + var originalCallback = callback; + callback = function() { + // Restore `datastore` to the mock API. + datastore = datastoreMock; + originalCallback.apply(null, arguments); + }; + + // [START transactional_single_entity_group_read_only] + function getTaskListEntities(callback) { + var error; + var taskListEntities; + + datastore.runInTransaction(function(transaction, done) { + var taskListKey = datastore.key(['TaskList', 'default']); + + datastore.get(taskListKey, function(err) { + if (err) { + error = err; + transaction.rollback(done); + return; + } + + var query = datastore.createQuery('Task') + .hasAncestor(taskListKey); + + datastore.runQuery(query, function(err, entities) { + if (err) { + // An error occurred while running the query. + error = err; + transaction.rollback(done); + return; + } + + taskListEntities = entities; + done(); + }); + }); + + }, function(transactionError) { + if (transactionError || error) { + callback(transactionError || error); + } else { + // The transaction completed successfully. + callback(null, taskListEntities); + } + }); + } + // [END transactional_single_entity_group_read_only] + + getTaskListEntities(function(err, entities) { + if (err) { + callback(err); + return; + } + + if (!entities) { + callback(new Error('Entities were not retrieved successfully.')); + } else { + callback(); + } + }); +}; \ No newline at end of file diff --git a/datastore/package.json b/datastore/package.json index 07b39f758ea..852e0be4146 100644 --- a/datastore/package.json +++ b/datastore/package.json @@ -9,6 +9,6 @@ }, "dependencies": { "async": "^1.5.0", - "gcloud": "^0.25.0" + "gcloud": "stephenplusplus/gcloud-node#spp--datastore-v1beta3" } } diff --git a/datastore/tasks.js b/datastore/tasks.js index 7d2c6f713c4..a06f9c4cc9f 100755 --- a/datastore/tasks.js +++ b/datastore/tasks.js @@ -33,7 +33,7 @@ if (keyFile) { options.keyFilename = keyFile; } -var datastore = gcloud.datastore.dataset(options); +var datastore = gcloud.datastore(options); // [END build_service] /* diff --git a/test/datastore/entity.test.js b/test/datastore/entity.test.js new file mode 100644 index 00000000000..7ae38a8440f --- /dev/null +++ b/test/datastore/entity.test.js @@ -0,0 +1,120 @@ +// Copyright 2015, Google, Inc. +// 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 Entity = require('../../datastore/concepts').Entity; +var entity; + +describe('datastore/concepts/entity', function () { + before(function() { + var projectId = process.env.TEST_PROJECT_ID || 'nodejs-docs-samples'; + entity = new Entity(projectId); + }); + + describe('incomplete key', function() { + it('saves with an incomplete key', function(done) { + entity.testIncompleteKey(done); + }); + }); + + describe('testNamedKey', function() { + it('saves with a named key', function(done) { + entity.testNamedKey(done); + }); + }); + + describe('testKeyWithParent', function() { + it('saves a key with a parent', function(done) { + entity.testKeyWithParent(done); + }); + }); + + describe('testKeyWithMultiLevelParent', function() { + it('saves a key with multiple parents', function(done) { + entity.testKeyWithMultiLevelParent(done); + }); + }); + + describe('testEntityWithParent', function() { + it('saves an entity with a parent', function(done) { + entity.testEntityWithParent(done); + }); + }); + + describe('testProperties', function() { + it('saves an entity with properties', function(done) { + entity.testProperties(done); + }); + }); + + describe('testArrayValue', function() { + it('saves an entity with arrays', function(done) { + entity.testArrayValue(done); + }); + }); + + describe('testBasicEntity', function() { + it('saves a basic entity', function(done) { + entity.testBasicEntity(done); + }); + }); + + describe('testUpsert', function() { + it('saves with an upsert', function(done) { + entity.testUpsert(done); + }); + }); + + describe('testInsert', function() { + it('saves with an insert', function(done) { + entity.testInsert(done); + }); + }); + + describe('testLookup', function() { + it('performs a lookup', function(done) { + entity.testLookup(done); + }); + }); + + describe('testUpdate', function() { + it('saves with an update', function(done) { + entity.testUpdate(done); + }); + }); + + describe('testDelete', function() { + it('deletes an entity', function(done) { + entity.testDelete(done); + }); + }); + + describe('testBatchUpsert', function() { + it('performs a batch upsert', function(done) { + entity.testBatchUpsert(done); + }); + }); + + describe('testBatchLookup', function() { + it('performs a batch lookup', function(done) { + entity.testBatchLookup(done); + }); + }); + + describe('testBatchDelete', function() { + it('performs a batch delete', function(done) { + entity.testBatchDelete(done); + }); + }); +}); diff --git a/test/datastore/index.yaml b/test/datastore/index.yaml new file mode 100644 index 00000000000..e4c911fa5ef --- /dev/null +++ b/test/datastore/index.yaml @@ -0,0 +1,37 @@ +indexes: +- kind: Task + properties: + - name: done + - name: priority + direction: desc +- kind: Task + properties: + - name: priority + - name: percent_complete +- kind: Task + properties: + - name: type + - name: priority +- kind: Task + properties: + - name: priority + - name: created +- kind: Task + properties: + - name: done + - name: created +- kind: Task + properties: + - name: type + - name: priority + direction: desc +- kind: Task + properties: + - name: type + - name: created +- kind: Task + properties: + - name: priority + direction: desc + - name: created + \ No newline at end of file diff --git a/test/datastore/indexes.test.js b/test/datastore/indexes.test.js new file mode 100644 index 00000000000..e208e1efbe8 --- /dev/null +++ b/test/datastore/indexes.test.js @@ -0,0 +1,38 @@ +// Copyright 2015, Google, Inc. +// 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 Index = require('../../datastore/concepts').Index; +var index; + +describe('datastore/concepts/indexes', function () { + before(function() { + var projectId = process.env.TEST_PROJECT_ID || 'nodejs-docs-samples'; + index = new Index(projectId); + }); + + describe('unindexed properties', function() { + it('performs a query with a filter on an unindexed property', + function(done) { + index.testUnindexedPropertyQuery(done); + } + ); + }); + + describe('exploding properties', function() { + it('inserts arrays of data', function(done) { + index.testExplodingProperties(done); + }); + }); +}); diff --git a/test/datastore/metadata.test.js b/test/datastore/metadata.test.js new file mode 100644 index 00000000000..182993bd5ef --- /dev/null +++ b/test/datastore/metadata.test.js @@ -0,0 +1,48 @@ +// Copyright 2015, Google, Inc. +// 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 Metadata = require('../../datastore/concepts').Metadata; +var metadata; + +describe('datastore/concepts/metadata', function () { + before(function() { + var projectId = process.env.TEST_PROJECT_ID || 'nodejs-docs-samples'; + metadata = new Metadata(projectId); + }); + + describe('namespace query', function() { + it('performs a namespace query', function(done) { + metadata.testNamespaceRunQuery(done); + }); + }); + + describe('kinds query', function() { + it('performs a kind query', function(done) { + metadata.testKindRunQuery(done); + }); + }); + + describe('property query', function() { + it('performs a property query', function(done) { + metadata.testPropertyRunQuery(done); + }); + }); + + describe('property by kind query', function() { + it('performs a property by kind query', function(done) { + metadata.testPropertyByKindRunQuery(done); + }); + }); +}); diff --git a/test/datastore/query.test.js b/test/datastore/query.test.js new file mode 100644 index 00000000000..3fbd6a4d8bc --- /dev/null +++ b/test/datastore/query.test.js @@ -0,0 +1,171 @@ +// Copyright 2015, Google, Inc. +// 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 Query = require('../../datastore/concepts').Query; +var query; + +describe('datastore/concepts/query', function () { + before(function() { + var projectId = process.env.TEST_PROJECT_ID || 'nodejs-docs-samples'; + query = new Query(projectId); + }); + + describe('basic query', function() { + it('performs a basic query', function(done) { + query.testRunQuery(done); + }); + }); + + describe('property filter', function() { + it('performs a query with a property filter', function(done) { + query.testPropertyFilter(done); + }); + }); + + describe('composite filter', function() { + it('performs a query with a composite filter', function(done) { + query.testCompositeFilter(done); + }); + }); + + describe('key filter', function() { + it('performs a query with a key filter', function(done) { + query.testKeyFilter(done); + }); + }); + + describe('ascending sort', function() { + it('performs a query with ascending sort', function(done) { + query.testAscendingSort(done); + }); + }); + + describe('descending sort', function() { + it('performs a query with descending sort', function(done) { + query.testDescendingSort(done); + }); + }); + + describe('multi sort', function() { + it('performs a query with multi sort', function(done) { + query.testMultiSort(done); + }); + }); + + describe('kindless query', function() { + it('performs a kindless query', function(done) { + query.testKindlessQuery(done); + }); + }); + + describe('projection query', function() { + it('performs a projection query', function(done) { + query.testRunQueryProjection(done); + }); + }); + + describe('keys only query', function() { + it('performs a keys only query', function(done) { + query.testKeysOnlyQuery(done); + }); + }); + + describe('distinct query', function() { + it('performs a distinct query', function(done) { + query.testDistinctQuery(done); + }); + }); + + describe('distinct on query', function() { + it('performs a distinct on query', function(done) { + query.testDistinctOnQuery(done); + }); + }); + + describe('array value inequality range', function() { + it('performs an array value inequality query', function(done) { + query.testArrayValueInequalityRange(done); + }); + }); + + describe('array value equality', function() { + it('performs an array value equality query', function(done) { + query.testArrayValueEquality(done); + }); + }); + + describe('inequality range', function() { + it('performs an inequality range query', function(done) { + query.testInequalityRange(done); + }); + }); + + describe('inequality invalid', function() { + it('returns an error from an invalid query', function(done) { + query.testInequalityInvalid(function(err) { + assert.notStrictEqual(err, null); + done(); + }); + }); + }); + + describe('equal and inequality range', function() { + it('performs an equal and inequality range query', function(done) { + query.testEqualAndInequalityRange(done); + }); + }); + + describe('inequality sort', function() { + it('performs an equality sort query', function(done) { + query.testInequalitySort(done); + }); + }); + + describe('inequality sort invalid', function() { + it('returns an error when not sorted on filtered property', function(done) { + query.testInequalitySortInvalidNotSame(function(err) { + assert.notStrictEqual(err, null); + done(); + }); + }); + + it('returns an error when not sorted on first filter prop', function(done) { + query.testInequalitySortInvalidNotFirst(function(err) { + assert.notStrictEqual(err, null); + done(); + }); + }); + }); + + describe('limit query', function() { + it('performs a query with a limit', function(done) { + query.testLimit(done); + }); + }); + + describe('cursor paging', function() { + it('allows manual pagination through results', function(done) { + query.testCursorPaging(done); + }); + }); + + describe.skip('eventually consistent query', function() { + it('performs an ancestor query', function(done) { + query.testEventualConsistentQuery(done); + }); + }); +}); diff --git a/test/datastore/transaction.test.js b/test/datastore/transaction.test.js new file mode 100644 index 00000000000..d5ed9236194 --- /dev/null +++ b/test/datastore/transaction.test.js @@ -0,0 +1,48 @@ +// Copyright 2015, Google, Inc. +// 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 Transaction = require('../../datastore/concepts').Transaction; +var transaction; + +describe('datastore/concepts/transaction', function () { + before(function() { + var projectId = process.env.TEST_PROJECT_ID || 'nodejs-docs-samples'; + transaction = new Transaction(projectId); + }); + + describe('update', function() { + it('performs a transactional update', function(done) { + transaction.testTransactionalUpdate(done); + }); + }); + + describe('retry', function() { + it('performs retries if necessary', function(done) { + transaction.testTransactionalRetry(done); + }); + }); + + describe('getOrCreate', function() { + it('performs a get or create', function(done) { + transaction.testTransactionalGetOrCreate(done); + }); + }); + + describe('single entity group read only', function() { + it('gets a snapshot of task list entities', function(done) { + transaction.testSingleEntityGroupReadOnly(done); + }); + }); +});