"use strict";
var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
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());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var __spreadArrays = (this && this.__spreadArrays) || function () {
    for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
    for (var r = Array(s), k = 0, i = 0; i < il; i++)
        for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
            r[k] = a[j];
    return r;
};
exports.__esModule = true;
exports.gqlParsePayload = exports.parseGraphQLInput = exports.modularMigration = exports.processModularSQL = exports.gatherModularSQLContentByID = exports.reducerSqlContent = exports.surveyDirectory = exports.genNextMigrationName = exports.encodeStruct = exports.encodeStructFields = exports.encodeStructField = exports.appendSemicolon = exports.appendIndex = exports.stringValues = exports.permitted = exports.operationFlag = exports.extractPermissions = exports.permit = exports.supportsReturnOnCreateAndUpdate = exports.initPostProcessing = exports.extractSelectedFields = exports.wktToGeoJSON = exports.transformNameforResolver = exports.genResourcesMap = exports.getFirstIfSeperated = exports.callComplexResource = exports.seperateByKeyPrefix = exports.genDatabaseResourceValidators = exports.joiRequired = exports.joiKeyComponent = exports.nameRestEndpointGetRecords = exports.resolveResource = exports.rejectResource = exports.removeContextKeys = exports.validateOneOrMany = exports.validationExpander = exports.metersToDecimalDegrees = exports.uniqueKeyComponents = exports.softDeleteFields = exports.queryContextParser = exports.searchQueryParser = exports.contextTransformer = exports.parseFieldAndOperation = exports.supportedOperation = exports.supportMultipleValues = exports.validArgsforOperation = exports.defineValidationErrorMessage = exports.badArgsLengthError = exports.generateSearchQueryError = exports.errorMessageInvalidValue = exports.validatorInspector = exports.reducerValidatorInspector = exports.validate = exports.modifyValidator = exports.typecastFn = exports.castOther = exports.castBoolean = exports.castNumber = exports.castString = void 0;
var buffer_1 = require("buffer");
var fs = require("fs");
var path = require("path");
var Joi = require("joi");
var change_case_1 = require("change-case");
var lodash_1 = require("lodash");
var wkx = require("wkx");
var cnst = require("./const");
/**
 * @description reducer that seperates fulfilled promises from those rejected. Used in places with Promise.AllSettled is implemented on async validators to quickly check for failures.
 * @param {*} accum
 * @param {*} { status, value, reason }
 * @returns
 */
var transformSettledValidation = function (accum, _a) {
    var status = _a.status, value = _a.value, reason = _a.reason;
    if (status === "fulfilled") {
        accum.values.push(value);
    }
    else {
        accum.errors.push(reason._original);
    }
    return accum;
};
/**
 * @description reduces settled array of validators to seperate valid values from errors
 * @param {*} settledPromises
 */
var reduceSettledAsyncValidation = function (settledPromises) {
    return settledPromises.reduce(transformSettledValidation, {
        values: [],
        errors: []
    });
};
/**
 * @description casts input as string. used as inputs are passed to validators.
 * @param {*} arg
 */
exports.castString = function (arg) { return String(arg); };
/**
 * @description attempts to case value as Number. if NaN - casts as verbose string that will fail validation and get returned to the user. used as inputs are passed to validators.
 * @param {*} arg
 * @returns
 */
exports.castNumber = function (arg) {
    var n = Number(arg);
    return !Number.isNaN(n)
        ? n
        : "number conversion failed to provide meaningful value (" + arg + " = NaN)";
};
/**
 * @description attempts to cast falsey values to Boolean False. used as inputs are passed to validators.
 * @param {*} arg
 */
exports.castBoolean = function (arg) {
    return Boolean(cnst.FALSEY_STRING_VALUES.includes(arg) ? false : arg);
};
/**
 * @description
 * @param {*} arg
 */
exports.castOther = function (arg) { return arg; };
/**
 * @description Fn that takes string input of a data type, and returns a Fn that will convert an input into target type. Boolean defines some string inputs as falesy and returns false.
 * @param {string} type
 * @returns {(ts.ITypeCastString
 *   | ts.ITypeCastNumber
 *   | ts.ITypeCastBoolean
 *   | ts.IDefaultTypeCast
 *   | any)}
 */
exports.typecastFn = function (type) {
    switch (type) {
        case cnst.STRING:
            return exports.castString;
        case cnst.NUMBER:
            return exports.castNumber;
        case cnst.BOOLEAN:
            return exports.castBoolean;
        default:
            return exports.castOther;
    }
};
/**
 * @description Fn that takes a JOI validator as input and modifies fields (making required or totally removing) fields that are marked with appropriate SYMBOLS. This is useful so that validators that define records 1:1 with db resource DDLs (table, view, materialized view), dynamically get modified to support full CRUD operations.
 * @param {Joi.Schema} validator
 * @param {string} [operation='original']
 * @returns {Joi.Schema}
 */
