"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getModelsFromManifest = exports.DbtExposureType = exports.getLatestSupportedDbtManifestVersion = exports.getDbtManifestVersion = exports.DbtManifestVersion = exports.convertColumnMetric = exports.convertModelMetric = exports.isDbtRpcRunSqlResults = exports.getEffectiveFieldAiHints = exports.convertToAiHints = exports.convertToGroups = exports.isDbtRpcCompileResults = exports.isDbtRpcManifestResults = exports.isSupportedDbtAdapterType = exports.isSupportedDbtAdapter = exports.isDbtPackages = exports.isDbtRpcDocsGenerateResults = exports.buildModelGraph = exports.patchPathParts = exports.normaliseModelDatabase = exports.SupportedDbtAdapter = void 0;
exports.getCompiledModels = getCompiledModels;
const tslib_1 = require("tslib");
const dependency_graph_1 = require("dependency-graph");
const lightdashProjectConfig_1 = require("../compiler/lightdashProjectConfig");
const assertUnreachable_1 = tslib_1.__importDefault(require("../utils/assertUnreachable"));
const item_1 = require("../utils/item");
const errors_1 = require("./errors");
const field_1 = require("./field");
const filterGrammar_1 = require("./filterGrammar");
var SupportedDbtAdapter;
(function (SupportedDbtAdapter) {
    SupportedDbtAdapter["BIGQUERY"] = "bigquery";
    SupportedDbtAdapter["DATABRICKS"] = "databricks";
    SupportedDbtAdapter["SNOWFLAKE"] = "snowflake";
    SupportedDbtAdapter["REDSHIFT"] = "redshift";
    SupportedDbtAdapter["POSTGRES"] = "postgres";
    SupportedDbtAdapter["DUCKDB"] = "duckdb";
    SupportedDbtAdapter["TRINO"] = "trino";
    SupportedDbtAdapter["CLICKHOUSE"] = "clickhouse";
    SupportedDbtAdapter["ATHENA"] = "athena";
    SupportedDbtAdapter["SPARK"] = "spark";
})(SupportedDbtAdapter || (exports.SupportedDbtAdapter = SupportedDbtAdapter = {}));
const normaliseModelDatabase = (model, targetWarehouse) => {
    switch (targetWarehouse) {
        case SupportedDbtAdapter.POSTGRES:
        case SupportedDbtAdapter.BIGQUERY:
        case SupportedDbtAdapter.SNOWFLAKE:
        case SupportedDbtAdapter.TRINO:
        case SupportedDbtAdapter.ATHENA:
        case SupportedDbtAdapter.REDSHIFT:
        case SupportedDbtAdapter.DUCKDB:
            if (model.database === null) {
                throw new errors_1.ParseError(`Cannot parse dbt model '${model.unique_id}' because the database field has null value.`, {});
            }
            return { ...model, database: model.database };
        case SupportedDbtAdapter.CLICKHOUSE:
            return { ...model, database: '' }; // Clickhouse doesn't have a database field
        case SupportedDbtAdapter.DATABRICKS:
        case SupportedDbtAdapter.SPARK:
            return { ...model, database: model.database || 'DEFAULT' };
        default:
            return (0, assertUnreachable_1.default)(targetWarehouse, `Cannot recognise warehouse ${targetWarehouse}`);
    }
};
exports.normaliseModelDatabase = normaliseModelDatabase;
const patchPathParts = (patchPath) => {
    // dbt-core format: `project://path/to/file.yml`
    // dbt-fusion format: raw path with no project prefix, may contain Windows backslashes
    const normalized = patchPath.replace(/\\/g, '/');
    const separatorIndex = normalized.indexOf('://');
    if (separatorIndex === -1) {
        return {
            project: null,
            path: normalized,
        };
    }
    return {
        project: normalized.slice(0, separatorIndex),
        path: normalized.slice(separatorIndex + '://'.length),
    };
};
exports.patchPathParts = patchPathParts;
const buildModelGraph = (allModels) => {
    const depGraph = new dependency_graph_1.DepGraph();
    const lookup = Object.fromEntries(allModels.map((model) => [model.unique_id, model]));
    allModels.forEach((model) => {
        depGraph.addNode(model.unique_id, { type: 'model', name: model.name });
        // Only use models for graph.
        model.depends_on?.nodes?.forEach((nodeId) => {
            const node = lookup[nodeId];
            if (node) {
                depGraph.addNode(node.unique_id, {
                    type: 'model',
                    name: node.name,
                });
                depGraph.addDependency(model.unique_id, node.unique_id);
            }
        });
    });
    return depGraph;
};
exports.buildModelGraph = buildModelGraph;
const isDbtRpcDocsGenerateResults = (results) => 'nodes' in results &&
    typeof results.nodes === 'object' &&
    results.nodes !== null &&
    Object.values(results.nodes).every((node) => typeof node === 'object' &&
        node !== null &&
        'metadata' in node &&
        'columns' in node);
