@memberjunction/metadata-sync 2.46.0 → 2.48.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.
package/README.md CHANGED
@@ -74,18 +74,29 @@ The Metadata Sync tool bridges the gap between database-stored metadata and file
74
74
 
75
75
  ## Supported Entities
76
76
 
77
- ### Phase 1: AI Prompts (Current)
78
- - Full support for all AI Prompt fields
79
- - Markdown files for prompt content
80
- - Category-based organization
81
- - AI Prompt Models as embedded collections
82
-
83
- ### Future Phases
84
- - Templates
85
- - AI Models
86
- - AI Vendors
87
- - Query definitions
88
- - Any MJ entity with metadata
77
+ The tool works with any MemberJunction entity - both core system entities and user-created entities. Each entity type can have its own directory structure, file naming conventions, and related entity configurations.
78
+
79
+ ### Important Limitation: Database-Reflected Metadata
80
+
81
+ **This tool should NOT be used to modify metadata that is reflected from the underlying database catalog.** Examples include:
82
+ - Entity field data types
83
+ - Column lengths/precision
84
+ - Primary key definitions
85
+ - Foreign key relationships
86
+ - Table/column existence
87
+
88
+ These properties are designed to flow **from** the database catalog **up** into MJ metadata, not the other way around. Attempting to modify these via file sync could create inconsistencies between the metadata and actual database schema.
89
+
90
+ The tool is intended for managing business-level metadata such as:
91
+ - Descriptions and documentation
92
+ - Display names and user-facing text
93
+ - Categories and groupings
94
+ - Custom properties and settings
95
+ - AI prompts, templates, and other content
96
+ - Permissions and security settings
97
+ - Any other data that is not reflected **up** from the underlying system database catalogs
98
+
99
+ For more information about how CodeGen reflects system-level data from the database into the MJ metadata layer, see the [CodeGen documentation](../CodeGen/README.md).
89
100
 
90
101
  ## File Structure
91
102
 
@@ -93,10 +104,55 @@ The tool uses a hierarchical directory structure with cascading defaults:
93
104
  - Each top-level directory represents an entity type
94
105
  - `.mj-sync.json` files define entities and base defaults
95
106
  - `.mj-folder.json` files define folder-specific defaults (optional)
96
- - All JSON files within are treated as records of that entity type
107
+ - Only dot-prefixed JSON files (e.g., `.prompt-template.json`, `.category.json`) are treated as metadata records
108
+ - Regular JSON files without the dot prefix are ignored, allowing package.json and other config files to coexist
97
109
  - External files (`.md`, `.html`, etc.) are referenced from the JSON files
98
110
  - Defaults cascade down through the folder hierarchy
99
111
 
112
+ ### File Naming Convention
113
+
114
+ **Metadata files must be prefixed with a dot (.)** to be recognized by the sync tool. This convention:
115
+ - Clearly distinguishes metadata files from regular configuration files
116
+ - Allows `package.json`, `tsconfig.json` and other standard files to coexist without being processed
117
+ - Follows established patterns like `.gitignore` and `.eslintrc.json`
118
+
119
+ Examples:
120
+ - ✅ `.greeting.json` - Will be processed as metadata
121
+ - ✅ `.customer-prompt.json` - Will be processed as metadata
122
+ - ❌ `greeting.json` - Will be ignored
123
+ - ❌ `package.json` - Will be ignored
124
+
125
+ ### File Format Options
126
+
127
+ #### Single Record per File (Default)
128
+ Each JSON file contains one record:
129
+ ```json
130
+ {
131
+ "fields": { ... },
132
+ "relatedEntities": { ... }
133
+ }
134
+ ```
135
+
136
+ #### Multiple Records per File (NEW)
137
+ JSON files can contain arrays of records:
138
+ ```json
139
+ [
140
+ {
141
+ "fields": { ... },
142
+ "relatedEntities": { ... }
143
+ },
144
+ {
145
+ "fields": { ... },
146
+ "relatedEntities": { ... }
147
+ }
148
+ ]
149
+ ```
150
+
151
+ This is useful for:
152
+ - Grouping related records in a single file
153
+ - Reducing file clutter for entities with many small records
154
+ - Maintaining logical groupings while using `@file:` references for large content
155
+
100
156
  ### Example Structure