exports.modifyValidator = function (validator, operation) {
    if (operation === void 0) { operation = "original"; }
    var newValidator = lodash_1.cloneDeep(validator);
    newValidator[cnst.UNDERSCORE_IDS][cnst.UNDERSCORE_BYKEY].forEach(function (_a) {
        var schema = _a.schema, id = _a.id;
        if (operation === cnst.CREATE) {
            if (schema._invalids &&
                schema._invalids.has(cnst.SYMBOL_CREATE_DISABLED)) {
                newValidator[cnst.UNDERSCORE_IDS][cnst.UNDERSCORE_BYKEY]["delete"](id);
            }
            if (schema._invalids &&
                schema._invalids.has(cnst.SYMBOL_CREATE_REQUIRED)) {
                schema._flags = schema._flags
                    ? __assign(__assign({}, schema._flags), cnst.REQUIRED_FLAG) : cnst.REQUIRED_FLAG;
            }
        }
        else if (operation === cnst.UPDATE) {
            // required all pk fields for update
            if (schema._invalids &&
                schema._invalids.has(cnst.SYMBOL_UNIQUE_KEY_COMPONENT)) {
                schema._flags = schema._flags
                    ? __assign(__assign({}, schema._flags), cnst.REQUIRED_FLAG) : cnst.REQUIRED_FLAG;
            }
            // blacklist immutable fields (future feature to set via config)
            if (schema._invalids &&
                schema._invalids.has(cnst.SYMBOL_UPDATE_DISABLED)) {
                newValidator[cnst.UNDERSCORE_IDS][cnst.UNDERSCORE_BYKEY]["delete"](id);
            }
        }
        else if (operation === cnst.READ) {
            if (schema._invalids &&
                schema._invalids.has(cnst.SYMBOL_UNIQUE_KEY_COMPONENT)) {
                schema._flags = schema._flags
                    ? __assign(__assign({}, schema._flags), cnst.REQUIRED_FLAG) : cnst.REQUIRED_FLAG;
            }
        }
        else if (operation === cnst.DELETE) {
            if (schema._invalids &&
                schema._invalids.has(cnst.SYMBOL_UNIQUE_KEY_COMPONENT)) {
                schema._flags = schema._flags
                    ? __assign(__assign({}, schema._flags), cnst.REQUIRED_FLAG) : cnst.REQUIRED_FLAG;
            }
            else {
                newValidator[cnst.UNDERSCORE_IDS][cnst.UNDERSCORE_BYKEY]["delete"](id);
            }
        }
    });
    return newValidator;
};
exports.validate = {
    /**
     * @description
     * @param {Joi.Schema} validator
     * @returns {Joi.Schema}
     */
    create: function (validator) {
        return exports.modifyValidator(validator, cnst.CREATE);
    },
    /**
     * @description
     * @param {Joi.Schema} validator
     * @returns {Joi.Schema}
     */
    read: function (validator) {
        return exports.modifyValidator(validator, cnst.READ);
    },
    /**
     * @description
     * @param {Joi.Schema} validator
     * @returns {Joi.Schema}
     */
    update: function (validator) {
        return exports.modifyValidator(validator, cnst.UPDATE);
    },
    /**
     * @description
     * @param {Joi.Schema} validator
     * @returns {Joi.Schema}
     */
    "delete": function (validator) {
        return exports.modifyValidator(validator, cnst.READ);
    },
    /**
     * @description
     * @param {Joi.Schema} validator
     * @returns {Joi.Schema}
     */
    search: function (validator) { return validator; }
};
/**
 * @description Reducer Fn that inspects a given schema from fields/keys within a JOI validator object and reports meta information about the specific field. This information is used extensively in this framework for autogeneration of openapi docs, pks to use for unique records, dynamic enabling of geoquery interfaces, and typecasting of query string input before building SQL statements.
 * @param {object} accum
 * @param {*} { id, schema }
 * @returns {ts.IValidatorInspectorReport}
 */
exports.reducerValidatorInspector = function (accum, _a) {
    var _b;
    var id = _a.id, schema = _a.schema;
    return (__assign(__assign({}, accum), (_b = {}, _b[id] = {
        type: schema.type,
        required: !!(schema._flags && schema._flags.presence),
        keyComponent: !!(schema._invalids && schema._invalids.has(cnst.SYMBOL_UNIQUE_KEY_COMPONENT)),
        // TODO: add json type. will be needed by grpc `jsonToStructs` in grpc.methods
        geoqueryType: schema._invalids && schema._invalids.has(cnst.SYMBOL_GEOQUERY)
            ? schema._invalids.has(cnst.SYMBOL_GEOQUERY_POINT)
                ? cnst.POINT
                : cnst.POLYGON
            : null,
        jsonType: !!(schema._invalids && schema._invalids.has(cnst.SYMBOL_JSON)),
        softDeleteFlag: !!(schema._invalids && schema._invalids.has(cnst.SYMBOL_SOFT_DELETE)),
        updateDisabled: !!(schema._invalids && schema._invalids.has(cnst.SYMBOL_UPDATE_DISABLED)),
        createRequired: !!(schema._invalids && schema._invalids.has(cnst.SYMBOL_CREATE_REQUIRED)),
        createDisabled: !!(schema._invalids && schema._invalids.has(cnst.SYMBOL_CREATE_DISABLED)),
        typecast: exports.typecastFn(schema.type),
        validate: function (value) {
            return schema.validateAsync(exports.typecastFn(schema.type)(value));
        }
    }, _b)));
};
/**
 * @description Used for swagger generation and for validating user queries. Real validator cannot be used as those are plain objects and may need to validate field multiple times (ex: sql where field <= 5 and >= 12 -- needs to validate high & low values against field level validator. `redactedFields` are omitted from this output as it powers openapi generation (which reads from resource.report[method]).
 * @param {Joi.Schema} validator
 * @param {string[]} redactedFields
 * @returns {ts.IValidatorInspectorReport}
 */
