UNPKG

bc-symbols-mcp

Version:

MCP server for analyzing Business Central .app files and BC object structures

192 lines 7.06 kB
import { pipeline } from 'stream'; import { promisify } from 'util'; const asyncPipeline = promisify(pipeline); export class StreamingSymbolParser { symbolIndex; rawData; constructor() { this.symbolIndex = { runtimeVersion: '', objectIndex: new Map(), totalObjects: 0, loadedObjects: 0 }; this.rawData = Buffer.alloc(0); } /** * Parse symbols with progressive loading - builds index first, loads objects on demand */ async parseSymbolsProgressive(symbolsBuffer) { this.rawData = symbolsBuffer; // Phase 1: Build object index by streaming through the JSON await this.buildObjectIndex(symbolsBuffer); return this.symbolIndex; } /** * Build a lightweight index of all objects without loading their full data */ async buildObjectIndex(buffer) { try { // For now, use simple JSON parsing - streaming implementation can be enhanced later const jsonString = buffer.toString('utf8'); const data = JSON.parse(jsonString); this.symbolIndex.runtimeVersion = data.RuntimeVersion || ''; // Process different object types const objectTypes = ['Tables', 'Codeunits', 'Pages', 'PageExtensions', 'Reports', 'XmlPorts', 'Queries', 'EnumTypes']; for (const arrayKey of objectTypes) { if (data[arrayKey] && Array.isArray(data[arrayKey])) { const objectType = this.getObjectTypeFromPath([], arrayKey); for (const obj of data[arrayKey]) { const metadata = this.createObjectMetadata(obj, objectType, 'Root'); if (metadata) { const indexKey = `${objectType}:${metadata.objectId}:${metadata.name}`; this.symbolIndex.objectIndex.set(indexKey, metadata); this.symbolIndex.totalObjects++; } } } } } catch (error) { console.warn('Error building object index:', error); // Fallback to empty index this.symbolIndex.runtimeVersion = ''; this.symbolIndex.totalObjects = 0; } } /** * Load specific object on demand */ async loadObject(objectType, objectId, objectName) { const indexKey = `${objectType}:${objectId}:${objectName}`; const metadata = this.symbolIndex.objectIndex.get(indexKey); if (!metadata) { throw new Error(`Object not found: ${indexKey}`); } if (metadata.loaded) { return metadata; // Return cached object } // Parse specific object from raw buffer using offset/size const objectData = await this.parseObjectAtOffset(metadata.offset, metadata.size); metadata.loaded = true; this.symbolIndex.loadedObjects++; return objectData; } /** * Load objects of a specific type on demand */ async loadObjectsByType(objectType) { const objects = []; for (const [key, metadata] of this.symbolIndex.objectIndex) { if (metadata.objectType === objectType) { const object = await this.loadObject(metadata.objectType, metadata.objectId, metadata.name); objects.push(object); } } return objects; } /** * Get object metadata without loading full object */ getObjectMetadata(objectType, objectId, objectName) { const results = []; for (const metadata of this.symbolIndex.objectIndex.values()) { if (metadata.objectType === objectType) { if (objectId !== undefined && metadata.objectId !== objectId) continue; if (objectName !== undefined && metadata.name !== objectName) continue; results.push(metadata); } } return results; } /** * Get loading statistics */ getLoadingStats() { const percentage = this.symbolIndex.totalObjects > 0 ? (this.symbolIndex.loadedObjects / this.symbolIndex.totalObjects) * 100 : 0; return { total: this.symbolIndex.totalObjects, loaded: this.symbolIndex.loadedObjects, percentage: Math.round(percentage * 100) / 100 }; } /** * Create minimal compatible symbol reference for existing code */ createLazySymbolReference() { return { runtimeVersion: this.symbolIndex.runtimeVersion, namespaces: [this.createLazyNamespace()] }; } createLazyNamespace() { return { name: 'Root', namespaces: [], tables: [], codeunits: [], pages: [], pageExtensions: [], reports: [], xmlPorts: [], queries: [], controlAddIns: [], enumTypes: [], dotNetPackages: [], interfaces: [], permissionSets: [], permissionSetExtensions: [], reportExtensions: [] }; } isObjectArray(path, key) { const objectArrays = [ 'Tables', 'Codeunits', 'Pages', 'PageExtensions', 'Reports', 'XmlPorts', 'Queries', 'EnumTypes', 'ControlAddIns', 'Interfaces', 'PermissionSets', 'PermissionSetExtensions', 'ReportExtensions' ]; return objectArrays.includes(key); } getObjectTypeFromPath(path, key) { const typeMap = { 'Tables': 'table', 'Codeunits': 'codeunit', 'Pages': 'page', 'PageExtensions': 'pageextension', 'Reports': 'report', 'XmlPorts': 'xmlport', 'Queries': 'query', 'EnumTypes': 'enum', 'ControlAddIns': 'controladdin', 'Interfaces': 'interface', 'PermissionSets': 'permissionset', 'PermissionSetExtensions': 'permissionsetextension', 'ReportExtensions': 'reportextension' }; return typeMap[key] || 'unknown'; } createObjectMetadata(value, objectType, namespace) { if (!value || typeof value !== 'object') return null; return { objectType, objectId: value.Id || 0, name: value.Name || '', namespace, offset: 0, // Will be calculated during actual parsing size: 0, // Will be calculated during actual parsing loaded: false }; } async parseObjectAtOffset(offset, size) { // Implementation would extract and parse specific JSON section // For now, return placeholder - full implementation would use // buffer slicing and targeted JSON parsing return {}; } } //# sourceMappingURL=streaming-parser.js.map