bc-symbols-mcp
Version:
MCP server for analyzing Business Central .app files and BC object structures
361 lines • 13 kB
JavaScript
export class ObjectIndex {
index = new Map();
typeIndex = new Map();
nameIndex = new Map();
appIndex = new Map();
keywordIndex = new Map();
dependencyIndex = new Map();
indexingStartTime = 0;
indexingEndTime = 0;
/**
* Index all objects from an app
*/
async indexApp(app, objectLoader) {
this.indexingStartTime = Date.now();
try {
const objectTypes = ['table', 'codeunit', 'page', 'pageextension', 'report', 'enum'];
for (const objectType of objectTypes) {
const metadata = objectLoader.getObjectMetadata(app.filePath, objectType);
for (const meta of metadata) {
await this.indexObject(app, objectType, meta, objectLoader);
}
}
}
finally {
this.indexingEndTime = Date.now();
}
}
/**
* Index a single object
*/
async indexObject(app, objectType, metadata, objectLoader) {
const objectKey = this.createObjectKey(app.id, objectType, metadata.objectId, metadata.name);
// Extract keywords for search
const keywords = this.extractKeywords(metadata.name, objectType);
// Get dependencies (this would need object details)
const dependencies = await this.extractDependencies(app, objectType, metadata, objectLoader);
const indexEntry = {
appId: app.id,
objectType,
objectId: metadata.objectId,
objectName: metadata.name,
namespace: metadata.namespace || 'Root',
filePath: app.filePath,
lastModified: Date.now(),
properties: this.extractProperties(metadata),
dependencies,
keywords
};
// Add to main index
this.index.set(objectKey, indexEntry);
// Add to secondary indexes
this.addToIndex(this.typeIndex, objectType, objectKey);
this.addToIndex(this.nameIndex, metadata.name.toLowerCase(), objectKey);
this.addToIndex(this.appIndex, app.id, objectKey);
// Add keywords to keyword index
for (const keyword of keywords) {
this.addToIndex(this.keywordIndex, keyword.toLowerCase(), objectKey);
}
// Add dependencies to dependency index
for (const dep of dependencies) {
this.addToIndex(this.dependencyIndex, dep, objectKey);
}
}
/**
* Search objects using filters
*/
search(filter) {
let candidates = new Set(this.index.keys());
// Filter by app IDs
if (filter.appIds && filter.appIds.length > 0) {
const appMatches = new Set();
for (const appId of filter.appIds) {
const appObjects = this.appIndex.get(appId);
if (appObjects) {
appObjects.forEach(key => appMatches.add(key));
}
}
candidates = this.intersectSets(candidates, appMatches);
}
// Filter by object types
if (filter.objectTypes && filter.objectTypes.length > 0) {
const typeMatches = new Set();
for (const objectType of filter.objectTypes) {
const typeObjects = this.typeIndex.get(objectType);
if (typeObjects) {
typeObjects.forEach(key => typeMatches.add(key));
}
}
candidates = this.intersectSets(candidates, typeMatches);
}
// Filter by object names
if (filter.objectNames && filter.objectNames.length > 0) {
const nameMatches = new Set();
for (const objectName of filter.objectNames) {
const nameObjects = this.nameIndex.get(objectName.toLowerCase());
if (nameObjects) {
nameObjects.forEach(key => nameMatches.add(key));
}
}
candidates = this.intersectSets(candidates, nameMatches);
}
// Filter by keywords
if (filter.keywords && filter.keywords.length > 0) {
const keywordMatches = new Set();
for (const keyword of filter.keywords) {
const keywordObjects = this.keywordIndex.get(keyword.toLowerCase());
if (keywordObjects) {
keywordObjects.forEach(key => keywordMatches.add(key));
}
}
candidates = this.intersectSets(candidates, keywordMatches);
}
// Convert to results and apply remaining filters
const results = [];
for (const key of candidates) {
const entry = this.index.get(key);
if (!entry)
continue;
// Filter by object IDs
if (filter.objectIds && filter.objectIds.length > 0) {
if (!filter.objectIds.includes(entry.objectId))
continue;
}
// Filter by ID range
if (filter.minId !== undefined && entry.objectId < filter.minId)
continue;
if (filter.maxId !== undefined && entry.objectId > filter.maxId)
continue;
// Filter by namespaces
if (filter.namespaces && filter.namespaces.length > 0) {
if (!filter.namespaces.includes(entry.namespace))
continue;
}
// Filter by properties
if (filter.properties) {
let propertyMatch = true;
for (const [propKey, propValue] of Object.entries(filter.properties)) {
if (entry.properties[propKey] !== propValue) {
propertyMatch = false;
break;
}
}
if (!propertyMatch)
continue;
}
results.push(entry);
}
return results.sort((a, b) => {
// Sort by object type, then by ID, then by name
if (a.objectType !== b.objectType) {
return a.objectType.localeCompare(b.objectType);
}
if (a.objectId !== b.objectId) {
return a.objectId - b.objectId;
}
return a.objectName.localeCompare(b.objectName);
});
}
/**
* Find objects that depend on a specific object
*/
findDependents(appId, objectType, objectName) {
const dependencyKey = `${appId}:${objectType}:${objectName}`;
const dependentKeys = this.dependencyIndex.get(dependencyKey);
if (!dependentKeys) {
return [];
}
const results = [];
for (const key of dependentKeys) {
const entry = this.index.get(key);
if (entry) {
results.push(entry);
}
}
return results;
}
/**
* Find objects by partial name match
*/
findByPartialName(partialName) {
const searchTerm = partialName.toLowerCase();
const matches = [];
for (const [name, objectKeys] of this.nameIndex) {
if (name.includes(searchTerm)) {
for (const key of objectKeys) {
const entry = this.index.get(key);
if (entry) {
matches.push(entry);
}
}
}
}
return matches;
}
/**
* Get object by exact match
*/
getObject(appId, objectType, objectId, objectName) {
const key = this.createObjectKey(appId, objectType, objectId, objectName);
return this.index.get(key) || null;
}
/**
* Get all objects of a specific type
*/
getObjectsByType(objectType) {
const objectKeys = this.typeIndex.get(objectType);
if (!objectKeys) {
return [];
}
const results = [];
for (const key of objectKeys) {
const entry = this.index.get(key);
if (entry) {
results.push(entry);
}
}
return results;
}
/**
* Get all objects from a specific app
*/
getObjectsByApp(appId) {
const objectKeys = this.appIndex.get(appId);
if (!objectKeys) {
return [];
}
const results = [];
for (const key of objectKeys) {
const entry = this.index.get(key);
if (entry) {
results.push(entry);
}
}
return results;
}
/**
* Get index statistics
*/
getStats() {
const objectsByType = {};
const objectsByApp = {};
for (const entry of this.index.values()) {
objectsByType[entry.objectType] = (objectsByType[entry.objectType] || 0) + 1;
objectsByApp[entry.appId] = (objectsByApp[entry.appId] || 0) + 1;
}
return {
totalObjects: this.index.size,
objectsByType,
objectsByApp,
memoryUsage: this.calculateMemoryUsage(),
indexingTime: this.indexingEndTime - this.indexingStartTime,
lastUpdated: this.indexingEndTime
};
}
/**
* Remove objects from an app
*/
removeApp(appId) {
const objectKeys = this.appIndex.get(appId);
if (!objectKeys) {
return;
}
for (const key of objectKeys) {
const entry = this.index.get(key);
if (entry) {
// Remove from all indexes
this.removeFromIndex(this.typeIndex, entry.objectType, key);
this.removeFromIndex(this.nameIndex, entry.objectName.toLowerCase(), key);
this.removeFromIndex(this.appIndex, entry.appId, key);
for (const keyword of entry.keywords) {
this.removeFromIndex(this.keywordIndex, keyword.toLowerCase(), key);
}
for (const dep of entry.dependencies) {
this.removeFromIndex(this.dependencyIndex, dep, key);
}
this.index.delete(key);
}
}
}
/**
* Clear all indexes
*/
clear() {
this.index.clear();
this.typeIndex.clear();
this.nameIndex.clear();
this.appIndex.clear();
this.keywordIndex.clear();
this.dependencyIndex.clear();
}
/**
* Helper methods
*/
createObjectKey(appId, objectType, objectId, objectName) {
return `${appId}:${objectType}:${objectId}:${objectName}`;
}
addToIndex(index, key, value) {
if (!index.has(key)) {
index.set(key, new Set());
}
index.get(key).add(value);
}
removeFromIndex(index, key, value) {
const set = index.get(key);
if (set) {
set.delete(value);
if (set.size === 0) {
index.delete(key);
}
}
}
intersectSets(set1, set2) {
const result = new Set();
for (const item of set1) {
if (set2.has(item)) {
result.add(item);
}
}
return result;
}
extractKeywords(objectName, objectType) {
const keywords = [objectType];
// Split camelCase and PascalCase names
const words = objectName.replace(/([A-Z])/g, ' $1').trim().split(' ');
keywords.push(...words.map(w => w.toLowerCase()));
// Add the full name
keywords.push(objectName.toLowerCase());
return [...new Set(keywords)]; // Remove duplicates
}
extractProperties(metadata) {
const properties = {};
if (metadata.properties && Array.isArray(metadata.properties)) {
for (const prop of metadata.properties) {
if (prop.name && prop.value) {
properties[prop.name] = prop.value;
}
}
}
return properties;
}
async extractDependencies(app, objectType, metadata, objectLoader) {
// This would require loading the full object to analyze dependencies
// For now, return empty array - full implementation would parse object references
return [];
}
calculateMemoryUsage() {
let totalSize = 0;
// Estimate memory usage of main index
for (const entry of this.index.values()) {
totalSize += JSON.stringify(entry).length;
}
// Estimate memory usage of secondary indexes
totalSize += JSON.stringify([...this.typeIndex.entries()]).length;
totalSize += JSON.stringify([...this.nameIndex.entries()]).length;
totalSize += JSON.stringify([...this.appIndex.entries()]).length;
totalSize += JSON.stringify([...this.keywordIndex.entries()]).length;
totalSize += JSON.stringify([...this.dependencyIndex.entries()]).length;
return Math.round((totalSize / (1024 * 1024)) * 100) / 100; // MB
}
}
//# sourceMappingURL=object-index.js.map