exports.validatorInspector = function (validator, redactedFields) {
    var allFields = Array.from(validator[cnst.UNDERSCORE_IDS][cnst.UNDERSCORE_BYKEY].values());
    var approvedFields = redactedFields.length
        ? allFields.filter(function (item) { return !redactedFields.includes(item.id); })
        : allFields;
    return approvedFields.reduce(exports.reducerValidatorInspector, {});
};
// description Fn that generates error message when value submitted for search is of incorrect data type.
exports.errorMessageInvalidValue = function (field, message) { return "'" + field + "': " + message; };
/**
 * @description Fn that generates error message when GET query produces error for various reasons. Defined errors were generated via JOI validation checks. undefined types occur when trying to query a column that doesn't exist on the record. Final fallback is for when a query operation is called on a field that is not supported.
 * @param {ts.IParamsGenerateSearchQueryError} {error, field, type, operation}
 * @returns {string}
 */
exports.generateSearchQueryError = function (_a) {
    var error = _a.error, field = _a.field, type = _a.type, operation = _a.operation;
    return error
        ? exports.errorMessageInvalidValue(field, error)
        : !type
            ? "'" + field + "' is not a supported property on this resource"
            : "'" + operation + "' operation not supported";
};
/**
 * @description Fn that generates verbose error message string when specific number of args not used with a given operation. Only a few operations have specific arg lengths.
 * @param {string} operation
 * @param {any[]} values
 * @returns {string}
 */
exports.badArgsLengthError = function (operation, values) {
    return "'" + operation + "' operation requires " + cnst.DEFINED_ARG_LENGTHS[operation] + " args. " + values.length + " were provided.";
};
/**
 * @description builds verbose string with multiple references specific to why a given request failed
 * @param {string} field
 */
exports.defineValidationErrorMessage = function (field) { return function (message, i) {
    return "'" + field + "' argument #" + i + ": " + message;
}; };
/**
 * @description Fn that checks to see if length of args meets the needs for a given operation. Only applies to a handful of operations, so majority default to true.
 * @param {string} operation
 * @param {any[]} values
 * @returns {boolean}
 */
exports.validArgsforOperation = function (operation, values) {
    return cnst.DEFINED_ARG_LENGTHS[operation]
        ? cnst.DEFINED_ARG_LENGTHS[operation] === values.length
        : true;
};
/**
 * @description Some operations on GET queries support operations via `field.operation`. A subset of these operations support multiple values seperated by commas. This `SUPPORTED_OPERATIONS` object holds a map with keys that represent all supported operations and values that define the support for multiple values. `undefined` responses to this function only occur of the operation isn't supported on the field at all.
 * @param {string} operation
 * @returns {(boolean)}
 */
exports.supportMultipleValues = function (operation) {
    return !!cnst.SUPPORTED_OPERATIONS[operation];
};
/**
 * @description Some operations on GET queries support operations via `field.operation`. This `SUPPORTED_OPERATIONS` object holds a map with keys that represent all supported operations.
 * @param {string} operation
 * @returns {boolean}
 */
exports.supportedOperation = function (operation) {
    return cnst.SUPPORTED_OPERATIONS.hasOwnProperty(operation);
};
/**
 * @description Fn that parses all GET querystring keys into fields and operations. These are set by the caller via `field.operation` syntax on all GET query string keys for searching. If `.operation` is not provided -- defaults to `.equal`.
 * @param {string} key
 * @returns {ts.IFieldAndOperation}
 */
exports.parseFieldAndOperation = function (key) {
    var _a = key.split(cnst.DOT), field = _a[0], op = _a[1];
    return {
        field: field,
        operation: op ? op : cnst.EQUAL
    };
};
/**
 * @description Used to transform context arguments sent in GraphQL and gRPC to REST standard -- so all requests can get processed by complex middleware.
 * @param {*} attribute
 * @param {*} input
 * @returns
 */
exports.contextTransformer = function (attribute, input) {
    switch (attribute) {
        case cnst.SEPERATOR:
            return input.toString();
        case cnst.FIELDS:
            return input.split(cnst.COMMA);
        case cnst.ORDERBY:
            return input.split(cnst.COMMA).map(function (fieldAndDirection) {
                var _a = fieldAndDirection.split(cnst.COLON), column = _a[0], direction = _a[1];
                return {
                    column: column,
                    order: direction && cnst.DESC === direction ? direction : cnst.ASC
                };
            });
        case cnst.DISTINCT:
            return !!exports.castBoolean(input);
        case cnst.PAGE:
        case cnst.LIMIT:
            return Number(input);
        case cnst.NOTWHERE:
            return !exports.castBoolean(input); // castBoolean returns bool based on falsey input. I want to default to the opposite.
        default:
            return false;
    }
};
/**
 * @description The Heart of the entire system. This Fn takes in a JOI validator and a query object (`ctx.request.query`) and submits both for processing. Search interfaces, fields & operations, are derived from the JOI validators, values from query object are typecasted to data types (if possible) using the types of each field from the JOI validator. Some query operations support multiple values seperated by commas, and this value parsing to arrays is also done here. The `context` object returned is used to build the query -- but is more superficial than the `components` which are used within the `where` part of the query. Any errors with a give query against a validator produces an array of errors, so the request can be stopped before submitting to database.
 * @param {Joi.Schema} validator
 * @param {ts.IParamsSearchQueryParser} query
 * @returns {Promise<ts.ISearchQueryResponse>}
 * @promise searchQueryParserPromise
 * @fulfill {ts.ISearchQueryResponse}
 * @reject {ts.ISearchQueryResponse}
 * @returns searchQueryParserPromise
 */
