"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports._resetClientCachesForTestingOnly = exports.run = void 0;
const client_ssm_1 = require("@aws-sdk/client-ssm");
const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
// @ts-ignore
const rds_query_processor_1 = require("rds-query-processor");
let adapter;
let ssmClient;
let secretsManagerClient;
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const WAIT_COMPLETE = 'WAIT_COMPLETE';
/**
 * This must match enum in src/resolvers/rds/resolver.ts
 */
var CredentialStorageMethod;
(function (CredentialStorageMethod) {
    CredentialStorageMethod["SSM"] = "SSM";
    CredentialStorageMethod["SECRETS_MANAGER"] = "SECRETS_MANAGER";
})(CredentialStorageMethod || (CredentialStorageMethod = {}));
const run = (event) => __awaiter(void 0, void 0, void 0, function* () {
    if (!adapter) {
        const config = yield getDBConfig();
        adapter = yield (0, rds_query_processor_1.getDBAdapter)(config);
    }
    const debugMode = process.env.DEBUG_MODE === 'true';
    try {
        return yield adapter.executeRequest(event, debugMode);
    }
    catch (e) {
        if (isRetryableError(e)) {
            return yield retryWithRefreshedCredentials(event, debugMode);
        }
        throw e;
    }
});
exports.run = run;
const retryWithRefreshedCredentials = (event, debugMode) => __awaiter(void 0, void 0, void 0, function* () {
    try {
        const config = yield getDBConfig();
        adapter = yield (0, rds_query_processor_1.getDBAdapter)(config);
        return yield adapter.executeRequest(event, debugMode);
    }
    catch (err) {
        adapter = null;
        throw err;
    }
});
const isRetryableError = (error) => {
    // https://www.postgresql.org/docs/current/errcodes-appendix.html
    const postgresRetryableError = error.code === '28P01';
    // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
    const mysqlRetryableError = error.errno === '1045';
    return postgresRetryableError || mysqlRetryableError;
};
const createSSMClient = () => {
    var _a;
    const PORT_SEPERATOR = ':';
    const endpoint = (_a = process.env.SSM_ENDPOINT) === null || _a === void 0 ? void 0 : _a.split(PORT_SEPERATOR).pop();
    ssmClient = new client_ssm_1.SSMClient({
        endpoint: `https://${endpoint}`,
    });
};
const createSecretsManagerClient = () => {
    var _a;
    const PORT_SEPERATOR = ':';
    const endpoint = (_a = process.env.SECRETS_MANAGER_ENDPOINT) === null || _a === void 0 ? void 0 : _a.split(PORT_SEPERATOR).pop();
    secretsManagerClient = new client_secrets_manager_1.SecretsManagerClient({
        endpoint: `https://${endpoint}`,
    });
};
const wait10Seconds = () => __awaiter(void 0, void 0, void 0, function* () {
    yield delay(10000);
    return WAIT_COMPLETE;
});
const getSSMValue = (key) => __awaiter(void 0, void 0, void 0, function* () {
    var _a, _b, _c;
    if (!key) {
        throw Error('Key not provided to retrieve database connection secret');
    }
    const parameterCommand = new client_ssm_1.GetParameterCommand({
        Name: key,
        WithDecryption: true,
    });
    // When the lambda is deployed in VPC and VPC endpoints for SSM are not defined or
    // the security group's inbound rule for port 443 is not defined,
    // the SSM client waits for the entire lambda execution time and times out.
    // If the parameter is not retrieved within 10 seconds, throw an error.
    const data = yield Promise.race([ssmClient.send(parameterCommand), wait10Seconds()]);
    // If string is returned, throw error.
    if ((typeof data === 'string' || data instanceof String) &&
        data === WAIT_COMPLETE) {
        console.log('Unable to retrieve secret for database connection from SSM. If your database is in VPC, verify that you have VPC endpoints for SSM defined and the security group\'s inbound rule for port 443 is defined.');
        throw new Error('Unable to get the database credentials. Check the logs for more details.');
    }
    // Read the value from the GetParameter response.
    const response = data;
    if ((((_a = response === null || response === void 0 ? void 0 : response.$metadata) === null || _a === void 0 ? void 0 : _a.httpStatusCode) && ((_b = response === null || response === void 0 ? void 0 : response.$metadata) === null || _b === void 0 ? void 0 : _b.httpStatusCode) >= 400) || !((_c = response === null || response === void 0 ? void 0 : response.Parameter) === null || _c === void 0 ? void 0 : _c.Value)) {
        throw new Error('Unable to get secret for database connection');
    }
    return response.Parameter.Value;
});
const getSecretManagerValue = (secretArn) => __awaiter(void 0, void 0, void 0, function* () {
    var _d, _e;
    if (!secretArn) {
        throw Error('Secret ARN not provided to retrieve database connection secret');
    }
    const secretValueCommand = new client_secrets_manager_1.GetSecretValueCommand({ SecretId: secretArn });
    // When the lambda is deployed in VPC and VPC endpoints for secrets manager are not defined or
    // the security group's inbound rule for port 443 is not defined,
    // the secrets manager client waits for the entire lambda execution time and times out.
    // If the parameter is not retrieved within 10 seconds, throw an error.
    const data = yield Promise.race([secretsManagerClient.send(secretValueCommand), wait10Seconds()]);
    if ((typeof data === 'string' || data instanceof String) &&
        data === WAIT_COMPLETE) {
        console.log('Unable to retrieve secret for database connection from Secrets Manager. If your database is in VPC, verify that you have VPC endpoints for Secrets Manager defined and the security group\'s inbound rule for port 443 is defined.');
        throw new Error('Unable to get the database credentials. Check the logs for more details.');
    }
    const response = data;
    if ((((_d = response === null || response === void 0 ? void 0 : response.$metadata) === null || _d === void 0 ? void 0 : _d.httpStatusCode) && ((_e = response === null || response === void 0 ? void 0 : response.$metadata) === null || _e === void 0 ? void 0 : _e.httpStatusCode) >= 400) || !response.SecretString) {
        throw new Error('Unable to get secret for database connection');
    }
    try {
        const secrets = JSON.parse(response.SecretString);
        if (!secrets.username || !secrets.password) {
            throw new Error('Unable to get secret for database connection');
        }
        return secrets;
    }
    catch (_f) {
        throw new Error('Unable to get secret for database connection');
    }
});
/**
 * Retrieves the value of the specified SSM path. The `path` argument can be either a single string or an array of strings. If an array,
 * this method will attempt to retrieve values from each path in order until it either successfully retrieves a value, or runs out of values
 * to try.
 * @param path a single path, or array of candidate paths
 * @returns the value of the first successful retrieval, or throws an error if no values can be retrieved
 */