exports.isDbtRpcDocsGenerateResults = isDbtRpcDocsGenerateResults;
const isDbtPackages = (results) => 'packages' in results;
exports.isDbtPackages = isDbtPackages;
const isDbtRawManifestMetadata = (x) => typeof x === 'object' &&
    x !== null &&
    'dbt_schema_version' in x &&
    'generated_at' in x &&
    'adapter_type' in x;
const isSupportedDbtAdapter = (x) => isDbtRawManifestMetadata(x) &&
    Object.values(SupportedDbtAdapter).includes(x.adapter_type);
exports.isSupportedDbtAdapter = isSupportedDbtAdapter;
const isSupportedDbtAdapterType = (x) => Object.values(SupportedDbtAdapter).includes(x);
exports.isSupportedDbtAdapterType = isSupportedDbtAdapterType;
const isDbtRpcManifestResults = (results) => 'manifest' in results &&
    typeof results.manifest === 'object' &&
    results.manifest !== null &&
    'nodes' in results.manifest &&
    'metadata' in results.manifest &&
    'metrics' in results.manifest &&
    isDbtRawManifestMetadata(results.manifest.metadata);
exports.isDbtRpcManifestResults = isDbtRpcManifestResults;
const isDbtRpcCompileResults = (results) => 'results' in results &&
    Array.isArray(results.results) &&
    results.results.every((result) => typeof result === 'object' &&
        result !== null &&
        'node' in result &&
        typeof result.node === 'object' &&
        result.node !== null &&
        'unique_id' in result.node &&
        'resource_type' in result.node);
exports.isDbtRpcCompileResults = isDbtRpcCompileResults;
const convertToGroups = (dbtGroups, dbtGroupLabel) => {
    let groups = [];
    if (dbtGroups) {
        if (typeof dbtGroups === 'string') {
            groups = [dbtGroups];
        }
        else {
            groups = [...dbtGroups];
        }
    }
    else if (dbtGroupLabel) {
        groups = [dbtGroupLabel];
    }
    return groups;
};
exports.convertToGroups = convertToGroups;
const convertToAiHints = (aiHint) => {
    if (!aiHint) {
        return undefined;
    }
    if (typeof aiHint === 'string') {
        return [aiHint];
    }
    return aiHint;
};
exports.convertToAiHints = convertToAiHints;
const getEffectiveFieldAiHints = (field, table) => {
    const hints = [
        ...((0, exports.convertToAiHints)(field.aiHint) ?? []),
        ...(field.groups ?? []).flatMap((group) => (0, exports.convertToAiHints)(table?.groupDetails?.[group]?.aiHint) ?? []),
    ];
    return hints.length > 0 ? [...new Set(hints)] : undefined;
};
exports.getEffectiveFieldAiHints = getEffectiveFieldAiHints;
const isDbtRpcRunSqlResults = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
results) => 'results' in results &&
    Array.isArray(results.results) &&
    results.results.every((result) => typeof result === 'object' &&
        result !== null &&
        'table' in result &&
        typeof result.table === 'object' &&
        result.table !== null &&
        'column_names' in result.table &&
        Array.isArray(result.table.column_names) &&
        'rows' in result.table &&
        Array.isArray(result.table.rows));