exports.searchQueryParser = function (validator, query, apiType, seperator) { return __awaiter(void 0, void 0, void 0, function () {
    var errors, components, sep, parseSearchQueryEntry;
    return __generator(this, function (_a) {
        switch (_a.label) {
            case 0:
                errors = [];
                components = [];
                sep = seperator || cnst.SEARCH_QUERY_CONTEXT.seperator;
                parseSearchQueryEntry = function (_a) {
                    var key = _a[0], rawValue = _a[1];
                    return __awaiter(void 0, void 0, void 0, function () {
                        var _b, field, operation, schema, type, record, typecast, values, asyncValidation, _c, _values, _errors, error, asyncValidation, _d, value, error;
                        return __generator(this, function (_e) {
                            switch (_e.label) {
                                case 0:
                                    _b = exports.parseFieldAndOperation(key), field = _b.field, operation = _b.operation;
                                    schema = (validator[cnst.UNDERSCORE_IDS][cnst.UNDERSCORE_BYKEY].get(field) || {}).schema;
                                    type = (schema || {}).type;
                                    record = { field: field, rawValue: rawValue, operation: operation, type: type };
                                    typecast = exports.typecastFn(type);
                                    if (!(schema && exports.supportMultipleValues(operation))) return [3 /*break*/, 2];
                                    values = rawValue.split(sep).map(typecast);
                                    if (!exports.validArgsforOperation(operation, values))
                                        errors.push({ field: field, error: exports.badArgsLengthError(operation, values) });
                                    asyncValidation = Promise.allSettled(values.map(function (item) { return schema.validateAsync(item); }));
                                    return [4 /*yield*/, asyncValidation.then(reduceSettledAsyncValidation)];
                                case 1:
                                    _c = _e.sent(), _values = _c.values, _errors = _c.errors;
                                    error = _errors
                                        .map(exports.defineValidationErrorMessage(field))
                                        .join(cnst.COMMA);
                                    if (error)
                                        errors.push({ field: field, error: error });
                                    components.push(__assign(__assign({}, record), { value: _values }));
                                    return [3 /*break*/, 4];
                                case 2:
                                    asyncValidation = schema
                                        ? schema.validateAsync(typecast(rawValue))
                                        : Promise.resolve({});
                                    return [4 /*yield*/, asyncValidation
                                            .then(function (_value) { return ({ value: _value }); })["catch"](function (err) { return ({ error: err._original }); })];
                                case 3:
                                    _d = _e.sent(), value = _d.value, error = _d.error;
                                    if (error || !type || !exports.supportedOperation(operation)) {
                                        errors.push({
                                            field: field,
                                            error: exports.generateSearchQueryError({ error: error, field: field, type: type, operation: operation })
                                        });
                                    }
                                    components.push(__assign(__assign({}, record), { value: value }));
                                    _e.label = 4;
                                case 4: return [2 /*return*/];
                            }
                        });
                    });
                };
                return [4 /*yield*/, Promise.all(Object.entries(query || {}).map(parseSearchQueryEntry))];
            case 1:
                _a.sent();
                return [2 /*return*/, { errors: errors, components: components }];
        }
    });
}); };
/**
 * @description produces query context object. For REST calls, first parses query string to assemble base arguments. Then checks for things like return fields or ordering fields do not exist.
 * @param {Joi.Schema} validator
 * @param {ts.IParamsSearchQueryParser} query
 * @param {string} apiType
 * @returns {ts.IQueryContextResponse}
 */
exports.queryContextParser = function (validator, query, apiType) {
    var errors = [];
    var context = __assign({}, cnst.SEARCH_QUERY_CONTEXT);
    // support user provided seperators for fields that support multiple values
    if (query[cnst.SEPERATOR]) {
        context.seperator = query[cnst.SEPERATOR];
    }
    Object.entries(query).forEach(function (_a) {
        var key = _a[0], rawValue = _a[1];
        if (context.hasOwnProperty(key)) {
            var value = rawValue;
            if (apiType === "REST") {
                // NEED TO SKIP THIS FOR GRAPHQL & GRPC CALLS -- they'll send the arrays & types
                value = exports.contextTransformer(key, rawValue);
            }
            // add order by and fields values to set to ensure they're all part of the validator
            if (["fields", "orderBy"].includes(key)) {
                // orderBy value is an array of obects. need to map to get the field names
                var values = key === "fields" ? value : value.map(function (_a) {
                    var column = _a.column;
                    return column;
                });
                var unsupportedFields = values.filter(function (field) {
                    return !validator[cnst.UNDERSCORE_IDS][cnst.UNDERSCORE_BYKEY].get(field);
                });
                // if attempting to use unsupported fields in context -- add error objects
                if (unsupportedFields.length) {
                    var error = "'" + key + "' in context does not support submitted fields: " + unsupportedFields.join(", ");
                    errors.push({ field: key, error: error });
                }
                else {
                    context[key] = value;
                }
            }
            else {
                context[key] = value;
            }
        }
    });
    return { errors: errors, context: context };
};
// this is to much dupe. can abstract and pass key.path to map
/**
 * @description Accepts Validator Inspection Report as input and lists all softDeleteFields (designed to only support one)
 * @param {ts.IValidatorInspectorReport} report
 */
exports.softDeleteFields = function (report) {
    return Object.entries(report).reduce(function (accum, _a) {
        var key = _a[0], softDeleteFlag = _a[1].softDeleteFlag;
        return !softDeleteFlag ? accum : __spreadArrays(accum, [key]);
    }, []);
};
/**
 * @description Accepts Validator Inspection Report as input and lists all fields that are components of primary key (designed to only support one)
 * @param {ts.IValidatorInspectorReport} report
 */