const retrieveSsmValueFromEnvPaths = (path) => __awaiter(void 0, void 0, void 0, function* () {
    const parsedJsonSsmPath = JSON.parse(path);
    const ssmRequestError = 'Unable to connect to the database. Check the logs for more details.';
    const ssmLoggedError = 'Unable to fetch the connection Uri from SSM for the provided paths.';
    if (Array.isArray(parsedJsonSsmPath)) {
        for (const path of parsedJsonSsmPath) {
            try {
                return yield getSSMValue(path);
            }
            catch (e) {
                // try the next secret path;
                continue;
            }
        }
        console.log(ssmLoggedError);
        throw new Error(ssmRequestError);
    }
    else {
        try {
            return yield getSSMValue(parsedJsonSsmPath);
        }
        catch (e) {
            console.log(ssmLoggedError);
            throw new Error(ssmRequestError);
        }
    }
});
const getDBConfig = () => __awaiter(void 0, void 0, void 0, function* () {
    const config = {};
    const sslCertificate = yield getCustomSslCert();
    if (sslCertificate) {
        config.sslCertificate = sslCertificate;
    }
    const credentialStorageMethod = process.env.CREDENTIAL_STORAGE_METHOD;
    if (credentialStorageMethod === CredentialStorageMethod.SSM) {
        if (!ssmClient) {
            createSSMClient();
        }
        const jsonConnectionString = process.env.connectionString;
        if (jsonConnectionString) {
            config.connectionString = yield retrieveSsmValueFromEnvPaths(jsonConnectionString);
            return config;
        }
        config.engine = getDBEngine(),
            config.host = yield getSSMValue(process.env.host);
        config.port = Number.parseInt(yield getSSMValue(process.env.port)) || 3306;
        config.username = yield getSSMValue(process.env.username);
        config.password = yield getSSMValue(process.env.password);
        config.database = yield getSSMValue(process.env.database);
    }
    else if (credentialStorageMethod === CredentialStorageMethod.SECRETS_MANAGER) {
        if (!secretsManagerClient) {
            createSecretsManagerClient();
        }
        config.engine = getDBEngine();
        config.port = Number.parseInt(process.env.port || '3306');
        config.database = process.env.database;
        config.host = process.env.host;
        const secrets = yield getSecretManagerValue(process.env.secretArn);
        config.username = secrets.username;
        config.password = secrets.password;
    }
    else {
        throw new Error('Unable to determine credentials storage method (SSM or SECRETS_MANAGER).');
    }
    if (!config.host || !config.port || !config.username || !config.password || !config.database) {
        throw Error('Missing database connection configuration');
    }
    return config;
});
const getDBEngine = () => {
    if (!process.env.engine) {
        throw Error('Missing database engine configuration');
    }
    return process.env.engine;
};
const getCustomSslCert = () => __awaiter(void 0, void 0, void 0, function* () {
    if (!ssmClient) {
        createSSMClient();
    }
    // This must match the env key in packages/amplify-graphql-model-transformer/src/resources/rds-model-resource-generator.ts
    const sslCertSsmPath = process.env.SSL_CERT_SSM_PATH;
    if (!sslCertSsmPath) {
        return;
    }
    try {
        const sslCert = yield retrieveSsmValueFromEnvPaths(sslCertSsmPath);
        return sslCert;
    }
    catch (_g) {
        // Catch the error from getSSMValue so we can provide a more targeted failure message
        console.log('Unable to retrieve custom SSL certificate from SSM. If your database is in VPC, verify that you have VPC endpoints for SSM defined and the security group\'s inbound rule for port 443 is defined.');
        throw new Error('Unable to get the custom SSL certificate. Check the logs for more details.');
    }
});
/**
 * Used to reset the cached DB Adapter, SSM Client, and Secrets Manager Clients. Should only be invoked for tests.
 */
const _resetClientCachesForTestingOnly = () => {
    adapter = null;
    ssmClient = null;
    secretsManagerClient = null;
};
exports._resetClientCachesForTestingOnly = _resetClientCachesForTestingOnly;
//# sourceMappingURL=handler.js.map