@memberjunction/metadata-sync 2.51.0 → 2.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;;;;AAGH,gDAAwB;AACxB,wDAA0B;AAC1B,yDAAqD;AA2MrD;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,YAAY;IAC1B,OAAO,8BAAa,CAAC,YAAY,EAAE,CAAC;AACtC,CAAC;AAFD,oCAEC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACI,KAAK,UAAU,cAAc,CAAC,GAAW;IAC9C,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IAEnD,IAAI,MAAM,kBAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,OAAO,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;YACnD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAbD,wCAaC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACI,KAAK,UAAU,gBAAgB,CAAC,GAAW;IAChD,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IAEnD,IAAI,MAAM,kBAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YAC7C,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBAClB,OAAO,MAAM,CAAC;YAChB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAfD,4CAeC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACI,KAAK,UAAU,gBAAgB,CAAC,GAAW;IAChD,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IAErD,IAAI,MAAM,kBAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,OAAO,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;YACrD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAbD,4CAaC","sourcesContent":["/**\n * @fileoverview Configuration types and loaders for MetadataSync\n * @module config\n * \n * This module defines configuration interfaces and provides utilities for loading\n * various configuration files used by the MetadataSync tool. It supports:\n * - MemberJunction database configuration (mj.config.cjs)\n * - Sync configuration (.mj-sync.json)\n * - Entity-specific configuration (.mj-sync.json with entity field)\n * - Folder-level defaults (.mj-folder.json)\n */\n\nimport { cosmiconfigSync } from 'cosmiconfig';\nimport path from 'path';\nimport fs from 'fs-extra';\nimport { configManager } from './lib/config-manager';\n\n/**\n * MemberJunction database configuration\n * \n * Defines connection parameters and settings for connecting to the MemberJunction\n * database. Typically loaded from mj.config.cjs in the project root.\n */\nexport interface MJConfig {\n /** Database server hostname or IP address */\n dbHost: string;\n /** Database server port (defaults to 1433 for SQL Server) */\n dbPort?: number;\n /** Database name to connect to */\n dbDatabase: string;\n /** Database authentication username */\n dbUsername: string;\n /** Database authentication password */\n dbPassword: string;\n /** Whether to trust the server certificate (Y/N) */\n dbTrustServerCertificate?: string;\n /** Whether to encrypt the connection (Y/N, auto-detected for Azure SQL) */\n dbEncrypt?: string;\n /** SQL Server instance name (for named instances) */\n dbInstanceName?: string;\n /** Schema name for MemberJunction core tables (defaults to __mj) */\n mjCoreSchema?: string;\n /** Allow additional properties for extensibility */\n [key: string]: any;\n}\n\n/**\n * Global sync configuration\n * \n * Defines settings that apply to the entire sync process, including push/pull\n * behaviors and watch mode configuration. Stored in .mj-sync.json at the root.\n */\nexport interface SyncConfig {\n /** Version of the sync configuration format */\n version: string;\n /** \n * Directory processing order (only applies to root-level config, not inherited by subdirectories)\n * Specifies the order in which subdirectories should be processed to handle dependencies.\n * Directories not listed in this array will be processed after the ordered ones in alphabetical order.\n */\n directoryOrder?: string[];\n /** Push command configuration */\n push?: {\n /** Whether to validate records before pushing to database */\n validateBeforePush?: boolean;\n /** Whether to require user confirmation before push */\n requireConfirmation?: boolean;\n };\n /** SQL logging configuration (only applies to root-level config, not inherited by subdirectories) */\n sqlLogging?: {\n /** Whether to enable SQL logging during push operations */\n enabled?: boolean;\n /** Directory to output SQL log files (relative to command execution directory, defaults to './sql_logging') */\n outputDirectory?: string;\n /** Whether to format SQL as migration-ready files with Flyway schema placeholders */\n formatAsMigration?: boolean;\n };\n /** Watch command configuration */\n watch?: {\n /** Milliseconds to wait before processing file changes */\n debounceMs?: number;\n /** File patterns to ignore during watch */\n ignorePatterns?: string[];\n };\n}\n\n/**\n * Configuration for related entity synchronization\n * \n * Defines how to pull and push related entities that have foreign key relationships\n * with a parent entity. Supports nested relationships for deep object graphs.\n * NEW: Supports automatic recursive patterns for self-referencing entities.\n */\nexport interface RelatedEntityConfig {\n /** Name of the related entity to sync */\n entity: string;\n /** Field name that contains the foreign key reference to parent (e.g., \"PromptID\") */\n foreignKey: string;\n /** Optional SQL filter to apply when pulling related records */\n filter?: string;\n /** \n * Enable recursive fetching for self-referencing entities\n * When true, automatically fetches all levels of the hierarchy until no more children found\n */\n recursive?: boolean;\n /** \n * Maximum depth for recursive fetching (optional, defaults to 10)\n * Prevents infinite loops and controls memory usage\n * Only applies when recursive is true\n */\n maxDepth?: number;\n /** Fields to externalize to separate files for this related entity */\n externalizeFields?: string[] | {\n [fieldName: string]: {\n /** File extension to use (e.g., \".md\", \".txt\", \".html\") */\n extension?: string;\n }\n } | Array<{\n /** Field name to externalize */\n field: string;\n /** Pattern for the output file. Supports placeholders from the entity */\n pattern: string;\n }>;\n /** Fields to exclude from the pulled data for this related entity */\n excludeFields?: string[];\n /** Foreign key fields to convert to @lookup references for this related entity */\n lookupFields?: {\n [fieldName: string]: {\n entity: string;\n field: string;\n };\n };\n /** Nested related entities for deep object graphs */\n relatedEntities?: Record<string, RelatedEntityConfig>;\n}\n\n/**\n * Entity-specific configuration\n * \n * Defines settings for a specific entity type within a directory. Stored in\n * .mj-sync.json files that contain an \"entity\" field. Supports defaults,\n * file patterns, and related entity configuration.\n */\nexport interface EntityConfig {\n /** Name of the entity this directory contains */\n entity: string;\n /** Glob pattern for finding data files (defaults to \"*.json\") */\n filePattern?: string;\n /** Default field values applied to all records in this directory */\n defaults?: Record<string, any>;\n /** Pull command specific configuration */\n pull?: {\n /** Glob pattern for finding existing files to update (defaults to filePattern) */\n filePattern?: string;\n /** Whether to create new files for records not found locally */\n createNewFileIfNotFound?: boolean;\n /** Filename for new records when createNewFileIfNotFound is true */\n newFileName?: string;\n /** Whether to append multiple new records to a single file */\n appendRecordsToExistingFile?: boolean;\n /** Whether to update existing records found in local files */\n updateExistingRecords?: boolean;\n /** Fields to preserve during updates (never overwrite these) */\n preserveFields?: string[];\n /** Strategy for merging updates: \"overwrite\" | \"merge\" | \"skip\" */\n mergeStrategy?: 'overwrite' | 'merge' | 'skip';\n /** Create backup files before updating existing files */\n backupBeforeUpdate?: boolean;\n /** Directory name for backup files (defaults to \".backups\") */\n backupDirectory?: string;\n /** SQL filter to apply when pulling records from database */\n filter?: string;\n /** Configuration for pulling related entities */\n relatedEntities?: Record<string, RelatedEntityConfig>;\n /** Fields to externalize to separate files with optional configuration */\n externalizeFields?: string[] | {\n [fieldName: string]: {\n /** File extension to use (e.g., \".md\", \".txt\", \".html\") */\n extension?: string;\n }\n } | Array<{\n /** Field name to externalize */\n field: string;\n /** Pattern for the output file. Supports placeholders:\n * - {Name}: Entity's name field value\n * - {ID}: Entity's ID\n * - {FieldName}: The field being externalized\n * - Any other {FieldName} from the entity\n * Example: \"@file:templates/{Name}.template.md\"\n */\n pattern: string;\n }>;\n /** Fields to exclude from the pulled data (e.g., [\"TemplateID\"]) */\n excludeFields?: string[];\n /** Foreign key fields to convert to @lookup references */\n lookupFields?: {\n /** Field name in this entity (e.g., \"CategoryID\") */\n [fieldName: string]: {\n /** Target entity name (e.g., \"AI Prompt Categories\") */\n entity: string;\n /** Field in target entity to use for lookup (e.g., \"Name\") */\n field: string;\n };\n };\n };\n}\n\n/**\n * Folder-level configuration\n * \n * Defines default values that cascade down to all subdirectories. Stored in\n * .mj-folder.json files. Child folders can override parent defaults.\n */\nexport interface FolderConfig {\n /** Default field values that apply to all entities in this folder and subfolders */\n defaults: Record<string, any>;\n}\n\n/**\n * Load MemberJunction configuration from the filesystem\n * \n * Searches for mj.config.cjs starting from the current directory and walking up\n * the directory tree. Uses cosmiconfig for flexible configuration loading.\n * \n * @returns MJConfig object if found, null if not found or invalid\n * \n * @example\n * ```typescript\n * const config = loadMJConfig();\n * if (config) {\n * console.log(`Connecting to ${config.dbHost}:${config.dbPort || 1433}`);\n * }\n * ```\n */\nexport function loadMJConfig(): MJConfig | null {\n return configManager.loadMJConfig();\n}\n\n/**\n * Load sync configuration from a directory\n * \n * Loads .mj-sync.json file from the specified directory. This file can contain\n * either global sync configuration (no entity field) or entity-specific\n * configuration (with entity field).\n * \n * @param dir - Directory path to load configuration from\n * @returns Promise resolving to SyncConfig if found and valid, null otherwise\n * \n * @example\n * ```typescript\n * const syncConfig = await loadSyncConfig('/path/to/project');\n * if (syncConfig?.push?.requireConfirmation) {\n * // Show confirmation prompt\n * }\n * ```\n */\nexport async function loadSyncConfig(dir: string): Promise<SyncConfig | null> {\n const configPath = path.join(dir, '.mj-sync.json');\n \n if (await fs.pathExists(configPath)) {\n try {\n return await fs.readJson(configPath);\n } catch (error) {\n console.error('Error loading sync config:', error);\n return null;\n }\n }\n \n return null;\n}\n\n/**\n * Load entity-specific configuration from a directory\n * \n * Loads .mj-sync.json file that contains an \"entity\" field, indicating this\n * directory contains data for a specific entity type. Returns null if the\n * file doesn't exist or doesn't contain an entity field.\n * \n * @param dir - Directory path to load configuration from\n * @returns Promise resolving to EntityConfig if found and valid, null otherwise\n * \n * @example\n * ```typescript\n * const entityConfig = await loadEntityConfig('./ai-prompts');\n * if (entityConfig) {\n * console.log(`Directory contains ${entityConfig.entity} records`);\n * }\n * ```\n */\nexport async function loadEntityConfig(dir: string): Promise<EntityConfig | null> {\n const configPath = path.join(dir, '.mj-sync.json');\n \n if (await fs.pathExists(configPath)) {\n try {\n const config = await fs.readJson(configPath);\n if (config.entity) {\n return config;\n }\n } catch (error) {\n console.error('Error loading entity config:', error);\n }\n }\n \n return null;\n}\n\n/**\n * Load folder-level configuration\n * \n * Loads .mj-folder.json file that contains default values to be applied to\n * all entities in this folder and its subfolders. Used for cascading defaults\n * in deep directory structures.\n * \n * @param dir - Directory path to load configuration from\n * @returns Promise resolving to FolderConfig if found and valid, null otherwise\n * \n * @example\n * ```typescript\n * const folderConfig = await loadFolderConfig('./templates');\n * if (folderConfig?.defaults) {\n * // Apply folder defaults to records\n * }\n * ```\n */\nexport async function loadFolderConfig(dir: string): Promise<FolderConfig | null> {\n const configPath = path.join(dir, '.mj-folder.json');\n \n if (await fs.pathExists(configPath)) {\n try {\n return await fs.readJson(configPath);\n } catch (error) {\n console.error('Error loading folder config:', error);\n return null;\n }\n }\n \n return null;\n}"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;;;;AAGH,gDAAwB;AACxB,wDAA0B;AAC1B,yDAAqD;AAkNrD;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,YAAY;IAC1B,OAAO,8BAAa,CAAC,YAAY,EAAE,CAAC;AACtC,CAAC;AAFD,oCAEC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACI,KAAK,UAAU,cAAc,CAAC,GAAW;IAC9C,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IAEnD,IAAI,MAAM,kBAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,OAAO,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;YACnD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAbD,wCAaC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACI,KAAK,UAAU,gBAAgB,CAAC,GAAW;IAChD,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IAEnD,IAAI,MAAM,kBAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YAC7C,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBAClB,OAAO,MAAM,CAAC;YAChB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAfD,4CAeC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACI,KAAK,UAAU,gBAAgB,CAAC,GAAW;IAChD,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IAErD,IAAI,MAAM,kBAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,OAAO,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;YACrD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAbD,4CAaC","sourcesContent":["/**\n * @fileoverview Configuration types and loaders for MetadataSync\n * @module config\n * \n * This module defines configuration interfaces and provides utilities for loading\n * various configuration files used by the MetadataSync tool. It supports:\n * - MemberJunction database configuration (mj.config.cjs)\n * - Sync configuration (.mj-sync.json)\n * - Entity-specific configuration (.mj-sync.json with entity field)\n * - Folder-level defaults (.mj-folder.json)\n */\n\nimport { cosmiconfigSync } from 'cosmiconfig';\nimport path from 'path';\nimport fs from 'fs-extra';\nimport { configManager } from './lib/config-manager';\n\n/**\n * MemberJunction database configuration\n * \n * Defines connection parameters and settings for connecting to the MemberJunction\n * database. Typically loaded from mj.config.cjs in the project root.\n */\nexport interface MJConfig {\n /** Database server hostname or IP address */\n dbHost: string;\n /** Database server port (defaults to 1433 for SQL Server) */\n dbPort?: number;\n /** Database name to connect to */\n dbDatabase: string;\n /** Database authentication username */\n dbUsername: string;\n /** Database authentication password */\n dbPassword: string;\n /** Whether to trust the server certificate (Y/N) */\n dbTrustServerCertificate?: string;\n /** Whether to encrypt the connection (Y/N, auto-detected for Azure SQL) */\n dbEncrypt?: string;\n /** SQL Server instance name (for named instances) */\n dbInstanceName?: string;\n /** Schema name for MemberJunction core tables (defaults to __mj) */\n mjCoreSchema?: string;\n /** Allow additional properties for extensibility */\n [key: string]: any;\n}\n\n/**\n * Global sync configuration\n * \n * Defines settings that apply to the entire sync process, including push/pull\n * behaviors and watch mode configuration. Stored in .mj-sync.json at the root.\n */\nexport interface SyncConfig {\n /** Version of the sync configuration format */\n version: string;\n /** Glob pattern for finding data files (defaults to \"*.json\") */\n filePattern?: string;\n /** \n * Directory processing order (only applies to root-level config, not inherited by subdirectories)\n * Specifies the order in which subdirectories should be processed to handle dependencies.\n * Directories not listed in this array will be processed after the ordered ones in alphabetical order.\n */\n directoryOrder?: string[];\n /** Push command configuration */\n push?: {\n /** Whether to validate records before pushing to database */\n validateBeforePush?: boolean;\n /** Whether to require user confirmation before push */\n requireConfirmation?: boolean;\n /** \n * Whether to automatically create new records when a primaryKey exists but record is not found\n * Defaults to false - will warn instead of creating\n */\n autoCreateMissingRecords?: boolean;\n };\n /** SQL logging configuration (only applies to root-level config, not inherited by subdirectories) */\n sqlLogging?: {\n /** Whether to enable SQL logging during push operations */\n enabled?: boolean;\n /** Directory to output SQL log files (relative to command execution directory, defaults to './sql_logging') */\n outputDirectory?: string;\n /** Whether to format SQL as migration-ready files with Flyway schema placeholders */\n formatAsMigration?: boolean;\n };\n /** Watch command configuration */\n watch?: {\n /** Milliseconds to wait before processing file changes */\n debounceMs?: number;\n /** File patterns to ignore during watch */\n ignorePatterns?: string[];\n };\n}\n\n/**\n * Configuration for related entity synchronization\n * \n * Defines how to pull and push related entities that have foreign key relationships\n * with a parent entity. Supports nested relationships for deep object graphs.\n * NEW: Supports automatic recursive patterns for self-referencing entities.\n */\nexport interface RelatedEntityConfig {\n /** Name of the related entity to sync */\n entity: string;\n /** Field name that contains the foreign key reference to parent (e.g., \"PromptID\") */\n foreignKey: string;\n /** Optional SQL filter to apply when pulling related records */\n filter?: string;\n /** \n * Enable recursive fetching for self-referencing entities\n * When true, automatically fetches all levels of the hierarchy until no more children found\n */\n recursive?: boolean;\n /** \n * Maximum depth for recursive fetching (optional, defaults to 10)\n * Prevents infinite loops and controls memory usage\n * Only applies when recursive is true\n */\n maxDepth?: number;\n /** Fields to externalize to separate files for this related entity */\n externalizeFields?: string[] | {\n [fieldName: string]: {\n /** File extension to use (e.g., \".md\", \".txt\", \".html\") */\n extension?: string;\n }\n } | Array<{\n /** Field name to externalize */\n field: string;\n /** Pattern for the output file. Supports placeholders from the entity */\n pattern: string;\n }>;\n /** Fields to exclude from the pulled data for this related entity */\n excludeFields?: string[];\n /** Foreign key fields to convert to @lookup references for this related entity */\n lookupFields?: {\n [fieldName: string]: {\n entity: string;\n field: string;\n };\n };\n /** Nested related entities for deep object graphs */\n relatedEntities?: Record<string, RelatedEntityConfig>;\n}\n\n/**\n * Entity-specific configuration\n * \n * Defines settings for a specific entity type within a directory. Stored in\n * .mj-sync.json files that contain an \"entity\" field. Supports defaults,\n * file patterns, and related entity configuration.\n */\nexport interface EntityConfig {\n /** Name of the entity this directory contains */\n entity: string;\n /** Glob pattern for finding data files (defaults to \"*.json\") */\n filePattern?: string;\n /** Default field values applied to all records in this directory */\n defaults?: Record<string, any>;\n /** Pull command specific configuration */\n pull?: {\n /** Glob pattern for finding existing files to update (defaults to filePattern) */\n filePattern?: string;\n /** Whether to create new files for records not found locally */\n createNewFileIfNotFound?: boolean;\n /** Filename for new records when createNewFileIfNotFound is true */\n newFileName?: string;\n /** Whether to append multiple new records to a single file */\n appendRecordsToExistingFile?: boolean;\n /** Whether to update existing records found in local files */\n updateExistingRecords?: boolean;\n /** Fields to preserve during updates (never overwrite these) */\n preserveFields?: string[];\n /** Strategy for merging updates: \"overwrite\" | \"merge\" | \"skip\" */\n mergeStrategy?: 'overwrite' | 'merge' | 'skip';\n /** Create backup files before updating existing files */\n backupBeforeUpdate?: boolean;\n /** Directory name for backup files (defaults to \".backups\") */\n backupDirectory?: string;\n /** SQL filter to apply when pulling records from database */\n filter?: string;\n /** Configuration for pulling related entities */\n relatedEntities?: Record<string, RelatedEntityConfig>;\n /** Fields to externalize to separate files with optional configuration */\n externalizeFields?: string[] | {\n [fieldName: string]: {\n /** File extension to use (e.g., \".md\", \".txt\", \".html\") */\n extension?: string;\n }\n } | Array<{\n /** Field name to externalize */\n field: string;\n /** Pattern for the output file. Supports placeholders:\n * - {Name}: Entity's name field value\n * - {ID}: Entity's ID\n * - {FieldName}: The field being externalized\n * - Any other {FieldName} from the entity\n * Example: \"@file:templates/{Name}.template.md\"\n */\n pattern: string;\n }>;\n /** Fields to exclude from the pulled data (e.g., [\"TemplateID\"]) */\n excludeFields?: string[];\n /** Foreign key fields to convert to @lookup references */\n lookupFields?: {\n /** Field name in this entity (e.g., \"CategoryID\") */\n [fieldName: string]: {\n /** Target entity name (e.g., \"AI Prompt Categories\") */\n entity: string;\n /** Field in target entity to use for lookup (e.g., \"Name\") */\n field: string;\n };\n };\n };\n}\n\n/**\n * Folder-level configuration\n * \n * Defines default values that cascade down to all subdirectories. Stored in\n * .mj-folder.json files. Child folders can override parent defaults.\n */\nexport interface FolderConfig {\n /** Default field values that apply to all entities in this folder and subfolders */\n defaults: Record<string, any>;\n}\n\n/**\n * Load MemberJunction configuration from the filesystem\n * \n * Searches for mj.config.cjs starting from the current directory and walking up\n * the directory tree. Uses cosmiconfig for flexible configuration loading.\n * \n * @returns MJConfig object if found, null if not found or invalid\n * \n * @example\n * ```typescript\n * const config = loadMJConfig();\n * if (config) {\n * console.log(`Connecting to ${config.dbHost}:${config.dbPort || 1433}`);\n * }\n * ```\n */\nexport function loadMJConfig(): MJConfig | null {\n return configManager.loadMJConfig();\n}\n\n/**\n * Load sync configuration from a directory\n * \n * Loads .mj-sync.json file from the specified directory. This file can contain\n * either global sync configuration (no entity field) or entity-specific\n * configuration (with entity field).\n * \n * @param dir - Directory path to load configuration from\n * @returns Promise resolving to SyncConfig if found and valid, null otherwise\n * \n * @example\n * ```typescript\n * const syncConfig = await loadSyncConfig('/path/to/project');\n * if (syncConfig?.push?.requireConfirmation) {\n * // Show confirmation prompt\n * }\n * ```\n */\nexport async function loadSyncConfig(dir: string): Promise<SyncConfig | null> {\n const configPath = path.join(dir, '.mj-sync.json');\n \n if (await fs.pathExists(configPath)) {\n try {\n return await fs.readJson(configPath);\n } catch (error) {\n console.error('Error loading sync config:', error);\n return null;\n }\n }\n \n return null;\n}\n\n/**\n * Load entity-specific configuration from a directory\n * \n * Loads .mj-sync.json file that contains an \"entity\" field, indicating this\n * directory contains data for a specific entity type. Returns null if the\n * file doesn't exist or doesn't contain an entity field.\n * \n * @param dir - Directory path to load configuration from\n * @returns Promise resolving to EntityConfig if found and valid, null otherwise\n * \n * @example\n * ```typescript\n * const entityConfig = await loadEntityConfig('./ai-prompts');\n * if (entityConfig) {\n * console.log(`Directory contains ${entityConfig.entity} records`);\n * }\n * ```\n */\nexport async function loadEntityConfig(dir: string): Promise<EntityConfig | null> {\n const configPath = path.join(dir, '.mj-sync.json');\n \n if (await fs.pathExists(configPath)) {\n try {\n const config = await fs.readJson(configPath);\n if (config.entity) {\n return config;\n }\n } catch (error) {\n console.error('Error loading entity config:', error);\n }\n }\n \n return null;\n}\n\n/**\n * Load folder-level configuration\n * \n * Loads .mj-folder.json file that contains default values to be applied to\n * all entities in this folder and its subfolders. Used for cascading defaults\n * in deep directory structures.\n * \n * @param dir - Directory path to load configuration from\n * @returns Promise resolving to FolderConfig if found and valid, null otherwise\n * \n * @example\n * ```typescript\n * const folderConfig = await loadFolderConfig('./templates');\n * if (folderConfig?.defaults) {\n * // Apply folder defaults to records\n * }\n * ```\n */\nexport async function loadFolderConfig(dir: string): Promise<FolderConfig | null> {\n const configPath = path.join(dir, '.mj-folder.json');\n \n if (await fs.pathExists(configPath)) {\n try {\n return await fs.readJson(configPath);\n } catch (error) {\n console.error('Error loading folder config:', error);\n return null;\n }\n }\n \n return null;\n}"]}
package/dist/index.d.ts CHANGED
@@ -1 +1,4 @@
1
1
  export { run } from '@oclif/core';