exports.uniqueKeyComponents = function (report) {
    return Object.entries(report).reduce(function (accum, _a) {
        var key = _a[0], required = _a[1].required;
        return (!required ? accum : __spreadArrays(accum, [key]));
    }, []);
};
/**
 * @description conversion of meters to units used in spatial queries
 * @param {number} meters
 */
exports.metersToDecimalDegrees = function (meters) { return meters / cnst.DD_BASE; };
/**
 * @description A core component of entire system. Each DB resource gets a single validator built on first pass that checks for types and values. This fn derives CRUD & Search specific validators from that original -- and applies flags to keys used in delete, update and read. The REPORT object, reviews each generated validator and generates an object describing each that is used for various logic through the system. Is simplier to perform here a single time and produce a comprehensive report object than continualy checking various properties (often deeply nested and private) in the validator. `redactedFields` is used to withold fields from report -- as it powers openapi generation.
 * @param {Joi.Schema} validator
 * @param {string[]} redactedFields
 * @returns {ts.IValidationExpander}
 */
exports.validationExpander = function (validator, redactedFields) {
    var schema = {
        create: exports.modifyValidator(validator, cnst.CREATE),
        read: exports.modifyValidator(validator, cnst.READ),
        update: exports.modifyValidator(validator, cnst.UPDATE),
        "delete": exports.modifyValidator(validator, cnst.DELETE),
        search: exports.modifyValidator(validator, cnst.SEARCH)
    };
    var report = {
        create: exports.validatorInspector(schema.create, redactedFields),
        read: exports.validatorInspector(schema.read, redactedFields),
        update: exports.validatorInspector(schema.update, redactedFields),
        "delete": exports.validatorInspector(schema["delete"], redactedFields),
        search: exports.validatorInspector(schema.search, redactedFields)
    };
    var meta = {
        softDeleteFields: exports.softDeleteFields(report.read),
        uniqueKeyComponents: exports.uniqueKeyComponents(report.read),
        /**
         * @description
         * @param {*} query
         * @param {*} apiType
         * @param {*} context
         */
        searchQueryParser: function (query, apiType, context) {
            return exports.searchQueryParser(validator, query, apiType, context && context.seperator ? context.seperator : undefined);
        }
    };
    return { schema: schema, report: report, meta: meta };
};
// need to remove context keys
/**
 * @description Used to standardize validation. Want to workwith single input and array of inputs as REST, GraphQL and gRPC can take 1 or many records.
 * @param {Joi.Schema} validator
 * @param {(any | any[])} payload
 */
exports.validateOneOrMany = function (validator, payload) {
    return !Array.isArray(payload)
        ? validator.validateAsync(payload)
        : Joi.array().items(validator).validateAsync(payload);
};
exports.removeContextKeys = function (context, payload) {
    var contextFreePayload = __assign({}, payload);
    Object.keys(context).forEach(function (item) {
        delete contextFreePayload[item];
    });
    return contextFreePayload;
};
/**
 * @description Transforms input into object with `errorType` & `error` keys
 * @param {string} errorType
 * @param {*} error
 * @returns {ts.IRejectResource}
 */
exports.rejectResource = function (errorType, error) { return ({
    errorType: errorType,
    error: error
}); };
/**
 * @description Transforms input into object with `result` key set to the input value
 * @param {*} result
 * @returns {ts.IResolveResource}
 */
exports.resolveResource = function (result) { return ({ result: result }); };
/**
 * @description generates URL paths for each resource. Applies a specific prefix to each.
 * @param {string} resource
 * @param {string} [prefix="service"]
 */
exports.nameRestEndpointGetRecords = function (resource, prefix) {
    if (prefix === void 0) { prefix = "service"; }
    return ({
        resourceEndpoint: "/" + prefix + "/" + resource,
        uniqueEndpoint: "/" + prefix + "/" + resource + "/record"
    });
};
/**
 * @description Adds invalid `key` flag to any validator for a field that is used as a key
 * @param {Joi.Schema} joi
 * @param {boolean} keyComponent
 */
exports.joiKeyComponent = function (joi, keyComponent) {
    return keyComponent ? joi.invalid(cnst.SYMBOL_UNIQUE_KEY_COMPONENT) : joi;
};
/**
 * @description DISABLED FOR EVAL. Goal was to modify validator for required fields. would be used in `genDatabaseResourceValidators`
 * @param {Joi.Schema} joi
 * @param {boolean} required
 */
exports.joiRequired = function (joi, required) {
    return required ? joi : joi;
}; // need to eval .required() here... think it's breaking the framework
/**
 * @description Generates JOI validators for each resource reported in the dbSurveyQuery. Basis of entire system. Also generates dbResources Map -- which contains the survey results -- but in a better format for use through the system.
 * @param {ts.IDatabaseBootstrapRaw} {
 *   db,
 *   dbResourceRawRows,
 *   joiBase,
 * }
 * @returns
 */
