var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { WorkflowHostname } from "../EnvironmentConfig";
import { ClickhouseAppInstance, ClickhouseIngestInstance, } from "../Infrastructure/ClickhouseDatabase";
import ClusterKeyAuthorization from "../Middleware/ClusterKeyAuthorization";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import ModelPermission from "../Types/AnalyticsDatabase/ModelPermission";
import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
import StatementGenerator from "../Utils/AnalyticsDatabase/StatementGenerator";
import { getQuerySettings } from "../Utils/AnalyticsDatabase/QuerySettingsHelper";
import logger from "../Utils/Logger";
import Realtime from "../Utils/Realtime";
import StreamUtil from "../Utils/Stream";
import BaseService from "./BaseService";
import { AsyncLocalStorage } from "node:async_hooks";
import AnalyticsBaseModel from "../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import { WorkflowRoute } from "../../ServiceRoute";
import Protocol from "../../Types/API/Protocol";
import Route from "../../Types/API/Route";
import URL from "../../Types/API/URL";
import AnalyticsTableColumn from "../../Types/AnalyticsDatabase/TableColumn";
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
import SortOrder from "../../Types/BaseDatabase/SortOrder";
import OneUptimeDate from "../../Types/Date";
import BadDataException from "../../Types/Exception/BadDataException";
import Exception from "../../Types/Exception/Exception";
import ExceptionCode from "../../Types/Exception/ExceptionCode";
import ObjectID from "../../Types/ObjectID";
import PositiveNumber from "../../Types/PositiveNumber";
import Text from "../../Types/Text";
import Typeof from "../../Types/Typeof";
import API from "../../Utils/API";
import AggregationType from "../../Types/BaseDatabase/AggregationType";
import ModelEventType from "../../Types/Realtime/ModelEventType";
const insertDedupContext = new AsyncLocalStorage();
export function runWithInsertDedup(tokenBase, fn) {
    return insertDedupContext.run({ tokenBase, chunkIndexByTable: new Map() }, fn);
}
export default class AnalyticsDatabaseService extends BaseService {
    constructor(data) {
        super();
        this.modelType = data.modelType;
        this.model = new this.modelType();
        if (data.database) {
            this.database = data.database; // used for testing.
        }
        else {
            this.database = ClickhouseAppInstance; // default database
        }
        if (data.ingestDatabase) {
            this.ingestDatabase = data.ingestDatabase;
        }
        else {
            this.ingestDatabase = ClickhouseIngestInstance;
        }
        this.databaseClient = this.database.getDataSource();
        this.ingestDatabaseClient = this.ingestDatabase.getDataSource();
        this.statementGenerator = new StatementGenerator({
            modelType: this.modelType,
            database: this.database,
        });
    }
    async insertJsonRows(rows, options) {
        var _a;
        if (!rows || rows.length === 0) {
            return;
        }
        const client = this.getIngestClient();
        const tableName = this.model.tableName;
        if (!tableName) {
            throw new Exception(ExceptionCode.BadDataException, "Analytics model table name not configured");
        }
        let dedupToken = options === null || options === void 0 ? void 0 : options.dedupToken;
        if (!dedupToken) {
            const dedupStore = insertDedupContext.getStore();
            if (dedupStore) {
                const chunkIndex = (_a = dedupStore.chunkIndexByTable.get(tableName)) !== null && _a !== void 0 ? _a : 0;
                dedupStore.chunkIndexByTable.set(tableName, chunkIndex + 1);
                dedupToken = `${dedupStore.tokenBase}:${tableName}:${chunkIndex}`;
            }
        }
        let clickhouseSettings = {
            async_insert: 1,
            wait_for_async_insert: 0,
        };
        if (dedupToken) {
            /*
             * wait_for_async_insert=1 so the worker only acks the job after
             * the block actually landed (or was deduplicated) — otherwise a
             * crash between buffer-write and flush loses data with no retry.
             */
            clickhouseSettings = {
                async_insert: 1,
                wait_for_async_insert: 1,
                async_insert_deduplicate: 1,
                insert_deduplication_token: dedupToken,
            };
        }
        if (options === null || options === void 0 ? void 0 : options.clickhouseSettings) {
            clickhouseSettings = Object.assign(Object.assign({}, clickhouseSettings), options.clickhouseSettings);
        }
        try {
            await client.insert({
                table: tableName,
                values: rows,
                format: "JSONEachRow",
                clickhouse_settings: clickhouseSettings,
            });
            logger.debug(`ClickHouse insert succeeded for table ${tableName} at ${OneUptimeDate.toString(OneUptimeDate.getCurrentDate())}`, { tableName });
        }
        catch (error) {
            logger.error(`ClickHouse insert failed for table ${tableName} at ${OneUptimeDate.toString(OneUptimeDate.getCurrentDate())}`, { tableName });
            logger.error(error, { tableName });
            throw error;
        }
    }
    async doesColumnExistInDatabase(columnName) {
        const statement = this.statementGenerator.toDoesColumnExistStatement(columnName);
        const dbResult = await this.execute(statement);
        const strResult = await StreamUtil.convertStreamToText(dbResult.stream);
        return strResult.trim().length > 0;
    }
    async getColumnTypeInDatabase(column) {
        if (!column) {
            return null;
        }
        const columnName = column.key;
        if (!this.doesColumnExistInDatabase(columnName)) {
            return null;
        }
        const statement = this.statementGenerator.getColumnTypesStatement(columnName);
        const dbResult = await this.execute(statement);
        let strResult = await StreamUtil.convertStreamToText(dbResult.stream);
        /*
         * Unwrap LowCardinality(...) first so dictionary-encoded columns
         * (e.g. LowCardinality(String), LowCardinality(Nullable(String)))
         * map back to their logical type instead of falling through to null.
         */
        if (strResult.includes("LowCardinality(")) {
            const inner = strResult.split("LowCardinality(")[1];
            strResult = inner.substring(0, inner.lastIndexOf(")"));
        }
        // if strResult includes Nullable(type) then extract type.
        if (strResult.includes("Nullable")) {
            let type = strResult.split("Nullable(")[1];
            type = type.split(")")[0];
            strResult = type;
        }
        return (this.statementGenerator.toTableColumnType(strResult.trim()) || null);
    }
    async countBy(countBy) {
        try {
            const checkReadPermissionType = await ModelPermission.checkReadPermission(this.modelType, countBy.query, null, countBy.props);
            countBy.query = checkReadPermissionType.query;
            const countStatement = this.toCountStatement(countBy);
            const dbResult = await this.executeQuery(countStatement);
            logger.debug(`${this.model.tableName} Count Statement executed`, {
                tableName: this.model.tableName,
            });
            logger.debug(countStatement, {
                tableName: this.model.tableName,
            });
            let countPositive = new PositiveNumber(0);
            try {
                const resultInJSON = await dbResult.json();
                if (resultInJSON.data &&
                    resultInJSON.data[0] &&
                    resultInJSON.data[0]["count"] &&
                    typeof resultInJSON.data[0]["count"] === "string") {
                    countPositive = new PositiveNumber(resultInJSON.data[0]["count"]);
                }
            }
            catch (_a) {
                /*
                 * When max_execution_time fires with timeout_overflow_mode='break',
                 * ClickHouse may return a truncated response for count() queries
                 * (the aggregation has no partial row to emit). Treat this as
                 * "count unavailable" rather than a fatal error — the list query
                 * itself still succeeds.
                 */
                logger.warn(`${this.model.tableName} count query returned unparseable response, defaulting to 0`, { tableName: this.model.tableName });
            }
            logger.debug(`Result: `, {
                tableName: this.model.tableName,
            });
            logger.debug(countPositive.toNumber(), {
                tableName: this.model.tableName,
            });
            countPositive = await this.onCountSuccess(countPositive);
            return countPositive;
        }
        catch (error) {
            await this.onCountError(error);
            throw this.getException(error);
        }
    }
    /**
     * Returns whether at least one row matches the query, without counting
     * every match. Prefer this over `countBy(...).toNumber() === 0` for
     * existence checks: `count()` scans every matching row, whereas this
     * issues `SELECT 1 ... LIMIT 1`, which lets ClickHouse short-circuit at
     * the first matching granule — dramatically cheaper on large tables
     * (Metric / Span / Log).
     */
    async existsBy(existsBy) {
        try {
            const checkReadPermissionType = await ModelPermission.checkReadPermission(this.modelType, existsBy.query, null, existsBy.props);
            existsBy.query = checkReadPermissionType.query;
            const existsStatement = this.toExistsStatement(existsBy);
            const dbResult = await this.executeQuery(existsStatement);
            const resultInJSON = await dbResult.json();
            return Boolean(resultInJSON.data &&
                Array.isArray(resultInJSON.data) &&
                resultInJSON.data.length > 0);
        }
        catch (error) {
            await this.onFindError(error);
            throw this.getException(error);
        }
    }
    async addColumnInDatabase(column) {
        const statement = this.statementGenerator.toAddColumnStatement(column);
        await this.execute(statement);
        // Add skip index separately (ClickHouse requires ADD INDEX as a separate ALTER statement)
        const indexStatement = this.statementGenerator.toAddSkipIndexStatement(column);
        if (indexStatement) {
            await this.execute(indexStatement);
        }
    }
    async dropColumnInDatabase(columnName) {
        /*
         * Drop any skip index associated with this column before dropping the column itself.
         * ClickHouse will reject a column drop if a skip index depends on it.
         */
        const column = this.model.tableColumns.find((col) => {
            return col.key === columnName;
        });
        if (column === null || column === void 0 ? void 0 : column.skipIndex) {
            await this.execute(this.statementGenerator.toDropSkipIndexStatement(column.skipIndex.name));
        }
        await this.execute(this.statementGenerator.toDropColumnStatement(columnName));
    }
    async doesColumnExist(columnName) {
        const tableName = this.model.tableName;
        const result = await (await this.executeQuery(`SELECT count() as cnt FROM system.columns WHERE database = currentDatabase() AND table = '${tableName}' AND name = '${columnName}'`)).json();
        const rows = result.data || [];
        return rows.length > 0 && Number(rows[0]["cnt"]) > 0;
    }
    async getColumnCodec(columnName) {
        const tableName = this.model.tableName;
        const result = await (await this.executeQuery(`SELECT compression_codec FROM system.columns WHERE database = currentDatabase() AND table = '${tableName}' AND name = '${columnName}'`)).json();
        const rows = result.data || [];
        if (rows.length === 0) {
            return "";
        }
        return rows[0]["compression_codec"] || "";
    }
    /**
     * The exact ClickHouse type string for a column as stored in the DB
     * (e.g. "String", "Nullable(Int32)", "LowCardinality(Nullable(String))").
     * Returns "" if the column does not exist. Used by migrations that need to
     * re-state a column's type in a MODIFY COLUMN without guessing it.
     */
    async getColumnDatabaseType(columnName) {
        const tableName = this.model.tableName;
        const result = await (await this.executeQuery(`SELECT type FROM system.columns WHERE database = currentDatabase() AND table = '${tableName}' AND name = '${columnName}'`)).json();
        const rows = result.data || [];
        if (rows.length === 0) {
            return "";
        }
        return rows[0]["type"] || "";
    }
    async setColumnCodecIfNotSet(data) {
        const tableName = this.model.tableName;
        const currentCodec = await this.getColumnCodec(data.columnName);
        if (currentCodec === data.expectedCodecValue) {
            logger.info(`${tableName}.${data.columnName} already has ${data.expectedCodecValue}, skipping`, { tableName });
            return;
        }
        await this.execute(`ALTER TABLE ${tableName} MODIFY COLUMN ${data.columnName} ${data.columnType} CODEC(${data.codec}) SETTINGS mutations_sync=0`);
        logger.info(`Applied ${data.codec} codec to ${tableName}.${data.columnName} (async)`, { tableName });
    }
    async findBy(findBy) {
        return await this._findBy(findBy);
    }
    /**
     * Group telemetry rows by (primaryEntityId, primaryEntityType) for a project over a
     * time window, returning the row count and an estimate of the ingested
     * byte size (ClickHouse `byteSize(*)`, the uncompressed in-memory size of
     * each row's columns). This is the enumeration source for usage billing:
     * a single aggregation scan surfaces EVERY resource that emitted
     * telemetry — real Services, Hosts, Docker hosts, Kubernetes clusters,
     * Monitors and unattributed (primaryEntityId = projectId) — without needing a
     * Postgres row per resource. The caller decides which serviceTypes to
     * bill and how to attribute retention.
     */
    async groupTelemetryUsageByService(data) {
        const timestampColumnName = data.timestampColumnName.toString();
        if (!this.model.getTableColumn(timestampColumnName)) {
            throw new BadDataException(`Invalid timestampColumnName: ${timestampColumnName}`);
        }
        if (!this.database) {
            this.useDefaultDatabase();
        }
        const databaseName = this.database.getDatasourceOptions().database;
        const statement = SQL `SELECT primaryEntityId AS primaryEntityId, primaryEntityType AS primaryEntityType, count() AS rowCount, sum(byteSize(*)) AS estimatedBytes FROM ${databaseName}.${this.model.tableName} WHERE projectId = ${{
            type: TableColumnType.ObjectID,
            value: data.projectId,
        }} AND ${timestampColumnName} >= ${{
            type: TableColumnType.DateTime64,
            value: data.startDate,
        }} AND ${timestampColumnName} <= ${{
            type: TableColumnType.DateTime64,
            value: data.endDate,
        }} GROUP BY primaryEntityId, primaryEntityType`;
        /*
         * Billing scan: deliberately NO timeout_overflow_mode='break'. A
         * partial aggregation here silently undercounts usage (rows that
         * weren't scanned before the cap simply never get billed). Failing
         * loudly lets the staging cron retry instead; the cap is raised to
         * compensate for the full-day scan on large projects.
         */
        statement.append(getQuerySettings({ maxExecutionTimeInSeconds: 120 }));
        const dbResult = await this.executeQuery(statement);
        const responseJSON = await dbResult.json();
        const items = responseJSON.data ? responseJSON.data : [];
        const results = [];
        for (const item of items) {
            const primaryEntityId = item["primaryEntityId"] || "";
            if (!primaryEntityId) {
                continue;
            }
            const serviceTypeRaw = item["primaryEntityType"];
            const primaryEntityType = typeof serviceTypeRaw === "string" && serviceTypeRaw.trim()
                ? serviceTypeRaw
                : null;
            results.push({
                primaryEntityId,
                primaryEntityType,
                rowCount: Number(item["rowCount"]) || 0,
                estimatedBytes: Number(item["estimatedBytes"]) || 0,
            });
        }
        return results;
    }
    async aggregateBy(aggregateBy) {
        return await this._aggregateBy(aggregateBy);
    }
    async _aggregateBy(aggregateBy) {
        try {
            if (!aggregateBy.sort || Object.keys(aggregateBy.sort).length === 0) {
                aggregateBy.sort = {
                    [aggregateBy.aggregationTimestampColumnName]: SortOrder.Descending,
                };
            }
            if (!aggregateBy.limit) {
                aggregateBy.limit = 10;
            }
            if (!aggregateBy.aggregationType) {
                throw new BadDataException("aggregationType is required");
            }
            const allowedAggregationTypes = Object.values(AggregationType);
            if (!allowedAggregationTypes.includes(aggregateBy.aggregationType)) {
                throw new BadDataException(`Invalid aggregationType: ${aggregateBy.aggregationType}. Allowed values: ${allowedAggregationTypes.join(", ")}`);
            }
            if (!aggregateBy.aggregationTimestampColumnName) {
                throw new BadDataException("aggregationTimestampColumnName is required");
            }
            if (!aggregateBy.aggregateColumnName) {
                throw new BadDataException("aggregateColumnName is required");
            }
            if (!this.model.getTableColumn(aggregateBy.aggregateColumnName.toString())) {
                throw new BadDataException(`Invalid aggregateColumnName: ${aggregateBy.aggregateColumnName.toString()}`);
            }
            if (!this.model.getTableColumn(aggregateBy.aggregationTimestampColumnName.toString())) {
                throw new BadDataException(`Invalid aggregationTimestampColumnName: ${aggregateBy.aggregationTimestampColumnName.toString()}`);
            }
            const result = await ModelPermission.checkReadPermission(this.modelType, aggregateBy.query, {
                [aggregateBy.aggregateColumnName]: true,
                [aggregateBy.aggregationTimestampColumnName]: true,
            }, aggregateBy.props);
            aggregateBy.query = result.query;
            const findStatement = this.toAggregateStatement(aggregateBy);
            const dbResult = await this.executeQuery(findStatement.statement);
            logger.debug(`${this.model.tableName} Aggregate Statement executed`, {
                tableName: this.model.tableName,
            });
            const responseJSON = await dbResult.json();
            const items = responseJSON.data
                ? responseJSON.data
                : [];
            const aggregatedItems = [];
            // convert date column from string to date.
            const groupByColumnNames = aggregateBy.groupBy
                ? Object.keys(aggregateBy.groupBy)
                : [];
            for (const item of items) {
                if (!item[aggregateBy.aggregationTimestampColumnName]) {
                    continue;
                }
                // if value is of type string then convert it to number.
                if (typeof item[aggregateBy.aggregateColumnName] === Typeof.String) {
                    item[aggregateBy.aggregateColumnName] =
                        Number.parseFloat(item[aggregateBy.aggregateColumnName]);
                }
                /*
                 * Preserve every group-by column on the aggregated row. The
                 * previous implementation only copied the first column, which
                 * silently dropped the rest when callers grouped by more than
                 * one dimension (e.g. attributes + name). `AggregatedModel`'s
                 * index signature already accepts arbitrary keys, so existing
                 * single-column consumers still work.
                 */
                const aggregatedModel = {
                    timestamp: OneUptimeDate.fromString(item[aggregateBy.aggregationTimestampColumnName]),
                    value: item[aggregateBy.aggregateColumnName],
                };
                for (const groupByColumnName of groupByColumnNames) {
                    aggregatedModel[groupByColumnName] = item[groupByColumnName];
                }
                aggregatedItems.push(aggregatedModel);
            }
            return {
                data: aggregatedItems,
            };
        }
        catch (error) {
            await this.onFindError(error);
            throw this.getException(error);
        }
    }
    async _findBy(findBy) {
        try {
            if (!findBy.sort || Object.keys(findBy.sort).length === 0) {
                /*
                 * Default sort uses the model's declared `defaultSortColumn`
                 * (e.g. `time` for Log, `startTime` for Span) so the query
                 * streams from the ClickHouse sort key. The historical
                 * fallback of `createdAt` is not in the sort key on most
                 * analytics tables, which triggered a full sort even on
                 * small LIMITed queries.
                 */
                const defaultSortColumn = this.model.defaultSortColumn || "createdAt";
                findBy.sort = {
                    [defaultSortColumn]: SortOrder.Descending,
                };
                if (!findBy.select) {
                    findBy.select = {};
                }
            }
            const onFind = findBy.props.ignoreHooks
                ? { findBy, carryForward: [] }
                : await this.onBeforeFind(findBy);
            const onBeforeFind = Object.assign({}, onFind.findBy);
            const carryForward = onFind.carryForward;
            if (!onBeforeFind.select ||
                Object.keys(onBeforeFind.select).length === 0) {
                onBeforeFind.select = {};
            }
            if (!onBeforeFind.select["_id"]) {
                onBeforeFind.select["_id"] = true;
            }
            const result = await ModelPermission.checkReadPermission(this.modelType, onBeforeFind.query, onBeforeFind.select || null, onBeforeFind.props);
            onBeforeFind.query = result.query;
            onBeforeFind.select = result.select || undefined;
            if (!(onBeforeFind.skip instanceof PositiveNumber)) {
                onBeforeFind.skip = new PositiveNumber(onBeforeFind.skip);
            }
            if (!(onBeforeFind.limit instanceof PositiveNumber)) {
                onBeforeFind.limit = new PositiveNumber(onBeforeFind.limit);
            }
            const findStatement = this.toFindStatement(onBeforeFind);
            const dbResult = await this.executeQuery(findStatement.statement);
            logger.debug(`${this.model.tableName} Find Statement executed`, {
                tableName: this.model.tableName,
            });
            logger.debug(findStatement.statement, {
                tableName: this.model.tableName,
            });
            const responseJSON = await dbResult.json();
            const jsonItems = responseJSON.data;
            let items = AnalyticsBaseModel.fromJSONArray(jsonItems, this.modelType);
            if (!findBy.props.ignoreHooks) {
                items = await (await this.onFindSuccess({ findBy, carryForward }, items)).carryForward;
            }
            return items;
        }
        catch (error) {
            await this.onFindError(error);
            throw this.getException(error);
        }
    }
    convertSelectReturnedDataToJson(strResult, columns) {
        if (!strResult || !strResult.trim()) {
            return [];
        }
        const jsonItems = [];
        const rows = strResult.split("\n");
        for (const row of rows) {
            if (!row) {
                continue;
            }
            const jsonItem = {};
            const values = row.split("\t");
            for (let i = 0; i < columns.length; i++) {
                jsonItem[columns[i]] = values[i];
                if (values[i] === "NULL") {
                    jsonItem[columns[i]] = null;
                }
                if (values[i] === "\\N") {
                    jsonItem[columns[i]] = null;
                }
            }
            jsonItems.push(jsonItem);
        }
        return jsonItems;
    }
    async onBeforeDelete(deleteBy) {
        // A place holder method used for overriding.
        return Promise.resolve({ deleteBy, carryForward: null });
    }
    async onBeforeUpdate(updateBy) {
        // A place holder method used for overriding.
        return Promise.resolve({ updateBy, carryForward: null });
    }
    async onBeforeFind(findBy) {
        // A place holder method used for overriding.
        return Promise.resolve({ findBy, carryForward: null });
    }
    /**
     * Read-side retention filter. TTL is `retentionDate DELETE` with
     * ttl_only_drop_parts=1, so a part survives until EVERY row in it has
     * expired — rows past their per-service retention stay on disk (and
     * were queryable) for up to a partition's worth of extra time. For
     * models that carry a retentionDate column, every centrally generated
     * read appends this predicate so expired rows become invisible the
     * moment they expire rather than when their part finally drops.
     *
     * Returns the raw SQL fragment (server-evaluated now(), no parameter)
     * or "" when the model has no retentionDate column.
     */
    getRetentionReadFilter() {
        if (!this.model.getTableColumn("retentionDate")) {
            return "";
        }
        return " AND retentionDate >= now()";
    }
    toCountStatement(countBy) {
        if (!this.database) {
            this.useDefaultDatabase();
        }
        const databaseName = this.database.getDatasourceOptions().database;
        const whereStatement = this.statementGenerator.toWhereStatement(countBy.query);
        /* eslint-disable prettier/prettier */
        const statement = SQL `
            SELECT
                count(`;
        if (countBy.groupBy && Object.keys(countBy.groupBy).length > 0) {
            const groupByKey = Object.keys(countBy.groupBy)[0];
            statement.append(SQL `DISTINCT ${groupByKey}`);
        }
        statement
            .append(SQL `) as count
            FROM ${databaseName}.${this.model.tableName}
            WHERE TRUE `)
            .append(whereStatement)
            .append(this.getRetentionReadFilter());
        if (countBy.limit) {
            statement.append(SQL `
            LIMIT ${{
                value: Number(countBy.limit),
                type: TableColumnType.Number,
            }}
            `);
        }
        if (countBy.skip) {
            statement.append(SQL `
            OFFSET ${{
                value: Number(countBy.skip),
                type: TableColumnType.Number,
            }}
            `);
        }
        /*
         * Cap count query runtime below the ClickHouse client's 58s
         * request_timeout. Wide time-range queries on large tables (e.g. Span)
         * can scan billions of rows; without a cap the query runs until the
         * HTTP client disconnects, wasting ClickHouse resources. With 'break'
         * mode ClickHouse returns a partial (lower-bound) count rather than
         * throwing, which is acceptable for pagination display.
         */
        statement.append(getQuerySettings({
            maxExecutionTimeInSeconds: 45,
            timeoutOverflowMode: "break",
        }));
        logger.debug(`${this.model.tableName} Count Statement`, { tableName: this.model.tableName });
        logger.debug(statement, { tableName: this.model.tableName });
        return statement;
    }
    toExistsStatement(existsBy) {
        if (!this.database) {
            this.useDefaultDatabase();
        }
        const databaseName = this.database.getDatasourceOptions().database;
        const whereStatement = this.statementGenerator.toWhereStatement(existsBy.query);
        /*
         * `SELECT 1 ... LIMIT 1` so ClickHouse stops at the first matching
         * row instead of scanning every match like count() does. The
         * max_execution_time cap is defense in depth; unlike the count and
         * find statements we deliberately do NOT set timeout_overflow_mode
         * = 'break' here, because a partial (empty) result would be read as
         * "does not exist" — a false negative that could, for example, let a
         * caller insert a duplicate. A LIMIT 1 over the sort key never gets
         * near this cap in practice; if it ever did, throwing is the safe
         * outcome.
         */
        /* eslint-disable prettier/prettier */
        const statement = SQL `
            SELECT 1 as existsFlag
            FROM ${databaseName}.${this.model.tableName}
            WHERE TRUE `
            .append(whereStatement)
            .append(this.getRetentionReadFilter());
        statement.append(SQL ` LIMIT 1`);
        statement.append(getQuerySettings({ maxExecutionTimeInSeconds: 45 }));
        logger.debug(`${this.model.tableName} Exists Statement`, { tableName: this.model.tableName });
        logger.debug(statement, { tableName: this.model.tableName });
        return statement;
    }
    toAggregateStatement(aggregateBy) {
        if (!this.database) {
            this.useDefaultDatabase();
        }
        const databaseName = this.database.getDatasourceOptions().database;
        const select = this.statementGenerator.toAggregateSelectStatement(aggregateBy);
        const whereStatement = this.statementGenerator.toWhereStatement(aggregateBy.query);
        const sortStatement = this.statementGenerator.toSortStatement(aggregateBy.sort);
        const statement = SQL ``;
        statement.append(SQL `SELECT `.append(select.statement));
        statement.append(SQL ` FROM ${databaseName}.${this.model.tableName}`);
        statement
            .append(SQL ` WHERE TRUE `)
            .append(whereStatement)
            .append(this.getRetentionReadFilter());
        statement
            .append(SQL ` GROUP BY `)
            .append(`${aggregateBy.aggregationTimestampColumnName.toString()}`);
        if (aggregateBy.groupBy && Object.keys(aggregateBy.groupBy).length > 0) {
            statement
                .append(SQL ` , `)
                .append(this.statementGenerator.toGroupByStatement(aggregateBy.groupBy));
        }
        statement.append(SQL ` ORDER BY `).append(sortStatement);
        statement.append(SQL ` LIMIT ${{
            value: Number(aggregateBy.limit),
            type: TableColumnType.Number,
        }}`);
        statement.append(SQL ` OFFSET ${{
            value: Number(aggregateBy.skip),
            type: TableColumnType.Number,
        }}
        `);
        /*
         * Aggregation read-path settings.
         *
         * - optimize_aggregation_in_order: when GROUP BY is a prefix of the
         *   sort key (we always group by a time bucket and the time column
         *   is at the tail of every analytics primary key), ClickHouse can
         *   stream rows in order and emit aggregates without an in-memory
         *   sort, which is a large speedup on wide time ranges.
         * - optimize_move_to_prewhere: PREWHERE is a default-on optimizer
         *   pass; we set it explicitly so the behavior is independent of
         *   server-side defaults.
         * - max_threads=4: caps per-query parallelism so a single dashboard
         *   load (which fans out to many aggregate calls) does not starve
         *   other tenants on the cluster. Per-query latency is essentially
         *   unchanged at 4 threads for the usual dashboard widget time
         *   ranges, but cluster headroom is preserved under burst.
         */
        statement.append(getQuerySettings({
            additionalSettings: {
                optimize_aggregation_in_order: 1,
                optimize_move_to_prewhere: 1,
                max_threads: 4,
            },
        }));
        logger.debug(`${this.model.tableName} Aggregate Statement`, { tableName: this.model.tableName });
        logger.debug(statement, { tableName: this.model.tableName });
        return { statement, columns: select.columns };
    }
    toFindStatement(findBy) {
        if (!this.database) {
            this.useDefaultDatabase();
        }
        const databaseName = this.database.getDatasourceOptions().database;
        let groupByStatement = null;
        if (findBy.groupBy && Object.keys(findBy.groupBy).length > 0) {
            // overwrite select object
            findBy.select = Object.assign({}, findBy.groupBy);
            groupByStatement = this.statementGenerator.toGroupByStatement(findBy.groupBy);
        }
        const select = this.statementGenerator.toSelectStatement(findBy.select);
        const whereStatement = this.statementGenerator.toWhereStatement(findBy.query);
        const sortStatement = this.statementGenerator.toSortStatement(findBy.sort);
        const statement = SQL ``;
        statement.append(SQL `SELECT `.append(select.statement));
        statement.append(SQL ` FROM ${databaseName}.${this.model.tableName}`);
        statement
            .append(SQL ` WHERE TRUE `)
            .append(whereStatement)
            .append(this.getRetentionReadFilter());
        if (groupByStatement) {
            statement.append(SQL ` GROUP BY `).append(groupByStatement);
        }
        statement.append(SQL ` ORDER BY `).append(sortStatement);
        statement.append(SQL ` LIMIT ${{
            value: Number(findBy.limit),
            type: TableColumnType.Number,
        }}`);
        statement.append(SQL ` OFFSET ${{
            value: Number(findBy.skip),
            type: TableColumnType.Number,
        }}
        `);
        /*
         * Defense in depth: cap find-query runtime below the ClickHouse
         * client's 58s request_timeout. The LIMIT clause keeps most queries
         * fast, but complex WHERE filters (e.g. parentSpanId IS NULL) on
         * wide time ranges can still cause long scans. 'break' mode returns
         * partial results rather than throwing.
         */
        statement.append(getQuerySettings({
            maxExecutionTimeInSeconds: 45,
            timeoutOverflowMode: "break",
        }));
        logger.debug(`${this.model.tableName} Find Statement`, { tableName: this.model.tableName });
        logger.debug(statement, { tableName: this.model.tableName });
        return { statement, columns: select.columns };
    }
    toDeleteStatement(deleteBy) {
        if (!this.database) {
            this.useDefaultDatabase();
        }
        const databaseName = this.database.getDatasourceOptions().database;
        const whereStatement = this.statementGenerator.toWhereStatement(deleteBy.query);
        /*
         * Use ClickHouse lightweight deletes (`DELETE FROM`) rather than
         * `ALTER TABLE … DELETE`. The latter creates an async mutation that
         * rewrites whole parts and is bounded by `number_of_mutations_to_throw`
         * (default 1000). Customers with chatty state transitions hit that
         * ceiling and every subsequent delete fails with TOO_MANY_MUTATIONS.
         * Lightweight deletes mark rows via the hidden `_row_exists` column
         * and are reconciled during normal merges, so they don't accumulate
         * in the mutations queue.
         */
        /* eslint-disable prettier/prettier */
        const statement = SQL `
            DELETE FROM ${databaseName}.${this.model.tableName}
            WHERE TRUE `.append(whereStatement);
        logger.debug(`${this.model.tableName} Delete Statement`, { tableName: this.model.tableName });
        logger.debug(statement, { tableName: this.model.tableName });
        return statement;
    }
    async findOneBy(findOneBy) {
        const findBy = findOneBy;
        findBy.limit = new PositiveNumber(1);
        findBy.skip = new PositiveNumber(0);
        const documents = await this._findBy(findBy);
        if (documents && documents[0]) {
            return documents[0];
        }
        return null;
    }
    async deleteBy(deleteBy) {
        return await this._deleteBy(deleteBy);
    }
    async _deleteBy(deleteBy) {
        var _a;
        try {
            const onDelete = deleteBy.props.ignoreHooks
                ? { deleteBy, carryForward: [] }
                : await this.onBeforeDelete(deleteBy);
            const beforeDeleteBy = onDelete.deleteBy;
            beforeDeleteBy.query = await ModelPermission.checkDeletePermission(this.modelType, beforeDeleteBy.query, deleteBy.props);
            const select = {};
            const tenantColumnName = ((_a = this.getModel().getTenantColumn()) === null || _a === void 0 ? void 0 : _a.key) || null;
            if (tenantColumnName) {
                select[tenantColumnName] = true;
            }
            const deleteStatement = this.toDeleteStatement(beforeDeleteBy);
            await this.execute(deleteStatement);
            logger.debug(`${this.model.tableName} Delete Statement executed`, { tableName: this.model.tableName });
            logger.debug(deleteStatement, { tableName: this.model.tableName });
        }
        catch (error) {
            await this.onDeleteError(error);
            throw this.getException(error);
        }
    }
    async findOneById(findOneById) {
        if (!findOneById.id) {
            throw new BadDataException("findOneById.id is required");
        }
        return await this.findOneBy({
            query: {
                _id: findOneById.id,
            },
            select: findOneById.select || {},
            props: findOneById.props,
        });
    }
    async updateBy(updateBy) {
        await this._updateBy(updateBy);
    }
    async _updateBy(updateBy) {
        var _a;
        try {
            const onUpdate = updateBy.props.ignoreHooks
                ? { updateBy, carryForward: [] }
                : await this.onBeforeUpdate(updateBy);
            const beforeUpdateBy = onUpdate.updateBy;
            beforeUpdateBy.query = await ModelPermission.checkUpdatePermissions(this.modelType, beforeUpdateBy.query, beforeUpdateBy.data, beforeUpdateBy.props);
            const select = {};
            const tenantColumnName = ((_a = this.getModel().getTenantColumn()) === null || _a === void 0 ? void 0 : _a.key) || null;
            if (tenantColumnName) {
                select[tenantColumnName] = true;
            }
            const statement = this.statementGenerator.toUpdateStatement(beforeUpdateBy);
            await this.execute(statement);
            logger.debug(`${this.model.tableName} Update Statement executed`, { tableName: this.model.tableName });
            logger.debug(statement, { tableName: this.model.tableName });
        }
        catch (error) {
            await this.onUpdateError(error);
            throw this.getException(error);
        }
    }
    generateDefaultValues(data) {
        const tableColumns = data.getTableColumns();
        for (const column of tableColumns) {
            if (column.forceGetDefaultValueOnCreate) {
                data.setColumnValue(column.key, column.forceGetDefaultValueOnCreate());
            }
        }
        return data;
    }
    useDefaultDatabase() {
        this.database = ClickhouseAppInstance;
        this.databaseClient = this.database.getDataSource();
        this.ingestDatabase = ClickhouseIngestInstance;
        this.ingestDatabaseClient = this.ingestDatabase.getDataSource();
    }
    async execute(statement, options) {
        const client = this.getDatabaseClient();
        const query = statement instanceof Statement ? statement.query : statement;
        const queryParams = statement instanceof Statement ? statement.query_params : undefined;
        return (await client.exec(Object.assign(Object.assign({ query: query, query_params: queryParams || undefined }, ((options === null || options === void 0 ? void 0 : options.clickhouseSettings)
            ? { clickhouse_settings: options.clickhouseSettings }
            : {})), ((options === null || options === void 0 ? void 0 : options.queryId) ? { query_id: options.queryId } : {}))));
    }
    async executeQuery(statement, options) {
        const client = this.getDatabaseClient();
        const query = statement instanceof Statement ? statement.query : statement;
        const queryParams = statement instanceof Statement ? statement.query_params : undefined;
        return await client.query(Object.assign(Object.assign({ query: query, format: "JSON", query_params: queryParams || undefined }, ((options === null || options === void 0 ? void 0 : options.clickhouseSettings)
            ? { clickhouse_settings: options.clickhouseSettings }
            : {})), ((options === null || options === void 0 ? void 0 : options.queryId) ? { query_id: options.queryId } : {})));
    }
    getDatabaseClient() {
        /*
         * Refresh the ClickHouse client lazily so services created before the
         * ClickHouse connection was established pick up the live client.
         */
        if (!this.database) {
            this.useDefaultDatabase();
        }
        if (!this.databaseClient && this.database) {
            this.databaseClient = this.database.getDataSource();
        }
        if (!this.databaseClient) {
            throw new Exception(ExceptionCode.DatabaseNotConnectedException, "ClickHouse client is not connected");
        }
        return this.databaseClient;
    }
    getIngestClient() {
        if (!this.ingestDatabase) {
            this.useDefaultDatabase();
        }
        if (!this.ingestDatabaseClient && this.ingestDatabase) {
            this.ingestDatabaseClient = this.ingestDatabase.getDataSource();
        }
        if (!this.ingestDatabaseClient) {
            throw new Exception(ExceptionCode.DatabaseNotConnectedException, "ClickHouse ingest client is not connected");
        }
        return this.ingestDatabaseClient;
    }
    async onUpdateSuccess(onUpdate, _updatedItemIds) {
        // A place holder method used for overriding.
        return Promise.resolve(onUpdate);
    }
    async onUpdateError(error) {
        // A place holder method used for overriding.
        return Promise.resolve(error);
    }
    async onDeleteSuccess(onDelete, _itemIdsBeforeDelete) {
        // A place holder method used for overriding.
        return Promise.resolve(onDelete);
    }
    async onDeleteError(error) {
        // A place holder method used for overriding.
        return Promise.resolve(error);
    }
    async onFindSuccess(onFind, items) {
        // A place holder method used for overriding.
        return Promise.resolve(Object.assign(Object.assign({}, onFind), { carryForward: items }));
    }
    async onFindError(error) {
        // A place holder method used for overriding.
        return Promise.resolve(error);
    }
    async onCountSuccess(count) {
        // A place holder method used for overriding.
        return Promise.resolve(count);
    }
    async onCountError(error) {
        // A place holder method used for overriding.
        return Promise.resolve(error);
    }
    async onCreateSuccess(_onCreate, createdItem) {
        // A place holder method used for overriding.
        return Promise.resolve(createdItem);
    }
    async onBeforeCreate(createBy) {
        // A place holder method used for overriding.
        return Promise.resolve({
            createBy: createBy,
            carryForward: undefined,
        });
    }
    async _onBeforeCreate(createBy) {
        var _a;
        // Private method that runs before create.
        const projectIdColumn = ((_a = this.model.getTenantColumn()) === null || _a === void 0 ? void 0 : _a.key) || null;
        if (projectIdColumn && createBy.props.tenantId) {
            createBy.data[projectIdColumn] = createBy.props.tenantId;
        }
        return await this.onBeforeCreate(createBy);
    }
    async createMany(createBy) {
        var _a, _b, _c;
        // add tenantId if present.
        const tenantColumnName = ((_a = this.model.getTenantColumn()) === null || _a === void 0 ? void 0 : _a.key) || null;
        const items = [];
        const carryForwards = [];
        for (const item of createBy.items) {
            let data = item;
            const onCreate = createBy.props.ignoreHooks
                ? {
                    createBy: {
                        data: data,
                        props: createBy.props,
                    },
                    carryForward: [],
                }
                : await this._onBeforeCreate({
                    data: data,
                    props: createBy.props,
                });
            data = onCreate.createBy.data;
            const carryForward = onCreate.carryForward;
            carryForwards.push(carryForward);
            if (tenantColumnName && createBy.props.tenantId) {
                data.setColumnValue(tenantColumnName, createBy.props.tenantId);
            }
            data = this.sanitizeCreate(data);
            data = this.generateDefaultValues(data);
            data = this.checkRequiredFields(data);
            if (!this.isValid(data)) {
                throw new BadDataException("Data is not valid");
            }
            // check total items by
            ModelPermission.checkCreatePermissions(this.modelType, data, createBy.props);
            items.push(data);
        }
        try {
            const insertStatement = this.statementGenerator.toCreateStatement({ item: items });
            await this.execute(insertStatement);
            logger.debug(`${this.model.tableName} Create Statement executed`, { tableName: this.model.tableName });
            logger.debug(insertStatement, { tableName: this.model.tableName });
            if (!createBy.props.ignoreHooks) {
                for (let i = 0; i < items.length; i++) {
                    if (!items[i]) {
                        continue;
                    }
                    items[i] = await this.onCreateSuccess({
                        createBy: {
                            data: items[i],
                            props: createBy.props,
                        },
                        carryForward: carryForwards[i],
                    }, items[i]);
                }
            }
            // hit workflow.;
            if ((_b = this.getModel().enableWorkflowOn) === null || _b === void 0 ? void 0 : _b.create) {
                let tenantId = createBy.props.tenantId;
                for (const item of items) {
                    if (!tenantId && this.getModel().getTenantColumn()) {
                        tenantId = item.getColumnValue(this.getModel().getTenantColumn().key);
                    }
                    if (tenantId) {
                        await this.onTrigger(item.id, tenantId, "on-create");
                    }
                }
            }
            // emit realtime events to the client.
            if (((_c = this.getModel().enableRealtimeEventsOn) === null || _c === void 0 ? void 0 : _c.create) &&
                this.model.getTenantColumn()) {
                if (Realtime.isInitialized()) {
                    const promises = [];
                    for (const item of items) {
                        const tenantId = item.getTenantColumnValue();
                        if (!tenantId) {
                            continue;
                        }
                        promises.push(Realtime.emitModelEvent({
                            modelId: item.id,
                            tenantId: tenantId,
                            eventType: ModelEventType.Create,
                            modelType: this.modelType,
                        }));
                    }
                    await Promise.allSettled(promises);
                }
                else {
                    logger.warn(`Realtime is not initialized. Skipping emitModelEvent for ${this.getModel().tableName}`, { tableName: this.getModel().tableName });
                }
            }
            return createBy.items;
        }
        catch (error) {
            await this.onCreateError(error);
            throw this.getException(error);
        }
    }
    async create(createBy) {
        const items = await this.createMany({
            props: createBy.props,
            items: [createBy.data],
        });
        const item = items[0];
        if (!item) {
            throw new BadDataException("Item not created");
        }
        return item;
    }
    sanitizeCreate(data) {
        if (!data.id) {
            data.id = ObjectID.generateTimeOrdered();
        }
        data.createdAt = OneUptimeDate.getCurrentDate();
        return data;
    }
    async getException(error) {
        throw error;
    }
    async onCreateError(error) {
        // A place holder method used for overriding.
        return Promise.resolve(error);
    }
    isValid(data) {
        if (!data) {
            throw new BadDataException("Data cannot be null");
        }
        return true;
    }
    async onTrigger(id, projectId, triggerType) {
        if (this.getModel().enableWorkflowOn) {
            API.post({
                url: new URL(Protocol.HTTP, WorkflowHostname, new Route(`/${WorkflowRoute.toString()}/analytics-model/${projectId.toString()}/${Text.pascalCaseToDashes(this.getModel().tableName)}/${triggerType}`)),
                data: {
                    _id: id.toString(),
                },
                headers: Object.assign({}, ClusterKeyAuthorization.getClusterKeyHeaders()),
            }).catch((error) => {
                logger.error(error, { projectId: projectId === null || projectId === void 0 ? void 0 : projectId.toString(), tableName: this.getModel().tableName });
            });
        }
    }
    checkRequiredFields(data) {
        // Check required fields.
        for (const columns of data.getRequiredColumns()) {
            const requiredField = columns.key;
            if (typeof data[requiredField] === Typeof.Boolean) {
                if (!data[requiredField] &&
                    data[requiredField] !== false &&
                    data.isDefaultValueColumn(requiredField)) {
                    data.setColumnValue(requiredField, data.getDefaultValueForColumn(requiredField));
                }
                else {
                    throw new BadDataException(`${requiredField} is required`);
                }
            }
            else if ((data[requiredField] === null ||
                data[requiredField] === undefined) &&
                data.isDefaultValueColumn(requiredField)) {
                // add default value.
                data.setColumnValue(requiredField, data.getDefaultValueForColumn(requiredField));
            }
            else if ((data[requiredField] === null ||
                data[requiredField] === undefined) &&
                !data.isDefaultValueColumn(requiredField)) {
                throw new BadDataException(`${requiredField} is required`);
            }
        }
        return data;
    }
    getModel() {
        return this.model;
    }
}
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Array, Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "insertJsonRows", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "doesColumnExistInDatabase", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [AnalyticsTableColumn]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "getColumnTypeInDatabase", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "countBy", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "existsBy", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [AnalyticsTableColumn]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "addColumnInDatabase", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "dropColumnInDatabase", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "findBy", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "groupTelemetryUsageByService", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "aggregateBy", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "findOneBy", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "deleteBy", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "findOneById", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "updateBy", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "execute", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "executeQuery", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "createMany", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "create", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [ObjectID,
        ObjectID, String]),
    __metadata("design:returntype", Promise)
], AnalyticsDatabaseService.prototype, "onTrigger", null);
//# sourceMappingURL=AnalyticsDatabaseService.js.map