@memberjunction/metadata-sync 2.67.0 → 2.69.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.
Files changed (34) hide show
  1. package/README.md +57 -0
  2. package/dist/config.d.ts +4 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +5 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/EntityPropertyExtractor.d.ts +60 -0
  8. package/dist/lib/EntityPropertyExtractor.js +166 -0
  9. package/dist/lib/EntityPropertyExtractor.js.map +1 -0
  10. package/dist/lib/FieldExternalizer.d.ts +62 -0
  11. package/dist/lib/FieldExternalizer.js +177 -0
  12. package/dist/lib/FieldExternalizer.js.map +1 -0
  13. package/dist/lib/RecordProcessor.d.ts +82 -0
  14. package/dist/lib/RecordProcessor.js +309 -0
  15. package/dist/lib/RecordProcessor.js.map +1 -0
  16. package/dist/lib/RelatedEntityHandler.d.ts +75 -0
  17. package/dist/lib/RelatedEntityHandler.js +273 -0
  18. package/dist/lib/RelatedEntityHandler.js.map +1 -0
  19. package/dist/lib/file-write-batch.d.ts +61 -0
  20. package/dist/lib/file-write-batch.js +180 -0
  21. package/dist/lib/file-write-batch.js.map +1 -0
  22. package/dist/lib/json-write-helper.d.ts +39 -0
  23. package/dist/lib/json-write-helper.js +105 -0
  24. package/dist/lib/json-write-helper.js.map +1 -0
  25. package/dist/services/FileResetService.js +2 -1
  26. package/dist/services/FileResetService.js.map +1 -1
  27. package/dist/services/PullService.d.ts +22 -2
  28. package/dist/services/PullService.js +268 -173
  29. package/dist/services/PullService.js.map +1 -1
  30. package/dist/services/PushService.js +3 -2
  31. package/dist/services/PushService.js.map +1 -1
  32. package/dist/services/WatchService.js +3 -2
  33. package/dist/services/WatchService.js.map +1 -1
  34. package/package.json +7 -7
package/README.md CHANGED
@@ -1549,6 +1549,8 @@ The pull command now supports smart update capabilities with extensive configura
1549
1549
  | `excludeFields` | string[] | [] | Fields to completely omit from pulled data (see detailed explanation below) |
1550
1550
  | `lookupFields` | object | - | Foreign keys to convert to @lookup references |
1551
1551
  | `relatedEntities` | object | - | Related entities to pull as embedded collections |
1552
+ | `ignoreNullFields` | boolean | false | Exclude fields with null values from pulled data |
1553
+ | `ignoreVirtualFields` | boolean | false | Exclude virtual fields (view-only fields) from pulled data |
1552
1554
 
1553
1555
  > **⚠️ Important Configuration Warning**
1554
1556
  >
@@ -1649,6 +1651,61 @@ Example configuration:
1649
1651
  }