exports.genDatabaseResourceValidators = function (_a) {
    var db = _a.db, dbResourceRawRows = _a.dbResourceRawRows, joiBase = _a.joiBase;
    return __awaiter(void 0, void 0, void 0, function () {
        var resources, dbResources, validators;
        return __generator(this, function (_b) {
            resources = dbResourceRawRows.reduce(function (catalog, _a) {
                var resource_schema = _a.resource_schema, resource_type = _a.resource_type, resource_name = _a.resource_name, resource_column_id = _a.resource_column_id, resource_column_name = _a.resource_column_name, notnull = _a.notnull, type = _a.type, primarykey = _a.primarykey, uniquekey = _a.uniquekey, foreignkey_fieldnum = _a.foreignkey_fieldnum, foreignkey = _a.foreignkey, foreignkey_connnum = _a.foreignkey_connnum;
                // this needs to be a fn as the upstream map generates the same string `mapSchemaResources`
                var resourceName = resource_schema + "_" + resource_name;
                if (!catalog[resourceName])
                    catalog[resourceName] = {};
                catalog[resourceName][resource_column_name] = exports.joiKeyComponent(joiBase(type), primarykey);
                return catalog;
            }, {});
            dbResources = dbResourceRawRows.reduce(function (collection, record) {
                var resourceName = record.resource_schema + "_" + record.resource_name;
                if (!collection[resourceName])
                    collection[resourceName] = {};
                collection[resourceName][record.resource_column_name] = record;
                return collection;
            }, {});
            validators = Object.entries(resources).reduce(function (jois, _a) {
                var _b;
                var key = _a[0], value = _a[1];
                return (__assign(__assign({}, jois), (_b = {}, _b[key] = Joi.object(value), _b)));
            }, {});
            return [2 /*return*/, { validators: validators, dbResources: dbResources }];
        });
    });
};
/**
 * @description Builds two objects from one by matching on key prefixes
 * @param {*} payload
 * @param {string} prefix
 * @returns {[ts.IObjectStringByString, ts.IObjectStringByString]}
 */
exports.seperateByKeyPrefix = function (payload, prefix) {
    var payloadWithoutPrefix = {};
    var payloadWithPrefix = {};
    Object.keys(payload || {}).forEach(function (key) {
        if (key.startsWith(prefix)) {
            // remove the lead char from the key -- as its utility ends here
            payloadWithPrefix[key.substring(1)] = payload[key];
        }
        else {
            payloadWithoutPrefix[key] = payload[key];
        }
    });
    return [payloadWithPrefix, payloadWithoutPrefix];
};
/**
 * @description Used for applying complex middleware to subqueries for all service calls and parsing subPayload from REST
 * @param {ts.IClassResourceMap} resourcesMap
 * @param {string} resourceName
 * @param {string} operation
 * @param {*} payload
 * @param {*} [subPayload]
 * @returns
 */
exports.callComplexResource = function (resourcesMap, resourceName, operation, payload, subPayload // subpayloads are passed from GraphQL. else they are parsed from REST `payload`
) { return __awaiter(void 0, void 0, void 0, function () {
    var _a, _subPayload, _restPayload, _b, _subContext, topPayload, resource, subquery, aggregationFn;
    return __generator(this, function (_c) {
        switch (_c.label) {
            case 0:
                if (!subPayload) {
                    _a = exports.seperateByKeyPrefix(payload.payload, ">"), _subPayload = _a[0], _restPayload = _a[1];
                    _b = exports.seperateByKeyPrefix(_restPayload, "]"), _subContext = _b[0], topPayload = _b[1];
                    subPayload = __assign(__assign({}, payload), { payload: _subPayload, context: _subContext });
                    payload.payload = topPayload;
                }
                resource = resourcesMap[resourceName];
                return [4 /*yield*/, resourcesMap[resource.subResourceName][operation](subPayload, { subqueryContext: true })];
            case 1:
                subquery = _c.sent();
                // need to return the 400 already
                if (!subquery.result)
                    return [2 /*return*/, subquery];
                aggregationFn = resource.aggregationFn;
                return [2 /*return*/, resource[operation](payload, {
                        subquery: subquery.result.sql,
                        aggregationFn: aggregationFn
                    })];
        }
    });
}); };
/**
 * @description Used to normalize processing of simple and complex resources. if complex, the first resource is used as the interface, which is the result of the middleware that aggregates or otherwise modifies the subquery data structure.
 * @param {string} str
 * @param {string} [seperator=":"]
 */
exports.getFirstIfSeperated = function (str, seperator) {
    if (seperator === void 0) { seperator = ":"; }
    return str.includes(seperator) ? str.split(seperator)[0] : str;
};
/**
 * @description Seemingly reducer that takes Object.entries and generates Object. Could probably be done with Object.fromEntries -- but the value here is likely typescript (IClassResourceMap). Could likely be culled with a small Pull Request
 * @param {*} Resources
 * @returns {ts.IClassResourceMap}
 */
exports.genResourcesMap = function (Resources) {
    return Resources.reduce(function (batch, _a) {
        var _b;
        var name = _a[0], _Resource = _a[1];
        return (__assign(__assign({}, batch), (_b = {}, _b[name] = _Resource, _b)));
    }, {});
};
/**
 * @description used to namespace complex resources from natural ones. fn name needs attention.
 * @param {*} str
 */
exports.transformNameforResolver = function (str) {
    return str
        .split(":")
        .map(function (item) { return change_case_1.pascalCase(item); }) // this is done to prevent collisions with db resources
        .join(cnst.COMPLEX_RESOLVER_SEPERATOR);
};
/**
 * @description Transform WKT strings to GeoJSON
 * @param {*} wktString
 */
exports.wktToGeoJSON = function (wktString) {
    return wktString
        ? wkx.Geometry.parse(buffer_1.Buffer.from(wktString, "hex")).toGeoJSON()
        : null;
};
/**
 * @description GraphQL calls naturally feature `fields`, however we must distinguish between meta keys and nested db fields. This supports that utility.
 * @param {*} information
 * @returns
 */
