// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { GeneratedClient } from "./generated/index.js";
import { isNamedKeyCredential, isSASCredential, isTokenCredential } from "@azure/core-auth";
import { COSMOS_SCOPE, STORAGE_SCOPE, TablesLoggingAllowedHeaderNames } from "./utils/constants.js";
import { decodeContinuationToken, encodeContinuationToken } from "./utils/continuationToken.js";
import { deserialize, deserializeObjectsArray, deserializeSignedIdentifier, serialize, serializeQueryOptions, serializeSignedIdentifiers, } from "./serialization.js";
import { parseXML, stringifyXML } from "@azure/core-xml";
import { InternalTableTransaction } from "./TableTransaction.js";
import { Uuid } from "./utils/uuid.js";
import { apiVersionPolicy } from "./utils/apiVersionPolicy.js";
import { cosmosPatchPolicy } from "./cosmosPathPolicy.js";
import { escapeQuotes } from "./odata.js";
import { getClientParamsFromConnectionString } from "./utils/connectionString.js";
import { handleTableAlreadyExists } from "./utils/errorHelpers.js";
import { isCosmosEndpoint } from "./utils/isCosmosEndpoint.js";
import { isCredential } from "./utils/isCredential.js";
import { logger } from "./logger.js";
import { setTokenChallengeAuthenticationPolicy } from "./utils/challengeAuthenticationUtils.js";
import { tablesNamedKeyCredentialPolicy } from "./tablesNamedCredentialPolicy.js";
import { tablesSASTokenPolicy } from "./tablesSASTokenPolicy.js";
import { tracingClient } from "./utils/tracing.js";
/**
 * A TableClient represents a Client to the Azure Tables service allowing you
 * to perform operations on a single table.
 */
