* try * dsf * Allow for scope to be passed in * comment * clang and others * Update sdk/tables/azure-data-tables/test/ut/table_client_test.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update sdk/tables/azure-data-tables/inc/azure/data/tables/tables_audience.hpp Co-authored-by: Anton Kolesnyk <41349689+antkmsft@users.noreply.github.com> * Update sdk/tables/azure-data-tables/inc/azure/data/tables/tables_audience.hpp Co-authored-by: Anton Kolesnyk <41349689+antkmsft@users.noreply.github.com> * PR comments * Update sdk/tables/azure-data-tables/inc/azure/data/tables/table_audience.hpp Co-authored-by: Anton Kolesnyk <41349689+antkmsft@users.noreply.github.com> * Update sdk/tables/azure-data-tables/inc/azure/data/tables/table_audience.hpp Co-authored-by: Anton Kolesnyk <41349689+antkmsft@users.noreply.github.com> * PR * clang --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Anton Kolesnyk <41349689+antkmsft@users.noreply.github.com>
955 lines
34 KiB
C++
955 lines
34 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
#include "azure/data/tables/table_client.hpp"
|
|
#include "azure/data/tables/table_service_client.hpp"
|
|
#include "private/package_version.hpp"
|
|
#include "private/policies/service_version_policy.hpp"
|
|
#include "private/policies/tenant_bearer_token_policy.hpp"
|
|
#include "private/policies/timeout_policy.hpp"
|
|
#include "private/serializers.hpp"
|
|
#include "private/tables_constants.hpp"
|
|
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
using namespace Azure::Data::Tables;
|
|
using namespace Azure::Data::Tables::_detail::Policies;
|
|
using namespace Azure::Data::Tables::_detail::Xml;
|
|
using namespace Azure::Data::Tables::_detail;
|
|
|
|
namespace Azure { namespace Data { namespace Tables { namespace _detail {
|
|
std::string GetDefaultScopeForAudience(const std::string& audience = "")
|
|
{
|
|
if (!audience.empty() && audience.back() == '/')
|
|
{
|
|
return audience + _detail::AudienceSuffix;
|
|
}
|
|
return audience + _detail::AudienceSuffixPath;
|
|
}
|
|
}}}} // namespace Azure::Data::Tables::_detail
|
|
|
|
TableServiceClient::TableServiceClient(
|
|
const std::string& serviceUrl,
|
|
const TableClientOptions& options)
|
|
{
|
|
m_url = Azure::Core::Url(serviceUrl);
|
|
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> perRetryPolicies;
|
|
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> perOperationPolicies;
|
|
perRetryPolicies.emplace_back(std::make_unique<TimeoutPolicy>());
|
|
perOperationPolicies.emplace_back(std::make_unique<ServiceVersionPolicy>(options.ApiVersion));
|
|
m_pipeline = std::make_shared<Azure::Core::Http::_internal::HttpPipeline>(
|
|
options,
|
|
_detail::TablesServicePackageName,
|
|
PackageVersion::ToString(),
|
|
std::move(perRetryPolicies),
|
|
std::move(perOperationPolicies));
|
|
}
|
|
|
|
TableServiceClient::TableServiceClient(
|
|
const std::string& serviceUrl,
|
|
std::shared_ptr<const Core::Credentials::TokenCredential> credential,
|
|
const TableClientOptions& options)
|
|
{
|
|
m_tokenCredential = credential;
|
|
TableClientOptions newOptions = options;
|
|
m_url = Azure::Core::Url(serviceUrl);
|
|
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> perRetryPolicies;
|
|
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> perOperationPolicies;
|
|
perRetryPolicies.emplace_back(std::make_unique<TimeoutPolicy>());
|
|
perOperationPolicies.emplace_back(std::make_unique<ServiceVersionPolicy>(options.ApiVersion));
|
|
|
|
perRetryPolicies.emplace_back(std::make_unique<TimeoutPolicy>());
|
|
{
|
|
Azure::Core::Credentials::TokenRequestContext tokenContext;
|
|
// if Scopes passed in are empty, use the default scope
|
|
tokenContext.Scopes.emplace_back(
|
|
options.Audience.HasValue()
|
|
? _detail::GetDefaultScopeForAudience(options.Audience.Value().GetAudience())
|
|
: _detail::GetDefaultScopeForAudience(m_url.GetAbsoluteUrl()));
|
|
|
|
perRetryPolicies.emplace_back(std::make_unique<TenantBearerTokenAuthenticationPolicy>(
|
|
credential, tokenContext, newOptions.EnableTenantDiscovery));
|
|
}
|
|
perOperationPolicies.emplace_back(std::make_unique<ServiceVersionPolicy>(newOptions.ApiVersion));
|
|
m_pipeline = std::make_shared<Azure::Core::Http::_internal::HttpPipeline>(
|
|
newOptions,
|
|
_detail::TablesServicePackageName,
|
|
PackageVersion::ToString(),
|
|
std::move(perRetryPolicies),
|
|
std::move(perOperationPolicies));
|
|
}
|
|
|
|
TableClient TableServiceClient::GetTableClient(
|
|
const std::string& tableName,
|
|
TableClientOptions const& options) const
|
|
{
|
|
if (m_tokenCredential != nullptr)
|
|
{
|
|
return TableClient(m_url.GetAbsoluteUrl(), tableName, m_tokenCredential, options);
|
|
}
|
|
if (!m_url.GetAbsoluteUrl().empty())
|
|
{
|
|
return TableClient(m_url.GetAbsoluteUrl(), tableName, options);
|
|
}
|
|
throw std::runtime_error("TableServiceClient is not properly initialized.");
|
|
}
|
|
|
|
Azure::Response<Models::PreflightCheckResult> TableServiceClient::PreflightCheck(
|
|
Models::PreflightCheckOptions const& options,
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendPath(Azure::Core::Url::Encode(options.TableName));
|
|
Core::Http::Request request(Core::Http::HttpMethod::Options, url);
|
|
request.SetHeader(OriginHeader, options.Origin);
|
|
request.SetHeader(AccessControlRequestMethodHeader, Core::Http::HttpMethod::Options.ToString());
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::Ok)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::PreflightCheckResult response;
|
|
|
|
return Response<Models::PreflightCheckResult>(std::move(response), std::move(rawResponse));
|
|
}
|
|
|
|
Azure::Response<Models::SetServicePropertiesResult> TableServiceClient::SetServiceProperties(
|
|
Models::SetServicePropertiesOptions const& options,
|
|
Core::Context const& context) const
|
|
{
|
|
std::string xmlBody = Serializers::SetServiceProperties(options);
|
|
auto url = m_url;
|
|
|
|
url.AppendQueryParameter(ResourceTypeHeader, ResourceTypeService);
|
|
url.AppendQueryParameter(CompHeader, ComponentProperties);
|
|
Core::IO::MemoryBodyStream requestBody(
|
|
reinterpret_cast<std::uint8_t const*>(xmlBody.data()), xmlBody.length());
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Put, url, &requestBody);
|
|
|
|
request.SetHeader(ContentTypeHeader, ContentTypeXml);
|
|
request.SetHeader(ContentLengthHeader, std::to_string(requestBody.Length()));
|
|
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::Accepted)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::SetServicePropertiesResult response;
|
|
|
|
return Response<Models::SetServicePropertiesResult>(std::move(response), std::move(rawResponse));
|
|
}
|
|
|
|
Azure::Response<Models::TableServiceProperties> TableServiceClient::GetServiceProperties(
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendQueryParameter(ResourceTypeHeader, ResourceTypeService);
|
|
url.AppendQueryParameter(CompHeader, ComponentProperties);
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Get, url);
|
|
|
|
auto pRawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = pRawResponse->GetStatusCode();
|
|
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::Ok)
|
|
{
|
|
throw Core::RequestFailedException(pRawResponse);
|
|
}
|
|
|
|
Models::TableServiceProperties response
|
|
= Serializers::ServicePropertiesFromXml(pRawResponse->GetBody());
|
|
|
|
return Response<Models::TableServiceProperties>(std::move(response), std::move(pRawResponse));
|
|
}
|
|
|
|
Azure::Response<Models::ServiceStatistics> TableServiceClient::GetStatistics(
|
|
const Core::Context& context) const
|
|
{
|
|
auto url = m_url;
|
|
std::string host = url.GetHost();
|
|
std::string accountName = host.substr(0, host.find('.'));
|
|
accountName += "-secondary";
|
|
url.SetHost(accountName + "." + host.substr(host.find('.') + 1));
|
|
url.AppendQueryParameter(ResourceTypeHeader, ResourceTypeService);
|
|
url.AppendQueryParameter(CompHeader, "stats");
|
|
Core::Http::Request request(Core::Http::HttpMethod::Get, url);
|
|
|
|
auto pRawResponse = m_pipeline->Send(request, context);
|
|
auto httpStatusCode = pRawResponse->GetStatusCode();
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::Ok)
|
|
{
|
|
throw Core::RequestFailedException(pRawResponse);
|
|
}
|
|
Models::ServiceStatistics response;
|
|
{
|
|
const auto& responseBody = pRawResponse->GetBody();
|
|
Xml::XmlReader reader(reinterpret_cast<const char*>(responseBody.data()), responseBody.size());
|
|
enum class XmlTagEnum
|
|
{
|
|
kUnknown,
|
|
kStorageServiceStats,
|
|
kGeoReplication,
|
|
kStatus,
|
|
kLastSyncTime,
|
|
};
|
|
const std::unordered_map<std::string, XmlTagEnum> XmlTagEnumMap{
|
|
{"StorageServiceStats", XmlTagEnum::kStorageServiceStats},
|
|
{"GeoReplication", XmlTagEnum::kGeoReplication},
|
|
{"Status", XmlTagEnum::kStatus},
|
|
{"LastSyncTime", XmlTagEnum::kLastSyncTime},
|
|
};
|
|
std::vector<XmlTagEnum> xmlPath;
|
|
|
|
while (true)
|
|
{
|
|
auto node = reader.Read();
|
|
if (node.Type == XmlNodeType::End)
|
|
{
|
|
break;
|
|
}
|
|
else if (node.Type == XmlNodeType::StartTag)
|
|
{
|
|
auto ite = XmlTagEnumMap.find(node.Name);
|
|
xmlPath.push_back(ite == XmlTagEnumMap.end() ? XmlTagEnum::kUnknown : ite->second);
|
|
}
|
|
else if (node.Type == XmlNodeType::Text)
|
|
{
|
|
if (xmlPath.size() == 3 && xmlPath[0] == XmlTagEnum::kStorageServiceStats
|
|
&& xmlPath[1] == XmlTagEnum::kGeoReplication && xmlPath[2] == XmlTagEnum::kStatus)
|
|
{
|
|
response.GeoReplication.Status = Models::GeoReplicationStatus(node.Value);
|
|
}
|
|
else if (
|
|
xmlPath.size() == 3 && xmlPath[0] == XmlTagEnum::kStorageServiceStats
|
|
&& xmlPath[1] == XmlTagEnum::kGeoReplication && xmlPath[2] == XmlTagEnum::kLastSyncTime)
|
|
{
|
|
response.GeoReplication.LastSyncedOn
|
|
= DateTime::Parse(node.Value, Azure::DateTime::DateFormat::Rfc1123);
|
|
}
|
|
}
|
|
else if (node.Type == XmlNodeType::Attribute)
|
|
{
|
|
}
|
|
else if (node.Type == XmlNodeType::EndTag)
|
|
{
|
|
|
|
xmlPath.pop_back();
|
|
}
|
|
}
|
|
}
|
|
return Response<Models::ServiceStatistics>(std::move(response), std::move(pRawResponse));
|
|
}
|
|
|
|
TableClient::TableClient(
|
|
std::string const& serviceUrl,
|
|
std::string const& tableName,
|
|
const TableClientOptions& options)
|
|
: m_url(serviceUrl), m_tableName(tableName)
|
|
{
|
|
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> perRetryPolicies;
|
|
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> perOperationPolicies;
|
|
perRetryPolicies.emplace_back(std::make_unique<TimeoutPolicy>());
|
|
perOperationPolicies.emplace_back(std::make_unique<ServiceVersionPolicy>(options.ApiVersion));
|
|
m_pipeline = std::make_shared<Azure::Core::Http::_internal::HttpPipeline>(
|
|
options,
|
|
_detail::TablesServicePackageName,
|
|
PackageVersion::ToString(),
|
|
std::move(perRetryPolicies),
|
|
std::move(perOperationPolicies));
|
|
}
|
|
|
|
TableClient::TableClient(
|
|
const std::string& serviceUrl,
|
|
const std::string& tableName,
|
|
std::shared_ptr<const Core::Credentials::TokenCredential> credential,
|
|
const TableClientOptions& options)
|
|
: TableClient(serviceUrl, tableName, options)
|
|
{
|
|
m_tableName = tableName;
|
|
TableClientOptions newOptions = options;
|
|
m_url = Azure::Core::Url(serviceUrl);
|
|
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> perRetryPolicies;
|
|
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>> perOperationPolicies;
|
|
perRetryPolicies.emplace_back(std::make_unique<TimeoutPolicy>());
|
|
perOperationPolicies.emplace_back(std::make_unique<ServiceVersionPolicy>(options.ApiVersion));
|
|
|
|
perRetryPolicies.emplace_back(std::make_unique<TimeoutPolicy>());
|
|
{
|
|
Azure::Core::Credentials::TokenRequestContext tokenContext;
|
|
tokenContext.Scopes.emplace_back(
|
|
options.Audience.HasValue()
|
|
? _detail::GetDefaultScopeForAudience(options.Audience.Value().GetAudience())
|
|
: _detail::GetDefaultScopeForAudience(m_url.GetAbsoluteUrl()));
|
|
|
|
perRetryPolicies.emplace_back(std::make_unique<TenantBearerTokenAuthenticationPolicy>(
|
|
credential, tokenContext, newOptions.EnableTenantDiscovery));
|
|
}
|
|
perOperationPolicies.emplace_back(std::make_unique<ServiceVersionPolicy>(newOptions.ApiVersion));
|
|
m_pipeline = std::make_shared<Azure::Core::Http::_internal::HttpPipeline>(
|
|
newOptions,
|
|
_detail::TablesServicePackageName,
|
|
PackageVersion::ToString(),
|
|
std::move(perRetryPolicies),
|
|
std::move(perOperationPolicies));
|
|
}
|
|
|
|
Azure::Response<Models::Table> TableServiceClient::CreateTable(
|
|
std::string const& tableName,
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendPath("Tables");
|
|
|
|
std::string jsonBody = Serializers::Create(tableName);
|
|
|
|
Core::IO::MemoryBodyStream requestBody(
|
|
reinterpret_cast<std::uint8_t const*>(jsonBody.data()), jsonBody.length());
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Post, url, &requestBody);
|
|
|
|
request.SetHeader(ContentTypeHeader, ContentTypeJson);
|
|
request.SetHeader(ContentLengthHeader, std::to_string(requestBody.Length()));
|
|
request.SetHeader(AcceptHeader, AcceptFullMeta);
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::Created)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::Table response{};
|
|
{
|
|
auto const& responseBody = rawResponse->GetBody();
|
|
if (responseBody.size() > 0)
|
|
{
|
|
auto const jsonRoot
|
|
= Core::Json::_internal::json::parse(responseBody.begin(), responseBody.end());
|
|
|
|
response.TableName = jsonRoot[TableName].get<std::string>();
|
|
response.EditLink = jsonRoot[ODataEditLink].get<std::string>();
|
|
response.Id = jsonRoot[ODataId].get<std::string>();
|
|
response.Metadata = jsonRoot[ODataMeta].get<std::string>();
|
|
response.Type = jsonRoot[ODataType].get<std::string>();
|
|
}
|
|
}
|
|
|
|
return Response<Models::Table>(std::move(response), std::move(rawResponse));
|
|
}
|
|
|
|
void Models::QueryTablesPagedResponse::OnNextPage(const Azure::Core::Context& context)
|
|
{
|
|
m_operationOptions.ContinuationToken = NextPageToken;
|
|
*this = m_tableServiceClient->QueryTables(m_operationOptions, context);
|
|
}
|
|
|
|
Models::QueryTablesPagedResponse TableServiceClient::QueryTables(
|
|
Models::QueryTablesOptions const& options,
|
|
Azure::Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendPath("Tables");
|
|
Core::Http::Request request(Core::Http::HttpMethod::Get, url);
|
|
request.SetHeader(AcceptHeader, AcceptFullMeta);
|
|
if (options.Prefix.HasValue())
|
|
{
|
|
request.GetUrl().AppendQueryParameter(IfMatch, options.Prefix.Value());
|
|
}
|
|
if (options.ContinuationToken.HasValue())
|
|
{
|
|
request.GetUrl().AppendQueryParameter("NextTableName", options.ContinuationToken.Value());
|
|
}
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::Ok)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::QueryTablesPagedResponse response{std::make_shared<TableServiceClient>(*this)};
|
|
{
|
|
auto const& responseBody = rawResponse->GetBody();
|
|
if (responseBody.size() > 0)
|
|
{
|
|
auto const jsonRoot
|
|
= Core::Json::_internal::json::parse(responseBody.begin(), responseBody.end());
|
|
std::string metadataLink = jsonRoot[ODataMeta].get<std::string>();
|
|
for (auto value : jsonRoot[Value])
|
|
{
|
|
Models::Table table;
|
|
table.TableName = value[TableName].get<std::string>();
|
|
table.EditLink = value[ODataEditLink].get<std::string>();
|
|
table.Id = value[ODataId].get<std::string>();
|
|
table.Type = value[ODataType].get<std::string>();
|
|
table.Metadata = metadataLink;
|
|
response.Tables.emplace_back(std::move(table));
|
|
}
|
|
}
|
|
|
|
response.ServiceEndpoint = url.GetAbsoluteUrl();
|
|
response.Prefix = options.Prefix;
|
|
response.m_operationOptions = options;
|
|
response.CurrentPageToken = options.ContinuationToken.ValueOr(std::string());
|
|
response.RawResponse = std::move(response.RawResponse);
|
|
auto headers = rawResponse->GetHeaders();
|
|
|
|
if (headers.find("x-ms-continuation-NextTableName") != headers.end())
|
|
{
|
|
response.NextPageToken = headers.at("x-ms-continuation-NextTableName");
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
Azure::Response<Models::DeleteTableResult> TableServiceClient::DeleteTable(
|
|
std::string const& tableName,
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendPath("Tables('" + Azure::Core::Url::Encode(tableName) + ClosingFragment);
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Delete, url);
|
|
|
|
request.SetHeader(ContentTypeHeader, ContentTypeJson);
|
|
request.SetHeader(AcceptHeader, AcceptFullMeta);
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::NoContent)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::DeleteTableResult response{};
|
|
|
|
return Response<Models::DeleteTableResult>(std::move(response), std::move(rawResponse));
|
|
}
|
|
|
|
Azure::Response<Models::AddEntityResult> TableClient::AddEntity(
|
|
Models::TableEntity const& tableEntity,
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendPath(Azure::Core::Url::Encode(m_tableName));
|
|
|
|
std::string jsonBody = Serializers::CreateEntity(tableEntity);
|
|
|
|
Core::IO::MemoryBodyStream requestBody(
|
|
reinterpret_cast<std::uint8_t const*>(jsonBody.data()), jsonBody.length());
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Post, url, &requestBody);
|
|
|
|
request.SetHeader(ContentTypeHeader, ContentTypeJson);
|
|
request.SetHeader(ContentLengthHeader, std::to_string(requestBody.Length()));
|
|
request.SetHeader(AcceptHeader, AcceptFullMeta);
|
|
request.SetHeader(PreferHeader, PreferNoContent);
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::NoContent)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::AddEntityResult response{};
|
|
response.ETag = rawResponse->GetHeaders().at("ETag");
|
|
return Response<Models::AddEntityResult>(std::move(response), std::move(rawResponse));
|
|
}
|
|
|
|
Azure::Response<Models::UpdateEntityResult> TableClient::UpdateEntityImpl(
|
|
Models::TableEntity const& tableEntity,
|
|
bool isUpsert,
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendPath(Azure::Core::Url::Encode(
|
|
m_tableName + PartitionKeyFragment + tableEntity.GetPartitionKey().Value + RowKeyFragment
|
|
+ tableEntity.GetRowKey().Value + ClosingFragment));
|
|
|
|
std::string jsonBody = Serializers::UpdateEntity(tableEntity);
|
|
|
|
Core::IO::MemoryBodyStream requestBody(
|
|
reinterpret_cast<std::uint8_t const*>(jsonBody.data()), jsonBody.length());
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Put, url, &requestBody);
|
|
|
|
request.SetHeader(ContentTypeHeader, ContentTypeJson);
|
|
request.SetHeader(ContentLengthHeader, std::to_string(requestBody.Length()));
|
|
request.SetHeader(AcceptHeader, AcceptFullMeta);
|
|
request.SetHeader(PreferHeader, PreferNoContent);
|
|
|
|
if (!isUpsert && !tableEntity.GetETag().Value.empty())
|
|
{
|
|
request.SetHeader(IfMatch, tableEntity.GetETag().Value);
|
|
}
|
|
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::NoContent)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::UpdateEntityResult response{};
|
|
|
|
response.ETag = rawResponse->GetHeaders().at("ETag");
|
|
return Response<Models::UpdateEntityResult>(std::move(response), std::move(rawResponse));
|
|
}
|
|
|
|
Azure::Response<Models::MergeEntityResult> TableClient::MergeEntityImpl(
|
|
Models::TableEntity const& tableEntity,
|
|
bool isUpsert,
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendPath(Azure::Core::Url::Encode(
|
|
m_tableName + PartitionKeyFragment + tableEntity.GetPartitionKey().Value + RowKeyFragment
|
|
+ tableEntity.GetRowKey().Value + ClosingFragment));
|
|
|
|
std::string jsonBody = Serializers::MergeEntity(tableEntity);
|
|
|
|
Core::IO::MemoryBodyStream requestBody(
|
|
reinterpret_cast<std::uint8_t const*>(jsonBody.data()), jsonBody.length());
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Patch, url, &requestBody);
|
|
|
|
request.SetHeader(ContentTypeHeader, ContentTypeJson);
|
|
request.SetHeader(ContentLengthHeader, std::to_string(requestBody.Length()));
|
|
request.SetHeader(AcceptHeader, AcceptFullMeta);
|
|
request.SetHeader(PreferHeader, PreferNoContent);
|
|
|
|
if (!isUpsert && !tableEntity.GetETag().Value.empty())
|
|
{
|
|
request.SetHeader(IfMatch, tableEntity.GetETag().Value);
|
|
}
|
|
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::NoContent)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::MergeEntityResult response{};
|
|
|
|
response.ETag = rawResponse->GetHeaders().at("ETag");
|
|
return Response<Models::MergeEntityResult>(std::move(response), std::move(rawResponse));
|
|
}
|
|
|
|
Azure::Response<Models::DeleteEntityResult> TableClient::DeleteEntity(
|
|
Models::TableEntity const& tableEntity,
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendPath(Azure::Core::Url::Encode(
|
|
m_tableName + PartitionKeyFragment + tableEntity.GetPartitionKey().Value + RowKeyFragment
|
|
+ tableEntity.GetRowKey().Value + ClosingFragment));
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Delete, url);
|
|
|
|
if (!tableEntity.GetETag().Value.empty())
|
|
{
|
|
request.SetHeader(IfMatch, tableEntity.GetETag().Value);
|
|
}
|
|
else
|
|
{
|
|
request.SetHeader(IfMatch, "*");
|
|
}
|
|
request.SetHeader(AcceptHeader, AcceptFullMeta);
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::NoContent)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::DeleteEntityResult response{};
|
|
return Response<Models::DeleteEntityResult>(std::move(response), std::move(rawResponse));
|
|
}
|
|
|
|
Azure::Response<Models::UpdateEntityResult> TableClient::UpdateOrInsertEntity(
|
|
Models::TableEntity const& tableEntity,
|
|
Core::Context const& context) const
|
|
{
|
|
return UpdateEntityImpl(tableEntity, true, context);
|
|
}
|
|
|
|
Azure::Response<Models::MergeEntityResult> TableClient::MergeOrInsertEntity(
|
|
Models::TableEntity const& tableEntity,
|
|
Core::Context const& context) const
|
|
{
|
|
return MergeEntityImpl(tableEntity, true, context);
|
|
}
|
|
|
|
Azure::Response<Models::UpdateEntityResult> TableClient::UpdateEntity(
|
|
Models::TableEntity const& tableEntity,
|
|
Core::Context const& context) const
|
|
{
|
|
return UpdateEntityImpl(tableEntity, false, context);
|
|
}
|
|
|
|
Azure::Response<Models::MergeEntityResult> TableClient::MergeEntity(
|
|
Models::TableEntity const& tableEntity,
|
|
Core::Context const& context) const
|
|
{
|
|
return MergeEntityImpl(tableEntity, false, context);
|
|
}
|
|
|
|
void Models::QueryEntitiesPagedResponse::OnNextPage(const Azure::Core::Context& context)
|
|
{
|
|
m_operationOptions.NextPartitionKey = NextPartitionKey;
|
|
m_operationOptions.NextRowKey = NextRowKey;
|
|
*this = m_tableClient->QueryEntities(m_operationOptions, context);
|
|
}
|
|
|
|
Azure::Response<Models::TableEntity> TableClient::GetEntity(
|
|
const std::string& partitionKey,
|
|
const std::string& rowKey,
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendPath(Azure::Core::Url::Encode(
|
|
m_tableName + PartitionKeyFragment + partitionKey + RowKeyFragment + rowKey
|
|
+ ClosingFragment));
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Get, url);
|
|
request.SetHeader(AcceptHeader, AcceptFullMeta);
|
|
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::Ok)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::TableEntity response{};
|
|
{
|
|
const auto& responseBody = rawResponse->GetBody();
|
|
|
|
auto const jsonRoot
|
|
= Azure::Core::Json::_internal::json::parse(responseBody.begin(), responseBody.end());
|
|
|
|
response = Serializers::DeserializeEntity(jsonRoot);
|
|
}
|
|
return Response<Models::TableEntity>(std::move(response), std::move(rawResponse));
|
|
}
|
|
|
|
Models::QueryEntitiesPagedResponse TableClient::QueryEntities(
|
|
Models::QueryEntitiesOptions const& options,
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
if (!options.NextPartitionKey.empty() && !options.NextRowKey.empty())
|
|
{
|
|
url.AppendPath(Azure::Core::Url::Encode(m_tableName));
|
|
url.AppendQueryParameter(
|
|
"NextPartitionKey", Azure::Core::Url::Encode(options.NextPartitionKey));
|
|
url.AppendQueryParameter("NextRowKey", Azure::Core::Url::Encode(options.NextRowKey));
|
|
}
|
|
else
|
|
{
|
|
std::string appendPath = Azure::Core::Url::Encode(m_tableName) + "(";
|
|
if (!options.PartitionKey.empty())
|
|
{
|
|
appendPath += "PartitionKey='" + Azure::Core::Url::Encode(options.PartitionKey) + "'";
|
|
}
|
|
if (!options.RowKey.empty())
|
|
{
|
|
appendPath += ",RowKey='" + Azure::Core::Url::Encode(options.RowKey) + "'";
|
|
}
|
|
appendPath += ")";
|
|
|
|
url.AppendPath(appendPath);
|
|
}
|
|
|
|
if (options.Filter.HasValue())
|
|
{
|
|
url.AppendQueryParameter("$filter", Azure::Core::Url::Encode(options.Filter.Value()));
|
|
}
|
|
if (!options.SelectColumns.empty())
|
|
{
|
|
url.AppendQueryParameter("$select", Azure::Core::Url::Encode(options.SelectColumns));
|
|
}
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Get, url);
|
|
request.SetHeader(AcceptHeader, AcceptFullMeta);
|
|
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::Ok)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::QueryEntitiesPagedResponse response(std::make_shared<TableClient>(*this));
|
|
{
|
|
const auto& responseBody = rawResponse->GetBody();
|
|
|
|
auto headers = rawResponse->GetHeaders();
|
|
if (headers.find("x-ms-continuation-NextPartitionKey") != headers.end())
|
|
{
|
|
response.NextPartitionKey = headers.at("x-ms-continuation-NextPartitionKey");
|
|
}
|
|
|
|
if (headers.find("x-ms-continuation-NextRowKey") != headers.end())
|
|
{
|
|
response.NextRowKey = headers.at("x-ms-continuation-NextRowKey");
|
|
}
|
|
|
|
if (!response.NextPartitionKey.empty() || !response.NextRowKey.empty())
|
|
{
|
|
response.NextPageToken = "true";
|
|
}
|
|
|
|
response.TableEntities.clear();
|
|
auto const jsonRoot
|
|
= Core::Json::_internal::json::parse(responseBody.begin(), responseBody.end());
|
|
|
|
if (!jsonRoot.contains(Value))
|
|
{
|
|
response.TableEntities.emplace_back(Serializers::DeserializeEntity(jsonRoot));
|
|
}
|
|
else
|
|
{
|
|
for (auto value : jsonRoot[Value])
|
|
{
|
|
response.TableEntities.emplace_back(Serializers::DeserializeEntity(value));
|
|
}
|
|
}
|
|
}
|
|
return response;
|
|
}
|
|
|
|
Azure::Response<Models::SubmitTransactionResult> TableClient::SubmitTransaction(
|
|
std::vector<Models::TransactionStep> const& steps,
|
|
Core::Context const& context) const
|
|
{
|
|
auto url = m_url;
|
|
url.AppendPath("$batch");
|
|
std::string const batchId = "batch_" + Azure::Core::Uuid::CreateUuid().ToString();
|
|
std::string const changesetId = "changeset_" + Azure::Core::Uuid::CreateUuid().ToString();
|
|
|
|
std::string const body = PreparePayload(batchId, changesetId, steps);
|
|
Core::IO::MemoryBodyStream requestBody(
|
|
reinterpret_cast<std::uint8_t const*>(body.data()), body.length());
|
|
|
|
Core::Http::Request request(Core::Http::HttpMethod::Post, url, &requestBody);
|
|
|
|
request.SetHeader(ContentTypeHeader, "multipart/mixed; boundary=" + batchId);
|
|
request.SetHeader(AcceptHeader, AcceptFullMeta);
|
|
request.SetHeader(ContentLengthHeader, std::to_string(requestBody.Length()));
|
|
request.SetHeader("Connection", "Keep-Alive");
|
|
request.SetHeader("DataServiceVersion", "3.0");
|
|
request.SetHeader("Accept-Charset", "UTF-8");
|
|
auto rawResponse = m_pipeline->Send(request, context);
|
|
auto const httpStatusCode = rawResponse->GetStatusCode();
|
|
if (httpStatusCode != Core::Http::HttpStatusCode::Accepted)
|
|
{
|
|
throw Core::RequestFailedException(rawResponse);
|
|
}
|
|
|
|
Models::SubmitTransactionResult response{};
|
|
{
|
|
const auto& responseBody = rawResponse->GetBody();
|
|
std::string responseString = std::string(responseBody.begin(), responseBody.end());
|
|
std::stringstream ss(responseString);
|
|
|
|
std::string line;
|
|
std::getline(ss, line, '\n');
|
|
Models::TransactionError error;
|
|
while (line.size() != 0)
|
|
{
|
|
std::getline(ss, line, '\n');
|
|
if (line.find("HTTP") != std::string::npos)
|
|
{
|
|
std::string status = line.substr(line.find(" ") + 1, 3);
|
|
response.StatusCode = status;
|
|
}
|
|
|
|
if (line.find(ODataError) != std::string::npos)
|
|
{
|
|
auto const jsonRoot = Core::Json::_internal::json::parse(line.begin(), line.end());
|
|
if (jsonRoot[ODataError].contains("code"))
|
|
{
|
|
error.Code = jsonRoot[ODataError]["code"].get<std::string>();
|
|
}
|
|
if (jsonRoot[ODataError].contains("message")
|
|
&& jsonRoot[ODataError]["message"].contains(Value))
|
|
{
|
|
error.Message = jsonRoot[ODataError]["message"][Value].get<std::string>();
|
|
}
|
|
}
|
|
}
|
|
if (error.Message.size() != 0)
|
|
{
|
|
response.Error = error;
|
|
}
|
|
}
|
|
return Response<Models::SubmitTransactionResult>(std::move(response), std::move(rawResponse));
|
|
}
|
|
|
|
std::string TableClient::PreparePayload(
|
|
std::string const& batchId,
|
|
std::string const& changesetId,
|
|
std::vector<Models::TransactionStep> const& steps) const
|
|
{
|
|
std::string accumulator
|
|
= "--" + batchId + "\nContent-Type: multipart/mixed; boundary=" + changesetId + "\n\n";
|
|
|
|
for (auto step : steps)
|
|
{
|
|
switch (step.Action)
|
|
{
|
|
case Models::TransactionActionType::Add:
|
|
accumulator += PrepAddEntity(changesetId, step.Entity);
|
|
break;
|
|
case Models::TransactionActionType::Delete:
|
|
accumulator += PrepDeleteEntity(changesetId, step.Entity);
|
|
break;
|
|
case Models::TransactionActionType::InsertMerge:
|
|
case Models::TransactionActionType::UpdateMerge:
|
|
accumulator += PrepMergeEntity(changesetId, step.Entity);
|
|
break;
|
|
case Models::TransactionActionType::InsertReplace:
|
|
accumulator += PrepInsertEntity(changesetId, step.Entity);
|
|
break;
|
|
case Models::TransactionActionType::UpdateReplace:
|
|
accumulator += PrepUpdateEntity(changesetId, step.Entity);
|
|
break;
|
|
}
|
|
}
|
|
|
|
accumulator += "\n\n--" + changesetId + "--\n";
|
|
accumulator += "--" + batchId + "\n";
|
|
return accumulator;
|
|
}
|
|
std::string TableClient::PrepAddEntity(std::string const& changesetId, Models::TableEntity entity)
|
|
const
|
|
{
|
|
std::string returnValue = "--" + changesetId + "\n";
|
|
returnValue += "Content-Type: application/http\n";
|
|
returnValue += "Content-Transfer-Encoding: binary\n\n";
|
|
auto url = m_url;
|
|
url.AppendPath(m_tableName);
|
|
returnValue += "POST " + url.GetAbsoluteUrl() + " HTTP/1.1\n";
|
|
returnValue += "Content-Type: application/json\n";
|
|
returnValue += "Accept: application/json;odata=minimalmetadata\n";
|
|
returnValue += "Prefer: return-no-content\n";
|
|
returnValue += "DataServiceVersion: 3.0;\n\n";
|
|
returnValue += Serializers::CreateEntity(entity);
|
|
return returnValue;
|
|
}
|
|
std::string TableClient::PrepDeleteEntity(
|
|
std::string const& changesetId,
|
|
Models::TableEntity entity) const
|
|
{
|
|
std::string returnValue = "--" + changesetId + "\n";
|
|
returnValue += "Content-Type: application/http\n";
|
|
returnValue += "Content-Transfer-Encoding: binary\n\n";
|
|
auto url = m_url;
|
|
url.AppendPath(
|
|
m_tableName + PartitionKeyFragment + entity.GetPartitionKey().Value + RowKeyFragment
|
|
+ entity.GetRowKey().Value + ClosingFragment);
|
|
returnValue += "DELETE " + url.GetAbsoluteUrl() + " HTTP/1.1\n";
|
|
returnValue += "Accept: application/json;odata=minimalmetadata\n";
|
|
returnValue += "Prefer: return-no-content\n";
|
|
returnValue += "DataServiceVersion: 3.0;\n";
|
|
if (!entity.GetETag().Value.empty())
|
|
{
|
|
returnValue += "If-Match: " + entity.GetETag().Value;
|
|
}
|
|
else
|
|
{
|
|
returnValue += "If-Match: *";
|
|
}
|
|
returnValue += "\n";
|
|
return returnValue;
|
|
}
|
|
|
|
std::string TableClient::PrepMergeEntity(std::string const& changesetId, Models::TableEntity entity)
|
|
const
|
|
{
|
|
std::string returnValue = "--" + changesetId + "\n";
|
|
returnValue += "Content-Type: application/http\n";
|
|
returnValue += "Content-Transfer-Encoding: binary\n\n";
|
|
auto url = m_url;
|
|
url.AppendPath(
|
|
m_tableName + PartitionKeyFragment + entity.GetPartitionKey().Value + RowKeyFragment
|
|
+ entity.GetRowKey().Value + ClosingFragment);
|
|
returnValue += "MERGE " + url.GetAbsoluteUrl() + " HTTP/1.1\n";
|
|
returnValue += "Content-Type: application/json\n";
|
|
returnValue += "Accept: application/json;odata=minimalmetadata\n";
|
|
returnValue += "DataServiceVersion: 3.0;\n\n";
|
|
returnValue += Serializers::MergeEntity(entity);
|
|
|
|
return returnValue;
|
|
}
|
|
|
|
std::string TableClient::PrepUpdateEntity(
|
|
std::string const& changesetId,
|
|
Models::TableEntity entity) const
|
|
{
|
|
std::string returnValue = "--" + changesetId + "\n";
|
|
returnValue += "Content-Type: application/http\n";
|
|
returnValue += "Content-Transfer-Encoding: binary\n\n";
|
|
auto url = m_url;
|
|
url.AppendPath(
|
|
m_tableName + PartitionKeyFragment + entity.GetPartitionKey().Value + RowKeyFragment
|
|
+ entity.GetRowKey().Value + ClosingFragment);
|
|
returnValue += "PUT " + url.GetAbsoluteUrl() + " HTTP/1.1\n";
|
|
returnValue += "Content-Type: application/json\n";
|
|
returnValue += "Accept: application/json;odata=minimalmetadata\n";
|
|
returnValue += "Prefer: return-no-content\n";
|
|
returnValue += "DataServiceVersion: 3.0;\n";
|
|
if (!entity.GetETag().Value.empty())
|
|
{
|
|
returnValue += "If-Match: " + entity.GetETag().Value;
|
|
}
|
|
else
|
|
{
|
|
returnValue += "If-Match: *";
|
|
}
|
|
returnValue += "\n\n";
|
|
returnValue += Serializers::UpdateEntity(entity);
|
|
return returnValue;
|
|
}
|
|
|
|
std::string TableClient::PrepInsertEntity(
|
|
std::string const& changesetId,
|
|
Models::TableEntity entity) const
|
|
{
|
|
std::string payload = Serializers::UpdateEntity(entity);
|
|
std::string returnValue = "--" + changesetId + "\n";
|
|
returnValue += "Content-Type: application/http\n";
|
|
returnValue += "Content-Transfer-Encoding: binary\n\n";
|
|
auto url = m_url;
|
|
url.AppendPath(
|
|
m_tableName + PartitionKeyFragment + entity.GetPartitionKey().Value + RowKeyFragment
|
|
+ entity.GetRowKey().Value + ClosingFragment);
|
|
returnValue += "PATCH " + url.GetAbsoluteUrl() + " HTTP/1.1\n";
|
|
returnValue += "Content-Type: application/json\n";
|
|
returnValue += "Content-Length: " + std::to_string(payload.length());
|
|
returnValue += "Accept: application/json;odata=minimalmetadata\n";
|
|
returnValue += "Prefer: return-no-content\n";
|
|
returnValue += "DataServiceVersion: 3.0;\n";
|
|
if (!entity.GetETag().Value.empty())
|
|
{
|
|
returnValue += "If-Match: " + entity.GetETag().Value;
|
|
}
|
|
else
|
|
{
|
|
returnValue += "If-Match: *";
|
|
}
|
|
returnValue += "\n\n";
|
|
return returnValue;
|
|
}
|