1650
1652
  ```
1651
1653
 
1654
+ #### Virtual Fields Configuration
1655
+
1656
+ The `ignoreVirtualFields` option controls whether virtual fields are included in pulled data:
1657
+
1658
+ ```json
1659
+ "pull": {
1660
+ "ignoreVirtualFields": true // Exclude virtual fields from pulled data
1661
+ }
1662
+ ```
1663
+
1664
+ **What are Virtual Fields?**
1665
+ Virtual fields are computed fields that exist only in database views, not in the underlying tables. They typically contain:
1666
+ - Foreign key display names (e.g., `"User": "John Smith"` alongside `"UserID": "123"`)
1667
+ - Computed/calculated values
1668
+ - Aggregate data from related tables
1669
+ - Derived fields from database functions
1670
+
1671
+ **When to use `ignoreVirtualFields: true`:**
1672
+ - **Cleaner JSON files**: Remove read-only display fields that don't need version control
1673
+ - **Reducing file size**: Eliminate redundant data that's computed from other fields
1674
+ - **Preventing confusion**: Avoid fields that can't be modified during push operations
1675
+ - **Database-focused workflow**: When you only want to manage actual table columns
1676
+
1677
+ **When to use `ignoreVirtualFields: false` (default):**
1678
+ - **Complete data capture**: Include all available information for reference
1679
+ - **Display purposes**: Keep human-readable field values for easy review
1680
+ - **Documentation**: Maintain context about related entity names and computed values
1681
+
1682
+ **Example difference:**
1683
+
1684
+ With `ignoreVirtualFields: false`:
1685
+ ```json
1686
+ {
1687
+ "fields": {
1688
+ "Name": "Test Action",
1689
+ "CategoryID": "@lookup:Action Categories.Name=System",
1690
+ "Category": "System", // ← Virtual field (display name)
1691
+ "UserID": "123",
1692
+ "User": "John Smith" // ← Virtual field (display name)
1693
+ }
1694
+ }
1695
+ ```
1696
+
1697
+ With `ignoreVirtualFields: true`:
1698
+ ```json
1699
+ {
1700
+ "fields": {
1701
+ "Name": "Test Action",
1702
+ "CategoryID": "@lookup:Action Categories.Name=System",
1703
+ "UserID": "123"
1704
+ // Virtual fields excluded
1705
+ }
1706
+ }
1707
+ ```
1708
+
1652
1709
  #### Externalize Fields Patterns
1653
1710
 
1654
1711
  The `externalizeFields` configuration supports dynamic file naming with placeholders:
package/dist/config.d.ts CHANGED
@@ -237,6 +237,10 @@ export interface EntityConfig {
237
237
  field: string;
238
238
  };
239
239
  };
240
+ /** Whether to ignore null field values during pull (defaults to false) */
241
+ ignoreNullFields?: boolean;
242
+ /** Whether to ignore virtual fields during pull (defaults to false) */
243
+ ignoreVirtualFields?: boolean;
240
244
  };
241
245
  }
242
246
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;;;;AAGH,gDAAwB;AACxB,wDAA0B;AAC1B,yDAAqD;AAuPrD;;;;;;;;;;;;;;;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 /** \n * Directories to ignore during processing\n * Can be directory names or glob patterns relative to the location of the .mj-sync.json file\n * Cumulative: subdirectories inherit and add to parent ignoreDirectories\n * Examples: [\"output\", \"examples\", \"temp\"]\n */\n ignoreDirectories?: 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 * Array of patterns to filter SQL statements.\n * Supports both regex strings and simple wildcard patterns:\n * - Regex: \"/spCreate.*Run/i\" (must start with \"/\" and optionally end with flags)\n * - Simple: \"*spCreateAIPromptRun*\" (uses * as wildcard, case-insensitive by default)\n * Examples: [\"*AIPrompt*\", \"/^EXEC sp_/i\", \"*EntityFieldValue*\"]\n */\n filterPatterns?: string[];\n /**\n * Determines how filterPatterns are applied:\n * - 'exclude': If ANY pattern matches, the SQL is NOT logged (default)\n * - 'include': If ANY pattern matches, the SQL IS logged\n */\n filterType?: 'exclude' | 'include';\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 /** User role validation configuration */\n userRoleValidation?: {\n /** Whether to enable user role validation for UserID fields */\n enabled?: boolean;\n /** List of role names that are allowed to be referenced in metadata */\n allowedRoles?: string[];\n /** Whether to allow users without any roles (defaults to false) */\n allowUsersWithoutRoles?: boolean;\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 /** \n * Directories to ignore during processing\n * Can be directory names or glob patterns relative to the location of the .mj-sync.json file\n * Cumulative: subdirectories inherit and add to parent ignoreDirectories\n * Examples: [\"output\", \"examples\", \"temp\"]\n */\n ignoreDirectories?: string[];\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;AA2PrD;;;;;;;;;;;;;;;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 /** \n * Directories to ignore during processing\n * Can be directory names or glob patterns relative to the location of the .mj-sync.json file\n * Cumulative: subdirectories inherit and add to parent ignoreDirectories\n * Examples: [\"output\", \"examples\", \"temp\"]\n */\n ignoreDirectories?: 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 * Array of patterns to filter SQL statements.\n * Supports both regex strings and simple wildcard patterns:\n * - Regex: \"/spCreate.*Run/i\" (must start with \"/\" and optionally end with flags)\n * - Simple: \"*spCreateAIPromptRun*\" (uses * as wildcard, case-insensitive by default)\n * Examples: [\"*AIPrompt*\", \"/^EXEC sp_/i\", \"*EntityFieldValue*\"]\n */\n filterPatterns?: string[];\n /**\n * Determines how filterPatterns are applied:\n * - 'exclude': If ANY pattern matches, the SQL is NOT logged (default)\n * - 'include': If ANY pattern matches, the SQL IS logged\n */\n filterType?: 'exclude' | 'include';\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 /** User role validation configuration */\n userRoleValidation?: {\n /** Whether to enable user role validation for UserID fields */\n enabled?: boolean;\n /** List of role names that are allowed to be referenced in metadata */\n allowedRoles?: string[];\n /** Whether to allow users without any roles (defaults to false) */\n allowUsersWithoutRoles?: boolean;\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 /** \n * Directories to ignore during processing\n * Can be directory names or glob patterns relative to the location of the .mj-sync.json file\n * Cumulative: subdirectories inherit and add to parent ignoreDirectories\n * Examples: [\"output\", \"examples\", \"temp\"]\n */\n ignoreDirectories?: string[];\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 /** Whether to ignore null field values during pull (defaults to false) */\n ignoreNullFields?: boolean;\n /** Whether to ignore virtual fields during pull (defaults to false) */\n ignoreVirtualFields?: boolean;\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
@@ -5,6 +5,8 @@ export { ConfigManager, configManager } from './lib/config-manager';
5
5
  export { getSyncEngine, resetSyncEngine } from './lib/singleton-manager';
6
6
  export { SQLLogger } from './lib/sql-logger';
7
7
  export { TransactionManager } from './lib/transaction-manager';
8
+ export { JsonWriteHelper } from './lib/json-write-helper';
9
+ export { FileWriteBatch } from './lib/file-write-batch';
8
10
  export { InitService } from './services/InitService';
9
11
  export type { InitOptions, InitCallbacks } from './services/InitService';
10
12
  export { PullService } from './services/PullService';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getDataProvider = exports.findEntityDirectories = exports.getSystemUser = exports.initializeProvider = exports.loadFolderConfig = exports.loadEntityConfig = exports.loadSyncConfig = exports.loadMJConfig = exports.FormattingService = exports.ValidationService = exports.WatchService = exports.FileResetService = exports.StatusService = exports.PushService = exports.PullService = exports.InitService = exports.TransactionManager = exports.SQLLogger = exports.resetSyncEngine = exports.getSyncEngine = exports.configManager = exports.ConfigManager = exports.SyncEngine = exports.FileBackupManager = void 0;
3
+ exports.getDataProvider = exports.findEntityDirectories = exports.getSystemUser = exports.initializeProvider = exports.loadFolderConfig = exports.loadEntityConfig = exports.loadSyncConfig = exports.loadMJConfig = exports.FormattingService = exports.ValidationService = exports.WatchService = exports.FileResetService = exports.StatusService = exports.PushService = exports.PullService = exports.InitService = exports.FileWriteBatch = exports.JsonWriteHelper = exports.TransactionManager = exports.SQLLogger = exports.resetSyncEngine = exports.getSyncEngine = exports.configManager = exports.ConfigManager = exports.SyncEngine = exports.FileBackupManager = void 0;
4
4
  const core_entities_server_1 = require("@memberjunction/core-entities-server");
5
5
  // Core library exports
6
6
  var file_backup_manager_1 = require("./lib/file-backup-manager");
@@ -17,6 +17,10 @@ var sql_logger_1 = require("./lib/sql-logger");
17
17
  Object.defineProperty(exports, "SQLLogger", { enumerable: true, get: function () { return sql_logger_1.SQLLogger; } });
18
18
  var transaction_manager_1 = require("./lib/transaction-manager");
19
19
  Object.defineProperty(exports, "TransactionManager", { enumerable: true, get: function () { return transaction_manager_1.TransactionManager; } });
20
+ var json_write_helper_1 = require("./lib/json-write-helper");
21
+ Object.defineProperty(exports, "JsonWriteHelper", { enumerable: true, get: function () { return json_write_helper_1.JsonWriteHelper; } });
22
+ var file_write_batch_1 = require("./lib/file-write-batch");
23
+ Object.defineProperty(exports, "FileWriteBatch", { enumerable: true, get: function () { return file_write_batch_1.FileWriteBatch; } });
20
24
  // Service exports
21
25
  var InitService_1 = require("./services/InitService");
22
26
  Object.defineProperty(exports, "InitService", { enumerable: true, get: function () { return InitService_1.InitService; } });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,+EAAgG;AAEhG,uBAAuB;AACvB,iEAA8D;AAArD,wHAAA,iBAAiB,OAAA;AAC1B,iDAA+C;AAAtC,yGAAA,UAAU,OAAA;AAEnB,uDAAoE;AAA3D,+GAAA,aAAa,OAAA;AAAE,+GAAA,aAAa,OAAA;AACrC,6DAAyE;AAAhE,kHAAA,aAAa,OAAA;AAAE,oHAAA,eAAe,OAAA;AACvC,+CAA6C;AAApC,uGAAA,SAAS,OAAA;AAClB,iEAA+D;AAAtD,yHAAA,kBAAkB,OAAA;AAE3B,kBAAkB;AAClB,sDAAqD;AAA5C,0GAAA,WAAW,OAAA;AAGpB,sDAAqD;AAA5C,0GAAA,WAAW,OAAA;AAGpB,sDAAqD;AAA5C,0GAAA,WAAW,OAAA;AAGpB,0DAAyD;AAAhD,8GAAA,aAAa,OAAA;AAGtB,gEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AAGzB,wDAAuD;AAA9C,4GAAA,YAAY,OAAA;AAGrB,kEAAiE;AAAxD,sHAAA,iBAAiB,OAAA;AAC1B,kEAAiE;AAAxD,sHAAA,iBAAiB,OAAA;AAE1B,sBAAsB;AACtB,mCAQkB;AAPhB,sGAAA,YAAY,OAAA;AACZ,wGAAA,cAAc,OAAA;AACd,0GAAA,gBAAgB,OAAA;AAChB,0GAAA,gBAAgB,OAAA;AAMlB,qBAAqB;AACrB,uDAK8B;AAJ5B,oHAAA,kBAAkB,OAAA;AAClB,+GAAA,aAAa,OAAA;AACb,uHAAA,qBAAqB,OAAA;AACrB,iHAAA,eAAe,OAAA;AAejB,IAAA,+DAAwC,GAAE,CAAC","sourcesContent":["import { LoadAIPromptEntityExtendedServerSubClass } from '@memberjunction/core-entities-server';\n\n// Core library exports\nexport { FileBackupManager } from './lib/file-backup-manager';\nexport { SyncEngine } from './lib/sync-engine';\nexport type { RecordData } from './lib/sync-engine';\nexport { ConfigManager, configManager } from './lib/config-manager';\nexport { getSyncEngine, resetSyncEngine } from './lib/singleton-manager';\nexport { SQLLogger } from './lib/sql-logger';\nexport { TransactionManager } from './lib/transaction-manager';\n\n// Service exports\nexport { InitService } from './services/InitService';\nexport type { InitOptions, InitCallbacks } from './services/InitService';\n\nexport { PullService } from './services/PullService';\nexport type { PullOptions, PullCallbacks, PullResult } from './services/PullService';\n\nexport { PushService } from './services/PushService';\nexport type { PushOptions, PushCallbacks, PushResult } from './services/PushService';\n\nexport { StatusService } from './services/StatusService';\nexport type { StatusOptions, StatusCallbacks, StatusResult } from './services/StatusService';\n\nexport { FileResetService } from './services/FileResetService';\nexport type { FileResetOptions, FileResetCallbacks, FileResetResult } from './services/FileResetService';\n\nexport { WatchService } from './services/WatchService';\nexport type { WatchOptions, WatchCallbacks, WatchResult } from './services/WatchService';\n\nexport { ValidationService } from './services/ValidationService';\nexport { FormattingService } from './services/FormattingService';\n\n// Configuration types\nexport {\n loadMJConfig,\n loadSyncConfig,\n loadEntityConfig,\n loadFolderConfig,\n type EntityConfig,\n type FolderConfig,\n type RelatedEntityConfig\n} from './config';\n\n// Provider utilities\nexport {\n initializeProvider,\n getSystemUser,\n findEntityDirectories,\n getDataProvider\n} from './lib/provider-utils';\n\n// Validation types\nexport type {\n ValidationResult,\n ValidationError,\n ValidationWarning,\n EntityDependency,\n FileValidationResult,\n ValidationOptions,\n ReferenceType,\n ParsedReference\n} from './types/validation';\n\nLoadAIPromptEntityExtendedServerSubClass();"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,+EAAgG;AAEhG,uBAAuB;AACvB,iEAA8D;AAArD,wHAAA,iBAAiB,OAAA;AAC1B,iDAA+C;AAAtC,yGAAA,UAAU,OAAA;AAEnB,uDAAoE;AAA3D,+GAAA,aAAa,OAAA;AAAE,+GAAA,aAAa,OAAA;AACrC,6DAAyE;AAAhE,kHAAA,aAAa,OAAA;AAAE,oHAAA,eAAe,OAAA;AACvC,+CAA6C;AAApC,uGAAA,SAAS,OAAA;AAClB,iEAA+D;AAAtD,yHAAA,kBAAkB,OAAA;AAC3B,6DAA0D;AAAjD,oHAAA,eAAe,OAAA;AACxB,2DAAwD;AAA/C,kHAAA,cAAc,OAAA;AAEvB,kBAAkB;AAClB,sDAAqD;AAA5C,0GAAA,WAAW,OAAA;AAGpB,sDAAqD;AAA5C,0GAAA,WAAW,OAAA;AAGpB,sDAAqD;AAA5C,0GAAA,WAAW,OAAA;AAGpB,0DAAyD;AAAhD,8GAAA,aAAa,OAAA;AAGtB,gEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AAGzB,wDAAuD;AAA9C,4GAAA,YAAY,OAAA;AAGrB,kEAAiE;AAAxD,sHAAA,iBAAiB,OAAA;AAC1B,kEAAiE;AAAxD,sHAAA,iBAAiB,OAAA;AAE1B,sBAAsB;AACtB,mCAQkB;AAPhB,sGAAA,YAAY,OAAA;AACZ,wGAAA,cAAc,OAAA;AACd,0GAAA,gBAAgB,OAAA;AAChB,0GAAA,gBAAgB,OAAA;AAMlB,qBAAqB;AACrB,uDAK8B;AAJ5B,oHAAA,kBAAkB,OAAA;AAClB,+GAAA,aAAa,OAAA;AACb,uHAAA,qBAAqB,OAAA;AACrB,iHAAA,eAAe,OAAA;AAejB,IAAA,+DAAwC,GAAE,CAAC","sourcesContent":["import { LoadAIPromptEntityExtendedServerSubClass } from '@memberjunction/core-entities-server';\n\n// Core library exports\nexport { FileBackupManager } from './lib/file-backup-manager';\nexport { SyncEngine } from './lib/sync-engine';\nexport type { RecordData } from './lib/sync-engine';\nexport { ConfigManager, configManager } from './lib/config-manager';\nexport { getSyncEngine, resetSyncEngine } from './lib/singleton-manager';\nexport { SQLLogger } from './lib/sql-logger';\nexport { TransactionManager } from './lib/transaction-manager';\nexport { JsonWriteHelper } from './lib/json-write-helper';\nexport { FileWriteBatch } from './lib/file-write-batch';\n\n// Service exports\nexport { InitService } from './services/InitService';\nexport type { InitOptions, InitCallbacks } from './services/InitService';\n\nexport { PullService } from './services/PullService';\nexport type { PullOptions, PullCallbacks, PullResult } from './services/PullService';\n\nexport { PushService } from './services/PushService';\nexport type { PushOptions, PushCallbacks, PushResult } from './services/PushService';\n\nexport { StatusService } from './services/StatusService';\nexport type { StatusOptions, StatusCallbacks, StatusResult } from './services/StatusService';\n\nexport { FileResetService } from './services/FileResetService';\nexport type { FileResetOptions, FileResetCallbacks, FileResetResult } from './services/FileResetService';\n\nexport { WatchService } from './services/WatchService';\nexport type { WatchOptions, WatchCallbacks, WatchResult } from './services/WatchService';\n\nexport { ValidationService } from './services/ValidationService';\nexport { FormattingService } from './services/FormattingService';\n\n// Configuration types\nexport {\n loadMJConfig,\n loadSyncConfig,\n loadEntityConfig,\n loadFolderConfig,\n type EntityConfig,\n type FolderConfig,\n type RelatedEntityConfig\n} from './config';\n\n// Provider utilities\nexport {\n initializeProvider,\n getSystemUser,\n findEntityDirectories,\n getDataProvider\n} from './lib/provider-utils';\n\n// Validation types\nexport type {\n ValidationResult,\n ValidationError,\n ValidationWarning,\n EntityDependency,\n FileValidationResult,\n ValidationOptions,\n ReferenceType,\n ParsedReference\n} from './types/validation';\n\nLoadAIPromptEntityExtendedServerSubClass();"]}
@@ -0,0 +1,60 @@
1
+ import { BaseEntity } from '@memberjunction/core';
2
+ /**
3
+ * Handles discovery and extraction of all properties from BaseEntity objects,
4
+ * including both database fields and virtual properties defined in subclasses.
5
+ */
6
+ export declare class EntityPropertyExtractor {
7
+ /**
8
+ * Gets ALL properties from a BaseEntity object, including both:
9
+ * 1. Database fields (from record.GetAll())
10
+ * 2. Virtual properties (getters defined in subclasses like TemplateText)
11
+ * @param record The BaseEntity object to get properties from
12
+ * @param fieldOverrides Optional field value overrides (e.g., for @parent:ID syntax)
13
+ */
14
+ extractAllProperties(record: BaseEntity, fieldOverrides?: Record<string, any>): Record<string, any>;
15
+ /**
16
+ * Extracts database fields from the entity using GetAll()
17
+ */
18
+ private extractDatabaseFields;
19
+ /**
20
+ * Applies field overrides to the properties collection
21
+ */
22
+ private applyFieldOverrides;
23
+ /**
24
+ * Extracts virtual properties by walking the prototype chain
25
+ */
26
+ private extractVirtualProperties;
27
+ /**
28
+ * Discovers virtual properties (getters) defined in BaseEntity subclasses
29
+ * Returns property names that are getters but not in the base database fields
30
+ */
31
+ private discoverVirtualProperties;
32
+ /**
33
+ * Gets the set of database field names from the entity
34
+ */
35
+ private getDatabaseFieldNames;
36
+ /**
37
+ * Extracts properties from a single prototype level
38
+ */
39
+ private extractPropertiesFromPrototype;
40
+ /**
41
+ * Determines if a property should be considered for inclusion
42
+ */
43
+ private shouldIncludeProperty;
44
+ /**
45
+ * Determines if a property descriptor represents a virtual property
46
+ */
47
+ private isVirtualProperty;
48
+ /**
49
+ * Determines if a property should be skipped during virtual property discovery
50
+ */
51
+ private shouldSkipProperty;
52
+ /**
53
+ * Checks if property is a common Object.prototype method
54
+ */
55
+ private isCommonObjectMethod;
56
+ /**
57
+ * Checks if property is a known BaseEntity method or property
58
+ */
59
+ private isBaseEntityMethod;
60
+ }
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EntityPropertyExtractor = void 0;
4
+ /**
5
+ * Handles discovery and extraction of all properties from BaseEntity objects,
6
+ * including both database fields and virtual properties defined in subclasses.
7
+ */
8
+ class EntityPropertyExtractor {
9
+ /**
10
+ * Gets ALL properties from a BaseEntity object, including both:
11
+ * 1. Database fields (from record.GetAll())
12
+ * 2. Virtual properties (getters defined in subclasses like TemplateText)
13
+ * @param record The BaseEntity object to get properties from
14
+ * @param fieldOverrides Optional field value overrides (e.g., for @parent:ID syntax)
15
+ */
16
+ extractAllProperties(record, fieldOverrides) {
17
+ const allProperties = {};
18
+ // 1. Get database fields using GetAll()
19
+ this.extractDatabaseFields(record, allProperties);
20
+ // 2. Apply field overrides (e.g., for @parent:ID replacement in related entities)
21
+ this.applyFieldOverrides(allProperties, fieldOverrides);
22
+ // 3. Extract virtual properties by walking the prototype chain
23
+ this.extractVirtualProperties(record, allProperties, fieldOverrides);
24
+ return allProperties;
25
+ }
26
+ /**
27
+ * Extracts database fields from the entity using GetAll()
28
+ */
29
+ extractDatabaseFields(record, allProperties) {
30
+ if (typeof record.GetAll === 'function') {
31
+ const dbFields = record.GetAll();
32
+ Object.assign(allProperties, dbFields);
33
+ }
34
+ }
35
+ /**
36
+ * Applies field overrides to the properties collection
37
+ */
38
+ applyFieldOverrides(allProperties, fieldOverrides) {
39
+ if (fieldOverrides) {
40
+ Object.assign(allProperties, fieldOverrides);
41
+ }
42
+ }
43
+ /**
44
+ * Extracts virtual properties by walking the prototype chain
45
+ */
46
+ extractVirtualProperties(record, allProperties, fieldOverrides) {
47
+ const virtualProperties = this.discoverVirtualProperties(record);
48
+ for (const propertyName of virtualProperties) {
49
+ try {
50
+ // Skip if this property is overridden
51
+ if (fieldOverrides && propertyName in fieldOverrides) {
52
+ continue;
53
+ }
54
+ // Use bracket notation to access the getter
55
+ const value = record[propertyName];
56
+ // Only include if the value is not undefined and not a function
57
+ if (value !== undefined && typeof value !== 'function') {
58
+ allProperties[propertyName] = value;
59
+ }
60
+ }
61
+ catch (error) {
62
+ // Skip properties that throw errors when accessed
63
+ continue;
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Discovers virtual properties (getters) defined in BaseEntity subclasses
69
+ * Returns property names that are getters but not in the base database fields
70
+ */
71
+ discoverVirtualProperties(record) {
72
+ const virtualProperties = [];
73
+ const dbFieldNames = this.getDatabaseFieldNames(record);
74
+ // Walk the prototype chain to find getters
75
+ let currentPrototype = Object.getPrototypeOf(record);
76
+ while (currentPrototype && currentPrototype !== Object.prototype) {
77
+ this.extractPropertiesFromPrototype(currentPrototype, virtualProperties, dbFieldNames);
78
+ currentPrototype = Object.getPrototypeOf(currentPrototype);
79
+ }
80
+ return virtualProperties;
81
+ }
82
+ /**
83
+ * Gets the set of database field names from the entity
84
+ */
85
+ getDatabaseFieldNames(record) {
86
+ const dbFieldNames = new Set();
87
+ if (typeof record.GetAll === 'function') {
88
+ const dbFields = record.GetAll();
89
+ Object.keys(dbFields).forEach(key => dbFieldNames.add(key));
90
+ }
91
+ return dbFieldNames;
92
+ }
93
+ /**
94
+ * Extracts properties from a single prototype level
95
+ */
96
+ extractPropertiesFromPrototype(prototype, virtualProperties, dbFieldNames) {
97
+ const propertyNames = Object.getOwnPropertyNames(prototype);
98
+ for (const propertyName of propertyNames) {
99
+ if (this.shouldIncludeProperty(propertyName, virtualProperties, dbFieldNames)) {
100
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName);
101
+ if (this.isVirtualProperty(descriptor)) {
102
+ virtualProperties.push(propertyName);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ /**
108
+ * Determines if a property should be considered for inclusion
109
+ */
110
+ shouldIncludeProperty(propertyName, virtualProperties, dbFieldNames) {
111
+ // Skip if already found or is a database field
112
+ if (virtualProperties.includes(propertyName) || dbFieldNames.has(propertyName)) {
113
+ return false;
114
+ }
115
+ // Skip internal properties and methods
116
+ return !this.shouldSkipProperty(propertyName);
117
+ }
118
+ /**
119
+ * Determines if a property descriptor represents a virtual property
120
+ */
121
+ isVirtualProperty(descriptor) {
122
+ if (!descriptor)
123
+ return false;
124
+ // Skip read-only getters (might be computed properties)
125
+ if (typeof descriptor.get === 'function' && !descriptor.set) {
126
+ return false;
127
+ }
128
+ // Include read-write getter/setter pairs (likely virtual properties)
129
+ return typeof descriptor.get === 'function' && typeof descriptor.set === 'function';
130
+ }
131
+ /**
132
+ * Determines if a property should be skipped during virtual property discovery
133
+ */
134
+ shouldSkipProperty(propertyName) {
135
+ // Skip private properties (starting with _ or __)
136
+ if (propertyName.startsWith('_') || propertyName.startsWith('__')) {
137
+ return true;
138
+ }
139
+ // Skip constructor and common Object.prototype methods
140
+ if (this.isCommonObjectMethod(propertyName)) {
141
+ return true;
142
+ }
143
+ // Skip known BaseEntity methods and properties
144
+ return this.isBaseEntityMethod(propertyName);
145
+ }
146
+ /**
147
+ * Checks if property is a common Object.prototype method
148
+ */
149
+ isCommonObjectMethod(propertyName) {
150
+ const commonMethods = ['constructor', 'toString', 'valueOf'];
151
+ return commonMethods.includes(propertyName);
152
+ }
153
+ /**
154
+ * Checks if property is a known BaseEntity method or property
155
+ */
156
+ isBaseEntityMethod(propertyName) {
157
+ const baseEntityMethods = [
158
+ 'Get', 'Set', 'GetAll', 'SetMany', 'LoadFromData', 'Save', 'Load', 'Delete',
159
+ 'Fields', 'Dirty', 'IsSaved', 'PrimaryKeys', 'EntityInfo', 'ContextCurrentUser',
160
+ 'ProviderToUse', 'RecordChanges', 'TransactionGroup'
161
+ ];
162
+ return baseEntityMethods.includes(propertyName);
163
+ }
164
+ }
165
+ exports.EntityPropertyExtractor = EntityPropertyExtractor;
166
+ //# sourceMappingURL=EntityPropertyExtractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityPropertyExtractor.js","sourceRoot":"","sources":["../../src/lib/EntityPropertyExtractor.ts"],"names":[],"mappings":";;;AAEA;;;GAGG;AACH,MAAa,uBAAuB;IAClC;;;;;;OAMG;IACH,oBAAoB,CAAC,MAAkB,EAAE,cAAoC;QAC3E,MAAM,aAAa,GAAwB,EAAE,CAAC;QAE9C,wCAAwC;QACxC,IAAI,CAAC,qBAAqB,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QAElD,kFAAkF;QAClF,IAAI,CAAC,mBAAmB,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;QAExD,+DAA+D;QAC/D,IAAI,CAAC,wBAAwB,CAAC,MAAM,EAAE,aAAa,EAAE,cAAc,CAAC,CAAC;QAErE,OAAO,aAAa,CAAC;IACvB,CAAC;IAED;;OAEG;IACK,qBAAqB,CAAC,MAAkB,EAAE,aAAkC;QAClF,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;YACjC,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED;;OAEG;IACK,mBAAmB,CAAC,aAAkC,EAAE,cAAoC;QAClG,IAAI,cAAc,EAAE,CAAC;YACnB,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED;;OAEG;IACK,wBAAwB,CAC9B,MAAkB,EAClB,aAAkC,EAClC,cAAoC;QAEpC,MAAM,iBAAiB,GAAG,IAAI,CAAC,yBAAyB,CAAC,MAAM,CAAC,CAAC;QAEjE,KAAK,MAAM,YAAY,IAAI,iBAAiB,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACH,sCAAsC;gBACtC,IAAI,cAAc,IAAI,YAAY,IAAI,cAAc,EAAE,CAAC;oBACrD,SAAS;gBACX,CAAC;gBAED,4CAA4C;gBAC5C,MAAM,KAAK,GAAI,MAAc,CAAC,YAAY,CAAC,CAAC;gBAE5C,gEAAgE;gBAChE,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;oBACvD,aAAa,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC;gBACtC,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,kDAAkD;gBAClD,SAAS;YACX,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;OAGG;IACK,yBAAyB,CAAC,MAAkB;QAClD,MAAM,iBAAiB,GAAa,EAAE,CAAC;QACvC,MAAM,YAAY,GAAG,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC;QAExD,2CAA2C;QAC3C,IAAI,gBAAgB,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAErD,OAAO,gBAAgB,IAAI,gBAAgB,KAAK,MAAM,CAAC,SAAS,EAAE,CAAC;YACjE,IAAI,CAAC,8BAA8B,CAAC,gBAAgB,EAAE,iBAAiB,EAAE,YAAY,CAAC,CAAC;YACvF,gBAAgB,GAAG,MAAM,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;QAC7D,CAAC;QAED,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IAED;;OAEG;IACK,qBAAqB,CAAC,MAAkB;QAC9C,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;QAEvC,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9D,CAAC;QAED,OAAO,YAAY,CAAC;IACtB,CAAC;IAED;;OAEG;IACK,8BAA8B,CACpC,SAAc,EACd,iBAA2B,EAC3B,YAAyB;QAEzB,MAAM,aAAa,GAAG,MAAM,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;QAE5D,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE,CAAC;YACzC,IAAI,IAAI,CAAC,qBAAqB,CAAC,YAAY,EAAE,iBAAiB,EAAE,YAAY,CAAC,EAAE,CAAC;gBAC9E,MAAM,UAAU,GAAG,MAAM,CAAC,wBAAwB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;gBAC5E,IAAI,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,EAAE,CAAC;oBACvC,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBACvC,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACK,qBAAqB,CAC3B,YAAoB,EACpB,iBAA2B,EAC3B,YAAyB;QAEzB,+CAA+C;QAC/C,IAAI,iBAAiB,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;YAC/E,OAAO,KAAK,CAAC;QACf,CAAC;QAED,uCAAuC;QACvC,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC;IAChD,CAAC;IAED;;OAEG;IACK,iBAAiB,CAAC,UAA0C;QAClE,IAAI,CAAC,UAAU;YAAE,OAAO,KAAK,CAAC;QAE9B,wDAAwD;QACxD,IAAI,OAAO,UAAU,CAAC,GAAG,KAAK,UAAU,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC;YAC5D,OAAO,KAAK,CAAC;QACf,CAAC;QAED,qEAAqE;QACrE,OAAO,OAAO,UAAU,CAAC,GAAG,KAAK,UAAU,IAAI,OAAO,UAAU,CAAC,GAAG,KAAK,UAAU,CAAC;IACtF,CAAC;IAED;;OAEG;IACK,kBAAkB,CAAC,YAAoB;QAC7C,kDAAkD;QAClD,IAAI,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,uDAAuD;QACvD,IAAI,IAAI,CAAC,oBAAoB,CAAC,YAAY,CAAC,EAAE,CAAC;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC;QAED,+CAA+C;QAC/C,OAAO,IAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC;IAC/C,CAAC;IAED;;OAEG;IACK,oBAAoB,CAAC,YAAoB;QAC/C,MAAM,aAAa,GAAG,CAAC,aAAa,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;QAC7D,OAAO,aAAa,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC9C,CAAC;IAED;;OAEG;IACK,kBAAkB,CAAC,YAAoB;QAC7C,MAAM,iBAAiB,GAAG;YACxB,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ;YAC3E,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,oBAAoB;YAC/E,eAAe,EAAE,eAAe,EAAE,kBAAkB;SACrD,CAAC;QAEF,OAAO,iBAAiB,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAClD,CAAC;CACF;AApMD,0DAoMC","sourcesContent":["import { BaseEntity } from '@memberjunction/core';\n\n/**\n * Handles discovery and extraction of all properties from BaseEntity objects,\n * including both database fields and virtual properties defined in subclasses.\n */\nexport class EntityPropertyExtractor {\n /**\n * Gets ALL properties from a BaseEntity object, including both:\n * 1. Database fields (from record.GetAll())\n * 2. Virtual properties (getters defined in subclasses like TemplateText)\n * @param record The BaseEntity object to get properties from\n * @param fieldOverrides Optional field value overrides (e.g., for @parent:ID syntax)\n */\n extractAllProperties(record: BaseEntity, fieldOverrides?: Record<string, any>): Record<string, any> {\n const allProperties: Record<string, any> = {};\n \n // 1. Get database fields using GetAll()\n this.extractDatabaseFields(record, allProperties);\n \n // 2. Apply field overrides (e.g., for @parent:ID replacement in related entities)\n this.applyFieldOverrides(allProperties, fieldOverrides);\n \n // 3. Extract virtual properties by walking the prototype chain\n this.extractVirtualProperties(record, allProperties, fieldOverrides);\n \n return allProperties;\n }\n\n /**\n * Extracts database fields from the entity using GetAll()\n */\n private extractDatabaseFields(record: BaseEntity, allProperties: Record<string, any>): void {\n if (typeof record.GetAll === 'function') {\n const dbFields = record.GetAll();\n Object.assign(allProperties, dbFields);\n }\n }\n\n /**\n * Applies field overrides to the properties collection\n */\n private applyFieldOverrides(allProperties: Record<string, any>, fieldOverrides?: Record<string, any>): void {\n if (fieldOverrides) {\n Object.assign(allProperties, fieldOverrides);\n }\n }\n\n /**\n * Extracts virtual properties by walking the prototype chain\n */\n private extractVirtualProperties(\n record: BaseEntity, \n allProperties: Record<string, any>, \n fieldOverrides?: Record<string, any>\n ): void {\n const virtualProperties = this.discoverVirtualProperties(record);\n \n for (const propertyName of virtualProperties) {\n try {\n // Skip if this property is overridden\n if (fieldOverrides && propertyName in fieldOverrides) {\n continue;\n }\n \n // Use bracket notation to access the getter\n const value = (record as any)[propertyName];\n \n // Only include if the value is not undefined and not a function\n if (value !== undefined && typeof value !== 'function') {\n allProperties[propertyName] = value;\n }\n } catch (error) {\n // Skip properties that throw errors when accessed\n continue;\n }\n }\n }\n\n /**\n * Discovers virtual properties (getters) defined in BaseEntity subclasses\n * Returns property names that are getters but not in the base database fields\n */\n private discoverVirtualProperties(record: BaseEntity): string[] {\n const virtualProperties: string[] = [];\n const dbFieldNames = this.getDatabaseFieldNames(record);\n \n // Walk the prototype chain to find getters\n let currentPrototype = Object.getPrototypeOf(record);\n \n while (currentPrototype && currentPrototype !== Object.prototype) {\n this.extractPropertiesFromPrototype(currentPrototype, virtualProperties, dbFieldNames);\n currentPrototype = Object.getPrototypeOf(currentPrototype);\n }\n \n return virtualProperties;\n }\n\n /**\n * Gets the set of database field names from the entity\n */\n private getDatabaseFieldNames(record: BaseEntity): Set<string> {\n const dbFieldNames = new Set<string>();\n \n if (typeof record.GetAll === 'function') {\n const dbFields = record.GetAll();\n Object.keys(dbFields).forEach(key => dbFieldNames.add(key));\n }\n \n return dbFieldNames;\n }\n\n /**\n * Extracts properties from a single prototype level\n */\n private extractPropertiesFromPrototype(\n prototype: any, \n virtualProperties: string[], \n dbFieldNames: Set<string>\n ): void {\n const propertyNames = Object.getOwnPropertyNames(prototype);\n \n for (const propertyName of propertyNames) {\n if (this.shouldIncludeProperty(propertyName, virtualProperties, dbFieldNames)) {\n const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName);\n if (this.isVirtualProperty(descriptor)) {\n virtualProperties.push(propertyName);\n }\n }\n }\n }\n\n /**\n * Determines if a property should be considered for inclusion\n */\n private shouldIncludeProperty(\n propertyName: string, \n virtualProperties: string[], \n dbFieldNames: Set<string>\n ): boolean {\n // Skip if already found or is a database field\n if (virtualProperties.includes(propertyName) || dbFieldNames.has(propertyName)) {\n return false;\n }\n \n // Skip internal properties and methods\n return !this.shouldSkipProperty(propertyName);\n }\n\n /**\n * Determines if a property descriptor represents a virtual property\n */\n private isVirtualProperty(descriptor: PropertyDescriptor | undefined): boolean {\n if (!descriptor) return false;\n \n // Skip read-only getters (might be computed properties)\n if (typeof descriptor.get === 'function' && !descriptor.set) {\n return false;\n }\n \n // Include read-write getter/setter pairs (likely virtual properties)\n return typeof descriptor.get === 'function' && typeof descriptor.set === 'function';\n }\n\n /**\n * Determines if a property should be skipped during virtual property discovery\n */\n private shouldSkipProperty(propertyName: string): boolean {\n // Skip private properties (starting with _ or __)\n if (propertyName.startsWith('_') || propertyName.startsWith('__')) {\n return true;\n }\n \n // Skip constructor and common Object.prototype methods\n if (this.isCommonObjectMethod(propertyName)) {\n return true;\n }\n \n // Skip known BaseEntity methods and properties\n return this.isBaseEntityMethod(propertyName);\n }\n\n /**\n * Checks if property is a common Object.prototype method\n */\n private isCommonObjectMethod(propertyName: string): boolean {\n const commonMethods = ['constructor', 'toString', 'valueOf'];\n return commonMethods.includes(propertyName);\n }\n\n /**\n * Checks if property is a known BaseEntity method or property\n */\n private isBaseEntityMethod(propertyName: string): boolean {\n const baseEntityMethods = [\n 'Get', 'Set', 'GetAll', 'SetMany', 'LoadFromData', 'Save', 'Load', 'Delete',\n 'Fields', 'Dirty', 'IsSaved', 'PrimaryKeys', 'EntityInfo', 'ContextCurrentUser',\n 'ProviderToUse', 'RecordChanges', 'TransactionGroup'\n ];\n \n return baseEntityMethods.includes(propertyName);\n }\n}"]}
@@ -0,0 +1,62 @@
1
+ import { BaseEntity } from '@memberjunction/core';
2
+ /**
3
+ * Handles externalization of field values to separate files with @file: references
4
+ */
5
+ export declare class FieldExternalizer {
6
+ /**
7
+ * Externalize a field value to a separate file and return @file: reference
8
+ */
9
+ externalizeField(fieldName: string, fieldValue: any, pattern: string, recordData: BaseEntity, targetDir: string, existingFileReference?: string, mergeStrategy?: string, verbose?: boolean): Promise<string>;
10
+ /**
11
+ * Determines the file path and reference for externalization
12
+ */
13
+ private determineFilePath;
14
+ /**
15
+ * Checks if we should use an existing file reference
16
+ */
17
+ private shouldUseExistingReference;
18
+ /**
19
+ * Uses an existing file reference
20
+ */
21
+ private useExistingFileReference;
22
+ /**
23
+ * Creates a new file reference using the pattern
24
+ */
25
+ private createNewFileReference;
26
+ /**
27
+ * Processes pattern placeholders with actual values
28
+ */
29
+ private processPattern;
30
+ /**
31
+ * Replaces a single placeholder in the pattern
32
+ */
33
+ private replacePlaceholder;
34
+ /**
35
+ * Replaces field placeholders with values from the record
36
+ */
37
+ private replaceFieldPlaceholders;
38
+ /**
39
+ * Removes @file: prefix if present
40
+ */
41
+ private removeFilePrefix;
42
+ /**
43
+ * Determines if the file should be written based on content comparison
44
+ */
45
+ private shouldWriteFile;
46
+ /**
47
+ * Writes the external file with the field content
48
+ */
49
+ private writeExternalFile;
50
+ /**
51
+ * Prepares content for writing, with JSON pretty-printing if applicable
52
+ */
53
+ private prepareContentForWriting;
54
+ /**
55
+ * Determines if content should be pretty-printed as JSON
56
+ */
57
+ private shouldPrettyPrintAsJson;
58
+ /**
59
+ * Sanitize a string for use in filenames
60
+ */
61
+ private sanitizeForFilename;
62
+ }