export class TableClient {
    /**
     * Table Account URL
     */
    url;
    /**
     * Represents a pipeline for making a HTTP request to a URL.
     * Pipelines can have multiple policies to manage manipulating each request before and after it is made to the server.
     */
    pipeline;
    table;
    generatedClient;
    credential;
    clientOptions;
    allowInsecureConnection;
    /**
     * Name of the table to perform operations on.
     */
    tableName;
    constructor(url, tableName, credentialOrOptions, options = {}) {
        this.url = url;
        this.tableName = tableName;
        const isCosmos = isCosmosEndpoint(this.url);
        const credential = isCredential(credentialOrOptions) ? credentialOrOptions : undefined;
        this.credential = credential;
        this.clientOptions = (!isCredential(credentialOrOptions) ? credentialOrOptions : options) || {};
        this.allowInsecureConnection = this.clientOptions.allowInsecureConnection ?? false;
        const internalPipelineOptions = {
            ...this.clientOptions,
            endpoint: this.clientOptions.endpoint || this.url,
            loggingOptions: {
                logger: logger.info,
                additionalAllowedHeaderNames: [...TablesLoggingAllowedHeaderNames],
            },
            deserializationOptions: {
                parseXML,
            },
            serializationOptions: {
                stringifyXML,
            },
        };
        const generatedClient = new GeneratedClient(this.url, internalPipelineOptions);
        if (isNamedKeyCredential(credential)) {
            generatedClient.pipeline.addPolicy(tablesNamedKeyCredentialPolicy(credential));
        }
        else if (isSASCredential(credential)) {
            generatedClient.pipeline.addPolicy(tablesSASTokenPolicy(credential));
        }
        if (isTokenCredential(credential)) {
            const scope = isCosmos ? COSMOS_SCOPE : STORAGE_SCOPE;
            setTokenChallengeAuthenticationPolicy(generatedClient.pipeline, credential, scope);
        }
        if (isCosmos) {
            generatedClient.pipeline.addPolicy(cosmosPatchPolicy());
        }
        if (options.version) {
            generatedClient.pipeline.addPolicy(apiVersionPolicy(options.version));
        }
        this.generatedClient = generatedClient;
        this.table = generatedClient.table;
        this.pipeline = generatedClient.pipeline;
    }
    /**
     * Permanently deletes the current table with all of its entities.
     * @param options - The options parameters.
     *
     * ### Example deleting a table
     * ```ts snippet:ReadmeSampleDeleteTable
     * import { DefaultAzureCredential } from "@azure/identity";
     * import { TableClient } from "@azure/data-tables";
     *
     * const account = "<account>";
     * const accountKey = "<accountkey>";
     * const tableName = "<tableName>";
     *
     * const credential = new DefaultAzureCredential();
     * const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
     *
     * await client.deleteTable();
     * ```
     */
    deleteTable(options = {}) {
        return tracingClient.withSpan("TableClient.deleteTable", options, async (updatedOptions) => {
            try {
                await this.table.delete(this.tableName, updatedOptions);
            }
            catch (e) {
                if (e.statusCode === 404) {
                    logger.info("TableClient.deleteTable: Table doesn't exist");
                }
                else {
                    throw e;
                }
            }
        });
    }
    /**
     *  Creates a table with the tableName passed to the client constructor
     * @param options - The options parameters.
     *
     * ### Example creating a table
     * ```ts snippet:ReadmeSampleTableClientCreateTable
     * import { DefaultAzureCredential } from "@azure/identity";
     * import { TableClient } from "@azure/data-tables";
     *
     * const account = "<account>";
     * const accountKey = "<accountkey>";
     * const tableName = "<tableName>";
     *
     * const credential = new DefaultAzureCredential();
     * const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
     *
     * // If the table 'newTable' already exists, createTable doesn't throw
     * await client.createTable();
     * ```
     */
    createTable(options = {}) {
        return tracingClient.withSpan("TableClient.createTable", options, async (updatedOptions) => {
            try {
                await this.table.create({ name: this.tableName }, updatedOptions);
            }
            catch (e) {
                handleTableAlreadyExists(e, { ...updatedOptions, logger, tableName: this.tableName });
            }
        });
    }
    /**
     * Returns a single entity in the table.
     * @param partitionKey - The partition key of the entity.
     * @param rowKey - The row key of the entity.
     * @param options - The options parameters.
     *
     * ### Example getting an entity
     * ```ts snippet:ReadmeSampleGetEntity
     * import { DefaultAzureCredential } from "@azure/identity";
     * import { TableClient } from "@azure/data-tables";
     *
     * const account = "<account>";
     * const accountKey = "<accountkey>";
     * const tableName = "<tableName>";
     *
     * const credential = new DefaultAzureCredential();
     * const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
     *
     * const entity = await client.getEntity("<partitionKey>", "<rowKey>");
     * console.log(`Entity: PartitionKey: ${entity.partitionKey} RowKey: ${entity.rowKey}`);
     * ```
     */
    getEntity(partitionKey, rowKey, 
    // eslint-disable-next-line @azure/azure-sdk/ts-naming-options
    options = {}) {
        if (partitionKey === undefined || partitionKey === null) {
            throw new Error("The entity's partitionKey cannot be undefined or null.");
        }
        if (rowKey === undefined || rowKey === null) {
            throw new Error("The entity's rowKey cannot be undefined or null.");
        }
        return tracingClient.withSpan("TableClient.getEntity", options, async (updatedOptions) => {
            let parsedBody;
            function onResponse(rawResponse, flatResponse) {
                parsedBody = rawResponse.parsedBody;
                if (updatedOptions.onResponse) {
                    updatedOptions.onResponse(rawResponse, flatResponse);
                }
            }
            const { disableTypeConversion, queryOptions, ...getEntityOptions } = updatedOptions;
            await this.table.queryEntitiesWithPartitionAndRowKey(this.tableName, escapeQuotes(partitionKey), escapeQuotes(rowKey), {
                ...getEntityOptions,
                queryOptions: serializeQueryOptions(queryOptions || {}),
                onResponse,
            });
            const tableEntity = deserialize(parsedBody, disableTypeConversion ?? false);
            return tableEntity;
        });
    }
    /**
     * Queries entities in a table.
     * @param options - The options parameters.
     *
     * Example listing entities
     * ```ts snippet:ReadmeSampleListEntities
     * import { DefaultAzureCredential } from "@azure/identity";
     * import { TableClient } from "@azure/data-tables";
     *
     * const account = "<account>";
     * const accountKey = "<accountkey>";
     * const tableName = "<tableName>";
     *
     * const credential = new DefaultAzureCredential();
     * const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
     *
     * let i = 0;
     * const entities = client.listEntities();
     * for await (const entity of entities) {
     *   console.log(`Entity${++i}: PartitionKey: ${entity.partitionKey} RowKey: ${entity.rowKey}`);
     * }
     * ```
     */
    listEntities(
    // eslint-disable-next-line @azure/azure-sdk/ts-naming-options
    options = {}) {
        const tableName = this.tableName;
        const iter = this.listEntitiesAll(tableName, options);
        return {
            next() {
                return iter.next();
            },
            [Symbol.asyncIterator]() {
                return this;
            },
            byPage: (settings) => {
                const pageOptions = {
                    ...options,
                    queryOptions: { ...options.queryOptions, top: settings?.maxPageSize },
                };
                if (settings?.continuationToken) {
                    pageOptions.continuationToken = settings.continuationToken;
                }
                return this.listEntitiesPage(tableName, pageOptions);
            },
        };
    }
    async *listEntitiesAll(tableName, options) {
        const firstPage = await this._listEntities(tableName, options);
        yield* firstPage;
        if (firstPage.continuationToken) {
            const optionsWithContinuation = {
                ...options,
                continuationToken: firstPage.continuationToken,
            };
            for await (const page of this.listEntitiesPage(tableName, optionsWithContinuation)) {
                yield* page;
            }
        }
    }
    async *listEntitiesPage(tableName, options = {}) {
        let result = await tracingClient.withSpan("TableClient.listEntitiesPage", options, (updatedOptions) => this._listEntities(tableName, updatedOptions));
        yield result;
        while (result.continuationToken) {
            const optionsWithContinuation = {
                ...options,
                continuationToken: result.continuationToken,
            };
            result = await tracingClient.withSpan("TableClient.listEntitiesPage", optionsWithContinuation, (updatedOptions, span) => {
                span.setAttribute("continuationToken", result.continuationToken);
                return this._listEntities(tableName, updatedOptions);
            });
            yield result;
        }
    }
    async _listEntities(tableName, options = {}) {
        const { disableTypeConversion = false } = options;
        const queryOptions = serializeQueryOptions(options.queryOptions || {});
        const listEntitiesOptions = {
            ...options,
            queryOptions,
        };
        // If a continuation token is used, decode it and set the next row and partition key
        if (options.continuationToken) {
            const continuationToken = decodeContinuationToken(options.continuationToken);
            listEntitiesOptions.nextRowKey = continuationToken.nextRowKey;
            listEntitiesOptions.nextPartitionKey = continuationToken.nextPartitionKey;
        }
        const { xMsContinuationNextPartitionKey: nextPartitionKey, xMsContinuationNextRowKey: nextRowKey, value, } = await this.table.queryEntities(tableName, listEntitiesOptions);
        const tableEntities = deserializeObjectsArray(value ?? [], disableTypeConversion);
        // Encode nextPartitionKey and nextRowKey as a single continuation token and add it as a
        // property to the page.
        const continuationToken = encodeContinuationToken(nextPartitionKey, nextRowKey);
        const page = Object.assign([...tableEntities], {
            continuationToken,
        });
        return page;
    }
    /**
     * Insert entity in the table.
     * @param entity - The properties for the table entity.
     * @param options - The options parameters.
     *
     * ### Example creating an entity
     * ```ts snippet:ReadmeSampleCreateEntity
     * import { DefaultAzureCredential } from "@azure/identity";
     * import { TableClient } from "@azure/data-tables";
     *
     * const account = "<account>";
     * const accountKey = "<accountkey>";
     * const tableName = "<tableName>";
     *
     * const credential = new DefaultAzureCredential();
     * const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
     *
     * const testEntity = {
     *   partitionKey: "P1",
     *   rowKey: "R1",
     *   foo: "foo",
     *   bar: 123,
     * };
     * await client.createEntity(testEntity);
     * ```
     */
    createEntity(entity, options = {}) {
        return tracingClient.withSpan("TableClient.createEntity", options, (updatedOptions) => {
            const { ...createTableEntity } = updatedOptions || {};
            return this.table.insertEntity(this.tableName, {
                ...createTableEntity,
                tableEntityProperties: serialize(entity),
                responsePreference: "return-no-content",
            });
        });
    }
    /**
     * Deletes the specified entity in the table.
     * @param partitionKey - The partition key of the entity.
     * @param rowKey - The row key of the entity.
     * @param options - The options parameters.
     *
     * ### Example deleting an entity
     * ```ts snippet:ReadmeSampleDeleteEntity
     * import { DefaultAzureCredential } from "@azure/identity";
     * import { TableClient } from "@azure/data-tables";
     *
     * const account = "<account>";
     * const accountKey = "<accountkey>";
     * const tableName = "<tableName>";
     *
     * const credential = new DefaultAzureCredential();
     * const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
     *
     * // deleteEntity deletes the entity that matches exactly the partitionKey and rowKey
     * await client.deleteEntity("<partitionKey>", "<rowKey>");
     * ```
     */
    deleteEntity(partitionKey, rowKey, 
    // eslint-disable-next-line @azure/azure-sdk/ts-naming-options
    options = {}) {
        if (partitionKey === undefined || partitionKey === null) {
            throw new Error("The entity's partitionKey cannot be undefined or null.");
        }
        if (rowKey === undefined || rowKey === null) {
            throw new Error("The entity's rowKey cannot be undefined or null.");
        }
        return tracingClient.withSpan("TableClient.deleteEntity", options, (updatedOptions) => {
            const { etag = "*", ...rest } = updatedOptions;
            const deleteOptions = {
                ...rest,
            };
            return this.table.deleteEntity(this.tableName, escapeQuotes(partitionKey), escapeQuotes(rowKey), etag, deleteOptions);
        });
    }
    /**
     * Update an entity in the table.
     * @param entity - The properties of the entity to be updated.
     * @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.
     *
     * ### Example updating an entity
     * ```ts snippet:ReadmeSampleUpdateEntity
     * import { DefaultAzureCredential } from "@azure/identity";
     * import { TableClient } from "@azure/data-tables";
     *
     * const account = "<account>";
     * const accountKey = "<accountkey>";
     * const tableName = "<tableName>";
     *
     * const credential = new DefaultAzureCredential();
     * const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
     *
     * const entity = { partitionKey: "p1", rowKey: "r1", bar: "updatedBar" };
     *
     * // Update uses update mode "Merge" as default
     * // merge means that update will match a stored entity
     * // that has the same partitionKey and rowKey as the entity
     * // passed to the method and then will only update the properties present in it.
     * // Any other properties that are not defined in the entity passed to updateEntity
     * // will remain as they are in the service
     * await client.updateEntity(entity);
     *
     * // We can also set the update mode to Replace, which will match the entity passed
     * // to updateEntity with one stored in the service and replace with the new one.
     * // If there are any missing properties in the entity passed to updateEntity, they
     * // will be removed from the entity stored in the service
     * await client.updateEntity(entity, "Replace");
     * ```
     */
    updateEntity(entity, mode = "Merge", 
    // eslint-disable-next-line @azure/azure-sdk/ts-naming-options
    options = {}) {
        if (entity.partitionKey === undefined || entity.partitionKey === null) {
            throw new Error("The entity's partitionKey cannot be undefined or null.");
        }
        if (entity.rowKey === undefined || entity.rowKey === null) {
            throw new Error("The entity's rowKey cannot be undefined or null.");
        }
        return tracingClient.withSpan("TableClient.updateEntity", options, async (updatedOptions) => {
            const partitionKey = escapeQuotes(entity.partitionKey);
            const rowKey = escapeQuotes(entity.rowKey);
            const { etag = "*", ...updateEntityOptions } = updatedOptions || {};
            if (mode === "Merge") {
                return this.table.mergeEntity(this.tableName, partitionKey, rowKey, {
                    tableEntityProperties: serialize(entity),
                    ifMatch: etag,
                    ...updateEntityOptions,
                });
            }
            if (mode === "Replace") {
                return this.table.updateEntity(this.tableName, partitionKey, rowKey, {
                    tableEntityProperties: serialize(entity),
                    ifMatch: etag,
                    ...updateEntityOptions,
                });
            }
            throw new Error(`Unexpected value for update mode: ${mode}`);
        }, {
            spanAttributes: {
                updateEntityMode: mode,
            },
        });
    }
    /**
     * Upsert an entity in the table.
     * @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.
     *
     * ### Example upserting an entity
     * ```ts snippet:ReadmeSampleUpsertEntity
     * import { DefaultAzureCredential } from "@azure/identity";
     * import { TableClient } from "@azure/data-tables";
     *
     * const account = "<account>";
     * const accountKey = "<accountkey>";
     * const tableName = "<tableName>";
     *
     * const credential = new DefaultAzureCredential();
     * const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
     *
     * const entity = { partitionKey: "p1", rowKey: "r1", bar: "updatedBar" };
     *
     * // Upsert uses update mode "Merge" as default.
     * // This behaves similarly to update but creates the entity
     * // if it doesn't exist in the service
     * await client.upsertEntity(entity);
     *
     * // We can also set the update mode to Replace.
     * // This behaves similarly to update but creates the entity
     * // if it doesn't exist in the service
     * await client.upsertEntity(entity, "Replace");
     * ```
     */
    upsertEntity(entity, mode = "Merge", options = {}) {
        return tracingClient.withSpan("TableClient.upsertEntity", options, async (updatedOptions) => {
            if (entity.partitionKey === undefined || entity.partitionKey === null) {
                throw new Error("The entity's partitionKey cannot be undefined or null.");
            }
            if (entity.rowKey === undefined || entity.rowKey === null) {
                throw new Error("The entity's rowKey cannot be undefined or null.");
            }
            const partitionKey = escapeQuotes(entity.partitionKey);
            const rowKey = escapeQuotes(entity.rowKey);
            if (mode === "Merge") {
                return this.table.mergeEntity(this.tableName, partitionKey, rowKey, {
                    tableEntityProperties: serialize(entity),
                    ...updatedOptions,
                });
            }
            if (mode === "Replace") {
                return this.table.updateEntity(this.tableName, partitionKey, rowKey, {
                    tableEntityProperties: serialize(entity),
                    ...updatedOptions,
                });
            }
            throw new Error(`Unexpected value for update mode: ${mode}`);
        }, {
            spanAttributes: {
                upsertEntityMode: mode,
            },
        });
    }
    /**
     * Retrieves details about any stored access policies specified on the table that may be used with
     * Shared Access Signatures.
     * @param options - The options parameters.
     */
    getAccessPolicy(options = {}) {
        return tracingClient.withSpan("TableClient.getAccessPolicy", options, async (updatedOptions) => {
            const signedIdentifiers = await this.table.getAccessPolicy(this.tableName, updatedOptions);
            return deserializeSignedIdentifier(signedIdentifiers);
        });
    }
    /**
     * Sets stored access policies for the table that may be used with Shared Access Signatures.
     * @param tableAcl - The Access Control List for the table.
     * @param options - The options parameters.
     */
    setAccessPolicy(tableAcl, options = {}) {
        return tracingClient.withSpan("TableClient.setAccessPolicy", options, (updatedOptions) => {
            const serlializedAcl = serializeSignedIdentifiers(tableAcl);
            return this.table.setAccessPolicy(this.tableName, {
                ...updatedOptions,
                tableAcl: serlializedAcl,
            });
        });
    }
    /**
     * Submits a Transaction which is composed of a set of actions. You can provide the actions as a list
     * or you can use {@link TableTransaction} to help building the transaction.
     *
     * Example usage:
     * ```ts snippet:ReadmeSampleSubmitTransaction
     * import { DefaultAzureCredential } from "@azure/identity";
     * import { TableClient, TransactionAction } from "@azure/data-tables";
     *
     * const account = "<account>";
     * const accountKey = "<accountkey>";
     * const tableName = "<tableName>";
     *
     * const credential = new DefaultAzureCredential();
     * const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
     *
     * const actions: TransactionAction[] = [
     *   ["create", { partitionKey: "p1", rowKey: "1", data: "test1" }],
     *   ["delete", { partitionKey: "p1", rowKey: "2" }],
     *   ["update", { partitionKey: "p1", rowKey: "3", data: "newTest" }, "Merge"],
     * ];
     * const result = await client.submitTransaction(actions);
     * ```
     *
     * Example usage with TableTransaction:
     * ```ts snippet:ReadmeSampleSubmitTransactionWithTableTransaction
     * import { DefaultAzureCredential } from "@azure/identity";
     * import { TableClient, TableTransaction } from "@azure/data-tables";
     *
     * const account = "<account>";
     * const accountKey = "<accountkey>";
     * const tableName = "<tableName>";
     *
     * const credential = new DefaultAzureCredential();
     * const client = new TableClient(`https://${account}.table.core.windows.net`, tableName, credential);
     *
     * const transaction = new TableTransaction();
     *
     * // Call the available action in the TableTransaction object
     * transaction.createEntity({ partitionKey: "p1", rowKey: "1", data: "test1" });
     * transaction.deleteEntity("p1", "2");
     * transaction.updateEntity({ partitionKey: "p1", rowKey: "3", data: "newTest" }, "Merge");
     *
     * // submitTransaction with the actions list on the transaction.
     * const result = await client.submitTransaction(transaction.actions);
     * ```
     *
     * @param actions - tuple that contains the action to perform, and the entity to perform the action with
     * @param options - Options for the request.
     */
    async submitTransaction(actions, options = {}) {
        const partitionKey = actions[0][1].partitionKey;
        const transactionId = Uuid.generateUuid();
        const changesetId = Uuid.generateUuid();
        // Add pipeline
        const transactionClient = new InternalTableTransaction(this.url, partitionKey, transactionId, changesetId, this.generatedClient, new TableClient(this.url, this.tableName), this.credential, this.allowInsecureConnection);
        for (const item of actions) {
            const [action, entity, updateMode = "Merge", updateOptions] = item;
            switch (action) {
                case "create":
                    transactionClient.createEntity(entity);
                    break;
                case "delete":
                    transactionClient.deleteEntity(entity.partitionKey, entity.rowKey);
                    break;
                case "update":
                    transactionClient.updateEntity(entity, updateMode, updateOptions);
                    break;
                case "upsert":
                    transactionClient.upsertEntity(entity, updateMode);
            }
        }
        return transactionClient.submitTransaction(options);
    }
    /**
     *
     * Creates an instance of TableClient from connection string.
     *
     * @param connectionString - Account connection string or a SAS connection string of an Azure storage account.
     *                           [ Note - Account connection string can only be used in NODE.JS runtime. ]
     *                           Account connection string example -
     *                           `DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=accountKey;EndpointSuffix=core.windows.net`
     *                           SAS connection string example -
     *                           `BlobEndpoint=https://myaccount.table.core.windows.net/;QueueEndpoint=https://myaccount.queue.core.windows.net/;FileEndpoint=https://myaccount.file.core.windows.net/;TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sasString`
     * @param options - Options to configure the HTTP pipeline.
     * @returns A new TableClient from the given connection string.
     */
    static fromConnectionString(connectionString, tableName, 
    // eslint-disable-next-line @azure/azure-sdk/ts-naming-options
    options) {
        const { url, options: clientOptions, credential, } = getClientParamsFromConnectionString(connectionString, options);
        if (credential) {
            return new TableClient(url, tableName, credential, clientOptions);
        }
        else {
            return new TableClient(url, tableName, clientOptions);
        }
    }
}
//# sourceMappingURL=TableClient.js.map