exports.extractSelectedFields = function (information) {
    var props = []; // top level fields from GraphQL Type
    var fields = []; // fields within `data` GraphQL Type
    var _filter = function (name) { return function (item) {
        return !name
            ? item.kind && item.name
            : item.kind && item.name && item.name.value === name;
    }; };
    var requestedFields = function (item) { return item.name.value; };
    var _reduce = function (accum, item) {
        return item.selectionSet &&
            item.selectionSet.selections &&
            item.selectionSet.selections.length
            ? __spreadArrays(accum, item.selectionSet.selections.map(requestedFields)) : accum;
    };
    information.fieldNodes.forEach(function (fieldNode) {
        if (fieldNode.selectionSet && fieldNode.selectionSet.selections) {
            var _props = fieldNode.selectionSet.selections
                .filter(_filter())
                .map(requestedFields);
            var _fields = fieldNode.selectionSet.selections
                .filter(_filter("data"))
                .reduce(_reduce, []);
            fields = __spreadArrays(fields, _fields);
            props = __spreadArrays(props, _props);
        }
    });
    return { props: props, fields: fields };
};
/**
 * @description Adds postProcessResponse fn for db dialects where knex returns TextRow objects
 * @param {*} knexConfig
 */
exports.initPostProcessing = function (knexConfig) {
    return !knexConfig.client.includes("mysql")
        ? knexConfig
        : __assign({ postProcessResponse: function (result, queryContext) {
                // only do this for mysql2
                return Array.isArray(result) &&
                    result.length &&
                    result[0].constructor.name === "Array"
                    ? result[0] // should be TextRow
                    : result;
            } }, knexConfig);
};
/**
 * @description checks against whitelist to see if db named supports return on DML
 * @param {string} client
 */
exports.supportsReturnOnCreateAndUpdate = function (client) {
    return ["pg", "mssql"].includes(client);
};
// oracledb removed from ^^ as knex failed to product useful sql.
// tslint:disable: no-bitwise
/**
 * @description
 * @returns {ts.IServicePermission}
 */
exports.permit = function () { return ({
    _permission: 0,
    /**
     * @description
     * @returns {ts.IServicePermission}
     */
    create: function () {
        this._permission = this._permission | cnst.PERMIT_CREATE;
        return this;
    },
    /**
     * @description
     * @returns {ts.IServicePermission}
     */
    read: function () {
        this._permission = this._permission | cnst.PERMIT_READ;
        return this;
    },
    /**
     * @description
     * @returns {ts.IServicePermission}
     */
    update: function () {
        this._permission = this._permission | cnst.PERMIT_UPDATE;
        return this;
    },
    /**
     * @description
     * @returns {ts.IServicePermission}
     */
    "delete": function () {
        this._permission = this._permission | cnst.PERMIT_DELETE;
        return this;
    },
    /**
     * @description
     * @returns {ts.IServicePermission}
     */
    crud: function () {
        return this.create().read().update()["delete"]();
    },
    /**
     * @description
     * @returns {ts.IServicePermission}
     */
    none: function () {
        this._permission = 0;
        return this;
    },
    /**
     * @description
     * @returns {number}
     */
    get: function () {
        return this._permission;
    }
}); };
/**
 * @description Transforms permission object into bitwise flags
 * @param {ts.IObjectStringByGeneric<ts.IServicePermission>} permissions
 * @returns {ts.IObjectStringByNumber}
 */
exports.extractPermissions = function (permissions) {
    return Object.fromEntries(Object.entries(permissions).map(function (_a) {
        var key = _a[0], value = _a[1];
        return [key, value.get()];
    }));
};
/**
 * @description String to Ints for BitWise operations (permission flags)
 * @param {string} operation
 * @returns
 */
exports.operationFlag = function (operation) {
    switch (operation) {
        case "create":
            return cnst.PERMIT_CREATE;
        case "read":
        case "search":
            return cnst.PERMIT_READ;
        case "update":
            return cnst.PERMIT_UPDATE;
        case "delete":
            return cnst.PERMIT_DELETE;
    }
    return 0;
};
// | because we are applying fine grain to higher policy
// TODO: the application of sysPerms and resourcePerms is wrong. Fix before release
/**
 * @description Used to determine if a given method is permitted on a resource.
 * @param {ts.IConfigServicePermission} permissions
 */
exports.permitted = function (permissions) {
    return function (resource, operation) {
        var hurdle = exports.operationFlag(operation); // this is the base bitwise value for this operation
        var sysPerms = permissions.systemPermissions;
        var rsrcPerms = permissions.resourcePermissions[resource];
        // if rsrcPerms set ? allowed by resource : allowed by system
        var grant = rsrcPerms !== undefined ? hurdle & rsrcPerms : hurdle & sysPerms;
        return !!grant;
    };
};
/**
 * @description Transforms nested object from REST, GraphQL, gRPC to simply entries. Used in validation and query building.
 * @param {string} sep
 */
exports.stringValues = function (sep) { return function (obj) {
    return Object.fromEntries(Object.entries(obj || {}).map(function (_a) {
        var key = _a[0], value = _a[1];
        return [
            key,
            Array.isArray(value)
                ? value.join(sep || cnst.SEARCH_QUERY_CONTEXT.seperator)
                : value,
        ];
    }));
}; };
/**
 * @description adds message field index numbers to protobuff messages
 * @param {*} el
 * @param {*} i
 */
exports.appendIndex = function (el, i) { return el + " = " + ++i; };
/**
 * @description appends semicolon to given string
 * @param {*} el
 */
