Skip to main content
Glama
metadata-merger.ts6.25 kB
/** * Metadata Merger * * Handles partial metadata updates by merging new values with existing metadata. * Supports field preservation, replacement, and removal (via null values). * * Requirements: 9.1, 9.2, 9.3, 9.4, 9.5 */ import { MemoryMetadata } from "./types.js"; /** * Partial metadata update input * - undefined: field not included in update (preserve existing) * - null: explicitly remove the field * - value: replace with new value */ export type MetadataUpdate = { [K in keyof MemoryMetadata]?: MemoryMetadata[K] | null; }; /** * Result of metadata merge operation */ export interface MetadataMergeResult { /** The merged metadata object */ merged: MemoryMetadata; /** Fields that were updated (replaced with new values) */ updatedFields: string[]; /** Fields that were removed (set to null in update) */ removedFields: string[]; /** Fields that were preserved (not in update) */ preservedFields: string[]; /** Fields that were added (new fields not in existing) */ addedFields: string[]; } /** * MetadataMerger class * * Merges partial metadata updates with existing metadata. * Implements the following rules: * - Fields not in update are preserved (Requirement 9.1) * - Fields in update replace existing values (Requirement 9.2) * - New fields are added to existing metadata (Requirement 9.3) * - Fields set to null are removed (Requirement 9.4) * - Empty update preserves all existing fields (Requirement 9.5) */ export class MetadataMerger { /** * Merge partial metadata update with existing metadata * * @param existing - The current metadata object * @param update - Partial update with fields to merge * @returns Merge result with merged metadata and change tracking * * Requirements: * - 9.1: Partial update merges with existing metadata * - 9.2: Existing fields are replaced with new values * - 9.3: New fields are added to existing metadata * - 9.4: Fields set to null are removed * - 9.5: Empty update preserves existing metadata */ merge(existing: MemoryMetadata, update: MetadataUpdate): MetadataMergeResult { const updatedFields: string[] = []; const removedFields: string[] = []; const preservedFields: string[] = []; const addedFields: string[] = []; // Start with a deep copy of existing metadata to avoid mutation const merged: MemoryMetadata = this.deepCopy(existing); // Get all keys from existing metadata const existingKeys = new Set(Object.keys(existing)); // Process each field in the update for (const [key, value] of Object.entries(update)) { const typedKey = key as keyof MemoryMetadata; if (value === null) { // Requirement 9.4: null means remove the field if (existingKeys.has(key)) { delete merged[typedKey]; removedFields.push(key); } // If field doesn't exist, null is a no-op } else if (value !== undefined) { // Requirement 9.2 & 9.3: Replace or add field if (existingKeys.has(key)) { // Field exists - this is an update updatedFields.push(key); } else { // Field doesn't exist - this is an addition addedFields.push(key); } // Type assertion needed due to TypeScript's strict typing (merged as Record<string, unknown>)[key] = value; } // undefined values are ignored (field not in update) } // Track preserved fields (in existing but not modified) for (const key of Array.from(existingKeys)) { if ( !updatedFields.includes(key) && !removedFields.includes(key) && !addedFields.includes(key) ) { preservedFields.push(key); } } return { merged, updatedFields, removedFields, preservedFields, addedFields, }; } /** * Check if an update would result in any changes * * @param existing - The current metadata object * @param update - Partial update to check * @returns true if the update would change the metadata */ hasChanges(existing: MemoryMetadata, update: MetadataUpdate): boolean { // Empty update means no changes if (Object.keys(update).length === 0) { return false; } for (const [key, value] of Object.entries(update)) { if (value === null) { // Removing a field that exists is a change if (key in existing) { return true; } } else if (value !== undefined) { // Adding or updating a field is a change const existingValue = existing[key as keyof MemoryMetadata]; if (!this.deepEqual(existingValue, value)) { return true; } } } return false; } /** * Deep copy of metadata object * Handles arrays and nested objects */ private deepCopy(obj: MemoryMetadata): MemoryMetadata { const copy: MemoryMetadata = {}; for (const [key, value] of Object.entries(obj)) { if (Array.isArray(value)) { (copy as Record<string, unknown>)[key] = [...value]; } else if (typeof value === "object" && value !== null) { (copy as Record<string, unknown>)[key] = { ...value }; } else { (copy as Record<string, unknown>)[key] = value; } } return copy; } /** * Deep equality check for metadata values * Handles arrays and primitive values */ private deepEqual(a: unknown, b: unknown): boolean { if (a === b) { return true; } if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) { return false; } return a.every((val, idx) => this.deepEqual(val, b[idx])); } if (typeof a === "object" && typeof b === "object" && a !== null && b !== null) { const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) { return false; } return keysA.every((key) => this.deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]) ); } return false; } } /** * Create a default metadata merger instance */ export function createMetadataMerger(): MetadataMerger { return new MetadataMerger(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server