src/services/assets.service.tsmodified
import { LoggerFactory } from "../util/logger";
import { query } from "../db/db";
import { BadRequestError, NotFoundError } from "../types/errortypes";
import { getLoggedInUsername } from "../util/username";
import { PagingQueryParam } from "../models/ingress";
import { IndexedItem } from "../util/pagination.util";
import { getAssetTypeToSystemTypeMap } from "./asset.type.systemtype.mappings.service";

const logger = LoggerFactory("AssetsService");

const SQL = {
    LIST: `SELECT * FROM xyz."fn_GetAssetList"($1, $2, $3)`,
    GET_BY_ID: `SELECT * FROM xyz."fn_GetAsset"($1, $2)`,
    INSERT: `SELECT * FROM xyz."fn_InsertAsset"($1, $2, $3, $4)`,
    UPDATE: `SELECT * FROM xyz."fn_UpdateAsset"($1, $2, $3, $4, $5, $6, $7)`,
};

// RFC 7396 merge-patch payload: only the fields present are updated. Absent fields
// are passed as null so the DB function leaves the existing column value unchanged.
// clearParentAssetId and parentAssetId are mutually exclusive: set clearParentAssetId
// to true to explicitly remove the parent; set parentAssetId to reassign it.
export interface AssetPatch {
    name?: string;
    assetTypeId?: string;
    parentAssetId?: string;
    clearParentAssetId?: boolean;
}

export interface Asset {
    assetId: string;
    name: string;
    assetTypeId: string;
    // Scalar id of the SystemType the asset's AssetType is mapped to, or null when
    // no AssetType->SystemType mapping exists. Populated on read paths only.
    systemId?: string | null;
    createdBy: string;
    insertedOn: Date;
    lastModifiedOn: Date | null;
    lastModifiedBy: string | null;
}

function mapRow(row: any): Asset {
    return {
        assetId: row.AssetId,
        name: row.Name,
        assetTypeId: row.AssetTypeId,
        createdBy: row.CreatedBy,
        insertedOn: row.InsertedOn,
        lastModifiedOn: row.LastModifiedOn ?? null,
        lastModifiedBy: row.LastModifiedBy ?? null,
    };
}

function mapError(err: unknown): never {
    if (err instanceof Error) {
        if (err.message.includes("AssetType with id") || err.message.includes("ParentAsset with id")) {
            throw new NotFoundError(err.message);
        }
        if (err.message.includes("cannot be its own parent")) {
            throw new BadRequestError(err.message);
        }
    }
    throw err;
}

export async function listAssets(projectId: string, paging: PagingQueryParam): Promise<(Asset & IndexedItem)[]> {
    try {
        const { rows } = await query(SQL.LIST, [projectId, paging.lastFetchedIndexId, paging.size]);
        const systemTypeMap = await getAssetTypeToSystemTypeMap(projectId);
        return rows.map((row: any) => {
            const asset = mapRow(row);
            return { ...asset, systemId: systemTypeMap.get(asset.assetTypeId) ?? null, indexId: row.Id };
        });
    } catch (err) {
        logger.error("Error while listing assets", err);
        throw err;
    }
}

export async function getAssetById(projectId: string, assetId: string): Promise<Asset> {
    try {
        const { rows } = await query(SQL.GET_BY_ID, [projectId, assetId]);
        if (rows.length === 0) {
            throw new NotFoundError(`Asset with id: ${assetId} not found.`);
        }
        const asset = mapRow(rows[0]);
        const systemTypeMap = await getAssetTypeToSystemTypeMap(projectId);
        asset.systemId = systemTypeMap.get(asset.assetTypeId) ?? null;
        return asset;
    } catch (err) {
        logger.error("Error while fetching asset", err);
        throw err;
    }
}

export async function createAsset(projectId: string, assetTypeId: string, name: string): Promise<Asset> {
    try {
        const { rows } = await query(SQL.INSERT, [projectId, assetTypeId, name, getLoggedInUsername()]);
        return mapRow(rows[0]);
    } catch (err) {
        logger.error("Error while creating asset", err);
        mapError(err);
    }
}

export async function updateAsset(projectId: string, assetId: string, patch: AssetPatch): Promise<Asset> {
    try {
        const { rows } = await query(SQL.UPDATE, [
            projectId,
            assetId,
            patch.assetTypeId ?? null,
            patch.parentAssetId ?? null,
            patch.clearParentAssetId ?? null,
            patch.name ?? null,
            getLoggedInUsername(),
        ]);
        if (rows.length === 0) {
            throw new NotFoundError(`Asset with id: ${assetId} not found.`);
        }
        return mapRow(rows[0]);
    } catch (err) {
        logger.error("Error while updating asset", err);
        mapError(err);
    }
}