101
157
  ```
102
158
  metadata/
@@ -105,28 +161,33 @@ metadata/
105
161
  │ ├── .mj-sync.json # Defines entity: "AI Prompts"
106
162
  │ ├── customer-service/
107
163
  │ │ ├── .mj-folder.json # Folder metadata (CategoryID, etc.)
108
- │ │ ├── greeting.json # AI Prompt record with embedded models
164
+ │ │ ├── .greeting.json # AI Prompt record with embedded models
109
165
  │ │ ├── greeting.prompt.md # Prompt content (referenced)
110
166
  │ │ └── greeting.notes.md # Notes field (referenced)
111
167
  │ └── analytics/
112
168
  │ ├── .mj-folder.json # Folder metadata (CategoryID, etc.)
113
- │ ├── daily-report.json # AI Prompt record
169
+ │ ├── .daily-report.json # AI Prompt record
114
170
  │ └── daily-report.prompt.md # Prompt content (referenced)
115
- └── templates/
171
+ ├── templates/ # Reusable JSON templates
172
+ │ ├── standard-prompt-settings.json # Common prompt configurations
173
+ │ ├── standard-ai-models.json # Standard model configurations
174
+ │ ├── high-performance-models.json # High-power model configurations
175
+ │ └── customer-service-defaults.json # CS-specific defaults
176
+ └── template-entities/
116
177
  ├── .mj-sync.json # Defines entity: "Templates"
117
178
  ├── email/
118
179
  │ ├── .mj-folder.json # Folder metadata
119
- │ ├── welcome.json # Template record
180
+ │ ├── .welcome.json # Template record (dot-prefixed)
120
181
  │ └── welcome.template.html # Template content (referenced)
121
182
  └── reports/
122
183
  ├── .mj-folder.json # Folder metadata
123
- ├── invoice.json # Template record
184
+ ├── .invoice.json # Template record (dot-prefixed)
124
185
  └── invoice.template.html # Template content (referenced)
125
186
  ```
126
187
 
127
188
  ## JSON Metadata Format
128
189
 
129
- ### Individual Record (e.g., ai-prompts/customer-service/greeting.json)
190
+ ### Individual Record (e.g., ai-prompts/customer-service/.greeting.json)
130
191
  ```json
131
192
  {
132
193
  "fields": {
@@ -302,6 +363,66 @@ Support environment-specific values:
302
363
  - `@env:VARIABLE_NAME`
303
364
  - Useful for different environments (dev/staging/prod)
304
365
 
366
+ ### @template: References (NEW)
367
+ Enable JSON template composition for reusable configurations:
368
+
369
+ #### String Template Reference
370
+ Use `@template:` to replace any value with template content:
371
+ ```json
372
+ {
373
+ "relatedEntities": {
374
+ "MJ: AI Prompt Models": "@template:templates/standard-ai-models.json"
375
+ }
376
+ }
377
+ ```
378
+
379
+ #### Object Template Merging
380
+ Use `@template` field within objects to merge template content:
381
+ ```json
382
+ {
383
+ "fields": {
384
+ "Name": "My Prompt",
385
+ "@template": "templates/standard-prompt-settings.json",
386
+ "Temperature": 0.9 // Overrides template value
387
+ }
388
+ }
389
+ ```
390
+
391
+ #### Multiple Template Merging
392
+ Merge multiple templates in order (later templates override earlier ones):
393
+ ```json
394
+ {
395
+ "fields": {
396
+ "@template": [
397
+ "templates/base-settings.json",
398
+ "templates/customer-service-defaults.json"
399
+ ],
400
+ "Name": "Customer Bot" // Local fields override all templates
401
+ }
402
+ }
403
+ ```
404
+
405
+ #### Nested Templates
406
+ Templates can reference other templates:
407
+ ```json
408
+ // templates/high-performance-models.json
409
+ [
410
+ {
411
+ "fields": {
412
+ "@template": "../templates/model-defaults.json",
413
+ "ModelID": "@lookup:AI Models.Name=GPT 4o"
414
+ }
415
+ }
416
+ ]
417
+ ```
418
+
419
+ #### Template Benefits
420
+ - **DRY Principle**: Define configurations once, use everywhere
421
+ - **Maintainability**: Update template to affect all uses
422
+ - **Flexibility**: Use at any JSON level
423
+ - **Composability**: Build complex configurations from simple parts
424
+ - **Override Support**: Local values always override template values
425
+
305
426
  ## CLI Commands
306
427
 
307
428
  ```bash
