@azure/data-tables
Version:
An isomorphic client library for the Azure Tables service.
287 lines • 12.3 kB
JavaScript
;
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.InternalTableTransaction = exports.TableTransaction = void 0;
exports.parseTransactionResponse = parseTransactionResponse;
exports.prepateTransactionPipeline = prepateTransactionPipeline;
const core_client_1 = require("@azure/core-client");
const core_rest_pipeline_1 = require("@azure/core-rest-pipeline");
const transactionHelpers_js_1 = require("./utils/transactionHelpers.js");
const TablePolicies_js_1 = require("./TablePolicies.js");
const cosmosPathPolicy_js_1 = require("./cosmosPathPolicy.js");
const transactionHeaders_js_1 = require("./utils/transactionHeaders.js");
const isCosmosEndpoint_js_1 = require("./utils/isCosmosEndpoint.js");
const tracing_js_1 = require("./utils/tracing.js");
/**
* Helper to build a list of transaction actions
*/
class TableTransaction {
/**
* List of actions to perform in a transaction
*/
actions;
constructor(actions) {
this.actions = actions ?? [];
}
/**
* Adds a create action to the transaction
* @param entity - entity to create
*/
createEntity(entity) {
this.actions.push(["create", entity]);
}
/**
* Adds a delete action to the transaction
* @param partitionKey - partition key of the entity to delete
* @param rowKey - rowKey of the entity to delete
*/
deleteEntity(partitionKey, rowKey) {
this.actions.push(["delete", { partitionKey, rowKey }]);
}
/**
* Adds an update action to the transaction
* @param entity - entity to update
* @param updateModeOrOptions - update mode or update options
* @param updateOptions - options for the update operation
*/
updateEntity(entity, updateModeOrOptions, updateOptions) {
// UpdateMode is a string union
const realUpdateMode = typeof updateModeOrOptions === "string" ? updateModeOrOptions : undefined;
const realUpdateOptions = typeof updateModeOrOptions === "object" ? updateModeOrOptions : updateOptions;
this.actions.push(["update", entity, realUpdateMode ?? "Merge", realUpdateOptions ?? {}]);
}
/**
* Adds an upsert action to the transaction, which inserts if the entity doesn't exist or updates the existing one
* @param entity - entity to upsert
* @param updateMode - update mode
*/
upsertEntity(entity, updateMode = "Merge") {
this.actions.push(["upsert", entity, updateMode]);
}
}
exports.TableTransaction = TableTransaction;
/**
* TableTransaction collects sub-operations that can be submitted together via submitTransaction
*/
class InternalTableTransaction {
/**
* Table Account URL
*/
url;
/**
* State that holds the information about a particular transation
*/
state;
interceptClient;
allowInsecureConnection;
client;
/**
* @param url - Tables account url
* @param partitionKey - partition key
* @param credential - credential to authenticate the transaction request
*/
constructor(url, partitionKey, transactionId, changesetId,
// eslint-disable-next-line @azure/azure-sdk/ts-use-interface-parameters
client, interceptClient, credential, allowInsecureConnection = false) {
this.client = client;
this.url = url;
this.interceptClient = interceptClient;
this.allowInsecureConnection = allowInsecureConnection;
// Initialize the state
this.state = this.initializeState(transactionId, changesetId, partitionKey);
// Depending on the auth method used we need to build the url
if (!credential) {
// When the SAS token is provided as part of the URL we need to move it after $batch
const urlParts = url.split("?");
this.url = urlParts[0];
const sas = urlParts.length > 1 ? `?${urlParts[1]}` : "";
this.url = `${this.getUrlWithSlash()}$batch${sas}`;
}
else {
// When using a SharedKey credential no SAS token is needed
this.url = `${this.getUrlWithSlash()}$batch`;
}
}
initializeState(transactionId, changesetId, partitionKey) {
const pendingOperations = [];
const bodyParts = (0, transactionHelpers_js_1.getInitialTransactionBody)(transactionId, changesetId);
const isCosmos = (0, isCosmosEndpoint_js_1.isCosmosEndpoint)(this.url);
prepateTransactionPipeline(this.interceptClient.pipeline, bodyParts, changesetId, isCosmos);
return {
transactionId,
changesetId,
partitionKey,
pendingOperations,
bodyParts,
};
}
/**
* Adds a createEntity operation to the transaction
* @param entity - Entity to create
*/
createEntity(entity) {
this.checkPartitionKey(entity.partitionKey);
this.state.pendingOperations.push(this.interceptClient.createEntity(entity));
}
/**
* Adds a createEntity operation to the transaction per each entity in the entities array
* @param entities - Array of entities to create
*/
createEntities(entities) {
for (const entity of entities) {
this.checkPartitionKey(entity.partitionKey);
this.state.pendingOperations.push(this.interceptClient.createEntity(entity));
}
}
/**
* Adds a deleteEntity operation to the transaction
* @param partitionKey - Partition key of the entity to delete
* @param rowKey - Row key of the entity to delete
* @param options - Options for the delete operation
*/
deleteEntity(partitionKey, rowKey, options) {
this.checkPartitionKey(partitionKey);
this.state.pendingOperations.push(this.interceptClient.deleteEntity(partitionKey, rowKey, options));
}
/**
* Adds an updateEntity operation to the transaction
* @param entity - Entity to update
* @param mode - Update mode (Merge or Replace)
* @param options - Options for the update operation
*/
updateEntity(entity, mode, options) {
this.checkPartitionKey(entity.partitionKey);
this.state.pendingOperations.push(this.interceptClient.updateEntity(entity, mode, options));
}
/**
* Adds an upsertEntity operation to the transaction
* @param entity - The properties for the table entity.
* @param mode - The different modes for updating the entity:
* - Merge: Updates an entity by updating the entity's properties without replacing the existing entity.
* - Replace: Updates an existing entity by replacing the entire entity.
* @param options - The options parameters.
*/
upsertEntity(entity, mode, options) {
this.checkPartitionKey(entity.partitionKey);
this.state.pendingOperations.push(this.interceptClient.upsertEntity(entity, mode, options));
}
/**
* Submits the operations in the transaction
* @param options - Options for the request.
*/
async submitTransaction(options = {}) {
await Promise.all(this.state.pendingOperations);
const body = (0, transactionHelpers_js_1.getTransactionHttpRequestBody)(this.state.bodyParts, this.state.transactionId, this.state.changesetId);
const headers = (0, transactionHeaders_js_1.getTransactionHeaders)(this.state.transactionId);
return tracing_js_1.tracingClient.withSpan("TableTransaction.submitTransaction", options, async (updatedOptions) => {
const request = (0, core_rest_pipeline_1.createPipelineRequest)({
...updatedOptions,
url: this.url,
method: "POST",
body,
headers: (0, core_rest_pipeline_1.createHttpHeaders)(headers),
allowInsecureConnection: this.allowInsecureConnection,
});
const rawTransactionResponse = await this.client.sendRequest(request);
return parseTransactionResponse(rawTransactionResponse);
});
}
checkPartitionKey(partitionKey) {
if (this.state.partitionKey !== partitionKey) {
throw new Error("All operations in a transaction must target the same partitionKey");
}
}
getUrlWithSlash() {
return this.url.endsWith("/") ? this.url : `${this.url}/`;
}
}
exports.InternalTableTransaction = InternalTableTransaction;
function parseTransactionResponse(transactionResponse) {
const subResponsePrefix = `--changesetresponse_`;
const status = transactionResponse.status;
const rawBody = transactionResponse.bodyAsText || "";
const splitBody = rawBody.split(subResponsePrefix);
const isSuccessByStatus = 200 <= status && status < 300;
if (!isSuccessByStatus) {
handleBodyError(rawBody, status, transactionResponse.request, transactionResponse);
}
// Dropping the first and last elements as they are the boundaries
// we just care about sub request content
const subResponses = splitBody.slice(1, splitBody.length - 1);
const responses = subResponses.map((subResponse) => {
const statusMatch = subResponse.match(/HTTP\/1.1 ([0-9]*)/);
if (statusMatch?.length !== 2) {
throw new Error(`Couldn't extract status from sub-response:\n ${subResponse}`);
}
const subResponseStatus = Number.parseInt(statusMatch[1]);
if (!Number.isInteger(subResponseStatus)) {
throw new Error(`Expected sub-response status to be an integer ${subResponseStatus}`);
}
const bodyMatch = subResponse.match(/\{(.*)\}/);
if (bodyMatch?.length === 2) {
handleBodyError(bodyMatch[0], subResponseStatus, transactionResponse.request, transactionResponse);
}
const etagMatch = subResponse.match(/ETag: (.*)/);
const rowKeyMatch = subResponse.match(/RowKey='(.*)'/);
return {
status: subResponseStatus,
...(rowKeyMatch?.length === 2 && { rowKey: rowKeyMatch[1] }),
...(etagMatch?.length === 2 && { etag: etagMatch[1] }),
};
});
return {
status,
subResponses: responses,
getResponseForEntity: (rowKey) => responses.find((r) => r.rowKey === rowKey),
};
}
function handleBodyError(bodyAsText, statusCode, request, response) {
let parsedError;
try {
parsedError = JSON.parse(bodyAsText);
}
catch {
parsedError = {};
}
let message = "Transaction Failed";
let code;
// Only transaction sub-responses return body
if (parsedError && parsedError["odata.error"]) {
const error = parsedError["odata.error"];
message = error.message?.value ?? message;
code = error.code;
}
throw new core_rest_pipeline_1.RestError(message, {
code,
statusCode,
request,
response,
});
}
/**
* Prepares the transaction pipeline to intercept operations
* @param pipeline - Client pipeline
*/
function prepateTransactionPipeline(pipeline, bodyParts, changesetId, isCosmos) {
// 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((0, core_client_1.serializationPolicy)(), { phase: "Serialize" });
pipeline.addPolicy((0, TablePolicies_js_1.transactionHeaderFilterPolicy)());
pipeline.addPolicy((0, TablePolicies_js_1.transactionRequestAssemblePolicy)(bodyParts, changesetId));
if (isCosmos) {
pipeline.addPolicy((0, cosmosPathPolicy_js_1.cosmosPatchPolicy)(), {
afterPolicies: [TablePolicies_js_1.transactionHeaderFilterPolicyName],
beforePolicies: [core_client_1.serializationPolicyName, TablePolicies_js_1.transactionRequestAssemblePolicyName],
});
}
}
//# sourceMappingURL=TableTransaction.js.map