/*! * SAPUI5 * Copyright (c) 2025 SAP SE or an SAP affiliate company. All rights reserved. */ import { Sina } from "../../sina/Sina"; import { DataSource } from "../../sina/DataSource"; import { AttributeMetadata } from "../../sina/AttributeMetadata"; import { SearchQuery } from "../../sina/SearchQuery"; import { Condition } from "../../sina/Condition"; import { ComplexCondition } from "../../sina/ComplexCondition"; import { SimpleCondition } from "../../sina/SimpleCondition"; import { ComparisonOperator } from "../../sina/ComparisonOperator"; import { RecordResponse, Record, RecordService } from "./RecordService"; import { isNotEmptyString } from "./Util"; export interface HierarchyRecord extends Record { isHierarchyRecord: boolean; hierarchyAttributeName: string; // hierarchy attribute name of current record selfValue: string; // current record's hierarchy attribute value parentValue: string; // parent record's hierarchy attribute value childValues: string[]; // child records' hierarchy attribute value ancestorValues: string[]; // ancestor records' hierarchy attribute value descendantValues: string[]; // descendant records' hierarchy attribute value } interface RootPathObject { name: string; // hierarchy attribute name, example: "TLT_HIERARCHY_ATTRIBUTE" value: string; // hierarchy attribute value, example: "$$ROOT$$" icon: string; // hierarchy attribute icon, example: "sap-icon://open-folder" label: string; // hierarchy attribute label, example: "All" } export type HierarchyOperator = | ComparisonOperator.ChildOf | ComparisonOperator.DescendantOf | "ParentOf" | "AncestorOf"; export class HierarchyService { constructor( readonly sina: Sina, readonly recordService: RecordService ) {} parseHierarchyRecords(): void { const recordMap = this.recordService.getRecordMap(); if (Object.keys(recordMap).length === 0) { return; } Object.keys(recordMap).forEach((dataSourceId) => { if (this.isHierarchyDataSource(dataSourceId) === true) { recordMap[dataSourceId] = this.parseHierarchyRecordsByHierarchyDataSource(dataSourceId); } }); } private parseHierarchyRecordsByHierarchyDataSource(hDataSourceId: string): HierarchyRecord[] { const records = this.recordService.getRecordsByDataSource(hDataSourceId); const hRecords: HierarchyRecord[] = JSON.parse(JSON.stringify(records)); const hAttributeName = this.getHierarchyAttributeName(hDataSourceId); hRecords.forEach((rcd) => { // 1. isHierarchyRecord rcd.isHierarchyRecord = true; // 2. hierarchy attribute name rcd.hierarchyAttributeName = hAttributeName; // 3. self value rcd.selfValue = rcd.valueMap[hAttributeName].stringValue; // 4. parent value rcd.parentValue = this.getParentValue(hDataSourceId, hAttributeName, rcd.selfValue); // 5. children rcd.childValues = this.getChildValues(hDataSourceId, hAttributeName, rcd.selfValue); const hasChild = rcd.childValues.length > 0 ? "true" : "false"; rcd.stringValues.push(hasChild); rcd.rawValues.push(hasChild); rcd.valueMap.HASHIERARCHYNODECHILD = { stringValue: hasChild, rawValue: hasChild, }; // 6. descendants rcd.descendantValues = this.getDescendantValues(hDataSourceId, hAttributeName, rcd.selfValue); // 7. ancestors rcd.ancestorValues = this.getAncestorValues(hDataSourceId, hAttributeName, rcd.selfValue); }); return hRecords; } // Look up parent value from the original record map via PARENT_ATTRIBUTE_VALUE field private getParentValue(hDataSourceId: string, hAttributeName: string, hAttributeValue: string): string { const record = this.recordService.getRecordsByDataSourceAndAttribute( hDataSourceId, hAttributeName, hAttributeValue )[0]; return record?.valueMap["PARENT_ATTRIBUTE_VALUE"]?.stringValue; } // Calculate child values during buffering phase private getChildValues(hDataSourceId: string, hAttributeName: string, hAttributeValue: string): string[] { const childRecords = this.recordService.getRecordsByDataSourceAndAttribute( hDataSourceId, "PARENT_ATTRIBUTE_VALUE", hAttributeValue ) as Array; return childRecords.map((childRcd) => childRcd.valueMap[hAttributeName].stringValue); } // Calculate descendant values during buffering phase (recursive) private getDescendantValues( hDataSourceId: string, hAttributeName: string, hAttributeValue: string ): string[] { const childValues = this.getChildValues(hDataSourceId, hAttributeName, hAttributeValue); if (!childValues || childValues.length === 0) { return []; } const descendants: string[] = []; for (const childValue of childValues) { descendants.push(childValue); descendants.push(...this.getDescendantValues(hDataSourceId, hAttributeName, childValue)); } return descendants; } // Calculate ancestor values during buffering phase (recursive) private getAncestorValues( hDataSourceId: string, hAttributeName: string, hAttributeValue: string ): string[] { const parentValue = this.getParentValue(hDataSourceId, hAttributeName, hAttributeValue); if (!parentValue) { return []; } return [parentValue, ...this.getAncestorValues(hDataSourceId, hAttributeName, parentValue)]; } private isHierarchyDataSource(hDataSourceId: string): boolean { return ( this.sina.dataSources .filter((ds) => ds.isHierarchyDataSource) .map((ds) => ds.id) .indexOf(hDataSourceId) >= 0 ); } getHierarchyAttributeName(hDataSourceId: string): string { return this.sina.getDataSource(hDataSourceId)?.hierarchyAttribute; // ! assume single hierarchy attribute for each hierarchy data source } // get joined hierarchy data source of normal data source getHierarchyDataSource(dataSourceId: string): DataSource { const ds = this.sina.getDataSource(dataSourceId); let joinedDs = undefined; ds.attributesMetadata.forEach((attr: AttributeMetadata) => { if (attr.isHierarchy) { joinedDs = this.sina.getDataSource(attr.hierarchyName); return; } }); return joinedDs; } // get child or descendant or parent or ancestor records of current hierarchy attribute value getHierarchyRecords( hDataSourceId: string, hAttributeValue: string, operator: HierarchyOperator ): Array { if (this.isHierarchyDataSource(hDataSourceId) !== true) { return undefined; } const hAttributeName = this.getHierarchyAttributeName(hDataSourceId); if (!isNotEmptyString(hAttributeName)) { return undefined; } // Get the self record to check buffered values const selfRecord = this.recordService.getRecordsByDataSourceAndAttribute( hDataSourceId, hAttributeName, hAttributeValue )[0] as HierarchyRecord; if (!selfRecord) { return []; } let targetValues: string[]; switch (operator) { case ComparisonOperator.ChildOf: targetValues = selfRecord.childValues || []; break; case ComparisonOperator.DescendantOf: targetValues = selfRecord.descendantValues || []; break; case "ParentOf": targetValues = selfRecord.parentValue ? [selfRecord.parentValue] : []; break; case "AncestorOf": targetValues = selfRecord.ancestorValues || []; break; default: return []; } // Map values to records return targetValues .map( (value) => this.recordService.getRecordsByDataSourceAndAttribute( hDataSourceId, hAttributeName, value )[0] ) .filter((rcd) => rcd !== undefined) as Array; } getHierarchySimpleCondition( rootCondition: Condition, operator: ComparisonOperator.DescendantOf | ComparisonOperator.ChildOf ): SimpleCondition { if ( rootCondition === undefined || (operator !== ComparisonOperator.DescendantOf && operator !== ComparisonOperator.ChildOf) ) { return undefined; } if ( rootCondition instanceof SimpleCondition && isNotEmptyString(rootCondition.attribute) && typeof rootCondition.value === "string" && isNotEmptyString(rootCondition.value) && rootCondition.operator === operator ) { return rootCondition; } if (rootCondition instanceof ComplexCondition) { for (const condition of rootCondition.conditions) { const hCondition = this.getHierarchySimpleCondition(condition, operator); if (hCondition !== undefined) { return hCondition; } } } return undefined; } getResponse(query: SearchQuery): RecordResponse { const hDataSourceId = query.filter.dataSource.id; const condition = this.getHierarchySimpleCondition( query.filter.rootCondition, ComparisonOperator.ChildOf ); if (this.isHierarchyDataSource(hDataSourceId) !== true || condition === undefined) { return { results: [], resultsToDisplay: [], totalCount: 0 }; } const hAttributeName = condition.attribute; const hAttributeValue = condition.value as string; // start node id of hierarchy tree const matchedRecords = []; if (isNotEmptyString(hAttributeValue)) { const hierarchyRecord = this.recordService.getRecordsByDataSourceAndAttribute( hDataSourceId, hAttributeName, hAttributeValue )[0] as HierarchyRecord; const childValues = hierarchyRecord?.childValues; if (childValues) { childValues.forEach((attrValue) => { matchedRecords.push( this.recordService.getRecordsByDataSourceAndAttribute( hDataSourceId, hAttributeName, attrValue )[0] as HierarchyRecord ); }); } matchedRecords.splice(query.top); // trim to top N in-place } return { results: matchedRecords, resultsToDisplay: matchedRecords, totalCount: matchedRecords.length, }; } getRootPathObjects(hDataSourceId: string, hAttributeValue: string): RootPathObject[] { const hAttributeName = this.getHierarchyAttributeName(hDataSourceId); if ( this.isHierarchyDataSource(hDataSourceId) !== true || !isNotEmptyString(hAttributeName) || !isNotEmptyString(hAttributeValue) ) { return []; } return this.getRootPathHierarchyRecords(hDataSourceId, hAttributeValue).map((rcd) => ({ name: hAttributeName, value: rcd.valueMap[hAttributeName].stringValue, icon: rcd.valueMap["ICON"].stringValue, label: rcd.valueMap["NAME"].stringValue, })); } private getRootPathHierarchyRecords( hDataSourceId: string, hAttributeValue: string ): Array { const hAttributeName = this.getHierarchyAttributeName(hDataSourceId); // 1. Get ancestors using buffered ancestorValues (returns [parent, grandparent, ..., root]) const ancestorRecords = this.getHierarchyRecords(hDataSourceId, hAttributeValue, "AncestorOf") || []; // 2. Get current record const currentRecord = this.recordService.getRecordsByDataSourceAndAttribute( hDataSourceId, hAttributeName, hAttributeValue )[0] as HierarchyRecord; // 3. Build root path: [root, ..., grandparent, parent, current] return [...[...ancestorRecords].reverse(), currentRecord]; } }