@@ -314,6 +435,10 @@ mj-sync pull --entity="AI Prompts"
314
435
  # Pull specific records by filter
315
436
  mj-sync pull --entity="AI Prompts" --filter="CategoryID='customer-service-id'"
316
437
 
438
+ # Pull multiple records into a single file (NEW)
439
+ mj-sync pull --entity="AI Prompts" --multi-file="all-prompts"
440
+ mj-sync pull --entity="AI Prompts" --filter="Status='Active'" --multi-file="active-prompts.json"
441
+
317
442
  # Push all changes from current directory and subdirectories
318
443
  mj-sync push
319
444
 
@@ -365,7 +490,7 @@ Configuration follows a hierarchical structure:
365
490
  ```json
366
491
  {
367
492
  "entity": "AI Prompts",
368
- "filePattern": "*.json",
493
+ "filePattern": ".*.json",
369
494
  "defaults": {
370
495
  "TypeID": "@lookup:AI Prompt Types.Name=Chat",
371
496
  "Temperature": 0.7,
@@ -373,11 +498,21 @@ Configuration follows a hierarchical structure:
373
498
  "Status": "Active"
374
499
  },
375
500
  "pull": {
501
+ "filePattern": ".*.json",
502
+ "updateExistingRecords": true,
503
+ "createNewFileIfNotFound": true,
504
+ "mergeStrategy": "merge",
376
505
  "filter": "Status = 'Active'",
506
+ "externalizeFields": [
507
+ {
508
+ "field": "Prompt",
509
+ "pattern": "@file:{Name}.prompt.md"
510
+ }
511
+ ],
377
512
  "relatedEntities": {
378
513
  "MJ: AI Prompt Models": {
379
514
  "entity": "MJ: AI Prompt Models",
380
- "foreignKey": "ID",
515
+ "foreignKey": "PromptID",
381
516
  "filter": "Status = 'Active'"
382
517
  }
383
518
  }
@@ -418,26 +553,204 @@ The tool now supports managing related entities as embedded collections within p
418
553
  - **Relationship Clarity**: Visual representation of data relationships
419
554
 
420
555
  ### Configuration for Pull
421
- Configure which related entities to pull in `.mj-sync.json`:
556
+
557
+ The pull command now supports smart update capabilities with extensive configuration options:
558
+
422
559
  ```json
423
560
  {
424
561
  "entity": "AI Prompts",
562
+ "filePattern": ".*.json",
425
563
  "pull": {
564
+ "filePattern": ".*.json",
565
+ "createNewFileIfNotFound": true,
566
+ "newFileName": ".all-new.json",
567
+ "appendRecordsToExistingFile": true,
568
+ "updateExistingRecords": true,
569
+ "preserveFields": ["customField", "localNotes"],
570
+ "mergeStrategy": "merge",
571
+ "backupBeforeUpdate": true,
572
+ "filter": "Status = 'Active'",
573
+ "externalizeFields": [
574
+ {
575
+ "field": "TemplateText",
576
+ "pattern": "@file:{Name}.template.md"
577
+ },
578
+ {
579
+ "field": "PromptText",
580
+ "pattern": "@file:prompts/{Name}.prompt.md"
581
+ }
582
+ ],
583
+ "excludeFields": ["InternalID", "TempField"],
584
+ "lookupFields": {
585
+ "CategoryID": {
586
+ "entity": "AI Prompt Categories",
587
+ "field": "Name"
588
+ },
589
+ "TypeID": {
590
+ "entity": "AI Prompt Types",
591
+ "field": "Name"
592
+ }
593
+ },
426
594
  "relatedEntities": {
427
595
  "MJ: AI Prompt Models": {
428
596
  "entity": "MJ: AI Prompt Models",
429
- "foreignKey": "ID",
430
- "filter": "Status = 'Active'"
431
- },
432
- "AI Prompt Parameters": {
433
- "entity": "AI Prompt Parameters",
434
- "foreignKey": "ID"
597
+ "foreignKey": "PromptID",
598
+ "filter": "Status = 'Active'",
599
+ "lookupFields": {
600
+ "ModelID": {
601
+ "entity": "AI Models",
602
+ "field": "Name"
603
+ }
604
+ }
435
605
  }
436
606
  }
437
607
  }
438
608
  }
