diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 40b49d2bf..638efabfb 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:abc68a9bbf4fa808b25fa16d3b11141059dc757dbc34f024744bba36c200b40f -# created: 2023-10-04T20:56:40.710775365Z + digest: sha256:e92044720ab3cb6984a70b0c6001081204375959ba3599ef6c42dd99a7783a67 +# created: 2023-11-10T00:24:05.581078808Z diff --git a/.kokoro/release/docs-devsite.sh b/.kokoro/release/docs-devsite.sh index 3596c1e4c..81a89f6c1 100755 --- a/.kokoro/release/docs-devsite.sh +++ b/.kokoro/release/docs-devsite.sh @@ -25,6 +25,6 @@ if [[ -z "$CREDENTIALS" ]]; then fi npm install -npm install --no-save @google-cloud/cloud-rad@^0.3.7 +npm install --no-save @google-cloud/cloud-rad@^0.4.0 # publish docs to devsite npx @google-cloud/cloud-rad . cloud-rad diff --git a/package.json b/package.json index 95c436c5b..221f7f8d0 100644 --- a/package.json +++ b/package.json @@ -53,12 +53,12 @@ "devDependencies": { "@google-cloud/storage": "^7.0.1", "@types/extend": "^3.0.1", - "@types/is": "0.0.24", + "@types/is": "0.0.25", "@types/js-yaml": "^4.0.0", "@types/mocha": "^9.0.0", "@types/node": "^20.4.9", "@types/proxyquire": "^1.3.28", - "@types/sinon": "^10.0.0", + "@types/sinon": "^17.0.0", "async": "^3.2.4", "c8": "^8.0.1", "gapic-tools": "^0.2.0", diff --git a/src/transaction.ts b/src/transaction.ts index 914442e4e..2ce153e07 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -161,117 +161,7 @@ class Transaction extends DatastoreRequest { : () => {}; const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {}; - - if (this.skipCommit) { - setImmediate(callback); - return; - } - - const keys: Entities = {}; - - 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((modifiedEntity: Entity) => { - const key = modifiedEntity.entity.key; - - if (!entity.isKeyComplete(key)) return true; - - const stringifiedKey = JSON.stringify(modifiedEntity.entity.key); - - if (!keys[stringifiedKey]) { - keys[stringifiedKey] = true; - return true; - } - - return false; - }) - // Group entities together by method: `save` mutations, then `delete`. - // Note: `save` mutations being first is required to maintain order when - // assigning IDs to incomplete keys. - .sort((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((acc: Entities, entityObject: Entity) => { - const lastEntityObject = acc[acc.length - 1]; - const 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( - (modifiedEntity: {method: string; args: {reverse: () => void}}) => { - const method = modifiedEntity.method; - const args = modifiedEntity.args.reverse(); - Datastore.prototype[method].call(this, args, () => {}); - } - ); - - // Take the `req` array built previously, and merge them into one request to - // send as the final transactional commit. - const reqOpts = { - mutations: this.requests_ - .map((x: {mutations: google.datastore.v1.Mutation}) => x.mutations) - .reduce( - (a: {concat: (arg0: Entity) => void}, b: Entity) => a.concat(b), - [] - ), - }; - - this.request_( - { - client: 'DatastoreClient', - method: 'commit', - reqOpts, - gaxOpts: gaxOptions || {}, - }, - (err, resp) => { - if (err) { - // Rollback automatically for the user. - this.rollback(() => { - // Provide the error & API response from the failed commit to the - // user. Even a failed rollback should be transparent. RE: - // https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1369#discussion_r66833976 - callback(err, resp); - }); - return; - } - - // The `callbacks` array was built previously. These are the callbacks - // that handle the API response normally when using the - // DatastoreRequest.save and .delete methods. - this.requestCallbacks_.forEach( - (cb: (arg0: null, arg1: Entity) => void) => { - cb(null, resp); - } - ); - callback(null, resp); - } - ); + this.#runCommit(gaxOptions, callback); } /** @@ -561,6 +451,131 @@ class Transaction extends DatastoreRequest { }); } + /** + * This function is a pass-through for the transaction.commit method + * It contains the business logic used for committing a transaction + * + * @param {object} [gaxOptions] Request configuration options, outlined here: + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions. + * @param {function} callback The callback function. + * @private + */ + #runCommit( + gaxOptions: CallOptions, + callback: CommitCallback + ): void | Promise { + if (this.skipCommit) { + setImmediate(callback); + return; + } + + const keys: Entities = {}; + + 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((modifiedEntity: Entity) => { + const key = modifiedEntity.entity.key; + + if (!entity.isKeyComplete(key)) return true; + + const stringifiedKey = JSON.stringify(modifiedEntity.entity.key); + + if (!keys[stringifiedKey]) { + keys[stringifiedKey] = true; + return true; + } + + return false; + }) + // Group entities together by method: `save` mutations, then `delete`. + // Note: `save` mutations being first is required to maintain order when + // assigning IDs to incomplete keys. + .sort((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((acc: Entities, entityObject: Entity) => { + const lastEntityObject = acc[acc.length - 1]; + const 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( + (modifiedEntity: {method: string; args: {reverse: () => void}}) => { + const method = modifiedEntity.method; + const args = modifiedEntity.args.reverse(); + Datastore.prototype[method].call(this, args, () => {}); + } + ); + + // Take the `req` array built previously, and merge them into one request to + // send as the final transactional commit. + const reqOpts = { + mutations: this.requests_ + .map((x: {mutations: google.datastore.v1.Mutation}) => x.mutations) + .reduce( + (a: {concat: (arg0: Entity) => void}, b: Entity) => a.concat(b), + [] + ), + }; + + this.request_( + { + client: 'DatastoreClient', + method: 'commit', + reqOpts, + gaxOpts: gaxOptions || {}, + }, + (err, resp) => { + if (err) { + // Rollback automatically for the user. + this.rollback(() => { + // Provide the error & API response from the failed commit to the + // user. Even a failed rollback should be transparent. RE: + // https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1369#discussion_r66833976 + callback(err, resp); + }); + return; + } + + // The `callbacks` array was built previously. These are the callbacks + // that handle the API response normally when using the + // DatastoreRequest.save and .delete methods. + this.requestCallbacks_.forEach( + (cb: (arg0: null, arg1: Entity) => void) => { + cb(null, resp); + } + ); + callback(null, resp); + } + ); + } + /** * This function parses results from a beginTransaction call * diff --git a/test/transaction.ts b/test/transaction.ts index 0535ae022..78bc0254c 100644 --- a/test/transaction.ts +++ b/test/transaction.ts @@ -26,25 +26,30 @@ import { Query, TransactionOptions, Transaction, + AggregateField, } from '../src'; -import {Entity} from '../src/entity'; +import {Entity, entity} from '../src/entity'; import * as tsTypes from '../src/transaction'; import * as sinon from 'sinon'; import {Callback, CallOptions, ClientStub} from 'google-gax'; -import {RequestConfig} from '../src/request'; +import { + CommitCallback, + GetCallback, + RequestCallback, + RequestConfig, +} from '../src/request'; import {SECOND_DATABASE_ID} from './index'; import {google} from '../protos/protos'; import {RunCallback} from '../src/transaction'; import * as protos from '../protos/protos'; +import {AggregateQuery} from '../src/aggregate'; +import {RunQueryCallback} from '../src/query'; const async = require('async'); // eslint-disable-next-line @typescript-eslint/no-explicit-any type Any = any; type Path = string | [string] | [string, number]; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const {entity} = require('../src/entity'); - let promisified = false; const fakePfy = Object.assign({}, pfy, { promisifyAll(klass: Function, options: pfy.PromisifyAllOptions) { @@ -154,10 +159,579 @@ async.each( }); }); + describe('testing various transaction functions when transaction.run returns a response', () => { + // These tests were created to ensure that various transaction functions work correctly after run is called. + // This allows us to catch any breaking changes to code usages that should remain the same. + const testRunResp = { + transaction: Buffer.from(Array.from(Array(100).keys())), + }; + + // MockedTransactionWrapper is a helper class for mocking out various + // Gapic functions and ensuring that responses and errors actually make it + // back to the user. + class MockedTransactionWrapper { + datastore: Datastore; + transaction: Transaction; + dataClient?: ClientStub; + mockedBeginTransaction: Function; + functionsMocked: {name: string; mockedFunction: Function}[]; + // The callBackSignaler lets the user of this object get a signal when the mocked function is called. + // This is useful for tests that need to know when the mocked function is called. + callBackSignaler: (callbackReached: string) => void = () => {}; + + constructor() { + const namespace = 'run-without-mock'; + const projectId = 'project-id'; + const options = { + projectId, + namespace, + }; + const datastore = new Datastore(options); + const dataClientName = 'DatastoreClient'; + // Create a fresh transaction for each test because transaction state changes after a commit. + this.transaction = datastore.transaction(); + // In this before hook, save the original beginTransaction method in a variable. + // After tests are finished, reassign beginTransaction to the variable. + // This way, mocking beginTransaction in this block doesn't affect other tests. + const gapic = Object.freeze({ + v1: require('../src/v1'), + }); + // Datastore Gapic clients haven't been initialized yet so we initialize them here. + datastore.clients_.set( + dataClientName, + new gapic.v1[dataClientName](options) + ); + const dataClient = datastore.clients_.get(dataClientName); + // Mock begin transaction + this.mockedBeginTransaction = () => {}; + if (dataClient && dataClient.beginTransaction) { + this.mockedBeginTransaction = dataClient.beginTransaction; + } + if (dataClient && dataClient.beginTransaction) { + dataClient.beginTransaction = ( + request: protos.google.datastore.v1.IBeginTransactionRequest, + options: CallOptions, + callback: Callback< + protos.google.datastore.v1.IBeginTransactionResponse, + | protos.google.datastore.v1.IBeginTransactionRequest + | null + | undefined, + {} | null | undefined + > + ) => { + // Calls a user provided function that will receive this string + // Usually used to track when this code was reached relative to other code + this.callBackSignaler('beginTransaction called'); + callback(null, testRunResp); + }; + } + this.dataClient = dataClient; + this.functionsMocked = []; + this.datastore = datastore; + } + + // This mocks out a gapic function to just call the callback received in the Gapic function. + // The callback will send back the error and response arguments provided as parameters. + mockGapicFunction( + functionName: string, + response: ResponseType, + error: Error | null + ) { + const dataClient = this.dataClient; + // Check here that function hasn't been mocked out already + // Ensures that this mocking object is not being misused. + this.functionsMocked.forEach(fn => { + if (fn.name === functionName) { + throw Error(`${functionName} has already been mocked out`); + } + }); + if (dataClient && dataClient[functionName]) { + this.functionsMocked.push({ + name: functionName, + mockedFunction: dataClient[functionName], + }); + } + if (dataClient && dataClient[functionName]) { + dataClient[functionName] = ( + request: any, // RequestType + options: CallOptions, + callback: Callback< + ResponseType, + | any // RequestType + | null + | undefined, + {} | null | undefined + > + ) => { + this.callBackSignaler(`${functionName} called`); + callback(error, response); + }; + } + } + + // This resets beginTransaction from the Gapic layer to what it originally was. + // Resetting beginTransaction ensures other tests don't use the beginTransaction mock. + resetBeginTransaction() { + if (this.dataClient && this.dataClient.beginTransaction) { + this.dataClient.beginTransaction = this.mockedBeginTransaction; + } + } + + // This resets Gapic functions mocked out by the tests to what they originally were. + // Resetting mocked out Gapic functions ensures other tests don't use these mocks. + resetGapicFunctions() { + this.functionsMocked.forEach(functionMocked => { + if (this.dataClient) { + this.dataClient[functionMocked.name] = + functionMocked.mockedFunction; + } + }); + } + } + + let transactionWrapper: MockedTransactionWrapper; + let transaction: Transaction; + + describe('commit', () => { + // These tests were created to catch regressions for transaction.commit changes. + const testCommitResp = { + mutationResults: [ + { + key: { + path: [ + { + kind: 'some-kind', + }, + ], + }, + }, + ], + }; + const testErrorMessage = 'test-commit-error'; + + beforeEach(async () => { + transactionWrapper = new MockedTransactionWrapper(); + }); + + afterEach(() => { + transactionWrapper.resetBeginTransaction(); + transactionWrapper.resetGapicFunctions(); + }); + + describe('should pass error back to the user', async () => { + beforeEach(() => { + transactionWrapper.mockGapicFunction( + 'commit', + testCommitResp, + new Error(testErrorMessage) + ); + }); + + it('should send back the error when awaiting a promise', async () => { + try { + await transactionWrapper.transaction.run(); + await transactionWrapper.transaction.commit(); + assert.fail('The run call should have failed.'); + } catch (error: any) { + assert.strictEqual(error['message'], testErrorMessage); + } + }); + it('should send back the error when using a callback', done => { + const commitCallback: CommitCallback = ( + error: Error | null | undefined, + response?: google.datastore.v1.ICommitResponse + ) => { + try { + assert(error); + assert.strictEqual(error.message, testErrorMessage); + assert.strictEqual(response, testCommitResp); + done(); + } catch (e) { + done(e); + } + }; + transactionWrapper.transaction.run(() => { + transactionWrapper.transaction.commit(commitCallback); + }); + }); + }); + describe('should pass response back to the user', async () => { + beforeEach(() => { + transactionWrapper.mockGapicFunction( + 'commit', + testCommitResp, + null + ); + }); + it('should send back the response when awaiting a promise', async () => { + await transactionWrapper.transaction.run(); + const [commitResults] = + await transactionWrapper.transaction.commit(); + assert.strictEqual(commitResults, testCommitResp); + }); + it('should send back the response when using a callback', done => { + const commitCallback: CommitCallback = ( + error: Error | null | undefined, + response?: google.datastore.v1.ICommitResponse + ) => { + try { + assert.strictEqual(error, null); + assert.strictEqual(response, testCommitResp); + done(); + } catch (e) { + done(e); + } + }; + transactionWrapper.transaction.run(() => { + transactionWrapper.transaction.commit(commitCallback); + }); + }); + }); + }); + describe('runAggregationQuery', () => { + // These tests were created to catch regressions for transaction.runAggregationQuery changes. + const runAggregationQueryUserResp = [{'average rating': 100}]; + const runAggregationQueryResp = { + batch: { + aggregationResults: [ + { + aggregateProperties: { + 'average rating': { + meaning: 0, + excludeFromIndexes: false, + doubleValue: 100, + valueType: 'doubleValue', + }, + }, + }, + ], + moreResults: + google.datastore.v1.QueryResultBatch.MoreResultsType + .NO_MORE_RESULTS, + readTime: {seconds: '1699390681', nanos: 961667000}, + }, + query: null, + transaction: testRunResp.transaction, + }; + const testErrorMessage = 'test-run-Aggregate-Query-error'; + let aggregate: AggregateQuery; + + beforeEach(async () => { + transactionWrapper = new MockedTransactionWrapper(); + transaction = transactionWrapper.transaction; + const q = transactionWrapper.datastore.createQuery('Character'); + aggregate = transactionWrapper.datastore + .createAggregationQuery(q) + .addAggregation(AggregateField.average('appearances')); + }); + + afterEach(() => { + transactionWrapper.resetBeginTransaction(); + transactionWrapper.resetGapicFunctions(); + }); + + describe('should pass error back to the user', async () => { + beforeEach(() => { + transactionWrapper.mockGapicFunction( + 'runAggregationQuery', + runAggregationQueryResp, + new Error(testErrorMessage) + ); + }); + + it('should send back the error when awaiting a promise', async () => { + try { + await transaction.run(); + await transaction.runAggregationQuery(aggregate); + assert.fail('The run call should have failed.'); + } catch (error: any) { + assert.strictEqual(error['message'], testErrorMessage); + } + }); + it('should send back the error when using a callback', done => { + const runAggregateQueryCallback: RequestCallback = ( + error: Error | null | undefined, + response?: any + ) => { + try { + assert(error); + assert.strictEqual(error.message, testErrorMessage); + assert.deepStrictEqual(response, runAggregationQueryUserResp); + done(); + } catch (e) { + done(e); + } + }; + transaction.run(() => { + transaction.runAggregationQuery( + aggregate, + runAggregateQueryCallback + ); + }); + }); + }); + describe('should pass response back to the user', async () => { + beforeEach(() => { + transactionWrapper.mockGapicFunction( + 'runAggregationQuery', + runAggregationQueryResp, + null + ); + }); + it('should send back the response when awaiting a promise', async () => { + await transaction.run(); + const allResults = + await transaction.runAggregationQuery(aggregate); + const [runAggregateQueryResults] = allResults; + assert.deepStrictEqual( + runAggregateQueryResults, + runAggregationQueryUserResp + ); + }); + it('should send back the response when using a callback', done => { + const runAggregateQueryCallback: CommitCallback = ( + error: Error | null | undefined, + response?: any + ) => { + try { + assert.strictEqual(error, null); + assert.deepStrictEqual(response, runAggregationQueryUserResp); + done(); + } catch (e) { + done(e); + } + }; + transaction.run(() => { + transaction.runAggregationQuery( + aggregate, + runAggregateQueryCallback + ); + }); + }); + }); + }); + describe('runQuery', () => { + // These tests were created to catch regressions for transaction.runQuery changes. + const runQueryResp = { + batch: { + entityResults: [], + endCursor: { + type: 'Buffer', + data: Buffer.from(Array.from(Array(100).keys())), + }, + }, + }; + const runQueryUserResp: Entity[] = []; + const testErrorMessage = 'test-run-Query-error'; + let q: Query; + + beforeEach(async () => { + transactionWrapper = new MockedTransactionWrapper(); + transaction = transactionWrapper.transaction; + q = transactionWrapper.datastore.createQuery('Character'); + }); + + afterEach(() => { + transactionWrapper.resetBeginTransaction(); + transactionWrapper.resetGapicFunctions(); + }); + + describe('should pass error back to the user', async () => { + beforeEach(() => { + transactionWrapper.mockGapicFunction( + 'runQuery', + runQueryResp, + new Error(testErrorMessage) + ); + }); + + it('should send back the error when awaiting a promise', async () => { + try { + await transaction.run(); + await transaction.runQuery(q); + assert.fail('The run call should have failed.'); + } catch (error: any) { + assert.strictEqual(error['message'], testErrorMessage); + } + }); + it('should send back the error when using a callback', done => { + const callback: RunQueryCallback = ( + error: Error | null | undefined, + response?: any + ) => { + try { + assert(error); + assert.strictEqual(error.message, testErrorMessage); + assert.deepStrictEqual(response, undefined); + done(); + } catch (e) { + done(e); + } + }; + transaction.run(() => { + transaction.runQuery(q, callback); + }); + }); + }); + describe('should pass response back to the user', async () => { + beforeEach(() => { + transactionWrapper.mockGapicFunction( + 'runQuery', + runQueryResp, + null + ); + }); + it('should send back the response when awaiting a promise', async () => { + await transaction.run(); + const allResults = await transaction.runQuery(q); + const [runAggregateQueryResults] = allResults; + assert.deepStrictEqual( + runAggregateQueryResults, + runQueryUserResp + ); + }); + it('should send back the response when using a callback', done => { + const callback: RunQueryCallback = ( + error: Error | null | undefined, + response?: any + ) => { + try { + assert.strictEqual(error, null); + assert.deepStrictEqual(response, runQueryUserResp); + done(); + } catch (e) { + done(e); + } + }; + transaction.run(() => { + transaction.runQuery(q, callback); + }); + }); + }); + }); + describe('get', () => { + // These tests were created to catch regressions for transaction.get changes. + const getResp = { + found: [ + { + entity: { + key: { + path: [ + { + kind: 'Post', + name: 'post1', + idType: 'name', + }, + ], + partitionId: { + projectId: 'projectId', + databaseId: 'databaseId', + namespaceId: 'namespaceId', + }, + }, + excludeFromIndexes: false, + properties: {}, + }, + }, + ], + missing: [], + deferred: [], + transaction: testRunResp.transaction, + readTime: { + seconds: '1699470605', + nanos: 201398000, + }, + }; + const getUserResp = 'post1'; + const testErrorMessage = 'test-run-Query-error'; + let q: Query; + let key: entity.Key; + + beforeEach(async () => { + transactionWrapper = new MockedTransactionWrapper(); + transaction = transactionWrapper.transaction; + q = transactionWrapper.datastore.createQuery('Character'); + key = transactionWrapper.datastore.key(['Company', 'Google']); + }); + + afterEach(() => { + transactionWrapper.resetBeginTransaction(); + transactionWrapper.resetGapicFunctions(); + }); + + describe('should pass error back to the user', async () => { + beforeEach(() => { + transactionWrapper.mockGapicFunction( + 'lookup', + getResp, + new Error(testErrorMessage) + ); + }); + + it('should send back the error when awaiting a promise', async () => { + try { + await transaction.run(); + await transaction.get(key); + assert.fail('The run call should have failed.'); + } catch (error: any) { + assert.strictEqual(error['message'], testErrorMessage); + } + }); + it('should send back the error when using a callback', done => { + const callback: GetCallback = ( + error: Error | null | undefined, + response?: any + ) => { + try { + assert(error); + assert.strictEqual(error.message, testErrorMessage); + assert.deepStrictEqual(response, undefined); + done(); + } catch (e) { + done(e); + } + }; + transaction.run(() => { + transaction.get(key, callback); + }); + }); + }); + describe('should pass response back to the user', async () => { + beforeEach(() => { + transactionWrapper.mockGapicFunction('lookup', getResp, null); + }); + it('should send back the response when awaiting a promise', async () => { + await transaction.run(); + const [results] = await transaction.get(key); + const result = results[transactionWrapper.datastore.KEY]; + assert.deepStrictEqual(result.name, getUserResp); + }); + it('should send back the response when using a callback', done => { + const callback: GetCallback = ( + error: Error | null | undefined, + response?: any + ) => { + try { + const result = response[transactionWrapper.datastore.KEY]; + assert.strictEqual(error, null); + assert.deepStrictEqual(result.name, getUserResp); + done(); + } catch (e) { + done(e); + } + }; + transaction.run(() => { + transaction.get(key, callback); + }); + }); + }); + }); + }); + describe('run without setting up transaction id', () => { // These tests were created so that when transaction.run is restructured we // can be confident that it works the same way as before. - const testResp = { + const testRunResp = { transaction: Buffer.from(Array.from(Array(100).keys())), }; const namespace = 'run-without-mock'; @@ -199,25 +773,29 @@ async.each( } }); + function setupBeginTransaction(err: Error | null | undefined) { + if (dataClient) { + dataClient.beginTransaction = ( + request: protos.google.datastore.v1.IBeginTransactionRequest, + options: CallOptions, + callback: Callback< + protos.google.datastore.v1.IBeginTransactionResponse, + | protos.google.datastore.v1.IBeginTransactionRequest + | null + | undefined, + {} | null | undefined + > + ) => { + callback(err, testRunResp); + }; + } + } + describe('should pass error back to the user', async () => { beforeEach(() => { // Mock out begin transaction and send error back to the user // from the Gapic layer. - if (dataClient) { - dataClient.beginTransaction = ( - request: protos.google.datastore.v1.IBeginTransactionRequest, - options: CallOptions, - callback: Callback< - protos.google.datastore.v1.IBeginTransactionResponse, - | protos.google.datastore.v1.IBeginTransactionRequest - | null - | undefined, - {} | null | undefined - > - ) => { - callback(new Error(testErrorMessage), testResp); - }; - } + setupBeginTransaction(new Error(testErrorMessage)); }); it('should send back the error when awaiting a promise', async () => { @@ -225,7 +803,6 @@ async.each( await transactionWithoutMock.run(); assert.fail('The run call should have failed.'); } catch (error: any) { - // TODO: Substitute type any assert.strictEqual(error['message'], testErrorMessage); } }); @@ -235,11 +812,15 @@ async.each( transaction: Transaction | null, response?: google.datastore.v1.IBeginTransactionResponse ) => { - assert(error); - assert.strictEqual(error.message, testErrorMessage); - assert.strictEqual(transaction, null); - assert.strictEqual(response, testResp); - done(); + try { + assert(error); + assert.strictEqual(error.message, testErrorMessage); + assert.strictEqual(transaction, null); + assert.strictEqual(response, testRunResp); + done(); + } catch (e) { + done(e); + } }; transactionWithoutMock.run({}, runCallback); }); @@ -248,26 +829,12 @@ async.each( beforeEach(() => { // Mock out begin transaction and send a response // back to the user from the Gapic layer. - if (dataClient) { - dataClient.beginTransaction = ( - request: protos.google.datastore.v1.IBeginTransactionRequest, - options: CallOptions, - callback: Callback< - protos.google.datastore.v1.IBeginTransactionResponse, - | protos.google.datastore.v1.IBeginTransactionRequest - | null - | undefined, - {} | null | undefined - > - ) => { - callback(null, testResp); - }; - } + setupBeginTransaction(null); }); it('should send back the response when awaiting a promise', async () => { const [transaction, resp] = await transactionWithoutMock.run(); assert.strictEqual(transaction, transactionWithoutMock); - assert.strictEqual(resp, testResp); + assert.strictEqual(resp, testRunResp); }); it('should send back the response when using a callback', done => { const runCallback: RunCallback = ( @@ -275,10 +842,14 @@ async.each( transaction: Transaction | null, response?: google.datastore.v1.IBeginTransactionResponse ) => { - assert.strictEqual(error, null); - assert.strictEqual(response, testResp); - assert.strictEqual(transaction, transactionWithoutMock); - done(); + try { + assert.strictEqual(error, null); + assert.deepStrictEqual(response, testRunResp); + assert.strictEqual(transaction, transactionWithoutMock); + done(); + } catch (e) { + done(e); + } }; transactionWithoutMock.run({}, runCallback); });