exports.isDbtRpcRunSqlResults = isDbtRpcRunSqlResults;
const convertModelMetric = ({ modelName, name, metric, source, tableLabel, dimensionReference, requiredAttributes, anyAttributes, spotlightConfig, modelCategories = [], modelOwner, defaultShowUnderlyingValues, baseDimensionType, baseDimensionTimeInterval, }) => {
    const groups = (0, exports.convertToGroups)(metric.groups, metric.group_label);
    const spotlightVisibility = metric.spotlight?.visibility ?? spotlightConfig?.default_visibility;
    const metricCategories = Array.from(new Set([...modelCategories, ...(metric.spotlight?.categories || [])]));
    const spotlightCategories = (0, lightdashProjectConfig_1.getCategoriesFromResource)('metric', name, spotlightConfig, metricCategories);
    // Metric owner takes precedence over model owner
    const owner = metric.spotlight?.owner ?? modelOwner;
    const [parsedDefaultFilter] = (0, filterGrammar_1.parseFilters)(metric.spotlight?.default_filter
        ? [metric.spotlight.default_filter]
        : undefined);
    // A spotlight default filter is pre-applied but removable, i.e. non-required.
    const defaultFilter = parsedDefaultFilter
        ? { ...parsedDefaultFilter, required: false }
        : undefined;
    return {
        fieldType: field_1.FieldType.METRIC,
        name,
        label: metric.label || (0, field_1.friendlyName)(name),
        sql: metric.sql,
        table: modelName,
        tableLabel,
        type: metric.type,
        description: metric.description,
        source,
        hidden: !!metric.hidden,
        round: metric.round,
        compact: metric.compact,
        format: metric.format,
        separator: metric.separator,
        groups,
        showUnderlyingValues: metric.show_underlying_values ?? defaultShowUnderlyingValues,
        filters: (0, filterGrammar_1.parseFilters)(metric.filters),
        percentile: metric.percentile,
        ...(metric.distinct_keys
            ? {
                distinctKeys: Array.isArray(metric.distinct_keys)
                    ? metric.distinct_keys
                    : [metric.distinct_keys],
            }
            : {}),
        dimensionReference,
        ...(0, field_1.getMinMaxBaseDimensionMetadata)(metric.type, baseDimensionType
            ? {
                type: baseDimensionType,
                timeInterval: baseDimensionTimeInterval,
            }
            : undefined),
        requiredAttributes,
        anyAttributes,
        ...(metric.urls ? { urls: metric.urls } : null),
        ...(metric.tags
            ? {
                tags: Array.isArray(metric.tags)
                    ? metric.tags
                    : [metric.tags],
            }
            : null),
        ...(metric.default_time_dimension
            ? {
                defaultTimeDimension: {
                    field: metric.default_time_dimension.field,
                    interval: metric.default_time_dimension.interval,
                },
            }
            : null),
        ...(0, lightdashProjectConfig_1.getSpotlightConfigurationForResource)({
            visibility: spotlightVisibility,
            categories: spotlightCategories,
            filterBy: metric.spotlight?.filter_by,
            segmentBy: metric.spotlight?.segment_by,
            defaultSegment: metric.spotlight?.default_segment,
            defaultFilter,
            owner,
        }),
        ...(metric.drivers ? { drivers: metric.drivers } : {}),
        ...(metric.ai_hint ? { aiHint: (0, exports.convertToAiHints)(metric.ai_hint) } : {}),
        ...(metric.richText ? { richText: metric.richText } : {}),
    };
};
exports.convertModelMetric = convertModelMetric;
const convertColumnMetric = ({ modelName, dimensionName, dimensionSql, dimensionType, dimensionTimeInterval, name, metric, source, tableLabel, requiredAttributes, anyAttributes, spotlightConfig, modelCategories = [], modelOwner, defaultShowUnderlyingValues, }) => {
    // The column's type is only a reliable base when the metric actually
    // aggregates it: no `sql`, or a `sql` equal to dimensionSql. A `sql` pointing
    // elsewhere (e.g. MAX(id) under a DATE column) must not inherit the type.
    const aggregatesOwnColumn = !metric.sql || metric.sql === dimensionSql;
    return (0, exports.convertModelMetric)({
        modelName,
        name,
        metric: {
            ...metric,
            sql: metric.sql || dimensionSql,
            description: metric.description ||
                (dimensionName
                    ? `${(0, field_1.friendlyName)(metric.type)} of ${(0, field_1.friendlyName)(dimensionName)}`
                    : undefined),
        },
        source,
        tableLabel,
        dimensionReference: dimensionName
            ? (0, item_1.getItemId)({ table: modelName, name: dimensionName })
            : undefined,
        baseDimensionType: aggregatesOwnColumn ? dimensionType : undefined,
        baseDimensionTimeInterval: aggregatesOwnColumn
            ? dimensionTimeInterval
            : undefined,
        requiredAttributes,
        anyAttributes,
        ...(metric.default_time_dimension
            ? {
                defaultTimeDimension: {
                    field: metric.default_time_dimension.field,
                    interval: metric.default_time_dimension.interval,
                },
            }
            : null),
        spotlightConfig,
        modelCategories,
        modelOwner,
        defaultShowUnderlyingValues,
    });
};
exports.convertColumnMetric = convertColumnMetric;
var DbtManifestVersion;
(function (DbtManifestVersion) {
    DbtManifestVersion["V7"] = "v7";
    DbtManifestVersion["V8"] = "v8";
    DbtManifestVersion["V9"] = "v9";
    DbtManifestVersion["V10"] = "v10";
    DbtManifestVersion["V11"] = "v11";
    DbtManifestVersion["V12"] = "v12";
    // dbt fusion
    DbtManifestVersion["V20"] = "v20";
})(DbtManifestVersion || (exports.DbtManifestVersion = DbtManifestVersion = {}));
const getDbtManifestVersion = (manifest) => {
    const version = manifest.metadata.dbt_schema_version.match(/\/(v\d+).json/)?.[1];
    if (!version) {
        throw new Error(`Could not determine dbt manifest version from ${manifest.metadata.dbt_schema_version}`);
    }
    if (Object.values(DbtManifestVersion).includes(version)) {
        return version;
    }
    throw new Error(`Unsupported dbt manifest version: ${version}`);
};
exports.getDbtManifestVersion = getDbtManifestVersion;
const getLatestSupportedDbtManifestVersion = () => {
    const versions = Object.values(DbtManifestVersion);
    return versions[versions.length - 1];
};
exports.getLatestSupportedDbtManifestVersion = getLatestSupportedDbtManifestVersion;
var DbtExposureType;
(function (DbtExposureType) {
    DbtExposureType["DASHBOARD"] = "dashboard";
    DbtExposureType["NOTEBOOK"] = "notebook";
    DbtExposureType["ANALYSIS"] = "analysis";
    DbtExposureType["ML"] = "ml";
    DbtExposureType["APPLICATION"] = "application";
})(DbtExposureType || (exports.DbtExposureType = DbtExposureType = {}));
const getModelsFromManifest = (manifest) => {
    const models = Object.values(manifest.nodes).filter((node) => ['model', 'seed'].includes(node.resource_type) &&
        node.config?.materialized !== 'ephemeral');
    if (!(0, exports.isSupportedDbtAdapter)(manifest.metadata)) {
        throw new errors_1.ParseError(`dbt adapter not supported. Lightdash does not support adapter ${manifest.metadata.adapter_type}`, {});
    }
    const adapterType = manifest.metadata.adapter_type;
    return models.map((model) => (0, exports.normaliseModelDatabase)(model, adapterType));
};
exports.getModelsFromManifest = getModelsFromManifest;
function getCompiledModels(manifestModels, compiledModelIds) {
    const isAnyModelCompiled = manifestModels.some((model) => model.compiled);
    return manifestModels.filter((model) => {
        // Seeds are always included — they don't appear in dbt ls output
        // and don't have a compiled field.
        if (model.resource_type === 'seed')
            return true;
        if (compiledModelIds) {
            return compiledModelIds.includes(model.unique_id);
        }
        // In case any model is compiled, we only return the compiled models otherwise we return all models (this maintains backwards compatibility + adds ability to use parse)
        return isAnyModelCompiled ? model.compiled : true;
    });
}
//# sourceMappingURL=dbt.js.map