439
609
  ```
440
610
 
611
+ #### Pull Configuration Options
612
+
613
+ | Option | Type | Default | Description |
614
+ |--------|------|---------|-------------|
615
+ | `filePattern` | string | Entity filePattern | Pattern for finding existing files to update |
616
+ | `createNewFileIfNotFound` | boolean | true | Create files for records not found locally |
617
+ | `newFileName` | string | - | Filename for new records when appending (see warning below) |
618
+ | `appendRecordsToExistingFile` | boolean | false | Append new records to a single file |
619
+ | `updateExistingRecords` | boolean | true | Update existing records found in local files |
620
+ | `preserveFields` | string[] | [] | Fields that retain local values during updates (see detailed explanation below) |
621
+ | `mergeStrategy` | string | "merge" | How to merge updates: "merge", "overwrite", or "skip" |
622
+ | `backupBeforeUpdate` | boolean | false | Create timestamped backups before updating files |
623
+ | `backupDirectory` | string | ".backups" | Directory name for backup files (relative to entity directory) |
624
+ | `filter` | string | - | SQL WHERE clause for filtering records |
625
+ | `externalizeFields` | array/object | - | Fields to save as external files with optional patterns |
626
+ | `excludeFields` | string[] | [] | Fields to completely omit from pulled data (see detailed explanation below) |
627
+ | `lookupFields` | object | - | Foreign keys to convert to @lookup references |
628
+ | `relatedEntities` | object | - | Related entities to pull as embedded collections |
629
+
630
+ > **⚠️ Important Configuration Warning**
631
+ >
632
+ > When both `appendRecordsToExistingFile: true` and `newFileName` are set, ALL new records will be appended to the single file specified by `newFileName`, effectively ignoring the standard per-record file pattern. This can lead to unexpected file organization:
633
+ >
634
+ > ```json
635
+ > // This configuration will put ALL new records in .all-new.json
636
+ > "pull": {
637
+ > "appendRecordsToExistingFile": true,
638
+ > "newFileName": ".all-new.json" // ⚠️ Overrides individual file creation
639
+ > }
640
+ > ```
641
+ >
642
+ > **Recommended configurations:**
643
+ > - For individual files per record: Set `appendRecordsToExistingFile: false` (or omit it)
644
+ > - For grouped new records: Set both `appendRecordsToExistingFile: true` and `newFileName`
645
+ > - For mixed approach: Omit `newFileName` to let new records follow the standard pattern
646
+
647
+ #### Merge Strategies
648
+
649
+ - **`merge`** (default): Combines fields from database and local file, with database values taking precedence for existing fields
650
+ - **`overwrite`**: Completely replaces local record with database version (except preserved fields)
651
+ - **`skip`**: Leaves existing records unchanged, only adds new records
652
+
653
+ #### Understanding excludeFields vs preserveFields
654
+
655
+ These two configuration options serve different purposes for managing fields during pull operations:
656
+
657
+ ##### excludeFields
658
+ - **Purpose**: Completely omit specified fields from your local files
659
+ - **Use Case**: Remove internal/system fields you don't want in version control
660
+ - **Effect**: Fields never appear in the JSON files
661
+ - **Example**: Excluding internal IDs, timestamps, or sensitive data
662
+
663
+ ##### preserveFields
664
+ - **Purpose**: Protect local customizations from being overwritten during updates
665
+ - **Use Case**: Keep locally modified values while updating other fields
666
+ - **Effect**: Fields exist in files but retain their local values during pull
667
+ - **Example**: Preserving custom file paths, local notes, or environment-specific values
668
+ - **Special Behavior for @file: references**: When a preserved field contains a `@file:` reference, the tool will update the content at the existing file path rather than creating a new file with a generated name
669
+
670
+ ##### Example Configuration
671
+ ```json
672
+ {
673
+ "pull": {
674
+ "excludeFields": ["TemplateID", "InternalNotes", "CreatedAt"],
675
+ "preserveFields": ["TemplateText", "OutputExample", "LocalConfig"]
676
+ }
677
+ }
678
+ ```
679
+
680
+ With this configuration:
681
+ - **TemplateID, InternalNotes, CreatedAt** → Never appear in local files
682
+ - **TemplateText, OutputExample, LocalConfig** → Keep their local values during updates
683
+
684
+ ##### Common Scenario: Customized File References
685
+ When you customize file paths (e.g., changing `@file:templates/skip-conductor.md` to `@file:templates/conductor.md`), use `preserveFields` to protect these customizations:
686
+
687
+ ```json
688
+ {
689
+ "pull": {
690
+ "preserveFields": ["TemplateText", "OutputExample"],
691
+ "externalizeFields": [
692
+ {
693
+ "field": "TemplateText",
694
+ "pattern": "@file:templates/{Name}.template.md"
695
+ }
696
+ ]
697
+ }
698
+ }
699
+ ```
700
+
701
+ This ensures your custom paths aren't overwritten when pulling updates from the database.
702
+
703
+ **How it works:**
704
+ 1. **Without preserveFields**: Pull would create a new file using the pattern (e.g., `templates/skip-conductor.template.md`) and update the JSON to point to it
705
+ 2. **With preserveFields**: Pull keeps your custom path (e.g., `@file:templates/conductor.md`) in the JSON and updates the content at that existing location
706
+
707
+ This is particularly useful when:
708
+ - You've reorganized your file structure after initial pull
709
+ - You've renamed files to follow your own naming conventions
710
+ - You want to maintain consistent paths across team members
711
+
712
+ #### Backup Configuration
713
+
714
+ When `backupBeforeUpdate` is enabled, the tool creates timestamped backups before updating existing files:
715
+
716
+ - **Backup Location**: Files are backed up to the `backupDirectory` (default: `.backups`) within the entity directory
717
+ - **Backup Naming**: Original filename + timestamp + `.backup` extension (e.g., `.greeting.json` → `.greeting.2024-03-15T10-30-45-123Z.backup`)
718
+ - **Extension**: All backup files use the `.backup` extension, preventing them from being processed by push/pull/status commands
719
+ - **Deduplication**: Only one backup is created per file per pull operation, even if the file contains multiple records
720
+
721
+ Example configuration:
722
+ ```json
723
+ "pull": {
724
+ "backupBeforeUpdate": true,
725
+ "backupDirectory": ".backups" // Custom backup directory name
726
+ }
727
+ ```
728
+
729
+ #### Externalize Fields Patterns
730
+
731
+ The `externalizeFields` configuration supports dynamic file naming with placeholders:
732
+
733
+ ```json
734
+ "externalizeFields": [
735
+ {
736
+ "field": "TemplateText",
737
+ "pattern": "@file:{Name}.template.md"
738
+ },
739
+ {
740
+ "field": "SQLQuery",
741
+ "pattern": "@file:queries/{CategoryName}/{Name}.sql"
742
+ }
743
+ ]
744
+ ```
745
+
746
+ Supported placeholders:
747
+ - `{Name}` - The entity's name field value
748
+ - `{ID}` - The entity's primary key
749
+ - `{FieldName}` - The field being externalized
750
+ - `{AnyFieldName}` - Any field from the entity record
751
+
752
+ All values are sanitized for filesystem compatibility (lowercase, spaces to hyphens, special characters removed).
753
+
441
754
  ### Nested Related Entities
442
755
  Support for multiple levels of nesting:
443
756
  ```json