diff --git a/sdk/tables/data-tables/recordings/browsers/batch_operations/recording_should_send_multiple_transactions_with_the_same_partition_key.json b/sdk/tables/data-tables/recordings/browsers/batch_operations/recording_should_send_multiple_transactions_with_the_same_partition_key.json new file mode 100644 index 000000000000..0d2d0cb835a8 --- /dev/null +++ b/sdk/tables/data-tables/recordings/browsers/batch_operations/recording_should_send_multiple_transactions_with_the_same_partition_key.json @@ -0,0 +1,88 @@ +{ + "recordings": [ + { + "method": "POST", + "url": "https://fakestorageaccount.table.core.windows.net/Tables", + "query": {}, + "requestBody": "{\"TableName\":\"batchTableTestbrowser\"}", + "status": 201, + "response": "{\"odata.metadata\":\"https://fakestorageaccount.table.core.windows.net/$metadata#Tables/@Element\",\"TableName\":\"batchTableTestbrowser\"}", + "responseHeaders": { + "cache-control": "no-cache", + "content-type": "application/json;odata=minimalmetadata;streaming=true;charset=utf-8", + "date": "Tue, 01 Jun 2021 17:51:48 GMT", + "location": "https://fakestorageaccount.table.core.windows.net/Tables('batchTableTestbrowser')", + "server": "Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ms-client-request-id": "9a6e4529-c933-44d3-86e2-0af4272e3597", + "x-ms-request-id": "2bdef6f7-8002-0028-0b0e-57f600000000", + "x-ms-version": "2019-02-02" + } + }, + { + "method": "POST", + "url": "https://fakestorageaccount.table.core.windows.net/$batch", + "query": {}, + "requestBody": "--batch_fakeId\r\ncontent-type: multipart/mixed; boundary=changeset_fakeId\r\n\r\n\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r1\",\"value\":\"1\"}\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r2\",\"value\":\"2\"}\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r3\",\"value\":\"3\"}\r\n--changeset_fakeId--\r\n--batch_fakeId\r\n", + "status": 202, + "response": "--batchresponse_c75b7306-cc54-4450-b216-42d47a9aaaec\r\nContent-Type: multipart/mixed; boundary=changesetresponse_9d7e7528-2eb3-4137-8eca-a6bd23cee8f8\r\n\r\n--changesetresponse_9d7e7528-2eb3-4137-8eca-a6bd23cee8f8\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r1')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r1')\r\nETag: W/\"datetime'2021-06-01T17%3A51%3A48.725642Z'\"\r\n\r\n\r\n--changesetresponse_9d7e7528-2eb3-4137-8eca-a6bd23cee8f8\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r2')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r2')\r\nETag: W/\"datetime'2021-06-01T17%3A51%3A48.725642Z'\"\r\n\r\n\r\n--changesetresponse_9d7e7528-2eb3-4137-8eca-a6bd23cee8f8\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r3')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r3')\r\nETag: W/\"datetime'2021-06-01T17%3A51%3A48.725642Z'\"\r\n\r\n\r\n--changesetresponse_9d7e7528-2eb3-4137-8eca-a6bd23cee8f8--\r\n--batchresponse_c75b7306-cc54-4450-b216-42d47a9aaaec--\r\n", + "responseHeaders": { + "cache-control": "no-cache", + "content-type": "multipart/mixed; boundary=batchresponse_c75b7306-cc54-4450-b216-42d47a9aaaec", + "date": "Tue, 01 Jun 2021 17:51:48 GMT", + "server": "Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ms-client-request-id": "fc560ee5-6d8c-4298-847a-3237a3d887a6", + "x-ms-request-id": "2bdef714-8002-0028-240e-57f600000000", + "x-ms-version": "2019-02-02" + } + }, + { + "method": "POST", + "url": "https://fakestorageaccount.table.core.windows.net/$batch", + "query": {}, + "requestBody": "--batch_fakeId\r\ncontent-type: multipart/mixed; boundary=changeset_fakeId\r\n\r\n\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r4\",\"value\":\"4\"}\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r5\",\"value\":\"5\"}\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r6\",\"value\":\"6\"}\r\n--changeset_fakeId--\r\n--batch_fakeId\r\n", + "status": 202, + "response": "--batchresponse_80a963cc-bc0b-4c10-8479-bb947fd11dd1\r\nContent-Type: multipart/mixed; boundary=changesetresponse_63e77d76-ac1b-4eb4-848b-4907c051d7fa\r\n\r\n--changesetresponse_63e77d76-ac1b-4eb4-848b-4907c051d7fa\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r4')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r4')\r\nETag: W/\"datetime'2021-06-01T17%3A51%3A48.8137036Z'\"\r\n\r\n\r\n--changesetresponse_63e77d76-ac1b-4eb4-848b-4907c051d7fa\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r5')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r5')\r\nETag: W/\"datetime'2021-06-01T17%3A51%3A48.8137036Z'\"\r\n\r\n\r\n--changesetresponse_63e77d76-ac1b-4eb4-848b-4907c051d7fa\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r6')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser(PartitionKey='multiBatch1',RowKey='r6')\r\nETag: W/\"datetime'2021-06-01T17%3A51%3A48.8137036Z'\"\r\n\r\n\r\n--changesetresponse_63e77d76-ac1b-4eb4-848b-4907c051d7fa--\r\n--batchresponse_80a963cc-bc0b-4c10-8479-bb947fd11dd1--\r\n", + "responseHeaders": { + "cache-control": "no-cache", + "content-type": "multipart/mixed; boundary=batchresponse_80a963cc-bc0b-4c10-8479-bb947fd11dd1", + "date": "Tue, 01 Jun 2021 17:51:48 GMT", + "server": "Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ms-client-request-id": "b89f760c-9824-42a5-af39-429378e96a06", + "x-ms-request-id": "2bdef73a-8002-0028-470e-57f600000000", + "x-ms-version": "2019-02-02" + } + }, + { + "method": "GET", + "url": "https://fakestorageaccount.table.core.windows.net/batchTableTestbrowser()", + "query": { + "$filter": "PartitionKey eq 'multiBatch1'" + }, + "requestBody": null, + "status": 200, + "response": "{\"odata.metadata\":\"https://fakestorageaccount.table.core.windows.net/$metadata#batchTableTestbrowser\",\"value\":[{\"odata.etag\":\"W/\\\"datetime'2021-06-01T17%3A51%3A48.725642Z'\\\"\",\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r1\",\"Timestamp\":\"2021-06-01T17:51:48.725642Z\",\"value\":\"1\"},{\"odata.etag\":\"W/\\\"datetime'2021-06-01T17%3A51%3A48.725642Z'\\\"\",\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r2\",\"Timestamp\":\"2021-06-01T17:51:48.725642Z\",\"value\":\"2\"},{\"odata.etag\":\"W/\\\"datetime'2021-06-01T17%3A51%3A48.725642Z'\\\"\",\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r3\",\"Timestamp\":\"2021-06-01T17:51:48.725642Z\",\"value\":\"3\"},{\"odata.etag\":\"W/\\\"datetime'2021-06-01T17%3A51%3A48.8137036Z'\\\"\",\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r4\",\"Timestamp\":\"2021-06-01T17:51:48.8137036Z\",\"value\":\"4\"},{\"odata.etag\":\"W/\\\"datetime'2021-06-01T17%3A51%3A48.8137036Z'\\\"\",\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r5\",\"Timestamp\":\"2021-06-01T17:51:48.8137036Z\",\"value\":\"5\"},{\"odata.etag\":\"W/\\\"datetime'2021-06-01T17%3A51%3A48.8137036Z'\\\"\",\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r6\",\"Timestamp\":\"2021-06-01T17:51:48.8137036Z\",\"value\":\"6\"}]}", + "responseHeaders": { + "cache-control": "no-cache", + "content-type": "application/json;odata=minimalmetadata;streaming=true;charset=utf-8", + "date": "Tue, 01 Jun 2021 17:51:48 GMT", + "server": "Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ms-client-request-id": "c43b6c2c-15d5-4317-9798-e76734715542", + "x-ms-request-id": "2bdef751-8002-0028-5e0e-57f600000000", + "x-ms-version": "2019-02-02" + } + } + ], + "uniqueTestInfo": { + "uniqueName": {}, + "newDate": {} + }, + "hash": "c9699701aaea0fee411293abbe4268e5" +} \ No newline at end of file diff --git a/sdk/tables/data-tables/recordings/node/batch_operations/recording_should_send_multiple_transactions_with_the_same_partition_key.js b/sdk/tables/data-tables/recordings/node/batch_operations/recording_should_send_multiple_transactions_with_the_same_partition_key.js new file mode 100644 index 000000000000..64ddd64fa69f --- /dev/null +++ b/sdk/tables/data-tables/recordings/node/batch_operations/recording_should_send_multiple_transactions_with_the_same_partition_key.js @@ -0,0 +1,95 @@ +let nock = require('nock'); + +module.exports.hash = "f0a834722cfc3432a47a34ce99f2bdd6"; + +module.exports.testInfo = {"uniqueName":{},"newDate":{}} + +nock('https://fakestorageaccount.table.core.windows.net:443', {"encodedQueryParams":true}) + .post('/Tables', {"TableName":"batchTableTestnode"}) + .query(true) + .reply(201, {"odata.metadata":"https://fakestorageaccount.table.core.windows.net/$metadata#Tables/@Element","TableName":"batchTableTestnode"}, [ 'Cache-Control', + 'no-cache', + 'Transfer-Encoding', + 'chunked', + 'Content-Type', + 'application/json;odata=minimalmetadata;streaming=true;charset=utf-8', + 'Location', + 'https://fakestorageaccount.table.core.windows.net/Tables(\'batchTableTestnode\')', + 'Server', + 'Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0', + 'x-ms-request-id', + '31cc3a90-8002-0127-6807-575da3000000', + 'x-ms-client-request-id', + '938b5a21-d8b5-45cf-86d0-19df3bc334f9', + 'x-ms-version', + '2019-02-02', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 01 Jun 2021 16:56:15 GMT' ]); + +nock('https://fakestorageaccount.table.core.windows.net:443', {"encodedQueryParams":true}) + .post('/$batch', "--batch_fakeId\r\ncontent-type: multipart/mixed; boundary=changeset_fakeId\r\n\r\n\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestnode HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r1\",\"value\":\"1\"}\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestnode HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r2\",\"value\":\"2\"}\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestnode HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r3\",\"value\":\"3\"}\r\n--changeset_fakeId--\r\n--batch_fakeId\r\n") + .query(true) + .reply(202, "--batchresponse_66ac1a09-28b5-474d-821f-d993ec447cac\r\nContent-Type: multipart/mixed; boundary=changesetresponse_7d8d1546-0848-442f-9284-7dc35897a6a4\r\n\r\n--changesetresponse_7d8d1546-0848-442f-9284-7dc35897a6a4\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r1')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r1')\r\nETag: W/\"datetime'2021-06-01T16%3A56%3A15.9634213Z'\"\r\n\r\n\r\n--changesetresponse_7d8d1546-0848-442f-9284-7dc35897a6a4\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r2')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r2')\r\nETag: W/\"datetime'2021-06-01T16%3A56%3A15.9634213Z'\"\r\n\r\n\r\n--changesetresponse_7d8d1546-0848-442f-9284-7dc35897a6a4\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r3')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r3')\r\nETag: W/\"datetime'2021-06-01T16%3A56%3A15.9634213Z'\"\r\n\r\n\r\n--changesetresponse_7d8d1546-0848-442f-9284-7dc35897a6a4--\r\n--batchresponse_66ac1a09-28b5-474d-821f-d993ec447cac--\r\n", [ 'Cache-Control', + 'no-cache', + 'Transfer-Encoding', + 'chunked', + 'Content-Type', + 'multipart/mixed; boundary=batchresponse_66ac1a09-28b5-474d-821f-d993ec447cac', + 'Server', + 'Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0', + 'x-ms-request-id', + '31cc3a98-8002-0127-6e07-575da3000000', + 'x-ms-client-request-id', + '801cfcef-beeb-4ddb-ae0f-08745153b408', + 'x-ms-version', + '2019-02-02', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 01 Jun 2021 16:56:15 GMT' ]); + +nock('https://fakestorageaccount.table.core.windows.net:443', {"encodedQueryParams":true}) + .post('/$batch', "--batch_fakeId\r\ncontent-type: multipart/mixed; boundary=changeset_fakeId\r\n\r\n\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestnode HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r4\",\"value\":\"4\"}\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestnode HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r5\",\"value\":\"5\"}\r\n--changeset_fakeId\r\ncontent-type: application/http\r\ncontent-transfer-encoding: binary\r\n\r\nPOST https://fakestorageaccount.table.core.windows.net/batchTableTestnode HTTP/1.1\r\ncontent-type: application/json;odata=nometadata\r\naccept: application/json;odata=minimalmetadata\r\ndataserviceversion: 3.0\r\nprefer: return-no-content\r\n\r\n\r\n{\"PartitionKey\":\"multiBatch1\",\"RowKey\":\"r6\",\"value\":\"6\"}\r\n--changeset_fakeId--\r\n--batch_fakeId\r\n") + .query(true) + .reply(202, "--batchresponse_7049d189-952f-4d8e-b1c7-3de49034e04d\r\nContent-Type: multipart/mixed; boundary=changesetresponse_6e0c0b9a-34a2-4588-a351-15a6d5081de6\r\n\r\n--changesetresponse_6e0c0b9a-34a2-4588-a351-15a6d5081de6\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r4')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r4')\r\nETag: W/\"datetime'2021-06-01T16%3A56%3A16.0124566Z'\"\r\n\r\n\r\n--changesetresponse_6e0c0b9a-34a2-4588-a351-15a6d5081de6\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r5')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r5')\r\nETag: W/\"datetime'2021-06-01T16%3A56%3A16.0124566Z'\"\r\n\r\n\r\n--changesetresponse_6e0c0b9a-34a2-4588-a351-15a6d5081de6\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nPreference-Applied: return-no-content\r\nDataServiceVersion: 3.0;\r\nLocation: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r6')\r\nDataServiceId: https://fakestorageaccount.table.core.windows.net/batchTableTestnode(PartitionKey='multiBatch1',RowKey='r6')\r\nETag: W/\"datetime'2021-06-01T16%3A56%3A16.0124566Z'\"\r\n\r\n\r\n--changesetresponse_6e0c0b9a-34a2-4588-a351-15a6d5081de6--\r\n--batchresponse_7049d189-952f-4d8e-b1c7-3de49034e04d--\r\n", [ 'Cache-Control', + 'no-cache', + 'Transfer-Encoding', + 'chunked', + 'Content-Type', + 'multipart/mixed; boundary=batchresponse_7049d189-952f-4d8e-b1c7-3de49034e04d', + 'Server', + 'Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0', + 'x-ms-request-id', + '31cc3aa1-8002-0127-7607-575da3000000', + 'x-ms-client-request-id', + '12b685e5-8477-448f-a58c-e0f290f3ce63', + 'x-ms-version', + '2019-02-02', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 01 Jun 2021 16:56:15 GMT' ]); + +nock('https://fakestorageaccount.table.core.windows.net:443', {"encodedQueryParams":true}) + .get('/batchTableTestnode()') + .query(true) + .reply(200, {"odata.metadata":"https://fakestorageaccount.table.core.windows.net/$metadata#batchTableTestnode","value":[{"odata.etag":"W/\"datetime'2021-06-01T16%3A56%3A15.9634213Z'\"","PartitionKey":"multiBatch1","RowKey":"r1","Timestamp":"2021-06-01T16:56:15.9634213Z","value":"1"},{"odata.etag":"W/\"datetime'2021-06-01T16%3A56%3A15.9634213Z'\"","PartitionKey":"multiBatch1","RowKey":"r2","Timestamp":"2021-06-01T16:56:15.9634213Z","value":"2"},{"odata.etag":"W/\"datetime'2021-06-01T16%3A56%3A15.9634213Z'\"","PartitionKey":"multiBatch1","RowKey":"r3","Timestamp":"2021-06-01T16:56:15.9634213Z","value":"3"},{"odata.etag":"W/\"datetime'2021-06-01T16%3A56%3A16.0124566Z'\"","PartitionKey":"multiBatch1","RowKey":"r4","Timestamp":"2021-06-01T16:56:16.0124566Z","value":"4"},{"odata.etag":"W/\"datetime'2021-06-01T16%3A56%3A16.0124566Z'\"","PartitionKey":"multiBatch1","RowKey":"r5","Timestamp":"2021-06-01T16:56:16.0124566Z","value":"5"},{"odata.etag":"W/\"datetime'2021-06-01T16%3A56%3A16.0124566Z'\"","PartitionKey":"multiBatch1","RowKey":"r6","Timestamp":"2021-06-01T16:56:16.0124566Z","value":"6"}]}, [ 'Cache-Control', + 'no-cache', + 'Transfer-Encoding', + 'chunked', + 'Content-Type', + 'application/json;odata=minimalmetadata;streaming=true;charset=utf-8', + 'Server', + 'Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0', + 'x-ms-request-id', + '31cc3aac-8002-0127-0107-575da3000000', + 'x-ms-client-request-id', + '8b307953-6478-46ba-b437-7f3324803d70', + 'x-ms-version', + '2019-02-02', + 'X-Content-Type-Options', + 'nosniff', + 'Date', + 'Tue, 01 Jun 2021 16:56:15 GMT' ]); diff --git a/sdk/tables/data-tables/src/TableClient.ts b/sdk/tables/data-tables/src/TableClient.ts index 25666e0d1888..4aba99bd038e 100644 --- a/sdk/tables/data-tables/src/TableClient.ts +++ b/sdk/tables/data-tables/src/TableClient.ts @@ -23,11 +23,7 @@ import { GetAccessPolicyResponse, SetAccessPolicyResponse } from "./generatedModels"; -import { - GeneratedClientOptionalParams, - QueryOptions as GeneratedQueryOptions, - SignedIdentifier -} from "./generated/models"; +import { QueryOptions as GeneratedQueryOptions, SignedIdentifier } from "./generated/models"; import { getClientParamsFromConnectionString } from "./utils/connectionString"; import { TablesSharedKeyCredential, @@ -40,16 +36,16 @@ import { GeneratedClient, TableDeleteEntityOptionalParams } from "./generated"; import { deserialize, deserializeObjectsArray, serialize } from "./serialization"; import { Table } from "./generated/operations"; import { LIB_INFO, TablesLoggingAllowedHeaderNames } from "./utils/constants"; -import { FullOperationResponse, OperationOptions } from "@azure/core-client"; +import { + FullOperationResponse, + InternalClientPipelineOptions, + OperationOptions +} from "@azure/core-client"; import { logger } from "./logger"; import { createSpan } from "./utils/tracing"; import { SpanStatusCode } from "@azure/core-tracing"; -import { InternalTableTransaction, createInnerTransactionRequest } from "./TableTransaction"; -import { - InternalTransactionClientOptions, - ListEntitiesResponse, - TableClientLike -} from "./utils/internalModels"; +import { InternalTableTransaction } from "./TableTransaction"; +import { ListEntitiesResponse } from "./utils/internalModels"; import { Uuid } from "./utils/uuid"; import { parseXML, stringifyXML } from "@azure/core-xml"; import { Pipeline } from "@azure/core-rest-pipeline"; @@ -70,7 +66,7 @@ export class TableClient { public pipeline: Pipeline; private table: Table; private credential: TablesSharedKeyCredentialLike | undefined; - private interceptClient: TableClientLike | undefined; + private transactionClient: InternalTableTransaction | undefined; /** * Name of the table to perform operations on. @@ -155,33 +151,20 @@ export class TableClient { clientOptions.userAgentOptions.userAgentPrefix = LIB_INFO; } - let internalPipelineOptions: GeneratedClientOptionalParams = { - ...clientOptions + const internalPipelineOptions: InternalClientPipelineOptions = { + ...clientOptions, + loggingOptions: { + logger: logger.info, + additionalAllowedHeaderNames: [...TablesLoggingAllowedHeaderNames] + }, + deserializationOptions: { + parseXML + }, + serializationOptions: { + stringifyXML + } }; - if (isInternalClientOptions(clientOptions)) { - // The client is meant to be an intercept client (for Transaction), so we need to create only the intercepting - // pipelines. - internalPipelineOptions.pipeline = clientOptions.innerTransactionRequest.createPipeline(); - } else { - // The client is a regular client (non-transaction), pass the pipeline options to create a pipeline - internalPipelineOptions = { - ...internalPipelineOptions, - ...{ - loggingOptions: { - logger: logger.info, - additionalAllowedHeaderNames: [...TablesLoggingAllowedHeaderNames] - }, - deserializationOptions: { - parseXML - }, - serializationOptions: { - stringifyXML - } - } - }; - } - this.tableName = tableName; this.credential = credential; const generatedClient = new GeneratedClient(url, internalPipelineOptions); @@ -602,43 +585,39 @@ export class TableClient { const partitionKey = actions[0][1].partitionKey; const transactionId = Uuid.generateUuid(); const changesetId = Uuid.generateUuid(); - const innerTransactionRequest = createInnerTransactionRequest(transactionId, changesetId); - const internalClientOptions: InternalTransactionClientOptions = { - innerTransactionRequest: innerTransactionRequest - }; - if (!this.interceptClient) { - // Cache intercept client so we just have to instantiate it once - this.interceptClient = new TableClient(this.url, this.tableName, internalClientOptions); + if (!this.transactionClient) { + // Add pipeline + this.transactionClient = new InternalTableTransaction( + this.url, + this.tableName, + partitionKey, + transactionId, + changesetId, + this.credential + ); + } else { + this.transactionClient.reset(transactionId, changesetId, partitionKey); } - const transactionClient = new InternalTableTransaction( - this.url, - partitionKey, - this.interceptClient, - transactionId, - innerTransactionRequest, - this.credential - ); - for (const item of actions) { const [action, entity, updateMode = "Merge"] = item; switch (action) { case "create": - transactionClient.createEntity(entity); + this.transactionClient.createEntity(entity); break; case "delete": - transactionClient.deleteEntity(entity.partitionKey, entity.rowKey); + this.transactionClient.deleteEntity(entity.partitionKey, entity.rowKey); break; case "update": - transactionClient.updateEntity(entity, updateMode); + this.transactionClient.updateEntity(entity, updateMode); break; case "upsert": - transactionClient.upsertEntity(entity, updateMode); + this.transactionClient.upsertEntity(entity, updateMode); } } - return transactionClient.submitTransaction(); + return this.transactionClient.submitTransaction(); } private convertQueryOptions(query: TableEntityQueryOptions): GeneratedQueryOptions { @@ -699,7 +678,3 @@ interface InternalListTableEntitiesOptions extends ListTableEntitiesOptions { */ disableTypeConversion?: boolean; } - -function isInternalClientOptions(options: any): options is InternalTransactionClientOptions { - return Boolean(options.innerTransactionRequest); -} diff --git a/sdk/tables/data-tables/src/TablePolicies.ts b/sdk/tables/data-tables/src/TablePolicies.ts index a3de02fb4b10..250d66ea039c 100644 --- a/sdk/tables/data-tables/src/TablePolicies.ts +++ b/sdk/tables/data-tables/src/TablePolicies.ts @@ -9,8 +9,12 @@ import { createHttpHeaders, createPipelineRequest } from "@azure/core-rest-pipeline"; -import { HeaderConstants } from "./utils/constants"; -import { InnerTransactionRequest } from "./utils/internalModels"; +import { + HeaderConstants, + TRANSACTION_HTTP_LINE_ENDING, + TRANSACTION_HTTP_VERSION_1_1 +} from "./utils/constants"; +import { getChangeSetBoundary } from "./utils/transactionHelpers"; export const transactionRequestAssemblePolicyName = "transactionRequestAssemblePolicy"; @@ -21,12 +25,14 @@ const dummyResponse: PipelineResponse = { }; export function transactionRequestAssemblePolicy( - transactionRequest: InnerTransactionRequest + bodyParts: string[], + changesetId: string ): PipelinePolicy { return { name: transactionRequestAssemblePolicyName, async sendRequest(request: PipelineRequest): Promise { - transactionRequest.appendSubRequestToBody(request); + const subRequest = getNextSubrequestBodyPart(request, changesetId); + bodyParts.push(subRequest); // Intercept request from going to wire return dummyResponse; } @@ -45,3 +51,37 @@ export function transactionHeaderFilterPolicy(): PipelinePolicy { } }; } + +function getSubRequestUrl(url: string): string { + const sasTokenParts = ["sv", "ss", "srt", "sp", "se", "st", "spr", "sig"]; + const urlParsed = new URL(url); + sasTokenParts.forEach((part) => urlParsed.searchParams.delete(part)); + return urlParsed.toString(); +} + +function getNextSubrequestBodyPart(request: PipelineRequest, changesetId: string) { + const changesetBoundary = getChangeSetBoundary(changesetId); + const subRequestPrefix = `--${changesetBoundary}${TRANSACTION_HTTP_LINE_ENDING}${HeaderConstants.CONTENT_TYPE}: application/http${TRANSACTION_HTTP_LINE_ENDING}${HeaderConstants.CONTENT_TRANSFER_ENCODING}: binary`; + + const subRequestUrl = getSubRequestUrl(request.url); + // Start to assemble sub request + const subRequest = [ + subRequestPrefix, // sub request constant prefix + "", // empty line after sub request's content ID + `${request.method.toString()} ${subRequestUrl} ${TRANSACTION_HTTP_VERSION_1_1}` // sub request start line with method, + ]; + + // Add required headers + for (const [name, value] of request.headers) { + subRequest.push(`${name}: ${value}`); + } + + // Append sub-request body + subRequest.push(`${TRANSACTION_HTTP_LINE_ENDING}`); // sub request's headers need end with an empty line + if (request.body) { + subRequest.push(String(request.body)); + } + + // Add subrequest to transaction body + return subRequest.join(TRANSACTION_HTTP_LINE_ENDING); +} diff --git a/sdk/tables/data-tables/src/TableTransaction.ts b/sdk/tables/data-tables/src/TableTransaction.ts index d4a4a3ca63b3..edee18655fb9 100644 --- a/sdk/tables/data-tables/src/TableTransaction.ts +++ b/sdk/tables/data-tables/src/TableTransaction.ts @@ -3,13 +3,11 @@ import { createHttpHeaders, - PipelineRequest, createPipelineRequest, PipelineResponse, - RestError, - createEmptyPipeline + RestError } from "@azure/core-rest-pipeline"; -import { ServiceClient, OperationOptions, serializationPolicy } from "@azure/core-client"; +import { ServiceClient, OperationOptions } from "@azure/core-client"; import { DeleteTableEntityOptions, TableEntity, @@ -21,14 +19,17 @@ import { } from "./models"; import { TablesSharedKeyCredentialLike } from "./TablesSharedKeyCredential"; import { getAuthorizationHeader } from "./TablesSharedKeyCredentialPolicy"; -import { HeaderConstants } from "./utils/constants"; -import { transactionHeaderFilterPolicy, transactionRequestAssemblePolicy } from "./TablePolicies"; -import { InnerTransactionRequest, TableClientLike } from "./utils/internalModels"; +import { TableClientLike } from "./utils/internalModels"; import { createSpan } from "./utils/tracing"; import { SpanStatusCode } from "@azure/core-tracing"; -import { URL } from "./utils/url"; import { TableServiceErrorOdataError } from "./generated"; import { getTransactionHeaders } from "./utils/transactionHeaders"; +import { TableClient } from "./TableClient"; +import { + prepateTransactionPipeline, + getTransactionHttpRequestBody, + getInitialTransactionBody +} from "./utils/transactionHelpers"; /** * Helper to build a list of transaction actions @@ -93,16 +94,21 @@ export class InternalTableTransaction { * Table Account URL */ public url: string; - private interceptClient: TableClientLike; - private transactionGuid: string; - private transactionRequest: InnerTransactionRequest; - private pendingOperations: Promise[]; - private credential?: TablesSharedKeyCredentialLike; - /** - * Partition key tagetted by the transaction + * This part of the state can be reset by + * calling the reset function. Other parts of the state + * such as the credentials remain the same throughout the life + * of the instance. */ - public readonly partitionKey: string; + private resetableState: { + transactionId: string; + changesetId: string; + pendingOperations: Promise[]; + bodyParts: string[]; + partitionKey: string; + }; + private interceptClient: TableClientLike; + private credential?: TablesSharedKeyCredentialLike; /** * @param url - Tables account url @@ -111,20 +117,18 @@ export class InternalTableTransaction { */ constructor( url: string, + tableName: string, partitionKey: string, - interceptClient: TableClientLike, - transactionGuid: string, - transactionRequest: InnerTransactionRequest, + transactionId: string, + changesetId: string, credential?: TablesSharedKeyCredentialLike ) { this.credential = credential; - this.partitionKey = partitionKey; this.url = url; - this.transactionGuid = transactionGuid; - this.transactionRequest = transactionRequest; - this.pendingOperations = []; + this.interceptClient = new TableClient(this.url, tableName); - this.interceptClient = interceptClient; + // Initialize Reset-able properties + this.resetableState = this.initializeSharedState(transactionId, changesetId, partitionKey); // Depending on the auth method used we need to build the url if (!credential) { @@ -139,13 +143,34 @@ export class InternalTableTransaction { } } + /** + * Resets the state of the Transaction. + */ + reset(transactionId: string, changesetId: string, partitionKey: string): void { + this.resetableState = this.initializeSharedState(transactionId, changesetId, partitionKey); + } + + private initializeSharedState(transactionId: string, changesetId: string, partitionKey: string) { + const pendingOperations: Promise[] = []; + const bodyParts = getInitialTransactionBody(transactionId, changesetId); + prepateTransactionPipeline(this.interceptClient.pipeline, bodyParts, changesetId); + + return { + transactionId, + changesetId, + partitionKey, + pendingOperations, + bodyParts + }; + } + /** * Adds a createEntity operation to the transaction * @param entity - Entity to create */ public createEntity(entity: TableEntity): void { this.checkPartitionKey(entity.partitionKey); - this.pendingOperations.push(this.interceptClient.createEntity(entity)); + this.resetableState.pendingOperations.push(this.interceptClient.createEntity(entity)); } /** @@ -155,7 +180,7 @@ export class InternalTableTransaction { public createEntities(entities: TableEntity[]): void { for (const entity of entities) { this.checkPartitionKey(entity.partitionKey); - this.pendingOperations.push(this.interceptClient.createEntity(entity)); + this.resetableState.pendingOperations.push(this.interceptClient.createEntity(entity)); } } @@ -171,7 +196,9 @@ export class InternalTableTransaction { options?: DeleteTableEntityOptions ): void { this.checkPartitionKey(partitionKey); - this.pendingOperations.push(this.interceptClient.deleteEntity(partitionKey, rowKey, options)); + this.resetableState.pendingOperations.push( + this.interceptClient.deleteEntity(partitionKey, rowKey, options) + ); } /** @@ -186,7 +213,9 @@ export class InternalTableTransaction { options?: UpdateTableEntityOptions ): void { this.checkPartitionKey(entity.partitionKey); - this.pendingOperations.push(this.interceptClient.updateEntity(entity, mode, options)); + this.resetableState.pendingOperations.push( + this.interceptClient.updateEntity(entity, mode, options) + ); } /** @@ -203,17 +232,23 @@ export class InternalTableTransaction { options?: OperationOptions ): void { this.checkPartitionKey(entity.partitionKey); - this.pendingOperations.push(this.interceptClient.upsertEntity(entity, mode, options)); + this.resetableState.pendingOperations.push( + this.interceptClient.upsertEntity(entity, mode, options) + ); } /** * Submits the operations in the transaction */ public async submitTransaction(): Promise { - await Promise.all(this.pendingOperations); - const body = this.transactionRequest.getHttpRequestBody(); + await Promise.all(this.resetableState.pendingOperations); + const body = getTransactionHttpRequestBody( + this.resetableState.bodyParts, + this.resetableState.transactionId, + this.resetableState.changesetId + ); const client = new ServiceClient(); - const headers = getTransactionHeaders(this.transactionGuid); + const headers = getTransactionHeaders(this.resetableState.transactionId); const { span, updatedOptions } = createSpan( "TableTransaction-submitTransaction", @@ -247,7 +282,7 @@ export class InternalTableTransaction { } private checkPartitionKey(partitionKey: string): void { - if (this.partitionKey !== partitionKey) { + if (this.resetableState.partitionKey !== partitionKey) { throw new Error("All operations in a transaction must target the same partitionKey"); } } @@ -304,73 +339,3 @@ function parseTransactionResponse(transactionResponse: PipelineResponse): TableT getResponseForEntity: (rowKey: string) => responses.find((r) => r.rowKey === rowKey) }; } - -/** - * Prepares the operation url to be added to the body, removing the SAS token if present - * @param url - Source URL string - */ -function getSubRequestUrl(url: string): string { - const sasTokenParts = ["sv", "ss", "srt", "sp", "se", "st", "spr", "sig"]; - const urlParsed = new URL(url); - sasTokenParts.forEach((part) => urlParsed.searchParams.delete(part)); - return urlParsed.toString(); -} - -/** - * This method creates a transaction request object that provides functions to build the envelope and body for a transaction request - * @param transactionGuid - Id of the transaction - */ -export function createInnerTransactionRequest( - transactionGuid: string, - changesetId: string -): InnerTransactionRequest { - const HTTP_LINE_ENDING = "\r\n"; - const HTTP_VERSION_1_1 = "HTTP/1.1"; - const transactionBoundary = `batch_${transactionGuid}`; - const changesetBoundary = `changeset_${changesetId}`; - - const subRequestPrefix = `--${changesetBoundary}${HTTP_LINE_ENDING}${HeaderConstants.CONTENT_TYPE}: application/http${HTTP_LINE_ENDING}${HeaderConstants.CONTENT_TRANSFER_ENCODING}: binary`; - const changesetEnding = `--${changesetBoundary}--`; - const transactionEnding = `--${transactionBoundary}`; - - return { - body: [ - `--${transactionBoundary}${HTTP_LINE_ENDING}${HeaderConstants.CONTENT_TYPE}: multipart/mixed; boundary=changeset_${changesetId}${HTTP_LINE_ENDING}${HTTP_LINE_ENDING}` - ], - createPipeline() { - // Use transaction assemble policy to assemble request and intercept request from going to wire - const pipeline = createEmptyPipeline(); - pipeline.addPolicy(serializationPolicy(), { phase: "Serialize" }); - pipeline.addPolicy(transactionHeaderFilterPolicy()); - pipeline.addPolicy(transactionRequestAssemblePolicy(this)); - return pipeline; - }, - appendSubRequestToBody(request: PipelineRequest) { - const subRequestUrl = getSubRequestUrl(request.url); - // Start to assemble sub request - const subRequest = [ - subRequestPrefix, // sub request constant prefix - "", // empty line after sub request's content ID - `${request.method.toString()} ${subRequestUrl} ${HTTP_VERSION_1_1}` // sub request start line with method, - ]; - - // Add required headers - for (const [name, value] of request.headers) { - subRequest.push(`${name}: ${value}`); - } - - // Append sub-request body - subRequest.push(`${HTTP_LINE_ENDING}`); // sub request's headers need end with an empty line - if (request.body) { - subRequest.push(String(request.body)); - } - - // Add subrequest to transaction body - this.body.push(subRequest.join(HTTP_LINE_ENDING)); - }, - getHttpRequestBody(): string { - const bodyContent = this.body.join(HTTP_LINE_ENDING); - return `${bodyContent}${HTTP_LINE_ENDING}${changesetEnding}${HTTP_LINE_ENDING}${transactionEnding}${HTTP_LINE_ENDING}`; - } - }; -} diff --git a/sdk/tables/data-tables/src/utils/constants.ts b/sdk/tables/data-tables/src/utils/constants.ts index 4388b8e01816..76193c54f74c 100644 --- a/sdk/tables/data-tables/src/utils/constants.ts +++ b/sdk/tables/data-tables/src/utils/constants.ts @@ -4,6 +4,9 @@ export const SDK_VERSION: string = "12.0.0-beta.3"; export const LIB_INFO = `azsdk-js-data-tables/${SDK_VERSION}`; +export const TRANSACTION_HTTP_VERSION_1_1 = "HTTP/1.1"; +export const TRANSACTION_HTTP_LINE_ENDING = "\r\n"; + export const HeaderConstants = { AUTHORIZATION: "authorization", CONTENT_LENGTH: "content-length", diff --git a/sdk/tables/data-tables/src/utils/internalModels.ts b/sdk/tables/data-tables/src/utils/internalModels.ts index ddc832362789..32cba82a326c 100644 --- a/sdk/tables/data-tables/src/utils/internalModels.ts +++ b/sdk/tables/data-tables/src/utils/internalModels.ts @@ -92,6 +92,10 @@ export interface InternalTransactionClientOptions extends TableServiceClientOpti * Describes the shape of a TableClient */ export interface TableClientLike { + /** + * Represents a pipeline for making a HTTP request to a URL. + */ + pipeline: Pipeline; /** * Name of the table to perform operations on. */ diff --git a/sdk/tables/data-tables/src/utils/transactionHelpers.ts b/sdk/tables/data-tables/src/utils/transactionHelpers.ts new file mode 100644 index 000000000000..1beb0e966a49 --- /dev/null +++ b/sdk/tables/data-tables/src/utils/transactionHelpers.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { HeaderConstants, TRANSACTION_HTTP_LINE_ENDING } from "./constants"; +import { Pipeline } from "@azure/core-rest-pipeline"; +import { serializationPolicy } from "@azure/core-client"; +import { transactionHeaderFilterPolicy, transactionRequestAssemblePolicy } from "../TablePolicies"; + +/** + * Builds a transaction change set boundary to be added to the transaction request body + * @param changesetId - Id of the transaction changeset + */ +export function getChangeSetBoundary(changesetId: string): string { + return `changeset_${changesetId}`; +} + +/** + * Builds a transaction boundary to be added to the transaction request body + * @param transactionId - Id of the transaction + */ +export function getTransactionBoundary(transactionId: string): string { + return `batch_${transactionId}`; +} + +/** + * Returns an initial representation of the Transaction body. + * @param transactionId - Id of the transaction + * @param changesetId - Id of the transaction changeset + */ +export function getInitialTransactionBody(transactionId: string, changesetId: string): string[] { + const transactionBoundary = `batch_${transactionId}`; + return [ + `--${transactionBoundary}${TRANSACTION_HTTP_LINE_ENDING}${HeaderConstants.CONTENT_TYPE}: multipart/mixed; boundary=changeset_${changesetId}${TRANSACTION_HTTP_LINE_ENDING}${TRANSACTION_HTTP_LINE_ENDING}` + ]; +} + +/** + * Build the Transaction http request body to send to the service. + * @param bodyParts - Parts of the transaction body, containing information about the actions to be included in the transaction request + * @param transactionId - Id of the transaction + * @param changesetId - Id of the transaction changeset + */ +export function getTransactionHttpRequestBody( + bodyParts: string[], + transactionId: string, + changesetId: string +): string { + const transactionBoundary = getTransactionBoundary(transactionId); + const changesetBoundary = getChangeSetBoundary(changesetId); + const changesetEnding = `--${changesetBoundary}--`; + const transactionEnding = `--${transactionBoundary}`; + const bodyContent = bodyParts.join(TRANSACTION_HTTP_LINE_ENDING); + return `${bodyContent}${TRANSACTION_HTTP_LINE_ENDING}${changesetEnding}${TRANSACTION_HTTP_LINE_ENDING}${transactionEnding}${TRANSACTION_HTTP_LINE_ENDING}`; +} + +/** + * Prepares the transaction pipeline to intercept operations + * @param pipeline - Client pipeline + */ +export function prepateTransactionPipeline( + pipeline: Pipeline, + bodyParts: string[], + changesetId: string +): void { + // Fist, we need to clear all the existing policies to make sure we start + // with a fresh state. + const policies = pipeline.getOrderedPolicies(); + for (const policy of policies) { + pipeline.removePolicy({ + name: policy.name + }); + } + + // With the clear state we now initialize the pipelines required for intercepting the requests. + // Use transaction assemble policy to assemble request and intercept request from going to wire + + pipeline.addPolicy(serializationPolicy(), { phase: "Serialize" }); + pipeline.addPolicy(transactionHeaderFilterPolicy()); + pipeline.addPolicy(transactionRequestAssemblePolicy(bodyParts, changesetId)); +} diff --git a/sdk/tables/data-tables/test/public/transaction.spec.ts b/sdk/tables/data-tables/test/public/transaction.spec.ts index fa2220d29083..6d17fb1ca2eb 100644 --- a/sdk/tables/data-tables/test/public/transaction.spec.ts +++ b/sdk/tables/data-tables/test/public/transaction.spec.ts @@ -176,4 +176,40 @@ describe("batch operations", () => { assert.isString(error.message); } }); + + it("should send multiple transactions with the same partition key", async () => { + const multiBatchPartitionKey = "multiBatch1"; + const actions1: TransactionAction[] = [ + ["create", { partitionKey: multiBatchPartitionKey, rowKey: "r1", value: "1" }], + ["create", { partitionKey: multiBatchPartitionKey, rowKey: "r2", value: "2" }], + ["create", { partitionKey: multiBatchPartitionKey, rowKey: "r3", value: "3" }] + ]; + + await client.submitTransaction(actions1); + + const actions2: TransactionAction[] = [ + ["create", { partitionKey: multiBatchPartitionKey, rowKey: "r4", value: "4" }], + ["create", { partitionKey: multiBatchPartitionKey, rowKey: "r5", value: "5" }], + ["create", { partitionKey: multiBatchPartitionKey, rowKey: "r6", value: "6" }] + ]; + + await client.submitTransaction(actions2); + + const entities = client.listEntities<{ name: string }>({ + queryOptions: { filter: odata`PartitionKey eq ${multiBatchPartitionKey}` } + }); + + let entityCount = 0; + for await (const entity of entities) { + if (entity.partitionKey !== multiBatchPartitionKey) { + throw new Error( + `Expected all entities to have the same partition key: ${multiBatchPartitionKey} but found ${entity.partitionKey}` + ); + } + + entityCount++; + } + + assert.equal(entityCount, 6); + }); });