"use strict";
// 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