"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 });
const datasource_1 = require("../definitions/datasource");
const lodash_1 = require("lodash");
const BodyTypes = {
    NONE: "none",
    FORM_DATA: "form",
    XML: "xml",
    ENCODED: "encoded",
    JSON: "json",
    TEXT: "text",
};
const coreFields = {
    path: {
        type: datasource_1.DatasourceFieldTypes.STRING,
        display: "URL",
    },
    queryString: {
        type: datasource_1.DatasourceFieldTypes.STRING,
    },
    headers: {
        type: datasource_1.DatasourceFieldTypes.OBJECT,
    },
    enabledHeaders: {
        type: datasource_1.DatasourceFieldTypes.OBJECT,
    },
    requestBody: {
        type: datasource_1.DatasourceFieldTypes.JSON,
    },
    bodyType: {
        type: datasource_1.DatasourceFieldTypes.STRING,
        enum: Object.values(BodyTypes),
    },
    pagination: {
        type: datasource_1.DatasourceFieldTypes.OBJECT,
    },
};
var RestModule;
(function (RestModule) {
    const fetch = require("node-fetch");
    const { formatBytes } = require("../utilities");
    const { performance } = require("perf_hooks");
    const FormData = require("form-data");
    const { URLSearchParams } = require("url");
    const { parseStringPromise: xmlParser, Builder: XmlBuilder, } = require("xml2js");
    const SCHEMA = {
        docs: "https://github.com/node-fetch/node-fetch",
        description: "With the REST API datasource, you can connect, query and pull data from multiple REST APIs. You can then use the retrieved data to build apps.",
        friendlyName: "REST API",
        type: "API",
        datasource: {
            url: {
                type: datasource_1.DatasourceFieldTypes.STRING,
                default: "",
                required: false,
                deprecated: true,
            },
            defaultHeaders: {
                type: datasource_1.DatasourceFieldTypes.OBJECT,
                required: false,
                default: {},
            },
        },
        query: {
            create: {
                readable: true,
                displayName: "POST",
                type: datasource_1.QueryTypes.FIELDS,
                fields: coreFields,
            },
            read: {
                displayName: "GET",
                readable: true,
                type: datasource_1.QueryTypes.FIELDS,
                fields: coreFields,
            },
            update: {
                displayName: "PUT",
                readable: true,
                type: datasource_1.QueryTypes.FIELDS,
                fields: coreFields,
            },
            patch: {
                displayName: "PATCH",
                readable: true,
                type: datasource_1.QueryTypes.FIELDS,
                fields: coreFields,
            },
            delete: {
                displayName: "DELETE",
                type: datasource_1.QueryTypes.FIELDS,
                fields: coreFields,
            },
        },
    };
    class RestIntegration {
        constructor(config) {
            this.headers = {};
            this.startTimeMs = performance.now();
            this.config = config;
        }
        parseResponse(response, pagination) {
            return __awaiter(this, void 0, void 0, function* () {
                let data, raw, headers;
                const contentType = response.headers.get("content-type") || "";
                try {
                    if (contentType.includes("application/json")) {
                        data = yield response.json();
                        raw = JSON.stringify(data);
                    }
                    else if (contentType.includes("text/xml") ||
                        contentType.includes("application/xml")) {
                        const rawXml = yield response.text();
                        data =
                            (yield xmlParser(rawXml, {
                                explicitArray: false,
                                trim: true,
                                explicitRoot: false,
                            })) || {};
                        // there is only one structure, its an array, return the array so it appears as rows
                        const keys = Object.keys(data);
                        if (keys.length === 1 && Array.isArray(data[keys[0]])) {
                            data = data[keys[0]];
                        }
                        raw = rawXml;
                    }
                    else {
                        data = yield response.text();
                        raw = data;
                    }
                }
                catch (err) {
                    throw "Failed to parse response body.";
                }
                const size = formatBytes(response.headers.get("content-length") || Buffer.byteLength(raw, "utf8"));
                const time = `${Math.round(performance.now() - this.startTimeMs)}ms`;
                headers = response.headers.raw();
                for (let [key, value] of Object.entries(headers)) {
                    headers[key] = Array.isArray(value) ? value[0] : value;
                }
                // Check if a pagination cursor exists in the response
                let nextCursor = null;
                if (pagination === null || pagination === void 0 ? void 0 : pagination.responseParam) {
                    nextCursor = (0, lodash_1.get)(data, pagination.responseParam);
                }
                return {
                    data,
                    info: {
                        code: response.status,
                        size,
                        time,
                    },
                    extra: {
                        raw,
                        headers,
                    },
                    pagination: {
                        cursor: nextCursor,
                    },
                };
            });
        }
        getUrl(path, queryString, pagination, paginationValues) {
            // Add pagination params to query string if required
            if ((pagination === null || pagination === void 0 ? void 0 : pagination.location) === "query" && paginationValues) {
                const { pageParam, sizeParam } = pagination;
                const params = new URLSearchParams();
                // Append page number or cursor param if configured
                if (pageParam && paginationValues.page != null) {
                    params.append(pageParam, paginationValues.page);
                }
                // Append page size param if configured
                if (sizeParam && paginationValues.limit != null) {
                    params.append(sizeParam, paginationValues.limit);
                }
                // Prepend query string with pagination params
                let paginationString = params.toString();
                if (paginationString) {
                    queryString = `${paginationString}&${queryString}`;
                }
            }
            const main = `${path}?${queryString}`;
            let complete = main;
            if (this.config.url && !main.startsWith("http")) {
                complete = !this.config.url ? main : `${this.config.url}/${main}`;
            }
            if (!complete.startsWith("http")) {
                complete = `http://${complete}`;
            }
            return complete;
        }
        addBody(bodyType, body, input, pagination, paginationValues) {
            if (!input.headers) {
                input.headers = {};
            }
            if (bodyType === BodyTypes.NONE) {
                return input;
            }
            let error, object = {}, string = "";
            try {
                if (body) {
                    string = typeof body !== "string" ? JSON.stringify(body) : body;
                    object = typeof body === "object" ? body : JSON.parse(body);
                }
            }
            catch (err) {
                error = err;
            }
            // Util to add pagination values to a certain body type
            const addPaginationToBody = (insertFn) => {
                if ((pagination === null || pagination === void 0 ? void 0 : pagination.location) === "body") {
                    if ((pagination === null || pagination === void 0 ? void 0 : pagination.pageParam) && (paginationValues === null || paginationValues === void 0 ? void 0 : paginationValues.page) != null) {
                        insertFn(pagination.pageParam, paginationValues.page);
                    }
                    if ((pagination === null || pagination === void 0 ? void 0 : pagination.sizeParam) && (paginationValues === null || paginationValues === void 0 ? void 0 : paginationValues.limit) != null) {
                        insertFn(pagination.sizeParam, paginationValues.limit);
                    }
                }
            };
            switch (bodyType) {
                case BodyTypes.TEXT:
                    // content type defaults to plaintext
                    input.body = string;
                    break;
                case BodyTypes.ENCODED:
                    const params = new URLSearchParams();
                    for (let [key, value] of Object.entries(object)) {
                        params.append(key, value);
                    }
                    addPaginationToBody((key, value) => {
                        params.append(key, value);
                    });
                    input.body = params;
                    break;
                case BodyTypes.FORM_DATA:
                    const form = new FormData();
                    for (let [key, value] of Object.entries(object)) {
                        form.append(key, value);
                    }
                    addPaginationToBody((key, value) => {
                        form.append(key, value);
                    });
                    input.body = form;
                    break;
                case BodyTypes.XML:
                    if (object != null && Object.keys(object).length) {
                        string = new XmlBuilder().buildObject(object);
                    }
                    input.body = string;
                    input.headers["Content-Type"] = "application/xml";
                    break;
                case BodyTypes.JSON:
                    // if JSON error, throw it
                    if (error) {
                        throw "Invalid JSON for request body";
                    }
                    addPaginationToBody((key, value) => {
                        object[key] = value;
                    });
                    input.body = JSON.stringify(object);
                    input.headers["Content-Type"] = "application/json";
                    break;
            }
            return input;
        }
        getAuthHeaders(authConfigId) {
            let headers = {};
            if (this.config.authConfigs && authConfigId) {
                const authConfig = this.config.authConfigs.filter(c => c._id === authConfigId)[0];
                // check the config still exists before proceeding
                // if not - do nothing
                if (authConfig) {
                    let config;
                    switch (authConfig.type) {
                        case datasource_1.AuthType.BASIC:
                            config = authConfig.config;
                            headers.Authorization = `Basic ${Buffer.from(`${config.username}:${config.password}`).toString("base64")}`;
                            break;
                        case datasource_1.AuthType.BEARER:
                            config = authConfig.config;
                            headers.Authorization = `Bearer ${config.token}`;
                            break;
                    }
                }
            }
            return headers;
        }
        _req(query) {
            return __awaiter(this, void 0, void 0, function* () {
                const { path = "", queryString = "", headers = {}, method = "GET", disabledHeaders, bodyType, requestBody, authConfigId, pagination, paginationValues, } = query;
                const authHeaders = this.getAuthHeaders(authConfigId);
                this.headers = Object.assign(Object.assign(Object.assign({}, this.config.defaultHeaders), headers), authHeaders);
                if (disabledHeaders) {
                    for (let headerKey of Object.keys(this.headers)) {
                        if (disabledHeaders[headerKey]) {
                            delete this.headers[headerKey];
                        }
                    }
                }
                let input = { method, headers: this.headers };
                input = this.addBody(bodyType, requestBody, input, pagination, paginationValues);
                this.startTimeMs = performance.now();
                const url = this.getUrl(path, queryString, pagination, paginationValues);
                const response = yield fetch(url, input);
                return yield this.parseResponse(response, pagination);
            });
        }
        create(opts) {
            return __awaiter(this, void 0, void 0, function* () {
                return this._req(Object.assign(Object.assign({}, opts), { method: "POST" }));
            });
        }
        read(opts) {
            return __awaiter(this, void 0, void 0, function* () {
                return this._req(Object.assign(Object.assign({}, opts), { method: "GET" }));
            });
        }
        update(opts) {
            return __awaiter(this, void 0, void 0, function* () {
                return this._req(Object.assign(Object.assign({}, opts), { method: "PUT" }));
            });
        }
        patch(opts) {
            return __awaiter(this, void 0, void 0, function* () {
                return this._req(Object.assign(Object.assign({}, opts), { method: "PATCH" }));
            });
        }
        delete(opts) {
            return __awaiter(this, void 0, void 0, function* () {
                return this._req(Object.assign(Object.assign({}, opts), { method: "DELETE" }));
            });
        }
    }
    module.exports = {
        schema: SCHEMA,
        integration: RestIntegration,
        AuthType: datasource_1.AuthType,
    };
})(RestModule || (RestModule = {}));