2
+ export { FileBackupManager } from './lib/file-backup-manager';
3
+ export { SyncEngine } from './lib/sync-engine';
4
+ export type { RecordData } from './lib/sync-engine';
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.run = void 0;
3
+ exports.SyncEngine = exports.FileBackupManager = exports.run = void 0;
4
4
  var core_1 = require("@oclif/core");
5
5
  Object.defineProperty(exports, "run", { enumerable: true, get: function () { return core_1.run; } });
6
+ var file_backup_manager_1 = require("./lib/file-backup-manager");
7
+ Object.defineProperty(exports, "FileBackupManager", { enumerable: true, get: function () { return file_backup_manager_1.FileBackupManager; } });
8
+ var sync_engine_1 = require("./lib/sync-engine");
9
+ Object.defineProperty(exports, "SyncEngine", { enumerable: true, get: function () { return sync_engine_1.SyncEngine; } });
6
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,oCAAkC;AAAzB,2FAAA,GAAG,OAAA","sourcesContent":["export { run } from '@oclif/core';"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,oCAAkC;AAAzB,2FAAA,GAAG,OAAA;AACZ,iEAA8D;AAArD,wHAAA,iBAAiB,OAAA;AAC1B,iDAA+C;AAAtC,yGAAA,UAAU,OAAA","sourcesContent":["export { run } from '@oclif/core';\nexport { FileBackupManager } from './lib/file-backup-manager';\nexport { SyncEngine } from './lib/sync-engine';\nexport type { RecordData } from './lib/sync-engine';"]}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @fileoverview File backup and rollback manager for MetadataSync operations
3
+ * @module file-backup-manager
4
+ *
5
+ * This module provides functionality to create backups of files before modification
6
+ * and allows rolling back all changes if any operation fails. It ensures atomic
7
+ * file operations by maintaining a temporary backup directory during push operations.
8
+ */
9
+ /**
10
+ * Manages file backups and rollback operations for MetadataSync
11
+ *
12
+ * Creates temporary backups of all modified files during a push operation,
13
+ * allowing atomic rollback of all file changes if any error occurs.
14
+ *
15
+ * @class FileBackupManager
16
+ * @example
17
+ * ```typescript
18
+ * const backupManager = new FileBackupManager();
19
+ * await backupManager.initialize();
20
+ *
21
+ * try {
22
+ * await backupManager.backupFile('/path/to/file.json');
23
+ * // Modify the file
24
+ * await fs.writeJson('/path/to/file.json', newData);
25
+ *
26
+ * // If all operations succeed
27
+ * await backupManager.cleanup();
28
+ * } catch (error) {
29
+ * // Rollback all file changes
30
+ * await backupManager.rollback();
31
+ * throw error;
32
+ * }
33
+ * ```
34
+ */
35
+ export declare class FileBackupManager {
36
+ private backupDir;
37
+ private backups;
38
+ private initialized;
39
+ /**
40
+ * Initialize the backup manager by creating a temporary directory
41
+ *
42
+ * Creates a unique temporary directory for storing file backups during
43
+ * the current operation. Must be called before any backup operations.
44
+ *
45
+ * @returns Promise that resolves when initialization is complete
46
+ * @throws Error if temporary directory creation fails
47
+ */
48
+ initialize(): Promise<void>;
49
+ /**
50
+ * Create a backup of a file before modification
51
+ *
52
+ * Copies the current state of a file to the backup directory. If the file
53
+ * doesn't exist, records that fact for proper rollback handling.
54
+ *
55
+ * @param filePath - Absolute path to the file to backup
56
+ * @returns Promise that resolves when backup is complete
57
+ * @throws Error if backup operation fails
58
+ */
59
+ backupFile(filePath: string): Promise<void>;
60
+ /**
61
+ * Rollback all file changes by restoring from backups
62
+ *
63
+ * Restores all backed-up files to their original state. Files that didn't
64
+ * exist before the operation are deleted. This operation is atomic - either
65
+ * all files are restored or none are.
66
+ *
67
+ * @returns Promise that resolves when rollback is complete
68
+ * @throws Error if any rollback operation fails
69
+ */
70
+ rollback(): Promise<void>;
71
+ /**
72
+ * Clean up backup directory after successful operation
73
+ *
74
+ * Removes all temporary backup files and the backup directory. Should be
75
+ * called after all operations complete successfully.
76
+ *
77
+ * @returns Promise that resolves when cleanup is complete
78
+ */
79
+ cleanup(): Promise<void>;
80
+ /**
81
+ * Get statistics about current backup session
82
+ *
83
+ * @returns Object containing backup statistics
84
+ */
85
+ getStats(): {
86
+ totalBackups: number;
87
+ backupDir: string;
88
+ initialized: boolean;
89
+ };
90
+ }
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview File backup and rollback manager for MetadataSync operations
4
+ * @module file-backup-manager
5
+ *
6
+ * This module provides functionality to create backups of files before modification
7
+ * and allows rolling back all changes if any operation fails. It ensures atomic
8
+ * file operations by maintaining a temporary backup directory during push operations.
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.FileBackupManager = void 0;
15
+ const fs_extra_1 = __importDefault(require("fs-extra"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const os_1 = __importDefault(require("os"));
18
+ const global_1 = require("@memberjunction/global");
19
+ /**
20
+ * Manages file backups and rollback operations for MetadataSync
21
+ *
22
+ * Creates temporary backups of all modified files during a push operation,
23
+ * allowing atomic rollback of all file changes if any error occurs.
24
+ *
25
+ * @class FileBackupManager
26
+ * @example
27
+ * ```typescript
28
+ * const backupManager = new FileBackupManager();
29
+ * await backupManager.initialize();
30
+ *
31
+ * try {
32
+ * await backupManager.backupFile('/path/to/file.json');
33
+ * // Modify the file
34
+ * await fs.writeJson('/path/to/file.json', newData);
35
+ *
36
+ * // If all operations succeed
37
+ * await backupManager.cleanup();
38
+ * } catch (error) {
39
+ * // Rollback all file changes
40
+ * await backupManager.rollback();
41
+ * throw error;
42
+ * }
43
+ * ```
44
+ */
45
+ class FileBackupManager {
46
+ backupDir = '';
47
+ backups = [];
48
+ initialized = false;
49
+ /**
50
+ * Initialize the backup manager by creating a temporary directory
51
+ *
52
+ * Creates a unique temporary directory for storing file backups during
53
+ * the current operation. Must be called before any backup operations.
54
+ *
55
+ * @returns Promise that resolves when initialization is complete
56
+ * @throws Error if temporary directory creation fails
57
+ */
58
+ async initialize() {
59
+ if (this.initialized) {
60
+ throw new Error('FileBackupManager already initialized');
61
+ }
62
+ // Create a unique temporary directory for this session
63
+ const tempRoot = os_1.default.tmpdir();
64
+ const sessionId = (0, global_1.uuidv4)();
65
+ this.backupDir = path_1.default.join(tempRoot, 'mj-metadata-sync', sessionId);
66
+ await fs_extra_1.default.ensureDir(this.backupDir);
67
+ this.initialized = true;
68
+ }
69
+ /**
70
+ * Create a backup of a file before modification
71
+ *
72
+ * Copies the current state of a file to the backup directory. If the file
73
+ * doesn't exist, records that fact for proper rollback handling.
74
+ *
75
+ * @param filePath - Absolute path to the file to backup
76
+ * @returns Promise that resolves when backup is complete
77
+ * @throws Error if backup operation fails
78
+ */
79
+ async backupFile(filePath) {
80
+ if (!this.initialized) {
81
+ throw new Error('FileBackupManager not initialized. Call initialize() first.');
82
+ }
83
+ // Check if we already have a backup for this file
84
+ const existingBackup = this.backups.find(b => b.originalPath === filePath);
85
+ if (existingBackup) {
86
+ // Already backed up, don't backup again
87
+ return;
88
+ }
89
+ const exists = await fs_extra_1.default.pathExists(filePath);
90
+ const backupFileName = `${path_1.default.basename(filePath)}_${Date.now()}_${this.backups.length}`;
91
+ const backupPath = path_1.default.join(this.backupDir, backupFileName);
92
+ const backupEntry = {
93
+ originalPath: filePath,
94
+ backupPath: backupPath,
95
+ existed: exists
96
+ };
97
+ if (exists) {
98
+ // Copy the file to backup location
99
+ await fs_extra_1.default.copy(filePath, backupPath);
100
+ }
101
+ this.backups.push(backupEntry);
102
+ }
103
+ /**
104
+ * Rollback all file changes by restoring from backups
105
+ *
106
+ * Restores all backed-up files to their original state. Files that didn't
107
+ * exist before the operation are deleted. This operation is atomic - either
108
+ * all files are restored or none are.
109
+ *
110
+ * @returns Promise that resolves when rollback is complete
111
+ * @throws Error if any rollback operation fails
112
+ */
113
+ async rollback() {
114
+ if (!this.initialized) {
115
+ return; // Nothing to rollback
116
+ }
117
+ const errors = [];
118
+ // Process backups in reverse order (last modified first)
119
+ for (const backup of this.backups.reverse()) {
120
+ try {
121
+ if (backup.existed) {
122
+ // Restore the original file
123
+ await fs_extra_1.default.copy(backup.backupPath, backup.originalPath, { overwrite: true });
124
+ }
125
+ else {
126
+ // File didn't exist before, remove it
127
+ if (await fs_extra_1.default.pathExists(backup.originalPath)) {
128
+ await fs_extra_1.default.remove(backup.originalPath);
129
+ }
130
+ }
131
+ }
132
+ catch (error) {
133
+ const errorMsg = `Failed to rollback ${backup.originalPath}: ${error instanceof Error ? error.message : String(error)}`;
134
+ errors.push(errorMsg);
135
+ console.error(errorMsg);
136
+ }
137
+ }
138
+ // Clean up backup directory
139
+ try {
140
+ await this.cleanup();
141
+ }
142
+ catch (error) {
143
+ console.error('Failed to cleanup backup directory during rollback:', error);
144
+ }
145
+ if (errors.length > 0) {
146
+ throw new Error(`Rollback completed with errors:\n${errors.join('\n')}`);
147
+ }
148
+ }
149
+ /**
150
+ * Clean up backup directory after successful operation
151
+ *
152
+ * Removes all temporary backup files and the backup directory. Should be
153
+ * called after all operations complete successfully.
154
+ *
155
+ * @returns Promise that resolves when cleanup is complete
156
+ */
157
+ async cleanup() {
158
+ if (!this.initialized || !this.backupDir) {
159
+ return;
160
+ }
161
+ try {
162
+ await fs_extra_1.default.remove(this.backupDir);
163
+ }
164
+ catch (error) {
165
+ // Log but don't throw - cleanup errors shouldn't fail the operation
166
+ console.warn(`Failed to cleanup backup directory ${this.backupDir}:`, error);
167
+ }
168
+ this.backups = [];
169
+ this.initialized = false;
170
+ this.backupDir = '';
171
+ }
172
+ /**
173
+ * Get statistics about current backup session
174
+ *
175
+ * @returns Object containing backup statistics
176
+ */
177
+ getStats() {
178
+ return {
179
+ totalBackups: this.backups.length,
180
+ backupDir: this.backupDir,
181
+ initialized: this.initialized
182
+ };
183
+ }
184
+ }
185
+ exports.FileBackupManager = FileBackupManager;
186
+ //# sourceMappingURL=file-backup-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-backup-manager.js","sourceRoot":"","sources":["../../src/lib/file-backup-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview File backup and rollback manager for MetadataSync operations\n * @module file-backup-manager\n * \n * This module provides functionality to create backups of files before modification\n * and allows rolling back all changes if any operation fails. It ensures atomic\n * file operations by maintaining a temporary backup directory during push operations.\n */\n\nimport fs from 'fs-extra';\nimport path from 'path';\nimport os from 'os';\nimport { uuidv4 } from '@memberjunction/global';\n\n/**\n * Represents a backed-up file with its original and backup paths\n */\ninterface BackupEntry {\n /** Original file path that was backed up */\n originalPath: string;\n /** Path to the backup copy of the file */\n backupPath: string;\n /** Whether the file existed before modification */\n existed: boolean;\n}\n\n/**\n * Manages file backups and rollback operations for MetadataSync\n * \n * Creates temporary backups of all modified files during a push operation,\n * allowing atomic rollback of all file changes if any error occurs.\n * \n * @class FileBackupManager\n * @example\n * ```typescript\n * const backupManager = new FileBackupManager();\n * await backupManager.initialize();\n * \n * try {\n * await backupManager.backupFile('/path/to/file.json');\n * // Modify the file\n * await fs.writeJson('/path/to/file.json', newData);\n * \n * // If all operations succeed\n * await backupManager.cleanup();\n * } catch (error) {\n * // Rollback all file changes\n * await backupManager.rollback();\n * throw error;\n * }\n * ```\n */\nexport class FileBackupManager {\n private backupDir: string = '';\n private backups: BackupEntry[] = [];\n private initialized: boolean = false;\n \n /**\n * Initialize the backup manager by creating a temporary directory\n * \n * Creates a unique temporary directory for storing file backups during\n * the current operation. Must be called before any backup operations.\n * \n * @returns Promise that resolves when initialization is complete\n * @throws Error if temporary directory creation fails\n */\n async initialize(): Promise<void> {\n if (this.initialized) {\n throw new Error('FileBackupManager already initialized');\n }\n \n // Create a unique temporary directory for this session\n const tempRoot = os.tmpdir();\n const sessionId = uuidv4();\n this.backupDir = path.join(tempRoot, 'mj-metadata-sync', sessionId);\n \n await fs.ensureDir(this.backupDir);\n this.initialized = true;\n }\n \n /**\n * Create a backup of a file before modification\n * \n * Copies the current state of a file to the backup directory. If the file\n * doesn't exist, records that fact for proper rollback handling.\n * \n * @param filePath - Absolute path to the file to backup\n * @returns Promise that resolves when backup is complete\n * @throws Error if backup operation fails\n */\n async backupFile(filePath: string): Promise<void> {\n if (!this.initialized) {\n throw new Error('FileBackupManager not initialized. Call initialize() first.');\n }\n \n // Check if we already have a backup for this file\n const existingBackup = this.backups.find(b => b.originalPath === filePath);\n if (existingBackup) {\n // Already backed up, don't backup again\n return;\n }\n \n const exists = await fs.pathExists(filePath);\n const backupFileName = `${path.basename(filePath)}_${Date.now()}_${this.backups.length}`;\n const backupPath = path.join(this.backupDir, backupFileName);\n \n const backupEntry: BackupEntry = {\n originalPath: filePath,\n backupPath: backupPath,\n existed: exists\n };\n \n if (exists) {\n // Copy the file to backup location\n await fs.copy(filePath, backupPath);\n }\n \n this.backups.push(backupEntry);\n }\n \n /**\n * Rollback all file changes by restoring from backups\n * \n * Restores all backed-up files to their original state. Files that didn't\n * exist before the operation are deleted. This operation is atomic - either\n * all files are restored or none are.\n * \n * @returns Promise that resolves when rollback is complete\n * @throws Error if any rollback operation fails\n */\n async rollback(): Promise<void> {\n if (!this.initialized) {\n return; // Nothing to rollback\n }\n \n const errors: string[] = [];\n \n // Process backups in reverse order (last modified first)\n for (const backup of this.backups.reverse()) {\n try {\n if (backup.existed) {\n // Restore the original file\n await fs.copy(backup.backupPath, backup.originalPath, { overwrite: true });\n } else {\n // File didn't exist before, remove it\n if (await fs.pathExists(backup.originalPath)) {\n await fs.remove(backup.originalPath);\n }\n }\n } catch (error) {\n const errorMsg = `Failed to rollback ${backup.originalPath}: ${error instanceof Error ? error.message : String(error)}`;\n errors.push(errorMsg);\n console.error(errorMsg);\n }\n }\n \n // Clean up backup directory\n try {\n await this.cleanup();\n } catch (error) {\n console.error('Failed to cleanup backup directory during rollback:', error);\n }\n \n if (errors.length > 0) {\n throw new Error(`Rollback completed with errors:\\n${errors.join('\\n')}`);\n }\n }\n \n /**\n * Clean up backup directory after successful operation\n * \n * Removes all temporary backup files and the backup directory. Should be\n * called after all operations complete successfully.\n * \n * @returns Promise that resolves when cleanup is complete\n */\n async cleanup(): Promise<void> {\n if (!this.initialized || !this.backupDir) {\n return;\n }\n \n try {\n await fs.remove(this.backupDir);\n } catch (error) {\n // Log but don't throw - cleanup errors shouldn't fail the operation\n console.warn(`Failed to cleanup backup directory ${this.backupDir}:`, error);\n }\n \n this.backups = [];\n this.initialized = false;\n this.backupDir = '';\n }\n \n /**\n * Get statistics about current backup session\n * \n * @returns Object containing backup statistics\n */\n getStats(): { totalBackups: number; backupDir: string; initialized: boolean } {\n return {\n totalBackups: this.backups.length,\n backupDir: this.backupDir,\n initialized: this.initialized\n };\n }\n}"]}
@@ -236,7 +236,8 @@ class SyncEngine {
236
236
  newEntity[key] = value;
237
237
  }
238
238
  }
239
- // Save the new record
239
+ // Save the new record (new records are always dirty)
240
+ console.log(`📝 Auto-creating ${entityName} record where ${fieldName}='${fieldValue}'`);
240
241
  const saved = await newEntity.Save();
241
242
  if (!saved) {
242
243
  const message = newEntity.LatestResult?.Message;
@@ -1 +1 @@
1
- {"version":3,"file":"sync-engine.js","sourceRoot":"","sources":["../../src/lib/sync-engine.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;;AAEH,gDAAwB;AACxB,wDAA0B;AAC1B,oDAA4B;AAC5B,kDAA0B;AAC1B,+CAAyG;AACzG,mDAAgD;AAsBhD;;;;;;;;;;;;GAYG;AACH,MAAa,UAAU;IACb,QAAQ,CAAW;IACnB,WAAW,CAAW;IAE9B;;;OAGG;IACH,YAAY,WAAqB;QAC/B,IAAI,CAAC,QAAQ,GAAG,IAAI,eAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACjC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU;QACd,sBAAsB;QACtB,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,KAAK,CAAC,iBAAiB,CAAC,KAAU,EAAE,OAAe,EAAE,YAAgC,EAAE,UAA8B;QACnH,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,KAAK,CAAC;QACf,CAAC;QAED,+BAA+B;QAC/B,IAAI,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClB,MAAM,IAAI,KAAK,CAAC,0DAA0D,KAAK,EAAE,CAAC,CAAC;YACrF,CAAC;YACD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACrC,OAAO,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,CAAC;QAED,6BAA6B;QAC7B,IAAI,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CAAC,sDAAsD,KAAK,EAAE,CAAC,CAAC;YACjF,CAAC;YACD,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACrC,OAAO,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACnC,CAAC;QAED,6BAA6B;QAC7B,IAAI,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/B,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACpC,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAEjD,IAAI,MAAM,kBAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAClC,OAAO,MAAM,kBAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAC9C,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,KAAK,CAAC,mBAAmB,QAAQ,EAAE,CAAC,CAAC;YACjD,CAAC;QACH,CAAC;QAED,4BAA4B;QAC5B,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAE/B,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,eAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACtC,OAAO,QAAQ,CAAC,IAAI,CAAC;YACvB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC;QAED,+BAA+B;QAC/B,IAAI,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YACjC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAErC,2CAA2C;YAC3C,6DAA6D;YAC7D,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAClD,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,0BAA0B,KAAK,EAAE,CAAC,CAAC;YACrD,CAAC;YAED,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;YAClC,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAE7D,mCAAmC;YACnC,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YAChD,MAAM,UAAU,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAEnE,8BAA8B;YAC9B,MAAM,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;YACpD,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CAAC,0BAA0B,KAAK,EAAE,CAAC,CAAC;YACrD,CAAC;YAED,MAAM,CAAC,EAAE,SAAS,EAAE,UAAU,CAAC,GAAG,UAAU,CAAC;YAE7C,6DAA6D;YAC7D,IAAI,YAAY,GAAwB,EAAE,CAAC;YAC3C,IAAI,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBAChD,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;oBACzB,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;oBACnC,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC;wBACf,YAAY,CAAC,GAAG,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;oBAC9C,CAAC;gBACH,CAAC;YACH,CAAC;YAED,OAAO,MAAM,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;QAC9F,CAAC;QAED,4BAA4B;QAC5B,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAClC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAErC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CAAC,mCAAmC,MAAM,EAAE,CAAC,CAAC;YAC/D,CAAC;YAED,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,KAAK,CAAC,aAAa,CACjB,UAAkB,EAClB,SAAiB,EACjB,UAAkB,EAClB,aAAsB,KAAK,EAC3B,eAAoC,EAAE;QAEtC,4CAA4C;QAE5C,MAAM,EAAE,GAAG,IAAI,cAAO,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QAC1D,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qBAAqB,UAAU,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;QAC1G,MAAM,MAAM,GAAG,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC;YAC9B,UAAU,EAAE,UAAU;YACtB,WAAW,EAAE,GAAG,SAAS,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,EAAE;YACjF,OAAO,EAAE,CAAC;SACX,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QAErB,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChD,IAAI,UAAU,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtC,MAAM,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBACjD,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;gBACxC,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QAED,6DAA6D;QAC7D,IAAI,UAAU,EAAE,CAAC;YAEf,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;YACpF,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,uCAAuC,UAAU,EAAE,CAAC,CAAC;YACvE,CAAC;YAED,SAAS,CAAC,SAAS,EAAE,CAAC;YAEtB,6CAA6C;YAC7C,IAAI,UAAU,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtC,KAAK,MAAM,EAAE,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;oBACxC,IAAI,CAAC,EAAE,CAAC,aAAa,IAAI,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,kBAAkB,EAAE,CAAC;wBACtE,2DAA2D;wBAC3D,MAAM,IAAI,GAAG,IAAA,eAAM,GAAE,CAAC;wBACrB,SAAiB,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;oBACrC,CAAC;gBACH,CAAC;YACH,CAAC;YAED,uBAAuB;YACvB,IAAI,SAAS,IAAI,SAAS,EAAE,CAAC;gBAC1B,SAAiB,CAAC,SAAS,CAAC,GAAG,UAAU,CAAC;YAC7C,CAAC;YAED,qCAAqC;YACrC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;gBACxD,IAAI,GAAG,IAAI,SAAS,EAAE,CAAC;oBACpB,SAAiB,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAClC,CAAC;YACH,CAAC;YAED,sBAAsB;YACtB,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,CAAC;YACrC,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,OAAO,GAAG,SAAS,CAAC,YAAY,EAAE,OAAO,CAAC;gBAChD,IAAI,OAAO,EAAE,CAAC;oBACZ,MAAM,IAAI,KAAK,CAAC,yBAAyB,UAAU,KAAK,OAAO,EAAE,CAAC,CAAC;gBACrE,CAAC;gBAED,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CACvD,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CACtE,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC;gBACjC,MAAM,IAAI,KAAK,CAAC,yBAAyB,UAAU,KAAK,MAAM,EAAE,CAAC,CAAC;YACpE,CAAC;YAED,oBAAoB;YACpB,IAAI,UAAU,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtC,MAAM,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBACjD,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBACvC,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,MAAM,IAAI,KAAK,CAAC,sCAAsC,UAAU,WAAW,SAAS,KAAK,UAAU,GAAG,CAAC,CAAC;IAC1G,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,aAAa,CAAC,QAAgB,EAAE,YAA0B;QAC9D,MAAM,KAAK,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,cAAI,CAAC,GAAG,CAAC,CAAC;QACrD,IAAI,QAAQ,GAAwB,EAAE,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC;QAEjE,+CAA+C;QAC/C,IAAI,WAAW,GAAG,EAAE,CAAC;QACrB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,WAAW,GAAG,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YAC3C,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC;YAE9D,IAAI,YAAY,EAAE,QAAQ,EAAE,CAAC;gBAC3B,QAAQ,GAAG,EAAE,GAAG,QAAQ,EAAE,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC;YACvD,CAAC;QACH,CAAC;QAED,8DAA8D;QAC9D,MAAM,iBAAiB,GAAwB,EAAE,CAAC;QAClD,MAAM,OAAO,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAEvC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACtD,IAAI,CAAC;gBACH,iBAAiB,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YACtF,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,IAAI,KAAK,CAAC,wCAAwC,KAAK,MAAM,KAAK,EAAE,CAAC,CAAC;YAC9E,CAAC;QACH,CAAC;QAED,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,gBAAgB,CAAC,GAAW;QACxC,MAAM,UAAU,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;QAErD,IAAI,MAAM,kBAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YACpC,IAAI,CAAC;gBACH,OAAO,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YACvC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,kCAAkC,UAAU,GAAG,EAAE,KAAK,CAAC,CAAC;gBACtE,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,iBAAiB,CAAC,IAAS;QACzB,MAAM,IAAI,GAAG,gBAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,aAAa,CAAC,UAAkB;QAC9B,OAAO,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAChD,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,kBAAkB,CAAC,UAAkB;QACzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QACjF,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,uCAAuC,UAAU,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,KAAK,CAAC,UAAU,CAAC,UAAkB,EAAE,UAA+B;QAClE,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QAElD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,qBAAqB,UAAU,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,+FAA+F;QAC/F,+EAA+E;QAC/E,MAAM,EAAE,GAAG,IAAI,cAAO,EAAE,CAAC;QAEzB,kCAAkC;QAClC,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,KAAK,MAAM,EAAE,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,UAAU,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBAC1C,MAAM,IAAI,KAAK,CAAC,iCAAiC,EAAE,CAAC,IAAI,cAAc,UAAU,EAAE,CAAC,CAAC;YACtF,CAAC;YAED,8BAA8B;YAC9B,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC;YAC9D,MAAM,MAAM,GAAG,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7C,MAAM,YAAY,GAAG,KAAK,EAAE,WAAW,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YACzG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,MAAM,MAAM,GAAG,YAAY,GAAG,MAAM,EAAE,CAAC,CAAC;QACjE,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC;YAC9B,UAAU,EAAE,UAAU;YACtB,WAAW,EAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;YAClC,OAAO,EAAE,CAAC;SACX,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QAErB,6DAA6D;QAC7D,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,yDAAyD;QACzD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QACzD,MAAM,YAAY,GAAG,IAAI,mBAAY,EAAE,CAAC;QACxC,YAAY,CAAC,oBAAoB,CAAC,UAAU,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QAEpD,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IAChC,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,KAAK,CAAC,gBAAgB,CAAC,IAAS,EAAE,OAAe;QAC/C,gBAAgB;QAChB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,MAAM,cAAc,GAAG,EAAE,CAAC;YAC1B,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;gBACxB,cAAc,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;YAClE,CAAC;YACD,OAAO,cAAc,CAAC;QACxB,CAAC;QAED,iBAAiB;QACjB,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrC,gCAAgC;YAChC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC9D,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBACxC,OAAO,MAAM,IAAI,CAAC,sBAAsB,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;YAClE,CAAC;YAED,+CAA+C;YAC/C,MAAM,SAAS,GAAQ,EAAE,CAAC;YAC1B,IAAI,YAAY,GAAQ,EAAE,CAAC;YAE3B,uDAAuD;YACvD,IAAI,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;gBACtB,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;gBAE7F,2CAA2C;gBAC3C,KAAK,MAAM,WAAW,IAAI,SAAS,EAAE,CAAC;oBACpC,MAAM,eAAe,GAAG,MAAM,IAAI,CAAC,sBAAsB,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;oBAChF,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC;gBAC/D,CAAC;YACH,CAAC;YAED,2BAA2B;YAC3B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChD,IAAI,GAAG,KAAK,WAAW;oBAAE,SAAS,CAAC,iCAAiC;gBAEpE,gCAAgC;gBAChC,SAAS,CAAC,GAAG,CAAC,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAC/D,CAAC;YAED,4EAA4E;YAC5E,OAAO,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;QACjD,CAAC;QAED,gCAAgC;QAChC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;;;;;;OAYG;IACK,KAAK,CAAC,sBAAsB,CAAC,YAAoB,EAAE,OAAe;QACxE,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAErD,IAAI,CAAC,MAAM,kBAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,EAAE,CAAC,CAAC;QAC1D,CAAC;QAED,IAAI,CAAC;YACH,MAAM,eAAe,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAEpD,2CAA2C;YAC3C,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC3C,OAAO,MAAM,IAAI,CAAC,gBAAgB,CAAC,eAAe,EAAE,WAAW,CAAC,CAAC;QACnE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,KAAK,KAAK,EAAE,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACK,SAAS,CAAC,MAAW,EAAE,MAAW;QACxC,IAAI,CAAC,MAAM;YAAE,OAAO,MAAM,CAAC;QAC3B,IAAI,CAAC,MAAM;YAAE,OAAO,MAAM,CAAC;QAE3B,6DAA6D;QAC7D,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,0CAA0C;QAC1C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,+BAA+B;QAC/B,MAAM,MAAM,GAAQ,EAAE,GAAG,MAAM,EAAE,CAAC;QAElC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAClD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,SAAS,CAAC,wBAAwB;YACpC,CAAC;YAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBACpE,OAAO,MAAM,CAAC,GAAG,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBAC3F,sCAAsC;gBACtC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;YACnD,CAAC;iBAAM,CAAC;gBACN,+BAA+B;gBAC/B,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACtB,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CAEF;AAloBD,gCAkoBC","sourcesContent":["/**\n * @fileoverview Core synchronization engine for MemberJunction metadata\n * @module sync-engine\n * \n * This module provides the core functionality for synchronizing metadata between\n * the MemberJunction database and local file system representations. It handles\n * special reference types (@file, @url, @lookup, @env, @parent, @root, @template),\n * manages entity operations, and provides utilities for data transformation.\n */\n\nimport path from 'path';\nimport fs from 'fs-extra';\nimport crypto from 'crypto';\nimport axios from 'axios';\nimport { EntityInfo, Metadata, RunView, BaseEntity, CompositeKey, UserInfo } from '@memberjunction/core';\nimport { uuidv4 } from '@memberjunction/global';\nimport { EntityConfig, FolderConfig } from '../config';\n\n/**\n * Represents the structure of a metadata record with optional sync tracking\n */\nexport interface RecordData {\n /** Primary key field(s) and their values */\n primaryKey?: Record<string, any>;\n /** Entity field names and their values */\n fields: Record<string, any>;\n /** Related entities organized by entity name */\n relatedEntities?: Record<string, RecordData[]>;\n /** Synchronization metadata for change tracking */\n sync?: {\n /** ISO timestamp of last modification */\n lastModified: string;\n /** SHA256 checksum of the fields object */\n checksum: string;\n };\n}\n\n/**\n * Core engine for synchronizing MemberJunction metadata between database and files\n * \n * @class SyncEngine\n * @example\n * ```typescript\n * const syncEngine = new SyncEngine(systemUser);\n * await syncEngine.initialize();\n * \n * // Process a field value with special references\n * const value = await syncEngine.processFieldValue('@lookup:Users.Email=admin@example.com', '/path/to/base');\n * ```\n */\nexport class SyncEngine {\n private metadata: Metadata;\n private contextUser: UserInfo;\n\n /**\n * Creates a new SyncEngine instance\n * @param contextUser - The user context for database operations\n */\n constructor(contextUser: UserInfo) {\n this.metadata = new Metadata();\n this.contextUser = contextUser;\n }\n \n /**\n * Initializes the sync engine by refreshing metadata cache\n * @returns Promise that resolves when initialization is complete\n */\n async initialize(): Promise<void> {\n // Initialize metadata\n await this.metadata.Refresh();\n }\n \n /**\n * Process special references in field values\n * \n * Handles the following reference types:\n * - `@parent:fieldName` - References a field from the parent record\n * - `@root:fieldName` - References a field from the root record\n * - `@file:path` - Reads content from an external file\n * - `@url:address` - Fetches content from a URL\n * - `@lookup:Entity.Field=Value` - Looks up an entity ID by field value\n * - `@env:VARIABLE` - Reads an environment variable\n * \n * @param value - The field value to process\n * @param baseDir - Base directory for resolving relative file paths\n * @param parentRecord - Optional parent entity for @parent references\n * @param rootRecord - Optional root entity for @root references\n * @returns The processed value with all references resolved\n * @throws Error if a reference cannot be resolved\n * \n * @example\n * ```typescript\n * // File reference\n * const content = await processFieldValue('@file:template.md', '/path/to/dir');\n * \n * // Lookup with auto-create\n * const userId = await processFieldValue('@lookup:Users.Email=john@example.com?create', '/path');\n * ```\n */\n async processFieldValue(value: any, baseDir: string, parentRecord?: BaseEntity | null, rootRecord?: BaseEntity | null): Promise<any> {\n if (typeof value !== 'string') {\n return value;\n }\n \n // Check for @parent: reference\n if (value.startsWith('@parent:')) {\n if (!parentRecord) {\n throw new Error(`@parent reference used but no parent record available: ${value}`);\n }\n const fieldName = value.substring(8);\n return parentRecord.Get(fieldName);\n }\n \n // Check for @root: reference\n if (value.startsWith('@root:')) {\n if (!rootRecord) {\n throw new Error(`@root reference used but no root record available: ${value}`);\n }\n const fieldName = value.substring(6);\n return rootRecord.Get(fieldName);\n }\n \n // Check for @file: reference\n if (value.startsWith('@file:')) {\n const filePath = value.substring(6);\n const fullPath = path.resolve(baseDir, filePath);\n \n if (await fs.pathExists(fullPath)) {\n return await fs.readFile(fullPath, 'utf-8');\n } else {\n throw new Error(`File not found: ${fullPath}`);\n }\n }\n \n // Check for @url: reference\n if (value.startsWith('@url:')) {\n const url = value.substring(5);\n \n try {\n const response = await axios.get(url);\n return response.data;\n } catch (error) {\n throw new Error(`Failed to fetch URL: ${url} - ${error}`);\n }\n }\n \n // Check for @lookup: reference\n if (value.startsWith('@lookup:')) {\n const lookupStr = value.substring(8);\n \n // Parse lookup with optional create syntax\n // Format: EntityName.FieldName=Value?create&OtherField=Value\n const entityMatch = lookupStr.match(/^([^.]+)\\./);\n if (!entityMatch) {\n throw new Error(`Invalid lookup format: ${value}`);\n }\n \n const entityName = entityMatch[1];\n const remaining = lookupStr.substring(entityName.length + 1);\n \n // Check if this has ?create syntax\n const hasCreate = remaining.includes('?create');\n const lookupPart = hasCreate ? remaining.split('?')[0] : remaining;\n \n // Parse the main lookup field\n const fieldMatch = lookupPart.match(/^(.+?)=(.+)$/);\n if (!fieldMatch) {\n throw new Error(`Invalid lookup format: ${value}`);\n }\n \n const [, fieldName, fieldValue] = fieldMatch;\n \n // Parse additional fields for creation if ?create is present\n let createFields: Record<string, any> = {};\n if (hasCreate && remaining.includes('?create&')) {\n const createPart = remaining.split('?create&')[1];\n const pairs = createPart.split('&');\n for (const pair of pairs) {\n const [key, val] = pair.split('=');\n if (key && val) {\n createFields[key] = decodeURIComponent(val);\n }\n }\n }\n \n return await this.resolveLookup(entityName, fieldName, fieldValue, hasCreate, createFields);\n }\n \n // Check for @env: reference\n if (value.startsWith('@env:')) {\n const envVar = value.substring(5);\n const envValue = process.env[envVar];\n \n if (envValue === undefined) {\n throw new Error(`Environment variable not found: ${envVar}`);\n }\n \n return envValue;\n }\n \n return value;\n }\n \n /**\n * Resolve a lookup reference to an ID, optionally creating the record if it doesn't exist\n * \n * @param entityName - Name of the entity to search in\n * @param fieldName - Field to match against\n * @param fieldValue - Value to search for\n * @param autoCreate - Whether to create the record if not found\n * @param createFields - Additional fields to set when creating\n * @returns The ID of the found or created record\n * @throws Error if lookup fails and autoCreate is false\n * \n * @example\n * ```typescript\n * // Simple lookup\n * const categoryId = await resolveLookup('Categories', 'Name', 'Technology');\n * \n * // Lookup with auto-create\n * const tagId = await resolveLookup('Tags', 'Name', 'New Tag', true, {\n * Description: 'Auto-created tag',\n * Status: 'Active'\n * });\n * ```\n */\n async resolveLookup(\n entityName: string, \n fieldName: string, \n fieldValue: string,\n autoCreate: boolean = false,\n createFields: Record<string, any> = {}\n ): Promise<string> {\n // Debug logging handled by caller if needed\n \n const rv = new RunView();\n const entityInfo = this.metadata.EntityByName(entityName);\n if (!entityInfo) {\n throw new Error(`Entity not found: ${entityName}`);\n }\n \n const field = entityInfo.Fields.find(f => f.Name.trim().toLowerCase() === fieldName.trim().toLowerCase());\n const quotes = field?.NeedsQuotes ? \"'\" : '';\n const result = await rv.RunView({\n EntityName: entityName,\n ExtraFilter: `${fieldName} = ${quotes}${fieldValue.replace(/'/g, \"''\")}${quotes}`,\n MaxRows: 1\n }, this.contextUser);\n \n if (result.Success && result.Results.length > 0) {\n if (entityInfo.PrimaryKeys.length > 0) {\n const pkeyField = entityInfo.PrimaryKeys[0].Name;\n const id = result.Results[0][pkeyField];\n return id;\n }\n }\n \n // If not found and auto-create is enabled, create the record\n if (autoCreate) {\n \n const newEntity = await this.metadata.GetEntityObject(entityName, this.contextUser);\n if (!newEntity) {\n throw new Error(`Failed to create entity object for: ${entityName}`);\n }\n \n newEntity.NewRecord();\n \n // Handle explicit ID setting for new records\n if (entityInfo.PrimaryKeys.length > 0) {\n for (const pk of entityInfo.PrimaryKeys) {\n if (!pk.AutoIncrement && pk.Type.toLowerCase() === 'uniqueidentifier') {\n // Generate UUID for this primary key and set it explicitly\n const uuid = uuidv4();\n (newEntity as any)[pk.Name] = uuid;\n }\n }\n }\n \n // Set the lookup field\n if (fieldName in newEntity) {\n (newEntity as any)[fieldName] = fieldValue;\n }\n \n // Set any additional fields provided\n for (const [key, value] of Object.entries(createFields)) {\n if (key in newEntity) {\n (newEntity as any)[key] = value;\n }\n }\n \n // Save the new record\n const saved = await newEntity.Save();\n if (!saved) {\n const message = newEntity.LatestResult?.Message;\n if (message) {\n throw new Error(`Failed to auto-create ${entityName}: ${message}`);\n }\n \n const errors = newEntity.LatestResult?.Errors?.map(err => \n typeof err === 'string' ? err : (err?.message || JSON.stringify(err))\n )?.join(', ') || 'Unknown error';\n throw new Error(`Failed to auto-create ${entityName}: ${errors}`);\n }\n \n // Return the new ID\n if (entityInfo.PrimaryKeys.length > 0) {\n const pkeyField = entityInfo.PrimaryKeys[0].Name;\n const newId = newEntity.Get(pkeyField);\n return newId;\n }\n }\n \n throw new Error(`Lookup failed: No record found in '${entityName}' where ${fieldName}='${fieldValue}'`);\n }\n \n /**\n * Build cascading defaults for a file path and process field values\n * \n * Walks up the directory tree from the file location, collecting defaults from\n * entity config and folder configs, with deeper folders overriding parent values.\n * All default values are processed for special references.\n * \n * @param filePath - Path to the file being processed\n * @param entityConfig - Entity configuration containing base defaults\n * @returns Processed defaults with all references resolved\n * @throws Error if any default value processing fails\n */\n async buildDefaults(filePath: string, entityConfig: EntityConfig): Promise<Record<string, any>> {\n const parts = path.dirname(filePath).split(path.sep);\n let defaults: Record<string, any> = { ...entityConfig.defaults };\n \n // Walk up the directory tree building defaults\n let currentPath = '';\n for (const part of parts) {\n currentPath = path.join(currentPath, part);\n const folderConfig = await this.loadFolderConfig(currentPath);\n \n if (folderConfig?.defaults) {\n defaults = { ...defaults, ...folderConfig.defaults };\n }\n }\n \n // Process all default values (lookups, file references, etc.)\n const processedDefaults: Record<string, any> = {};\n const baseDir = path.dirname(filePath);\n \n for (const [field, value] of Object.entries(defaults)) {\n try {\n processedDefaults[field] = await this.processFieldValue(value, baseDir, null, null);\n } catch (error) {\n throw new Error(`Failed to process default for field '${field}': ${error}`);\n }\n }\n \n return processedDefaults;\n }\n \n /**\n * Load folder configuration from .mj-folder.json file\n * \n * @param dir - Directory to check for configuration\n * @returns Folder configuration or null if not found/invalid\n * @private\n */\n private async loadFolderConfig(dir: string): Promise<FolderConfig | null> {\n const configPath = path.join(dir, '.mj-folder.json');\n \n if (await fs.pathExists(configPath)) {\n try {\n return await fs.readJson(configPath);\n } catch (error) {\n console.error(`Error loading folder config at ${configPath}:`, error);\n return null;\n }\n }\n \n return null;\n }\n \n /**\n * Calculate SHA256 checksum for data\n * \n * Generates a deterministic hash of the provided data by converting it to\n * formatted JSON and calculating a SHA256 digest. Used for change detection\n * in sync operations.\n * \n * @param data - Any data structure to calculate checksum for\n * @returns Hexadecimal string representation of the SHA256 hash\n * \n * @example\n * ```typescript\n * const checksum = syncEngine.calculateChecksum({\n * name: 'Test Record',\n * value: 42,\n * tags: ['a', 'b']\n * });\n * // Returns consistent hash for same data structure\n * ```\n */\n calculateChecksum(data: any): string {\n const hash = crypto.createHash('sha256');\n hash.update(JSON.stringify(data, null, 2));\n return hash.digest('hex');\n }\n \n /**\n * Get entity metadata information by name\n * \n * Retrieves the EntityInfo object containing schema metadata for the specified entity.\n * Returns null if the entity is not found in the metadata cache.\n * \n * @param entityName - Name of the entity to look up\n * @returns EntityInfo object with schema details or null if not found\n * \n * @example\n * ```typescript\n * const entityInfo = syncEngine.getEntityInfo('AI Prompts');\n * if (entityInfo) {\n * console.log(`Primary keys: ${entityInfo.PrimaryKeys.map(pk => pk.Name).join(', ')}`);\n * }\n * ```\n */\n getEntityInfo(entityName: string): EntityInfo | null {\n return this.metadata.EntityByName(entityName);\n }\n \n /**\n * Create a new entity object instance\n * \n * Uses the MemberJunction metadata system to properly instantiate an entity object.\n * This ensures correct class registration and respects any custom entity subclasses.\n * \n * @param entityName - Name of the entity to create\n * @returns Promise resolving to the new BaseEntity instance\n * @throws Error if entity creation fails\n * \n * @example\n * ```typescript\n * const entity = await syncEngine.createEntityObject('AI Prompts');\n * entity.NewRecord();\n * entity.Set('Name', 'My Prompt');\n * await entity.Save();\n * ```\n */\n async createEntityObject(entityName: string): Promise<BaseEntity> {\n const entity = await this.metadata.GetEntityObject(entityName, this.contextUser);\n if (!entity) {\n throw new Error(`Failed to create entity object for: ${entityName}`);\n }\n return entity;\n }\n \n /**\n * Load an entity record by primary key\n * \n * Retrieves an existing entity record from the database using its primary key values.\n * Supports both single and composite primary keys. Returns null if the record is not found.\n * \n * @param entityName - Name of the entity to load\n * @param primaryKey - Object containing primary key field names and values\n * @returns Promise resolving to the loaded entity or null if not found\n * @throws Error if entity metadata is not found\n * \n * @example\n * ```typescript\n * // Single primary key\n * const entity = await syncEngine.loadEntity('Users', { ID: '123-456' });\n * \n * // Composite primary key\n * const entity = await syncEngine.loadEntity('UserRoles', { \n * UserID: '123-456',\n * RoleID: '789-012'\n * });\n * ```\n */\n async loadEntity(entityName: string, primaryKey: Record<string, any>): Promise<BaseEntity | null> {\n const entityInfo = this.getEntityInfo(entityName);\n \n if (!entityInfo) {\n throw new Error(`Entity not found: ${entityName}`);\n }\n \n // First, check if the record exists using RunView to avoid \"Error in BaseEntity.Load\" messages\n // when records don't exist (which is a normal scenario during sync operations)\n const rv = new RunView();\n \n // Build filter for primary key(s)\n const filters: string[] = [];\n for (const pk of entityInfo.PrimaryKeys) {\n const value = primaryKey[pk.Name];\n if (value === undefined || value === null) {\n throw new Error(`Missing primary key value for ${pk.Name} in entity ${entityName}`);\n }\n \n // Check if field needs quotes\n const field = entityInfo.Fields.find(f => f.Name === pk.Name);\n const quotes = field?.NeedsQuotes ? \"'\" : '';\n const escapedValue = field?.NeedsQuotes && typeof value === 'string' ? value.replace(/'/g, \"''\") : value;\n filters.push(`${pk.Name} = ${quotes}${escapedValue}${quotes}`);\n }\n \n const result = await rv.RunView({\n EntityName: entityName,\n ExtraFilter: filters.join(' AND '),\n MaxRows: 1\n }, this.contextUser);\n \n // If no record found, return null without attempting to load\n if (!result.Success || result.Results.length === 0) {\n return null;\n }\n \n // Record exists, now load it properly through the entity\n const entity = await this.createEntityObject(entityName);\n const compositeKey = new CompositeKey();\n compositeKey.LoadFromSimpleObject(primaryKey);\n const loaded = await entity.InnerLoad(compositeKey);\n \n return loaded ? entity : null;\n }\n \n /**\n * Process JSON object with template references\n * \n * Recursively processes JSON data structures to resolve `@template` references.\n * Templates can be defined at any level and support:\n * - Single template references: `\"@template:path/to/template.json\"`\n * - Object with @template field: `{ \"@template\": \"file.json\", \"override\": \"value\" }`\n * - Array of templates for merging: `{ \"@template\": [\"base.json\", \"overrides.json\"] }`\n * - Nested template references within templates\n * \n * @param data - JSON data structure to process\n * @param baseDir - Base directory for resolving relative template paths\n * @returns Promise resolving to the processed data with all templates resolved\n * @throws Error if template file is not found or contains invalid JSON\n * \n * @example\n * ```typescript\n * // Input data with template reference\n * const data = {\n * \"@template\": \"defaults/ai-prompt.json\",\n * \"Name\": \"Custom Prompt\",\n * \"Prompt\": \"Override the template prompt\"\n * };\n * \n * // Resolves template and merges with overrides\n * const result = await syncEngine.processTemplates(data, '/path/to/dir');\n * ```\n */\n async processTemplates(data: any, baseDir: string): Promise<any> {\n // Handle arrays\n if (Array.isArray(data)) {\n const processedArray = [];\n for (const item of data) {\n processedArray.push(await this.processTemplates(item, baseDir));\n }\n return processedArray;\n }\n \n // Handle objects\n if (data && typeof data === 'object') {\n // Check for @template reference\n if (typeof data === 'string' && data.startsWith('@template:')) {\n const templatePath = data.substring(10);\n return await this.loadAndProcessTemplate(templatePath, baseDir);\n }\n \n // Process object with possible @template field\n const processed: any = {};\n let templateData: any = {};\n \n // First, check if there's a @template field to process\n if (data['@template']) {\n const templates = Array.isArray(data['@template']) ? data['@template'] : [data['@template']];\n \n // Process templates in order, merging them\n for (const templateRef of templates) {\n const templateContent = await this.loadAndProcessTemplate(templateRef, baseDir);\n templateData = this.deepMerge(templateData, templateContent);\n }\n }\n \n // Process all other fields\n for (const [key, value] of Object.entries(data)) {\n if (key === '@template') continue; // Skip the template field itself\n \n // Process the value recursively\n processed[key] = await this.processTemplates(value, baseDir);\n }\n \n // Merge template data with processed data (processed data takes precedence)\n return this.deepMerge(templateData, processed);\n }\n \n // Return primitive values as-is\n return data;\n }\n \n /**\n * Load and process a template file\n * \n * Loads a JSON template file from the filesystem and recursively processes any\n * nested template references within it. Template paths are resolved relative to\n * the template file's directory, enabling template composition.\n * \n * @param templatePath - Path to the template file (relative or absolute)\n * @param baseDir - Base directory for resolving relative paths\n * @returns Promise resolving to the processed template content\n * @throws Error if template file not found or contains invalid JSON\n * @private\n */\n private async loadAndProcessTemplate(templatePath: string, baseDir: string): Promise<any> {\n const fullPath = path.resolve(baseDir, templatePath);\n \n if (!await fs.pathExists(fullPath)) {\n throw new Error(`Template file not found: ${fullPath}`);\n }\n \n try {\n const templateContent = await fs.readJson(fullPath);\n \n // Recursively process any nested templates\n const templateDir = path.dirname(fullPath);\n return await this.processTemplates(templateContent, templateDir);\n } catch (error) {\n throw new Error(`Failed to load template ${fullPath}: ${error}`);\n }\n }\n \n /**\n * Deep merge two objects with target taking precedence\n * \n * Recursively merges two objects, with values from the target object overriding\n * values from the source object. Arrays and primitive values are not merged but\n * replaced entirely by the target value. Undefined values in target are skipped.\n * \n * @param source - Base object to merge from\n * @param target - Object with values that override source\n * @returns New object with merged values\n * @private\n * \n * @example\n * ```typescript\n * const source = {\n * a: 1,\n * b: { x: 10, y: 20 },\n * c: [1, 2, 3]\n * };\n * const target = {\n * a: 2,\n * b: { y: 30, z: 40 },\n * d: 'new'\n * };\n * const result = deepMerge(source, target);\n * // Result: { a: 2, b: { x: 10, y: 30, z: 40 }, c: [1, 2, 3], d: 'new' }\n * ```\n */\n private deepMerge(source: any, target: any): any {\n if (!source) return target;\n if (!target) return source;\n \n // If target is not an object, it completely overrides source\n if (typeof target !== 'object' || target === null || Array.isArray(target)) {\n return target;\n }\n \n // If source is not an object, target wins\n if (typeof source !== 'object' || source === null || Array.isArray(source)) {\n return target;\n }\n \n // Both are objects, merge them\n const result: any = { ...source };\n \n for (const [key, value] of Object.entries(target)) {\n if (value === undefined) {\n continue; // Skip undefined values\n }\n \n if (typeof value === 'object' && value !== null && !Array.isArray(value) &&\n typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key])) {\n // Both are objects, merge recursively\n result[key] = this.deepMerge(result[key], value);\n } else {\n // Otherwise, target value wins\n result[key] = value;\n }\n }\n \n return result;\n }\n \n}"]}
1
+ {"version":3,"file":"sync-engine.js","sourceRoot":"","sources":["../../src/lib/sync-engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Core synchronization engine for MemberJunction metadata\n * @module sync-engine\n * \n * This module provides the core functionality for synchronizing metadata between\n * the MemberJunction database and local file system representations. It handles\n * special reference types (@file, @url, @lookup, @env, @parent, @root, @template),\n * manages entity operations, and provides utilities for data transformation.\n */\n\nimport path from 'path';\nimport fs from 'fs-extra';\nimport crypto from 'crypto';\nimport axios from 'axios';\nimport { EntityInfo, Metadata, RunView, BaseEntity, CompositeKey, UserInfo } from '@memberjunction/core';\nimport { uuidv4 } from '@memberjunction/global';\nimport { EntityConfig, FolderConfig } from '../config';\n\n/**\n * Represents the structure of a metadata record with optional sync tracking\n */\nexport interface RecordData {\n /** Primary key field(s) and their values */\n primaryKey?: Record<string, any>;\n /** Entity field names and their values */\n fields: Record<string, any>;\n /** Related entities organized by entity name */\n relatedEntities?: Record<string, RecordData[]>;\n /** Synchronization metadata for change tracking */\n sync?: {\n /** ISO timestamp of last modification */\n lastModified: string;\n /** SHA256 checksum of the fields object */\n checksum: string;\n };\n}\n\n/**\n * Core engine for synchronizing MemberJunction metadata between database and files\n * \n * @class SyncEngine\n * @example\n * ```typescript\n * const syncEngine = new SyncEngine(systemUser);\n * await syncEngine.initialize();\n * \n * // Process a field value with special references\n * const value = await syncEngine.processFieldValue('@lookup:Users.Email=admin@example.com', '/path/to/base');\n * ```\n */\nexport class SyncEngine {\n private metadata: Metadata;\n private contextUser: UserInfo;\n\n /**\n * Creates a new SyncEngine instance\n * @param contextUser - The user context for database operations\n */\n constructor(contextUser: UserInfo) {\n this.metadata = new Metadata();\n this.contextUser = contextUser;\n }\n \n /**\n * Initializes the sync engine by refreshing metadata cache\n * @returns Promise that resolves when initialization is complete\n */\n async initialize(): Promise<void> {\n // Initialize metadata\n await this.metadata.Refresh();\n }\n \n /**\n * Process special references in field values\n * \n * Handles the following reference types:\n * - `@parent:fieldName` - References a field from the parent record\n * - `@root:fieldName` - References a field from the root record\n * - `@file:path` - Reads content from an external file\n * - `@url:address` - Fetches content from a URL\n * - `@lookup:Entity.Field=Value` - Looks up an entity ID by field value\n * - `@env:VARIABLE` - Reads an environment variable\n * \n * @param value - The field value to process\n * @param baseDir - Base directory for resolving relative file paths\n * @param parentRecord - Optional parent entity for @parent references\n * @param rootRecord - Optional root entity for @root references\n * @returns The processed value with all references resolved\n * @throws Error if a reference cannot be resolved\n * \n * @example\n * ```typescript\n * // File reference\n * const content = await processFieldValue('@file:template.md', '/path/to/dir');\n * \n * // Lookup with auto-create\n * const userId = await processFieldValue('@lookup:Users.Email=john@example.com?create', '/path');\n * ```\n */\n async processFieldValue(value: any, baseDir: string, parentRecord?: BaseEntity | null, rootRecord?: BaseEntity | null): Promise<any> {\n if (typeof value !== 'string') {\n return value;\n }\n \n // Check for @parent: reference\n if (value.startsWith('@parent:')) {\n if (!parentRecord) {\n throw new Error(`@parent reference used but no parent record available: ${value}`);\n }\n const fieldName = value.substring(8);\n return parentRecord.Get(fieldName);\n }\n \n // Check for @root: reference\n if (value.startsWith('@root:')) {\n if (!rootRecord) {\n throw new Error(`@root reference used but no root record available: ${value}`);\n }\n const fieldName = value.substring(6);\n return rootRecord.Get(fieldName);\n }\n \n // Check for @file: reference\n if (value.startsWith('@file:')) {\n const filePath = value.substring(6);\n const fullPath = path.resolve(baseDir, filePath);\n \n if (await fs.pathExists(fullPath)) {\n return await fs.readFile(fullPath, 'utf-8');\n } else {\n throw new Error(`File not found: ${fullPath}`);\n }\n }\n \n // Check for @url: reference\n if (value.startsWith('@url:')) {\n const url = value.substring(5);\n \n try {\n const response = await axios.get(url);\n return response.data;\n } catch (error) {\n throw new Error(`Failed to fetch URL: ${url} - ${error}`);\n }\n }\n \n // Check for @lookup: reference\n if (value.startsWith('@lookup:')) {\n const lookupStr = value.substring(8);\n \n // Parse lookup with optional create syntax\n // Format: EntityName.FieldName=Value?create&OtherField=Value\n const entityMatch = lookupStr.match(/^([^.]+)\\./);\n if (!entityMatch) {\n throw new Error(`Invalid lookup format: ${value}`);\n }\n \n const entityName = entityMatch[1];\n const remaining = lookupStr.substring(entityName.length + 1);\n \n // Check if this has ?create syntax\n const hasCreate = remaining.includes('?create');\n const lookupPart = hasCreate ? remaining.split('?')[0] : remaining;\n \n // Parse the main lookup field\n const fieldMatch = lookupPart.match(/^(.+?)=(.+)$/);\n if (!fieldMatch) {\n throw new Error(`Invalid lookup format: ${value}`);\n }\n \n const [, fieldName, fieldValue] = fieldMatch;\n \n // Parse additional fields for creation if ?create is present\n let createFields: Record<string, any> = {};\n if (hasCreate && remaining.includes('?create&')) {\n const createPart = remaining.split('?create&')[1];\n const pairs = createPart.split('&');\n for (const pair of pairs) {\n const [key, val] = pair.split('=');\n if (key && val) {\n createFields[key] = decodeURIComponent(val);\n }\n }\n }\n \n return await this.resolveLookup(entityName, fieldName, fieldValue, hasCreate, createFields);\n }\n \n // Check for @env: reference\n if (value.startsWith('@env:')) {\n const envVar = value.substring(5);\n const envValue = process.env[envVar];\n \n if (envValue === undefined) {\n throw new Error(`Environment variable not found: ${envVar}`);\n }\n \n return envValue;\n }\n \n return value;\n }\n \n /**\n * Resolve a lookup reference to an ID, optionally creating the record if it doesn't exist\n * \n * @param entityName - Name of the entity to search in\n * @param fieldName - Field to match against\n * @param fieldValue - Value to search for\n * @param autoCreate - Whether to create the record if not found\n * @param createFields - Additional fields to set when creating\n * @returns The ID of the found or created record\n * @throws Error if lookup fails and autoCreate is false\n * \n * @example\n * ```typescript\n * // Simple lookup\n * const categoryId = await resolveLookup('Categories', 'Name', 'Technology');\n * \n * // Lookup with auto-create\n * const tagId = await resolveLookup('Tags', 'Name', 'New Tag', true, {\n * Description: 'Auto-created tag',\n * Status: 'Active'\n * });\n * ```\n */\n async resolveLookup(\n entityName: string, \n fieldName: string, \n fieldValue: string,\n autoCreate: boolean = false,\n createFields: Record<string, any> = {}\n ): Promise<string> {\n // Debug logging handled by caller if needed\n \n const rv = new RunView();\n const entityInfo = this.metadata.EntityByName(entityName);\n if (!entityInfo) {\n throw new Error(`Entity not found: ${entityName}`);\n }\n \n const field = entityInfo.Fields.find(f => f.Name.trim().toLowerCase() === fieldName.trim().toLowerCase());\n const quotes = field?.NeedsQuotes ? \"'\" : '';\n const result = await rv.RunView({\n EntityName: entityName,\n ExtraFilter: `${fieldName} = ${quotes}${fieldValue.replace(/'/g, \"''\")}${quotes}`,\n MaxRows: 1\n }, this.contextUser);\n \n if (result.Success && result.Results.length > 0) {\n if (entityInfo.PrimaryKeys.length > 0) {\n const pkeyField = entityInfo.PrimaryKeys[0].Name;\n const id = result.Results[0][pkeyField];\n return id;\n }\n }\n \n // If not found and auto-create is enabled, create the record\n if (autoCreate) {\n \n const newEntity = await this.metadata.GetEntityObject(entityName, this.contextUser);\n if (!newEntity) {\n throw new Error(`Failed to create entity object for: ${entityName}`);\n }\n \n newEntity.NewRecord();\n \n // Handle explicit ID setting for new records\n if (entityInfo.PrimaryKeys.length > 0) {\n for (const pk of entityInfo.PrimaryKeys) {\n if (!pk.AutoIncrement && pk.Type.toLowerCase() === 'uniqueidentifier') {\n // Generate UUID for this primary key and set it explicitly\n const uuid = uuidv4();\n (newEntity as any)[pk.Name] = uuid;\n }\n }\n }\n \n // Set the lookup field\n if (fieldName in newEntity) {\n (newEntity as any)[fieldName] = fieldValue;\n }\n \n // Set any additional fields provided\n for (const [key, value] of Object.entries(createFields)) {\n if (key in newEntity) {\n (newEntity as any)[key] = value;\n }\n }\n \n // Save the new record (new records are always dirty)\n console.log(`📝 Auto-creating ${entityName} record where ${fieldName}='${fieldValue}'`);\n const saved = await newEntity.Save();\n if (!saved) {\n const message = newEntity.LatestResult?.Message;\n if (message) {\n throw new Error(`Failed to auto-create ${entityName}: ${message}`);\n }\n \n const errors = newEntity.LatestResult?.Errors?.map(err => \n typeof err === 'string' ? err : (err?.message || JSON.stringify(err))\n )?.join(', ') || 'Unknown error';\n throw new Error(`Failed to auto-create ${entityName}: ${errors}`);\n }\n \n // Return the new ID\n if (entityInfo.PrimaryKeys.length > 0) {\n const pkeyField = entityInfo.PrimaryKeys[0].Name;\n const newId = newEntity.Get(pkeyField);\n return newId;\n }\n }\n \n throw new Error(`Lookup failed: No record found in '${entityName}' where ${fieldName}='${fieldValue}'`);\n }\n \n /**\n * Build cascading defaults for a file path and process field values\n * \n * Walks up the directory tree from the file location, collecting defaults from\n * entity config and folder configs, with deeper folders overriding parent values.\n * All default values are processed for special references.\n * \n * @param filePath - Path to the file being processed\n * @param entityConfig - Entity configuration containing base defaults\n * @returns Processed defaults with all references resolved\n * @throws Error if any default value processing fails\n */\n async buildDefaults(filePath: string, entityConfig: EntityConfig): Promise<Record<string, any>> {\n const parts = path.dirname(filePath).split(path.sep);\n let defaults: Record<string, any> = { ...entityConfig.defaults };\n \n // Walk up the directory tree building defaults\n let currentPath = '';\n for (const part of parts) {\n currentPath = path.join(currentPath, part);\n const folderConfig = await this.loadFolderConfig(currentPath);\n \n if (folderConfig?.defaults) {\n defaults = { ...defaults, ...folderConfig.defaults };\n }\n }\n \n // Process all default values (lookups, file references, etc.)\n const processedDefaults: Record<string, any> = {};\n const baseDir = path.dirname(filePath);\n \n for (const [field, value] of Object.entries(defaults)) {\n try {\n processedDefaults[field] = await this.processFieldValue(value, baseDir, null, null);\n } catch (error) {\n throw new Error(`Failed to process default for field '${field}': ${error}`);\n }\n }\n \n return processedDefaults;\n }\n \n /**\n * Load folder configuration from .mj-folder.json file\n * \n * @param dir - Directory to check for configuration\n * @returns Folder configuration or null if not found/invalid\n * @private\n */\n private async loadFolderConfig(dir: string): Promise<FolderConfig | null> {\n const configPath = path.join(dir, '.mj-folder.json');\n \n if (await fs.pathExists(configPath)) {\n try {\n return await fs.readJson(configPath);\n } catch (error) {\n console.error(`Error loading folder config at ${configPath}:`, error);\n return null;\n }\n }\n \n return null;\n }\n \n /**\n * Calculate SHA256 checksum for data\n * \n * Generates a deterministic hash of the provided data by converting it to\n * formatted JSON and calculating a SHA256 digest. Used for change detection\n * in sync operations.\n * \n * @param data - Any data structure to calculate checksum for\n * @returns Hexadecimal string representation of the SHA256 hash\n * \n * @example\n * ```typescript\n * const checksum = syncEngine.calculateChecksum({\n * name: 'Test Record',\n * value: 42,\n * tags: ['a', 'b']\n * });\n * // Returns consistent hash for same data structure\n * ```\n */\n calculateChecksum(data: any): string {\n const hash = crypto.createHash('sha256');\n hash.update(JSON.stringify(data, null, 2));\n return hash.digest('hex');\n }\n \n /**\n * Get entity metadata information by name\n * \n * Retrieves the EntityInfo object containing schema metadata for the specified entity.\n * Returns null if the entity is not found in the metadata cache.\n * \n * @param entityName - Name of the entity to look up\n * @returns EntityInfo object with schema details or null if not found\n * \n * @example\n * ```typescript\n * const entityInfo = syncEngine.getEntityInfo('AI Prompts');\n * if (entityInfo) {\n * console.log(`Primary keys: ${entityInfo.PrimaryKeys.map(pk => pk.Name).join(', ')}`);\n * }\n * ```\n */\n getEntityInfo(entityName: string): EntityInfo | null {\n return this.metadata.EntityByName(entityName);\n }\n \n /**\n * Create a new entity object instance\n * \n * Uses the MemberJunction metadata system to properly instantiate an entity object.\n * This ensures correct class registration and respects any custom entity subclasses.\n * \n * @param entityName - Name of the entity to create\n * @returns Promise resolving to the new BaseEntity instance\n * @throws Error if entity creation fails\n * \n * @example\n * ```typescript\n * const entity = await syncEngine.createEntityObject('AI Prompts');\n * entity.NewRecord();\n * entity.Set('Name', 'My Prompt');\n * await entity.Save();\n * ```\n */\n async createEntityObject(entityName: string): Promise<BaseEntity> {\n const entity = await this.metadata.GetEntityObject(entityName, this.contextUser);\n if (!entity) {\n throw new Error(`Failed to create entity object for: ${entityName}`);\n }\n return entity;\n }\n \n /**\n * Load an entity record by primary key\n * \n * Retrieves an existing entity record from the database using its primary key values.\n * Supports both single and composite primary keys. Returns null if the record is not found.\n * \n * @param entityName - Name of the entity to load\n * @param primaryKey - Object containing primary key field names and values\n * @returns Promise resolving to the loaded entity or null if not found\n * @throws Error if entity metadata is not found\n * \n * @example\n * ```typescript\n * // Single primary key\n * const entity = await syncEngine.loadEntity('Users', { ID: '123-456' });\n * \n * // Composite primary key\n * const entity = await syncEngine.loadEntity('UserRoles', { \n * UserID: '123-456',\n * RoleID: '789-012'\n * });\n * ```\n */\n async loadEntity(entityName: string, primaryKey: Record<string, any>): Promise<BaseEntity | null> {\n const entityInfo = this.getEntityInfo(entityName);\n \n if (!entityInfo) {\n throw new Error(`Entity not found: ${entityName}`);\n }\n \n // First, check if the record exists using RunView to avoid \"Error in BaseEntity.Load\" messages\n // when records don't exist (which is a normal scenario during sync operations)\n const rv = new RunView();\n \n // Build filter for primary key(s)\n const filters: string[] = [];\n for (const pk of entityInfo.PrimaryKeys) {\n const value = primaryKey[pk.Name];\n if (value === undefined || value === null) {\n throw new Error(`Missing primary key value for ${pk.Name} in entity ${entityName}`);\n }\n \n // Check if field needs quotes\n const field = entityInfo.Fields.find(f => f.Name === pk.Name);\n const quotes = field?.NeedsQuotes ? \"'\" : '';\n const escapedValue = field?.NeedsQuotes && typeof value === 'string' ? value.replace(/'/g, \"''\") : value;\n filters.push(`${pk.Name} = ${quotes}${escapedValue}${quotes}`);\n }\n \n const result = await rv.RunView({\n EntityName: entityName,\n ExtraFilter: filters.join(' AND '),\n MaxRows: 1\n }, this.contextUser);\n \n // If no record found, return null without attempting to load\n if (!result.Success || result.Results.length === 0) {\n return null;\n }\n \n // Record exists, now load it properly through the entity\n const entity = await this.createEntityObject(entityName);\n const compositeKey = new CompositeKey();\n compositeKey.LoadFromSimpleObject(primaryKey);\n const loaded = await entity.InnerLoad(compositeKey);\n \n return loaded ? entity : null;\n }\n \n /**\n * Process JSON object with template references\n * \n * Recursively processes JSON data structures to resolve `@template` references.\n * Templates can be defined at any level and support:\n * - Single template references: `\"@template:path/to/template.json\"`\n * - Object with @template field: `{ \"@template\": \"file.json\", \"override\": \"value\" }`\n * - Array of templates for merging: `{ \"@template\": [\"base.json\", \"overrides.json\"] }`\n * - Nested template references within templates\n * \n * @param data - JSON data structure to process\n * @param baseDir - Base directory for resolving relative template paths\n * @returns Promise resolving to the processed data with all templates resolved\n * @throws Error if template file is not found or contains invalid JSON\n * \n * @example\n * ```typescript\n * // Input data with template reference\n * const data = {\n * \"@template\": \"defaults/ai-prompt.json\",\n * \"Name\": \"Custom Prompt\",\n * \"Prompt\": \"Override the template prompt\"\n * };\n * \n * // Resolves template and merges with overrides\n * const result = await syncEngine.processTemplates(data, '/path/to/dir');\n * ```\n */\n async processTemplates(data: any, baseDir: string): Promise<any> {\n // Handle arrays\n if (Array.isArray(data)) {\n const processedArray = [];\n for (const item of data) {\n processedArray.push(await this.processTemplates(item, baseDir));\n }\n return processedArray;\n }\n \n // Handle objects\n if (data && typeof data === 'object') {\n // Check for @template reference\n if (typeof data === 'string' && data.startsWith('@template:')) {\n const templatePath = data.substring(10);\n return await this.loadAndProcessTemplate(templatePath, baseDir);\n }\n \n // Process object with possible @template field\n const processed: any = {};\n let templateData: any = {};\n \n // First, check if there's a @template field to process\n if (data['@template']) {\n const templates = Array.isArray(data['@template']) ? data['@template'] : [data['@template']];\n \n // Process templates in order, merging them\n for (const templateRef of templates) {\n const templateContent = await this.loadAndProcessTemplate(templateRef, baseDir);\n templateData = this.deepMerge(templateData, templateContent);\n }\n }\n \n // Process all other fields\n for (const [key, value] of Object.entries(data)) {\n if (key === '@template') continue; // Skip the template field itself\n \n // Process the value recursively\n processed[key] = await this.processTemplates(value, baseDir);\n }\n \n // Merge template data with processed data (processed data takes precedence)\n return this.deepMerge(templateData, processed);\n }\n \n // Return primitive values as-is\n return data;\n }\n \n /**\n * Load and process a template file\n * \n * Loads a JSON template file from the filesystem and recursively processes any\n * nested template references within it. Template paths are resolved relative to\n * the template file's directory, enabling template composition.\n * \n * @param templatePath - Path to the template file (relative or absolute)\n * @param baseDir - Base directory for resolving relative paths\n * @returns Promise resolving to the processed template content\n * @throws Error if template file not found or contains invalid JSON\n * @private\n */\n private async loadAndProcessTemplate(templatePath: string, baseDir: string): Promise<any> {\n const fullPath = path.resolve(baseDir, templatePath);\n \n if (!await fs.pathExists(fullPath)) {\n throw new Error(`Template file not found: ${fullPath}`);\n }\n \n try {\n const templateContent = await fs.readJson(fullPath);\n \n // Recursively process any nested templates\n const templateDir = path.dirname(fullPath);\n return await this.processTemplates(templateContent, templateDir);\n } catch (error) {\n throw new Error(`Failed to load template ${fullPath}: ${error}`);\n }\n }\n \n /**\n * Deep merge two objects with target taking precedence\n * \n * Recursively merges two objects, with values from the target object overriding\n * values from the source object. Arrays and primitive values are not merged but\n * replaced entirely by the target value. Undefined values in target are skipped.\n * \n * @param source - Base object to merge from\n * @param target - Object with values that override source\n * @returns New object with merged values\n * @private\n * \n * @example\n * ```typescript\n * const source = {\n * a: 1,\n * b: { x: 10, y: 20 },\n * c: [1, 2, 3]\n * };\n * const target = {\n * a: 2,\n * b: { y: 30, z: 40 },\n * d: 'new'\n * };\n * const result = deepMerge(source, target);\n * // Result: { a: 2, b: { x: 10, y: 30, z: 40 }, c: [1, 2, 3], d: 'new' }\n * ```\n */\n private deepMerge(source: any, target: any): any {\n if (!source) return target;\n if (!target) return source;\n \n // If target is not an object, it completely overrides source\n if (typeof target !== 'object' || target === null || Array.isArray(target)) {\n return target;\n }\n \n // If source is not an object, target wins\n if (typeof source !== 'object' || source === null || Array.isArray(source)) {\n return target;\n }\n \n // Both are objects, merge them\n const result: any = { ...source };\n \n for (const [key, value] of Object.entries(target)) {\n if (value === undefined) {\n continue; // Skip undefined values\n }\n \n if (typeof value === 'object' && value !== null && !Array.isArray(value) &&\n typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key])) {\n // Both are objects, merge recursively\n result[key] = this.deepMerge(result[key], value);\n } else {\n // Otherwise, target value wins\n result[key] = value;\n }\n }\n \n return result;\n }\n \n}"]}
@@ -19,6 +19,7 @@ export declare class FormattingService {
19
19
  skipped: number;
20
20
  errors: number;
21
21
  duration: number;
22
+ unchanged?: number;
22
23
  }): string;
23
24
  private formatHeader;
24
25
  private formatSectionHeader;
@@ -108,11 +108,14 @@ class FormattingService {
108
108
  const lines = [];
109
109
  lines.push(this.formatHeader(`${operation.charAt(0).toUpperCase() + operation.slice(1)} Summary`));
110
110
  lines.push('');
111
- const total = stats.created + stats.updated + stats.deleted + stats.skipped;
111
+ const total = stats.created + stats.updated + stats.deleted + stats.skipped + (stats.unchanged || 0);
112
112
  lines.push(chalk_1.default.bold('Operation Statistics:'));
113
113
  lines.push('');
114
114
  lines.push(` ${chalk_1.default.green(this.symbols.success)} Created: ${chalk_1.default.green(stats.created)}`);
115
115
  lines.push(` ${chalk_1.default.blue(this.symbols.info)} Updated: ${chalk_1.default.blue(stats.updated)}`);
116
+ if (stats.unchanged !== undefined) {
117
+ lines.push(` ${chalk_1.default.gray('-')} Unchanged: ${chalk_1.default.gray(stats.unchanged)}`);
118
+ }
116
119
  lines.push(` ${chalk_1.default.red(this.symbols.error)} Deleted: ${chalk_1.default.red(stats.deleted)}`);
117
120
  lines.push(` ${chalk_1.default.gray('-')} Skipped: ${chalk_1.default.gray(stats.skipped)}`);
118
121
  lines.push('');