exports.appendSemicolon = function (el) { return el + ";"; };
// https://github.com/mesg-foundation/mesg-js/blob/238e70e56cc8a35cfc8ffeb1ffa92c3160ff5d87/src/util/encoder.ts
/**
 * @description Transforms values in js object to protobuff struct scalars
 * @param {*} data
 * @param {*} key
 * @returns
 */
exports.encodeStructField = function (data, key) {
    var value = data[key];
    switch (Object.prototype.toString.call(value)) {
        case "[object Null]":
        case "[object Undefined]":
            return { nullValue: value };
        case "[object Object]":
            return {
                structValue: {
                    fields: exports.encodeStructFields(value)
                }
            };
        case "[object Array]":
            return {
                listValue: {
                    values: value.map(function (k, i) { return exports.encodeStructField(value, i); })
                }
            };
        case "[object Number]":
            return { numberValue: value };
        case "[object Boolean]":
            return { boolValue: value };
        case "[object String]":
            return { stringValue: value };
        case "[object Date]":
            return { stringValue: value.toJSON() };
        default:
            throw new Error("not supported");
    }
};
/**
 * @description Transforms entries in js object to protobuff struct scalars
 * @param {*} data
 */
exports.encodeStructFields = function (data) {
    return Object.keys(data).reduce(function (prev, next) {
        var _a;
        return (__assign(__assign({}, prev), (_a = {}, _a[next] = exports.encodeStructField(data, next), _a)));
    }, {});
};
/**
 * @description Transforms js object to protobuff struct
 * @param {{ [key: string]: any }} data
 * @returns
 */
exports.encodeStruct = function (data) {
    return {
        fields: data ? exports.encodeStructFields(data) : data
    };
};
// the functions below only exist for implementation within `service-engine-docker`
// it is used in the custom migration feature
/**
 * @description generates migration script name needed for reading from sql & up/down directories
 * @param {*} {date, id, args}
 * @returns
 */
exports.genNextMigrationName = function (_a) {
    var date = _a.date, id = _a.id, args = _a.args;
    var timestamp = date
        .toISOString()
        .replace(cnst.REGEX_NON_DIGIT, "")
        .substr(0, 14);
    var name = change_case_1.snakeCase(args.length ? args.join("-") : cnst.DEFAULT_MIGRATION_SCRIPT_NAME);
    return [timestamp, id, name].join("_");
};
/**
 * @description List all files in a directory in Node.js recursively in a synchronous fashion (taken from a gist)
 * @param {*} dir
 * @param {*} filelist
 * @returns
 */
exports.surveyDirectory = function (dir, filelist) {
    if (filelist === void 0) { filelist = []; }
    var files = fs.readdirSync(dir);
    filelist = filelist || [];
    files.forEach(function (file) {
        if (fs.statSync(path.join(dir, file)).isDirectory()) {
            filelist = exports.surveyDirectory(path.join(dir, file), filelist);
        }
        else {
            filelist.push(path.join(dir, file));
        }
    });
    return filelist;
};
/**
 * @description seperate files into up & down batches for knex
 * @param {*} accum
 * @param {*} curr
 * @returns
 */
exports.reducerSqlContent = function (accum, curr) {
    var direction = curr.includes("/up/") ? "up" : "down";
    accum[direction].push(curr);
    return accum;
};
/**
 * @description collects and sorts filenames for all content that make up a migration batch
 * @param {*} dir
 * @param {*} id
 * @returns
 */
exports.gatherModularSQLContentByID = function (dir, id) {
    var source = exports.surveyDirectory([dir, "sql", id].join("/")).reduce(exports.reducerSqlContent, { up: [], down: [] });
    source.up.sort();
    source.down.sort();
    return source;
};
/**
 * @description generic knex up/down fns that intake file paths and process the content via knex.raw(file_content)
 * @param {*} files
 */
exports.processModularSQL = function (files) { return function (knex) {
    return !files.length
        ? knex
        : files.reduce(function (tnx, file) { return tnx.raw(fs.readFileSync(file, "utf-8")); }, knex.schema);
}; };
/**
 * @description convenience fn that minimizes implementation code for utilizing these methods. Implemented in custom knex stub
 * @param {*} dirname
 * @param {*} filename
 * @returns
 */
exports.modularMigration = function (dirname, filename) {
    var id = filename.replace(dirname, "").split("_")[1];
    var _a = exports.gatherModularSQLContentByID(dirname, id), up = _a.up, down = _a.down;
    return {
        up: exports.processModularSQL(up),
        down: exports.processModularSQL(down)
    };
};
exports.parseGraphQLInput = function (field, op, value) {
    if (op === "geo") {
        var _a = field.split("_"), _op = _a[0], _field = _a.slice(1);
        var _value = _op === "polygon"
            ? [value]
            : _op === "radius"
                ? [value.long, value.lat, value.meters]
                : [value.xMin, value.yMin, value.xMax, value.yMax];
        return [_field + ".geo_" + _op, _value];
    }
    else if (["not_range", "range"].includes(op)) {
        return [field + "." + op, [value.min, value.max]];
    }
    else if (["not_in", "in"].includes(op)) {
        return [field + "." + op, value.join(",")];
    }
    return [field + "." + op, value];
};
exports.gqlParsePayload = function (userQuery) {
    return Object.fromEntries(Object.entries(userQuery).flatMap(function (_a) {
        var op = _a[0], values = _a[1];
        return Object.entries(values).map(function (_a) {
            var field = _a[0], value = _a[1];
            return exports.parseGraphQLInput(field, op, value);
        });
    }));
};
//# sourceMappingURL=utils.js.map