@memberjunction/metadata-sync 2.50.0 → 2.51.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/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 +4 -0
- package/dist/commands/push/index.js +196 -22
- 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/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 +27 -2
- package/dist/lib/sync-engine.js.map +1 -1
- package/dist/services/FormattingService.d.ts +44 -0
- package/dist/services/FormattingService.js +561 -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 +98 -1
- 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
|
|
@@ -38,6 +38,7 @@ export default class Pull extends Command {
|
|
|
38
38
|
'dry-run': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
39
39
|
'multi-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
40
40
|
verbose: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
41
|
+
'no-validate': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
41
42
|
};
|
|
42
43
|
run(): Promise<void>;
|
|
43
44
|
/**
|
|
@@ -11,6 +11,29 @@
|
|
|
11
11
|
* - Creating multi-record JSON files
|
|
12
12
|
* - Recursive directory search for entity configurations
|
|
13
13
|
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
14
37
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
38
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
39
|
};
|
|
@@ -25,6 +48,7 @@ const core_2 = require("@memberjunction/core");
|
|
|
25
48
|
const provider_utils_1 = require("../../lib/provider-utils");
|
|
26
49
|
const config_manager_1 = require("../../lib/config-manager");
|
|
27
50
|
const singleton_manager_1 = require("../../lib/singleton-manager");
|
|
51
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
28
52
|
/**
|
|
29
53
|
* Pull metadata records from database to local files
|
|
30
54
|
*
|
|
@@ -55,6 +79,7 @@ class Pull extends core_1.Command {
|
|
|
55
79
|
'dry-run': core_1.Flags.boolean({ description: 'Show what would be pulled without actually pulling' }),
|
|
56
80
|
'multi-file': core_1.Flags.string({ description: 'Create a single file with multiple records (provide filename)' }),
|
|
57
81
|
verbose: core_1.Flags.boolean({ char: 'v', description: 'Show detailed output' }),
|
|
82
|
+
'no-validate': core_1.Flags.boolean({ description: 'Skip validation before pull' }),
|
|
58
83
|
};
|
|
59
84
|
async run() {
|
|
60
85
|
const { flags } = await this.parse(Pull);
|
|
@@ -73,7 +98,43 @@ class Pull extends core_1.Command {
|
|
|
73
98
|
// Get singleton sync engine
|
|
74
99
|
const syncEngine = await (0, singleton_manager_1.getSyncEngine)((0, provider_utils_1.getSystemUser)());
|
|
75
100
|
// Show success after all initialization is complete
|
|
76
|
-
|
|
101
|
+
if (flags.verbose) {
|
|
102
|
+
spinner.succeed('Configuration and metadata loaded');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
spinner.stop();
|
|
106
|
+
}
|
|
107
|
+
// Run validation unless disabled
|
|
108
|
+
if (!flags['no-validate']) {
|
|
109
|
+
const { ValidationService } = await Promise.resolve().then(() => __importStar(require('../../services/ValidationService')));
|
|
110
|
+
const { FormattingService } = await Promise.resolve().then(() => __importStar(require('../../services/FormattingService')));
|
|
111
|
+
spinner.start('Validating metadata...');
|
|
112
|
+
const validator = new ValidationService({ verbose: flags.verbose });
|
|
113
|
+
const formatter = new FormattingService();
|
|
114
|
+
const validationResult = await validator.validateDirectory(config_manager_1.configManager.getOriginalCwd());
|
|
115
|
+
spinner.stop();
|
|
116
|
+
if (!validationResult.isValid || validationResult.warnings.length > 0) {
|
|
117
|
+
// Show validation results
|
|
118
|
+
this.log('\n' + formatter.formatValidationResult(validationResult, flags.verbose));
|
|
119
|
+
if (!validationResult.isValid) {
|
|
120
|
+
// Ask for confirmation
|
|
121
|
+
const shouldContinue = await (0, prompts_1.select)({
|
|
122
|
+
message: 'Validation failed with errors. Do you want to continue anyway?',
|
|
123
|
+
choices: [
|
|
124
|
+
{ name: 'No, fix the errors first', value: false },
|
|
125
|
+
{ name: 'Yes, continue anyway', value: true }
|
|
126
|
+
],
|
|
127
|
+
default: false
|
|
128
|
+
});
|
|
129
|
+
if (!shouldContinue) {
|
|
130
|
+
this.error('Pull cancelled due to validation errors.');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
this.log(chalk_1.default.green('✓ Validation passed'));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
77
138
|
let targetDir;
|
|
78
139
|
let entityConfig;
|
|
79
140
|
// Check if we should use a specific target directory
|
|
@@ -114,8 +175,8 @@ class Pull extends core_1.Command {
|
|
|
114
175
|
this.error(`Invalid entity configuration in ${targetDir}`);
|
|
115
176
|
}
|
|
116
177
|
}
|
|
117
|
-
// Show configuration notice only if relevant
|
|
118
|
-
if (entityConfig.pull?.appendRecordsToExistingFile && entityConfig.pull?.newFileName) {
|
|
178
|
+
// Show configuration notice only if relevant and in verbose mode
|
|
179
|
+
if (flags.verbose && entityConfig.pull?.appendRecordsToExistingFile && entityConfig.pull?.newFileName) {
|
|
119
180
|
const targetFile = path_1.default.join(targetDir, entityConfig.pull.newFileName.endsWith('.json')
|
|
120
181
|
? entityConfig.pull.newFileName
|
|
121
182
|
: `${entityConfig.pull.newFileName}.json`);
|
|
@@ -175,9 +236,16 @@ class Pull extends core_1.Command {
|
|
|
175
236
|
// Check if any externalized fields are NOT in metadata (likely computed properties)
|
|
176
237
|
const computedFields = fieldsToExternalize.filter(f => !metadataFieldNames.includes(f));
|
|
177
238
|
if (computedFields.length > 0) {
|
|
178
|
-
|
|
239
|
+
if (flags.verbose) {
|
|
240
|
+
spinner.start(`Waiting 5 seconds for async property loading in ${flags.entity} (${computedFields.join(', ')})...`);
|
|
241
|
+
}
|
|
179
242
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
180
|
-
|
|
243
|
+
if (flags.verbose) {
|
|
244
|
+
spinner.succeed('Async property loading wait complete');
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
spinner.stop();
|
|
248
|
+
}
|
|
181
249
|
}
|
|
182
250
|
}
|
|
183
251
|
}
|
|
@@ -218,12 +286,14 @@ class Pull extends core_1.Command {
|
|
|
218
286
|
const fileName = flags['multi-file'].endsWith('.json') ? flags['multi-file'] : `${flags['multi-file']}.json`;
|
|
219
287
|
const filePath = path_1.default.join(targetDir, fileName);
|
|
220
288
|
await fs_extra_1.default.writeJson(filePath, allRecords, { spaces: 2 });
|
|
221
|
-
spinner.succeed(`Pulled ${processed} records to ${filePath}`);
|
|
289
|
+
spinner.succeed(`Pulled ${processed} records to ${path_1.default.basename(filePath)}`);
|
|
222
290
|
}
|
|
223
291
|
}
|
|
224
292
|
else {
|
|
225
293
|
// Smart update logic for single-file-per-record
|
|
226
|
-
|
|
294
|
+
if (flags.verbose) {
|
|
295
|
+
spinner.text = 'Scanning for existing files...';
|
|
296
|
+
}
|
|
227
297
|
// Find existing files
|
|
228
298
|
const filePattern = entityConfig.pull?.filePattern || entityConfig.filePattern || '*.json';
|
|
229
299
|
const existingFiles = await this.findExistingFiles(targetDir, filePattern);
|
|
@@ -424,10 +494,10 @@ class Pull extends core_1.Command {
|
|
|
424
494
|
this.error(error);
|
|
425
495
|
}
|
|
426
496
|
finally {
|
|
427
|
-
//
|
|
428
|
-
await (0, provider_utils_1.cleanupProvider)();
|
|
497
|
+
// Reset singletons
|
|
429
498
|
(0, singleton_manager_1.resetSyncEngine)();
|
|
430
499
|
// Exit process to prevent background MJ tasks from throwing errors
|
|
500
|
+
// We don't explicitly close the connection - let the process termination handle it
|
|
431
501
|
process.exit(0);
|
|
432
502
|
}
|
|
433
503
|
}
|
|
@@ -1082,7 +1152,9 @@ class Pull extends core_1.Command {
|
|
|
1082
1152
|
// Check if any externalized fields are NOT in metadata (likely computed properties)
|
|
1083
1153
|
const computedFields = fieldsToExternalize.filter(f => !metadataFieldNames.includes(f));
|
|
1084
1154
|
if (computedFields.length > 0) {
|
|
1085
|
-
|
|
1155
|
+
if (flags?.verbose) {
|
|
1156
|
+
console.log(`Waiting 5 seconds for async property loading in related entity ${config.entity} (${computedFields.join(', ')})...`);
|
|
1157
|
+
}
|
|
1086
1158
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
1087
1159
|
}
|
|
1088
1160
|
}
|