@memberjunction/metadata-sync 2.50.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.
- package/README.md +423 -2
- package/dist/commands/file-reset/index.d.ts +15 -0
- package/dist/commands/file-reset/index.js +221 -0
- package/dist/commands/file-reset/index.js.map +1 -0
- package/dist/commands/pull/index.d.ts +1 -0
- package/dist/commands/pull/index.js +82 -10
- package/dist/commands/pull/index.js.map +1 -1
- package/dist/commands/push/index.d.ts +21 -0
- package/dist/commands/push/index.js +589 -45
- package/dist/commands/push/index.js.map +1 -1
- package/dist/commands/validate/index.d.ts +15 -0
- package/dist/commands/validate/index.js +149 -0
- package/dist/commands/validate/index.js.map +1 -0
- package/dist/commands/watch/index.js +39 -1
- package/dist/commands/watch/index.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/file-backup-manager.d.ts +90 -0
- package/dist/lib/file-backup-manager.js +186 -0
- package/dist/lib/file-backup-manager.js.map +1 -0
- package/dist/lib/provider-utils.d.ts +2 -2
- package/dist/lib/provider-utils.js +3 -4
- package/dist/lib/provider-utils.js.map +1 -1
- package/dist/lib/sync-engine.js +29 -3
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/services/FormattingService.d.ts +45 -0
- package/dist/services/FormattingService.js +564 -0
- package/dist/services/FormattingService.js.map +1 -0
- package/dist/services/ValidationService.d.ts +110 -0
- package/dist/services/ValidationService.js +737 -0
- package/dist/services/ValidationService.js.map +1 -0
- package/dist/types/validation.d.ts +98 -0
- package/dist/types/validation.js +97 -0
- package/dist/types/validation.js.map +1 -0
- package/oclif.manifest.json +205 -39
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -806,6 +806,18 @@ Templates can reference other templates:
|
|
|
806
806
|
## CLI Commands
|
|
807
807
|
|
|
808
808
|
```bash
|
|
809
|
+
# Validate all metadata files
|
|
810
|
+
mj-sync validate
|
|
811
|
+
|
|
812
|
+
# Validate a specific directory
|
|
813
|
+
mj-sync validate --dir="./metadata"
|
|
814
|
+
|
|
815
|
+
# Validate with detailed output
|
|
816
|
+
mj-sync validate --verbose
|
|
817
|
+
|
|
818
|
+
# Validate with JSON output for CI/CD
|
|
819
|
+
mj-sync validate --format=json
|
|
820
|
+
|
|
809
821
|
# Initialize a directory for metadata sync
|
|
810
822
|
mj-sync init
|
|
811
823
|
|
|
@@ -838,8 +850,12 @@ mj-sync status
|
|
|
838
850
|
# Watch for changes and auto-push
|
|
839
851
|
mj-sync watch
|
|
840
852
|
|
|
841
|
-
# CI/CD mode (push with no prompts)
|
|
853
|
+
# CI/CD mode (push with no prompts, fails on validation errors)
|
|
842
854
|
mj-sync push --ci
|
|
855
|
+
|
|
856
|
+
# Push/Pull without validation
|
|
857
|
+
mj-sync push --no-validate
|
|
858
|
+
mj-sync pull --entity="AI Prompts" --no-validate
|
|
843
859
|
```
|
|
844
860
|
|
|
845
861
|
## Configuration
|
|
@@ -1450,12 +1466,417 @@ Processing AI Prompts in demo/ai-prompts
|
|
|
1450
1466
|
- No hardcoded assumptions about entity structure
|
|
1451
1467
|
- Proper database connection cleanup
|
|
1452
1468
|
|
|
1469
|
+
## Validation System
|
|
1470
|
+
|
|
1471
|
+
The MetadataSync tool includes a comprehensive validation system that checks your metadata files for correctness before pushing to the database. This helps catch errors early and ensures data integrity.
|
|
1472
|
+
|
|
1473
|
+
### Validation Features
|
|
1474
|
+
|
|
1475
|
+
#### Automatic Validation
|
|
1476
|
+
By default, validation runs automatically before push and pull operations:
|
|
1477
|
+
```bash
|
|
1478
|
+
# These commands validate first, then proceed if valid
|
|
1479
|
+
mj-sync push
|
|
1480
|
+
mj-sync pull --entity="AI Prompts"
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
#### Manual Validation
|
|
1484
|
+
Run validation without performing any sync operations:
|
|
1485
|
+
```bash
|
|
1486
|
+
# Validate current directory
|
|
1487
|
+
mj-sync validate
|
|
1488
|
+
|
|
1489
|
+
# Validate specific directory
|
|
1490
|
+
mj-sync validate --dir="./metadata"
|
|
1491
|
+
|
|
1492
|
+
# Verbose output shows all files checked
|
|
1493
|
+
mj-sync validate --verbose
|
|
1494
|
+
```
|
|
1495
|
+
|
|
1496
|
+
#### CI/CD Integration
|
|
1497
|
+
Get JSON output for automated pipelines:
|
|
1498
|
+
```bash
|
|
1499
|
+
# JSON output for parsing
|
|
1500
|
+
mj-sync validate --format=json
|
|
1501
|
+
|
|
1502
|
+
# In CI mode, validation failures cause immediate exit
|
|
1503
|
+
mj-sync push --ci
|
|
1504
|
+
```
|
|
1505
|
+
|
|
1506
|
+
#### Validation During Push
|
|
1507
|
+
|
|
1508
|
+
**Important:** The `push` command automatically validates your metadata before pushing to the database:
|
|
1509
|
+
- ❌ **Push stops on any validation errors** - You cannot push invalid metadata
|
|
1510
|
+
- 🛑 **In CI mode** - Push fails immediately without prompts
|
|
1511
|
+
- 💬 **In interactive mode** - You'll be asked if you want to continue despite errors
|
|
1512
|
+
- ✅ **Clean validation** - Push proceeds automatically
|
|
1513
|
+
|
|
1514
|
+
#### Skip Validation
|
|
1515
|
+
For emergency fixes or when you know validation will fail:
|
|
1516
|
+
```bash
|
|
1517
|
+
# Skip validation checks (USE WITH CAUTION!)
|
|
1518
|
+
mj-sync push --no-validate
|
|
1519
|
+
mj-sync pull --entity="AI Prompts" --no-validate
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
⚠️ **Warning:** Using `--no-validate` may push invalid metadata to your database, potentially breaking your application. Only use this flag when absolutely necessary.
|
|
1523
|
+
|
|
1524
|
+
### What Gets Validated
|
|
1525
|
+
|
|
1526
|
+
#### Entity Validation
|
|
1527
|
+
- ✓ Entity names exist in database metadata
|
|
1528
|
+
- ✓ Entity is accessible to current user
|
|
1529
|
+
- ✓ Entity allows data modifications
|
|
1530
|
+
|
|
1531
|
+
#### Field Validation
|
|
1532
|
+
- ✓ Field names exist on the entity
|
|
1533
|
+
- ✓ Virtual properties (getter/setter methods) are automatically detected
|
|
1534
|
+
- ✓ Fields are settable (not system fields)
|
|
1535
|
+
- ✓ Field values match expected data types
|
|
1536
|
+
- ✓ Required fields are checked intelligently:
|
|
1537
|
+
- Skips fields with default values
|
|
1538
|
+
- Skips computed/virtual fields (e.g., `Action` derived from `ActionID`)
|
|
1539
|
+
- Skips fields when related virtual property is used (e.g., `TemplateID` when `TemplateText` is provided)
|
|
1540
|
+
- Skips ReadOnly and AutoUpdateOnly fields
|
|
1541
|
+
- ✓ Foreign key relationships are valid
|
|
1542
|
+
|
|
1543
|
+
#### Reference Validation
|
|
1544
|
+
- ✓ `@file:` references point to existing files
|
|
1545
|
+
- ✓ `@lookup:` references find matching records
|
|
1546
|
+
- ✓ `@template:` references load valid JSON
|
|
1547
|
+
- ✓ `@parent:` and `@root:` have proper context
|
|
1548
|
+
- ✓ Circular references are detected
|
|
1549
|
+
|
|
1550
|
+
#### Best Practice Checks
|
|
1551
|
+
- ⚠️ Deep nesting (>10 levels) generates warnings
|
|
1552
|
+
- ⚠️ Missing required fields are flagged
|
|
1553
|
+
- ⚠️ Large file sizes trigger performance warnings
|
|
1554
|
+
- ⚠️ Naming convention violations
|
|
1555
|
+
|
|
1556
|
+
#### Dependency Order Validation
|
|
1557
|
+
- ✓ Entities are processed in dependency order
|
|
1558
|
+
- ✓ Parent entities exist before children
|
|
1559
|
+
- ✓ Circular dependencies are detected
|
|
1560
|
+
- ✓ Suggests corrected directory order
|
|
1561
|
+
|
|
1562
|
+
### Validation Output
|
|
1563
|
+
|
|
1564
|
+
#### Human-Readable Format (Default)
|
|
1565
|
+
```
|
|
1566
|
+
════════════════════════════════════════════════════════════
|
|
1567
|
+
║ Validation Report ║
|
|
1568
|
+
════════════════════════════════════════════════════════════
|
|
1569
|
+
|
|
1570
|
+
┌────────────────────────────────────────────────┐
|
|
1571
|
+
│ Files: 4 │
|
|
1572
|
+
│ Entities: 29 │
|
|
1573
|
+
│ Errors: 2 │
|
|
1574
|
+
│ Warnings: 5 │
|
|
1575
|
+
├────────────────────────────────────────────────┤
|
|
1576
|
+
│ Errors by Type: │
|
|
1577
|
+
│ field: 1 │
|
|
1578
|
+
│ reference: 1 │
|
|
1579
|
+
├────────────────────────────────────────────────┤
|
|
1580
|
+
│ Warnings by Type: │
|
|
1581
|
+
│ bestpractice: 3 │
|
|
1582
|
+
│ nesting: 2 │
|
|
1583
|
+
└────────────────────────────────────────────────┘
|
|
1584
|
+
|
|
1585
|
+
Errors
|
|
1586
|
+
|
|
1587
|
+
1. Field "Status" does not exist on entity "Templates"
|
|
1588
|
+
Entity: Templates
|
|
1589
|
+
Field: Status
|
|
1590
|
+
File: ./metadata/templates/.my-template.json
|
|
1591
|
+
→ Suggestion: Check spelling of 'Status'. Run 'mj-sync list-entities' to see available entities.
|
|
1592
|
+
|
|
1593
|
+
2. File not found: ./shared/footer.html
|
|
1594
|
+
Entity: Templates
|
|
1595
|
+
Field: FooterHTML
|
|
1596
|
+
File: ./metadata/templates/.my-template.json
|
|
1597
|
+
→ Suggestion: Ensure file './shared/footer.html' exists and path is relative to the metadata directory.
|
|
1598
|
+
```
|
|
1599
|
+
|
|
1600
|
+
#### JSON Format (CI/CD)
|
|
1601
|
+
```json
|
|
1602
|
+
{
|
|
1603
|
+
"isValid": false,
|
|
1604
|
+
"summary": {
|
|
1605
|
+
"totalFiles": 4,
|
|
1606
|
+
"totalEntities": 29,
|
|
1607
|
+
"totalErrors": 2,
|
|
1608
|
+
"totalWarnings": 5,
|
|
1609
|
+
"errorsByType": {
|
|
1610
|
+
"field": 1,
|
|
1611
|
+
"reference": 1
|
|
1612
|
+
},
|
|
1613
|
+
"warningsByType": {
|
|
1614
|
+
"bestpractice": 3,
|
|
1615
|
+
"nesting": 2
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
"errors": [
|
|
1619
|
+
{
|
|
1620
|
+
"type": "field",
|
|
1621
|
+
"entity": "Templates",
|
|
1622
|
+
"field": "Status",
|
|
1623
|
+
"file": "./metadata/templates/.my-template.json",
|
|
1624
|
+
"message": "Field \"Status\" does not exist on entity \"Templates\"",
|
|
1625
|
+
"suggestion": "Check spelling of 'Status'. Run 'mj-sync list-entities' to see available entities."
|
|
1626
|
+
}
|
|
1627
|
+
],
|
|
1628
|
+
"warnings": [...]
|
|
1629
|
+
}
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
### Virtual Properties Support
|
|
1633
|
+
|
|
1634
|
+
Some MemberJunction entities include virtual properties - getter/setter methods that aren't database fields but provide convenient access to related data. The validation system automatically detects these properties.
|
|
1635
|
+
|
|
1636
|
+
#### Example: TemplateText Virtual Property
|
|
1637
|
+
The `Templates` entity includes a `TemplateText` virtual property that:
|
|
1638
|
+
- Automatically manages `Template` and `TemplateContent` records
|
|
1639
|
+
- Isn't a database field but appears as a property on the entity class
|
|
1640
|
+
- Can be used in metadata files just like regular fields
|
|
1641
|
+
|
|
1642
|
+
```json
|
|
1643
|
+
{
|
|
1644
|
+
"fields": {
|
|
1645
|
+
"Name": "My Template",
|
|
1646
|
+
"TemplateText": "@file:template.html" // Virtual property - works!
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
```
|
|
1650
|
+
|
|
1651
|
+
The validator checks both database metadata AND entity class properties, ensuring virtual properties are properly recognized.
|
|
1652
|
+
|
|
1653
|
+
### Intelligent Required Field Validation
|
|
1654
|
+
|
|
1655
|
+
The validator intelligently handles required fields to avoid false warnings:
|
|
1656
|
+
|
|
1657
|
+
#### Fields with Default Values
|
|
1658
|
+
Required fields that have database defaults are not flagged:
|
|
1659
|
+
```json
|
|
1660
|
+
{
|
|
1661
|
+
"fields": {
|
|
1662
|
+
"Name": "My Entity"
|
|
1663
|
+
// CreatedAt is required but has default value - no warning
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
```
|
|
1667
|
+
|
|
1668
|
+
#### Computed/Virtual Fields
|
|
1669
|
+
Fields that are computed from other fields are skipped:
|
|
1670
|
+
```json
|
|
1671
|
+
{
|
|
1672
|
+
"fields": {
|
|
1673
|
+
"ActionID": "123-456-789"
|
|
1674
|
+
// Action field is computed from ActionID - no warning
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
```
|
|
1678
|
+
|
|
1679
|
+
#### Virtual Property Relationships
|
|
1680
|
+
When using virtual properties, related required fields are skipped:
|
|
1681
|
+
```json
|
|
1682
|
+
{
|
|
1683
|
+
"fields": {
|
|
1684
|
+
"Name": "My Prompt",
|
|
1685
|
+
"TemplateText": "@file:template.md"
|
|
1686
|
+
// TemplateID and Template are not required when TemplateText is used
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
```
|
|
1690
|
+
|
|
1691
|
+
### Common Validation Errors
|
|
1692
|
+
|
|
1693
|
+
| Error | Cause | Solution |
|
|
1694
|
+
|-------|-------|----------|
|
|
1695
|
+
| `Field "X" does not exist` | Typo or wrong entity | Check entity definition in generated files |
|
|
1696
|
+
| `Entity "X" not found` | Wrong entity name | Use exact entity name from database |
|
|
1697
|
+
| `File not found` | Bad @file: reference | Check file path is relative and exists |
|
|
1698
|
+
| `Lookup not found` | No matching record | Verify lookup value or use ?create |
|
|
1699
|
+
| `Circular dependency` | A→B→A references | Restructure to avoid cycles |
|
|
1700
|
+
| `Required field missing` | Missing required field | Add field with appropriate value |
|
|
1701
|
+
|
|
1702
|
+
### Validation Configuration
|
|
1703
|
+
|
|
1704
|
+
Control validation behavior in your workflow:
|
|
1705
|
+
|
|
1706
|
+
```json
|
|
1707
|
+
{
|
|
1708
|
+
"push": {
|
|
1709
|
+
"validateBeforePush": true // Default: true
|
|
1710
|
+
},
|
|
1711
|
+
"pull": {
|
|
1712
|
+
"validateBeforePull": false // Default: false
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
```
|
|
1716
|
+
|
|
1717
|
+
### Best Practices
|
|
1718
|
+
|
|
1719
|
+
1. **Run validation during development**: `mj-sync validate` frequently
|
|
1720
|
+
2. **Fix errors before warnings**: Errors block operations, warnings don't
|
|
1721
|
+
3. **Use verbose mode** to understand issues: `mj-sync validate -v`
|
|
1722
|
+
4. **Include in CI/CD**: Parse JSON output for automated checks
|
|
1723
|
+
5. **Don't skip validation** unless absolutely necessary
|
|
1724
|
+
|
|
1725
|
+
## Troubleshooting
|
|
1726
|
+
|
|
1727
|
+
### Validation Errors
|
|
1728
|
+
|
|
1729
|
+
If validation fails:
|
|
1730
|
+
|
|
1731
|
+
1. **Read the error message carefully** - It includes specific details
|
|
1732
|
+
2. **Check the suggestion** - Most errors include how to fix them
|
|
1733
|
+
3. **Use verbose mode** for more context: `mj-sync validate -v`
|
|
1734
|
+
4. **Verify entity definitions** in generated entity files
|
|
1735
|
+
5. **Check file paths** are relative to the metadata directory
|
|
1736
|
+
|
|
1737
|
+
### Performance Issues
|
|
1738
|
+
|
|
1739
|
+
For large metadata sets:
|
|
1740
|
+
|
|
1741
|
+
1. **Disable best practice checks**: `mj-sync validate --no-best-practices`
|
|
1742
|
+
2. **Validate specific directories**: `mj-sync validate --dir="./prompts"`
|
|
1743
|
+
3. **Reduce nesting depth warning**: `mj-sync validate --max-depth=20`
|
|
1744
|
+
|
|
1745
|
+
## Programmatic Usage
|
|
1746
|
+
|
|
1747
|
+
### Using ValidationService in Your Code
|
|
1748
|
+
|
|
1749
|
+
The MetadataSync validation can be used programmatically in any Node.js project:
|
|
1750
|
+
|
|
1751
|
+
```typescript
|
|
1752
|
+
import { ValidationService, FormattingService } from '@memberjunction/metadata-sync';
|
|
1753
|
+
import { ValidationOptions } from '@memberjunction/metadata-sync/dist/types/validation';
|
|
1754
|
+
|
|
1755
|
+
// Initialize validation options
|
|
1756
|
+
const options: ValidationOptions = {
|
|
1757
|
+
verbose: false,
|
|
1758
|
+
outputFormat: 'human',
|
|
1759
|
+
maxNestingDepth: 10,
|
|
1760
|
+
checkBestPractices: true
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
// Create validator instance
|
|
1764
|
+
const validator = new ValidationService(options);
|
|
1765
|
+
|
|
1766
|
+
// Validate a directory
|
|
1767
|
+
const result = await validator.validateDirectory('/path/to/metadata');
|
|
1768
|
+
|
|
1769
|
+
// Check results
|
|
1770
|
+
if (result.isValid) {
|
|
1771
|
+
console.log('Validation passed!');
|
|
1772
|
+
} else {
|
|
1773
|
+
console.log(`Found ${result.errors.length} errors`);
|
|
1774
|
+
|
|
1775
|
+
// Format results for display
|
|
1776
|
+
const formatter = new FormattingService();
|
|
1777
|
+
|
|
1778
|
+
// Get human-readable output
|
|
1779
|
+
const humanOutput = formatter.formatValidationResult(result, true);
|
|
1780
|
+
console.log(humanOutput);
|
|
1781
|
+
|
|
1782
|
+
// Get JSON output
|
|
1783
|
+
const jsonOutput = formatter.formatValidationResultAsJson(result);
|
|
1784
|
+
|
|
1785
|
+
// Get beautiful markdown report
|
|
1786
|
+
const markdownReport = formatter.formatValidationResultAsMarkdown(result);
|
|
1787
|
+
}
|
|
1788
|
+
```
|
|
1789
|
+
|
|
1790
|
+
### ValidationResult Structure
|
|
1791
|
+
|
|
1792
|
+
The validation service returns a structured object with complete details:
|
|
1793
|
+
|
|
1794
|
+
```typescript
|
|
1795
|
+
interface ValidationResult {
|
|
1796
|
+
isValid: boolean;
|
|
1797
|
+
errors: ValidationError[];
|
|
1798
|
+
warnings: ValidationWarning[];
|
|
1799
|
+
summary: {
|
|
1800
|
+
totalFiles: number;
|
|
1801
|
+
totalEntities: number;
|
|
1802
|
+
totalErrors: number;
|
|
1803
|
+
totalWarnings: number;
|
|
1804
|
+
fileResults: Map<string, FileValidationResult>;
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
interface ValidationError {
|
|
1809
|
+
type: 'entity' | 'field' | 'reference' | 'circular' | 'dependency' | 'nesting' | 'bestpractice';
|
|
1810
|
+
severity: 'error' | 'warning';
|
|
1811
|
+
entity?: string;
|
|
1812
|
+
field?: string;
|
|
1813
|
+
file: string;
|
|
1814
|
+
message: string;
|
|
1815
|
+
suggestion?: string;
|
|
1816
|
+
details?: any;
|
|
1817
|
+
}
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
### Integration Example
|
|
1821
|
+
|
|
1822
|
+
```typescript
|
|
1823
|
+
import { ValidationService } from '@memberjunction/metadata-sync';
|
|
1824
|
+
|
|
1825
|
+
export async function validateBeforeDeploy(metadataPath: string): Promise<boolean> {
|
|
1826
|
+
const validator = new ValidationService({
|
|
1827
|
+
checkBestPractices: true,
|
|
1828
|
+
maxNestingDepth: 10
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
const result = await validator.validateDirectory(metadataPath);
|
|
1832
|
+
|
|
1833
|
+
if (!result.isValid) {
|
|
1834
|
+
// Log errors to your monitoring system
|
|
1835
|
+
result.errors.forEach(error => {
|
|
1836
|
+
logger.error('Metadata validation error', {
|
|
1837
|
+
type: error.type,
|
|
1838
|
+
entity: error.entity,
|
|
1839
|
+
field: error.field,
|
|
1840
|
+
file: error.file,
|
|
1841
|
+
message: error.message
|
|
1842
|
+
});
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
// Optionally save report
|
|
1846
|
+
const report = new FormattingService().formatValidationResultAsMarkdown(result);
|
|
1847
|
+
await fs.writeFile('validation-report.md', report);
|
|
1848
|
+
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
return true;
|
|
1853
|
+
}
|
|
1854
|
+
```
|
|
1855
|
+
|
|
1856
|
+
### CI/CD Integration
|
|
1857
|
+
|
|
1858
|
+
```yaml
|
|
1859
|
+
# Example GitHub Actions workflow
|
|
1860
|
+
- name: Validate Metadata
|
|
1861
|
+
run: |
|
|
1862
|
+
npm install @memberjunction/metadata-sync
|
|
1863
|
+
npx mj-sync validate --dir=./metadata --format=json > validation-results.json
|
|
1864
|
+
|
|
1865
|
+
- name: Check Validation Results
|
|
1866
|
+
run: |
|
|
1867
|
+
if [ $(jq '.isValid' validation-results.json) = "false" ]; then
|
|
1868
|
+
echo "Metadata validation failed!"
|
|
1869
|
+
jq '.errors' validation-results.json
|
|
1870
|
+
exit 1
|
|
1871
|
+
fi
|
|
1872
|
+
```
|
|
1873
|
+
|
|
1453
1874
|
## Future Enhancements
|
|
1454
1875
|
|
|
1455
1876
|
- Plugin system for custom entity handlers
|
|
1456
1877
|
- Merge conflict resolution UI
|
|
1457
1878
|
- Bulk operations across entities
|
|
1458
|
-
-
|
|
1879
|
+
- Extended validation rules
|
|
1459
1880
|
- Schema migration support
|
|
1460
1881
|
- Team collaboration features
|
|
1461
1882
|
- Bidirectional sync for related entities
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class FileReset extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
sections: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
7
|
+
'dry-run': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
'no-backup': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
verbose: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
};
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
private countSections;
|
|
14
|
+
private removeSections;
|
|
15
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const core_1 = require("@oclif/core");
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
10
|
+
const ora_classic_1 = __importDefault(require("ora-classic"));
|
|
11
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
12
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
13
|
+
const config_1 = require("../../config");
|
|
14
|
+
const config_manager_1 = require("../../lib/config-manager");
|
|
15
|
+
class FileReset extends core_1.Command {
|
|
16
|
+
static description = 'Remove primaryKey and sync sections from metadata JSON files';
|
|
17
|
+
static examples = [
|
|
18
|
+
`<%= config.bin %> <%= command.id %>`,
|
|
19
|
+
`<%= config.bin %> <%= command.id %> --sections=primaryKey`,
|
|
20
|
+
`<%= config.bin %> <%= command.id %> --sections=sync`,
|
|
21
|
+
`<%= config.bin %> <%= command.id %> --dry-run`,
|
|
22
|
+
`<%= config.bin %> <%= command.id %> --no-backup`,
|
|
23
|
+
`<%= config.bin %> <%= command.id %> --yes`,
|
|
24
|
+
];
|
|
25
|
+
static flags = {
|
|
26
|
+
sections: core_1.Flags.string({
|
|
27
|
+
description: 'Which sections to remove',
|
|
28
|
+
options: ['both', 'primaryKey', 'sync'],
|
|
29
|
+
default: 'both',
|
|
30
|
+
}),
|
|
31
|
+
'dry-run': core_1.Flags.boolean({
|
|
32
|
+
description: 'Show what would be removed without actually removing'
|
|
33
|
+
}),
|
|
34
|
+
'no-backup': core_1.Flags.boolean({
|
|
35
|
+
description: 'Skip creating backup files'
|
|
36
|
+
}),
|
|
37
|
+
yes: core_1.Flags.boolean({
|
|
38
|
+
char: 'y',
|
|
39
|
+
description: 'Skip confirmation prompt'
|
|
40
|
+
}),
|
|
41
|
+
verbose: core_1.Flags.boolean({
|
|
42
|
+
char: 'v',
|
|
43
|
+
description: 'Show detailed output'
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
async run() {
|
|
47
|
+
const { flags } = await this.parse(FileReset);
|
|
48
|
+
const spinner = (0, ora_classic_1.default)();
|
|
49
|
+
try {
|
|
50
|
+
// Load sync config
|
|
51
|
+
const syncConfig = await (0, config_1.loadSyncConfig)(config_manager_1.configManager.getOriginalCwd());
|
|
52
|
+
if (!syncConfig) {
|
|
53
|
+
this.error('No .mj-sync.json found in current directory');
|
|
54
|
+
}
|
|
55
|
+
// Find all metadata JSON files
|
|
56
|
+
spinner.start('Finding metadata files');
|
|
57
|
+
const pattern = syncConfig.filePattern || '.*.json';
|
|
58
|
+
const files = await (0, fast_glob_1.default)(pattern, {
|
|
59
|
+
cwd: config_manager_1.configManager.getOriginalCwd(),
|
|
60
|
+
absolute: true,
|
|
61
|
+
ignore: ['.mj-sync.json', '.mj-folder.json'],
|
|
62
|
+
});
|
|
63
|
+
spinner.stop();
|
|
64
|
+
if (files.length === 0) {
|
|
65
|
+
this.log('No metadata files found');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.log(`Found ${files.length} metadata file${files.length === 1 ? '' : 's'}`);
|
|
69
|
+
// Count what will be removed
|
|
70
|
+
let filesWithPrimaryKey = 0;
|
|
71
|
+
let filesWithSync = 0;
|
|
72
|
+
let totalPrimaryKeys = 0;
|
|
73
|
+
let totalSyncs = 0;
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const content = await fs_extra_1.default.readJson(file);
|
|
76
|
+
const stats = this.countSections(content);
|
|
77
|
+
if (stats.primaryKeyCount > 0) {
|
|
78
|
+
filesWithPrimaryKey++;
|
|
79
|
+
totalPrimaryKeys += stats.primaryKeyCount;
|
|
80
|
+
}
|
|
81
|
+
if (stats.syncCount > 0) {
|
|
82
|
+
filesWithSync++;
|
|
83
|
+
totalSyncs += stats.syncCount;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Show what will be removed
|
|
87
|
+
this.log('');
|
|
88
|
+
if (flags.sections === 'both' || flags.sections === 'primaryKey') {
|
|
89
|
+
this.log(`Will remove ${chalk_1.default.yellow(totalPrimaryKeys)} primaryKey section${totalPrimaryKeys === 1 ? '' : 's'} from ${chalk_1.default.yellow(filesWithPrimaryKey)} file${filesWithPrimaryKey === 1 ? '' : 's'}`);
|
|
90
|
+
}
|
|
91
|
+
if (flags.sections === 'both' || flags.sections === 'sync') {
|
|
92
|
+
this.log(`Will remove ${chalk_1.default.yellow(totalSyncs)} sync section${totalSyncs === 1 ? '' : 's'} from ${chalk_1.default.yellow(filesWithSync)} file${filesWithSync === 1 ? '' : 's'}`);
|
|
93
|
+
}
|
|
94
|
+
if (flags['dry-run']) {
|
|
95
|
+
this.log('');
|
|
96
|
+
this.log(chalk_1.default.cyan('Dry run mode - no files will be modified'));
|
|
97
|
+
if (flags.verbose) {
|
|
98
|
+
this.log('');
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const content = await fs_extra_1.default.readJson(file);
|
|
101
|
+
const stats = this.countSections(content);
|
|
102
|
+
if (stats.primaryKeyCount > 0 || stats.syncCount > 0) {
|
|
103
|
+
this.log(`${path_1.default.relative(config_manager_1.configManager.getOriginalCwd(), file)}:`);
|
|
104
|
+
if (stats.primaryKeyCount > 0) {
|
|
105
|
+
this.log(` - ${stats.primaryKeyCount} primaryKey section${stats.primaryKeyCount === 1 ? '' : 's'}`);
|
|
106
|
+
}
|
|
107
|
+
if (stats.syncCount > 0) {
|
|
108
|
+
this.log(` - ${stats.syncCount} sync section${stats.syncCount === 1 ? '' : 's'}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Confirm before proceeding
|
|
116
|
+
if (!flags.yes) {
|
|
117
|
+
const confirmed = await (0, prompts_1.confirm)({
|
|
118
|
+
message: 'Do you want to proceed?',
|
|
119
|
+
default: false,
|
|
120
|
+
});
|
|
121
|
+
if (!confirmed) {
|
|
122
|
+
this.log('Operation cancelled');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Process files
|
|
127
|
+
spinner.start('Processing files');
|
|
128
|
+
let processedFiles = 0;
|
|
129
|
+
let modifiedFiles = 0;
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
processedFiles++;
|
|
132
|
+
const content = await fs_extra_1.default.readJson(file);
|
|
133
|
+
const originalContent = JSON.stringify(content);
|
|
134
|
+
// Remove sections
|
|
135
|
+
const cleanedContent = this.removeSections(content, flags.sections);
|
|
136
|
+
// Only write if content changed
|
|
137
|
+
if (JSON.stringify(cleanedContent) !== originalContent) {
|
|
138
|
+
// Create backup if requested
|
|
139
|
+
if (!flags['no-backup']) {
|
|
140
|
+
const backupPath = `${file}.backup`;
|
|
141
|
+
await fs_extra_1.default.writeJson(backupPath, content, { spaces: 2 });
|
|
142
|
+
}
|
|
143
|
+
// Write cleaned content
|
|
144
|
+
await fs_extra_1.default.writeJson(file, cleanedContent, { spaces: 2 });
|
|
145
|
+
modifiedFiles++;
|
|
146
|
+
if (flags.verbose) {
|
|
147
|
+
spinner.stop();
|
|
148
|
+
this.log(`✓ ${path_1.default.relative(config_manager_1.configManager.getOriginalCwd(), file)}`);
|
|
149
|
+
spinner.start('Processing files');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
spinner.stop();
|
|
154
|
+
// Show summary
|
|
155
|
+
this.log('');
|
|
156
|
+
this.log(chalk_1.default.green(`✓ Reset complete`));
|
|
157
|
+
this.log(` Processed: ${processedFiles} file${processedFiles === 1 ? '' : 's'}`);
|
|
158
|
+
this.log(` Modified: ${modifiedFiles} file${modifiedFiles === 1 ? '' : 's'}`);
|
|
159
|
+
if (!flags['no-backup'] && modifiedFiles > 0) {
|
|
160
|
+
this.log(` Backups created: ${modifiedFiles}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
spinner.stop();
|
|
165
|
+
this.error(error instanceof Error ? error.message : String(error));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
countSections(data) {
|
|
169
|
+
let primaryKeyCount = 0;
|
|
170
|
+
let syncCount = 0;
|
|
171
|
+
if (Array.isArray(data)) {
|
|
172
|
+
for (const item of data) {
|
|
173
|
+
const stats = this.countSections(item);
|
|
174
|
+
primaryKeyCount += stats.primaryKeyCount;
|
|
175
|
+
syncCount += stats.syncCount;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (data && typeof data === 'object') {
|
|
179
|
+
if ('primaryKey' in data)
|
|
180
|
+
primaryKeyCount++;
|
|
181
|
+
if ('sync' in data)
|
|
182
|
+
syncCount++;
|
|
183
|
+
// Check related entities
|
|
184
|
+
if (data.relatedEntities) {
|
|
185
|
+
for (const entityData of Object.values(data.relatedEntities)) {
|
|
186
|
+
const stats = this.countSections(entityData);
|
|
187
|
+
primaryKeyCount += stats.primaryKeyCount;
|
|
188
|
+
syncCount += stats.syncCount;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { primaryKeyCount, syncCount };
|
|
193
|
+
}
|
|
194
|
+
removeSections(data, sections) {
|
|
195
|
+
if (Array.isArray(data)) {
|
|
196
|
+
return data.map(item => this.removeSections(item, sections));
|
|
197
|
+
}
|
|
198
|
+
else if (data && typeof data === 'object') {
|
|
199
|
+
const cleaned = { ...data };
|
|
200
|
+
// Remove specified sections
|
|
201
|
+
if (sections === 'both' || sections === 'primaryKey') {
|
|
202
|
+
delete cleaned.primaryKey;
|
|
203
|
+
}
|
|
204
|
+
if (sections === 'both' || sections === 'sync') {
|
|
205
|
+
delete cleaned.sync;
|
|
206
|
+
}
|
|
207
|
+
// Process related entities
|
|
208
|
+
if (cleaned.relatedEntities) {
|
|
209
|
+
const cleanedRelated = {};
|
|
210
|
+
for (const [entityName, entityData] of Object.entries(cleaned.relatedEntities)) {
|
|
211
|
+
cleanedRelated[entityName] = this.removeSections(entityData, sections);
|
|
212
|
+
}
|
|
213
|
+
cleaned.relatedEntities = cleanedRelated;
|
|
214
|
+
}
|
|
215
|
+
return cleaned;
|
|
216
|
+
}
|
|
217
|
+
return data;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
exports.default = FileReset;
|
|
221
|
+
//# sourceMappingURL=index.js.map
|