@memberjunction/metadata-sync 2.116.0 → 2.118.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 +24 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/database-reference-scanner.d.ts +56 -0
- package/dist/lib/database-reference-scanner.js +175 -0
- package/dist/lib/database-reference-scanner.js.map +1 -0
- package/dist/lib/deletion-auditor.d.ts +76 -0
- package/dist/lib/deletion-auditor.js +219 -0
- package/dist/lib/deletion-auditor.js.map +1 -0
- package/dist/lib/deletion-report-generator.d.ts +58 -0
- package/dist/lib/deletion-report-generator.js +287 -0
- package/dist/lib/deletion-report-generator.js.map +1 -0
- package/dist/lib/entity-foreign-key-helper.d.ts +51 -0
- package/dist/lib/entity-foreign-key-helper.js +83 -0
- package/dist/lib/entity-foreign-key-helper.js.map +1 -0
- package/dist/lib/provider-utils.d.ts +9 -1
- package/dist/lib/provider-utils.js +42 -5
- package/dist/lib/provider-utils.js.map +1 -1
- package/dist/lib/record-dependency-analyzer.d.ts +44 -0
- package/dist/lib/record-dependency-analyzer.js +133 -0
- package/dist/lib/record-dependency-analyzer.js.map +1 -1
- package/dist/services/PullService.d.ts +2 -0
- package/dist/services/PullService.js +4 -0
- package/dist/services/PullService.js.map +1 -1
- package/dist/services/PushService.d.ts +42 -2
- package/dist/services/PushService.js +451 -109
- package/dist/services/PushService.js.map +1 -1
- package/dist/services/StatusService.d.ts +2 -0
- package/dist/services/StatusService.js +5 -1
- package/dist/services/StatusService.js.map +1 -1
- package/dist/services/ValidationService.d.ts +4 -0
- package/dist/services/ValidationService.js +32 -2
- package/dist/services/ValidationService.js.map +1 -1
- package/dist/types/validation.d.ts +2 -0
- package/dist/types/validation.js.map +1 -1
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -77,7 +77,18 @@ The Metadata Sync tool bridges the gap between database-stored metadata and file
|
|
|
77
77
|
- **Push**: Upload local file changes to database
|
|
78
78
|
- Process embedded collections automatically
|
|
79
79
|
- Verbose mode (`-v`) for detailed output
|
|
80
|
+
- Directory filtering with `--include` and `--exclude` options
|
|
80
81
|
- **Status**: Show what would change without making modifications
|
|
82
|
+
- Directory filtering with `--include` and `--exclude` options
|
|
83
|
+
|
|
84
|
+
### Directory Filtering
|
|
85
|
+
- **Selective Processing**: Use `--include` or `--exclude` to filter which entity directories are processed
|
|
86
|
+
- **Pattern Support**: Supports glob patterns like `ai-*`, `*-test`, etc.
|
|
87
|
+
- **Mutually Exclusive**: Cannot use both `--include` and `--exclude` together
|
|
88
|
+
- **Use Cases**:
|
|
89
|
+
- Speed up development by excluding large/slow directories
|
|
90
|
+
- Focus on specific entities during debugging
|
|
91
|
+
- Create specialized sync workflows for different teams
|
|
81
92
|
|
|
82
93
|
### Development Workflow Integration
|
|
83
94
|
- Watch mode for automatic syncing during development
|
|
@@ -1363,6 +1374,19 @@ mj sync push --dry-run
|
|
|
1363
1374
|
# Push with parallel processing
|
|
1364
1375
|
mj sync push --parallel-batch-size=20 # Process 20 records in parallel (default: 10, max: 50)
|
|
1365
1376
|
|
|
1377
|
+
# Directory filtering - exclude specific directories
|
|
1378
|
+
mj sync push --exclude="actions" # Exclude single directory
|
|
1379
|
+
mj sync push --exclude="actions,templates" # Exclude multiple directories (comma-separated)
|
|
1380
|
+
mj sync push --exclude="*-test,*-old" # Exclude using glob patterns
|
|
1381
|
+
|
|
1382
|
+
# Directory filtering - include only specific directories
|
|
1383
|
+
mj sync push --include="prompts,agent-types" # Include only these directories
|
|
1384
|
+
mj sync push --include="ai-*" # Include using glob patterns
|
|
1385
|
+
|
|
1386
|
+
# Filtering works with other options
|
|
1387
|
+
mj sync push --dir="metadata" --exclude="actions" --dry-run
|
|
1388
|
+
mj sync status --exclude="actions,templates"
|
|
1389
|
+
|
|
1366
1390
|
# Show status of local vs database
|
|
1367
1391
|
mj sync status
|
|
1368
1392
|
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,15 @@ export { SQLLogger } from './lib/sql-logger';
|
|
|
7
7
|
export { TransactionManager } from './lib/transaction-manager';
|
|
8
8
|
export { JsonWriteHelper } from './lib/json-write-helper';
|
|
9
9
|
export { FileWriteBatch } from './lib/file-write-batch';
|
|
10
|
+
export { RecordDependencyAnalyzer } from './lib/record-dependency-analyzer';
|
|
11
|
+
export type { FlattenedRecord, ReverseDependency, DependencyAnalysisResult } from './lib/record-dependency-analyzer';
|
|
12
|
+
export { EntityForeignKeyHelper } from './lib/entity-foreign-key-helper';
|
|
13
|
+
export type { ReverseFKInfo } from './lib/entity-foreign-key-helper';
|
|
14
|
+
export { DatabaseReferenceScanner } from './lib/database-reference-scanner';
|
|
15
|
+
export type { DatabaseReference } from './lib/database-reference-scanner';
|
|
16
|
+
export { DeletionAuditor } from './lib/deletion-auditor';
|
|
17
|
+
export type { DeletionAudit } from './lib/deletion-auditor';
|
|
18
|
+
export { DeletionReportGenerator } from './lib/deletion-report-generator';
|
|
10
19
|
export { InitService } from './services/InitService';
|
|
11
20
|
export type { InitOptions, InitCallbacks } from './services/InitService';
|
|
12
21
|
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.FileWriteBatch = exports.JsonWriteHelper = 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.DeletionReportGenerator = exports.DeletionAuditor = exports.DatabaseReferenceScanner = exports.EntityForeignKeyHelper = exports.RecordDependencyAnalyzer = 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");
|
|
@@ -21,6 +21,17 @@ var json_write_helper_1 = require("./lib/json-write-helper");
|
|
|
21
21
|
Object.defineProperty(exports, "JsonWriteHelper", { enumerable: true, get: function () { return json_write_helper_1.JsonWriteHelper; } });
|
|
22
22
|
var file_write_batch_1 = require("./lib/file-write-batch");
|
|
23
23
|
Object.defineProperty(exports, "FileWriteBatch", { enumerable: true, get: function () { return file_write_batch_1.FileWriteBatch; } });
|
|
24
|
+
// Deletion audit exports
|
|
25
|
+
var record_dependency_analyzer_1 = require("./lib/record-dependency-analyzer");
|
|
26
|
+
Object.defineProperty(exports, "RecordDependencyAnalyzer", { enumerable: true, get: function () { return record_dependency_analyzer_1.RecordDependencyAnalyzer; } });
|
|
27
|
+
var entity_foreign_key_helper_1 = require("./lib/entity-foreign-key-helper");
|
|
28
|
+
Object.defineProperty(exports, "EntityForeignKeyHelper", { enumerable: true, get: function () { return entity_foreign_key_helper_1.EntityForeignKeyHelper; } });
|
|
29
|
+
var database_reference_scanner_1 = require("./lib/database-reference-scanner");
|
|
30
|
+
Object.defineProperty(exports, "DatabaseReferenceScanner", { enumerable: true, get: function () { return database_reference_scanner_1.DatabaseReferenceScanner; } });
|
|
31
|
+
var deletion_auditor_1 = require("./lib/deletion-auditor");
|
|
32
|
+
Object.defineProperty(exports, "DeletionAuditor", { enumerable: true, get: function () { return deletion_auditor_1.DeletionAuditor; } });
|
|
33
|
+
var deletion_report_generator_1 = require("./lib/deletion-report-generator");
|
|
34
|
+
Object.defineProperty(exports, "DeletionReportGenerator", { enumerable: true, get: function () { return deletion_report_generator_1.DeletionReportGenerator; } });
|
|
24
35
|
// Service exports
|
|
25
36
|
var InitService_1 = require("./services/InitService");
|
|
26
37
|
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;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();"]}
|
|
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,yBAAyB;AACzB,+EAA4E;AAAnE,sIAAA,wBAAwB,OAAA;AAEjC,6EAAyE;AAAhE,mIAAA,sBAAsB,OAAA;AAE/B,+EAA4E;AAAnE,sIAAA,wBAAwB,OAAA;AAEjC,2DAAyD;AAAhD,mHAAA,eAAe,OAAA;AAExB,6EAA0E;AAAjE,oIAAA,uBAAuB,OAAA;AAEhC,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// Deletion audit exports\nexport { RecordDependencyAnalyzer } from './lib/record-dependency-analyzer';\nexport type { FlattenedRecord, ReverseDependency, DependencyAnalysisResult } from './lib/record-dependency-analyzer';\nexport { EntityForeignKeyHelper } from './lib/entity-foreign-key-helper';\nexport type { ReverseFKInfo } from './lib/entity-foreign-key-helper';\nexport { DatabaseReferenceScanner } from './lib/database-reference-scanner';\nexport type { DatabaseReference } from './lib/database-reference-scanner';\nexport { DeletionAuditor } from './lib/deletion-auditor';\nexport type { DeletionAudit } from './lib/deletion-auditor';\nexport { DeletionReportGenerator } from './lib/deletion-report-generator';\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,56 @@
|
|
|
1
|
+
import { Metadata, UserInfo, CompositeKey } from '@memberjunction/core';
|
|
2
|
+
import { FlattenedRecord } from './record-dependency-analyzer';
|
|
3
|
+
import { ReverseFKInfo } from './entity-foreign-key-helper';
|
|
4
|
+
/**
|
|
5
|
+
* Represents a database record that references a record being deleted
|
|
6
|
+
*/
|
|
7
|
+
export interface DatabaseReference {
|
|
8
|
+
entityName: string;
|
|
9
|
+
primaryKey: CompositeKey;
|
|
10
|
+
referencingField: string;
|
|
11
|
+
referencedEntity: string;
|
|
12
|
+
referencedKey: CompositeKey;
|
|
13
|
+
existsInMetadata: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Scans the database for existing records that reference records marked for deletion
|
|
17
|
+
* This helps identify:
|
|
18
|
+
* 1. Database-only records that will prevent deletion
|
|
19
|
+
* 2. Records that should be included in the deletion plan
|
|
20
|
+
*/
|
|
21
|
+
export declare class DatabaseReferenceScanner {
|
|
22
|
+
private metadata;
|
|
23
|
+
private contextUser;
|
|
24
|
+
constructor(metadata: Metadata, contextUser: UserInfo);
|
|
25
|
+
/**
|
|
26
|
+
* Scan database for records that reference records marked for deletion
|
|
27
|
+
*
|
|
28
|
+
* @param recordsToDelete Records that will be deleted
|
|
29
|
+
* @param reverseFKMap Map of entity -> entities that reference it
|
|
30
|
+
* @param allMetadataRecords All records from metadata (for checking if DB record exists in metadata)
|
|
31
|
+
* @returns Array of database references found
|
|
32
|
+
*/
|
|
33
|
+
scanForReferences(recordsToDelete: FlattenedRecord[], reverseFKMap: Map<string, ReverseFKInfo[]>, allMetadataRecords: FlattenedRecord[]): Promise<DatabaseReference[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Extract primary key from a flattened record
|
|
36
|
+
*/
|
|
37
|
+
private extractPrimaryKey;
|
|
38
|
+
/**
|
|
39
|
+
* Get primary key from a simple record (plain object from RunView with ResultType: 'simple')
|
|
40
|
+
*/
|
|
41
|
+
private getPrimaryKeyFromSimpleRecord;
|
|
42
|
+
/**
|
|
43
|
+
* Check if a database record exists in metadata files
|
|
44
|
+
*/
|
|
45
|
+
private checkIfInMetadata;
|
|
46
|
+
/**
|
|
47
|
+
* Get orphaned references (database-only records not in metadata)
|
|
48
|
+
* These will prevent deletion unless handled
|
|
49
|
+
*/
|
|
50
|
+
getOrphanedReferences(references: DatabaseReference[]): DatabaseReference[];
|
|
51
|
+
/**
|
|
52
|
+
* Get metadata references (records in metadata that reference deletion targets)
|
|
53
|
+
* These should already be marked for deletion if the user set things up correctly
|
|
54
|
+
*/
|
|
55
|
+
getMetadataReferences(references: DatabaseReference[]): DatabaseReference[];
|
|
56
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DatabaseReferenceScanner = void 0;
|
|
4
|
+
const core_1 = require("@memberjunction/core");
|
|
5
|
+
/**
|
|
6
|
+
* Scans the database for existing records that reference records marked for deletion
|
|
7
|
+
* This helps identify:
|
|
8
|
+
* 1. Database-only records that will prevent deletion
|
|
9
|
+
* 2. Records that should be included in the deletion plan
|
|
10
|
+
*/
|
|
11
|
+
class DatabaseReferenceScanner {
|
|
12
|
+
metadata;
|
|
13
|
+
contextUser;
|
|
14
|
+
constructor(metadata, contextUser) {
|
|
15
|
+
this.metadata = metadata;
|
|
16
|
+
this.contextUser = contextUser;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Scan database for records that reference records marked for deletion
|
|
20
|
+
*
|
|
21
|
+
* @param recordsToDelete Records that will be deleted
|
|
22
|
+
* @param reverseFKMap Map of entity -> entities that reference it
|
|
23
|
+
* @param allMetadataRecords All records from metadata (for checking if DB record exists in metadata)
|
|
24
|
+
* @returns Array of database references found
|
|
25
|
+
*/
|
|
26
|
+
async scanForReferences(recordsToDelete, reverseFKMap, allMetadataRecords) {
|
|
27
|
+
const references = [];
|
|
28
|
+
const rv = new core_1.RunView();
|
|
29
|
+
for (const record of recordsToDelete) {
|
|
30
|
+
const entityName = record.entityName;
|
|
31
|
+
const primaryKey = this.extractPrimaryKey(record);
|
|
32
|
+
if (!primaryKey) {
|
|
33
|
+
console.warn(`Cannot scan references for ${entityName}: no primary key found`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Find all entities that could reference this one
|
|
37
|
+
const referencingEntities = reverseFKMap.get(entityName) || [];
|
|
38
|
+
for (const refInfo of referencingEntities) {
|
|
39
|
+
try {
|
|
40
|
+
// Query database for existing references
|
|
41
|
+
// Use the actual value from the primary key, not the concatenated string format
|
|
42
|
+
const pkValue = primaryKey.KeyValuePairs.length === 1
|
|
43
|
+
? primaryKey.KeyValuePairs[0].Value
|
|
44
|
+
: primaryKey.ToConcatenatedString();
|
|
45
|
+
const filter = `${refInfo.fieldName} = '${pkValue}'`;
|
|
46
|
+
const result = await rv.RunView({
|
|
47
|
+
EntityName: refInfo.entityName,
|
|
48
|
+
ExtraFilter: filter,
|
|
49
|
+
ResultType: 'simple' // More efficient - we don't need entity objects
|
|
50
|
+
}, this.contextUser);
|
|
51
|
+
if (result.Success && result.Results && result.Results.length > 0) {
|
|
52
|
+
// Found database records that reference this record
|
|
53
|
+
for (const dbRecord of result.Results) {
|
|
54
|
+
const refPrimaryKey = this.getPrimaryKeyFromSimpleRecord(dbRecord, refInfo.entityName);
|
|
55
|
+
if (!refPrimaryKey) {
|
|
56
|
+
console.warn(`Cannot get primary key for ${refInfo.entityName} record`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
references.push({
|
|
60
|
+
entityName: refInfo.entityName,
|
|
61
|
+
primaryKey: refPrimaryKey,
|
|
62
|
+
referencingField: refInfo.fieldName,
|
|
63
|
+
referencedEntity: entityName,
|
|
64
|
+
referencedKey: primaryKey,
|
|
65
|
+
existsInMetadata: this.checkIfInMetadata(refInfo.entityName, refPrimaryKey, allMetadataRecords // Check against ALL metadata, not just deletes
|
|
66
|
+
)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error(`Error scanning references from ${refInfo.entityName}.${refInfo.fieldName} ` +
|
|
73
|
+
`to ${entityName}:`, error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return references;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Extract primary key from a flattened record
|
|
81
|
+
*/
|
|
82
|
+
extractPrimaryKey(record) {
|
|
83
|
+
const entityInfo = this.metadata.Entities.find(e => e.Name === record.entityName);
|
|
84
|
+
if (!entityInfo) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const primaryKeys = entityInfo.PrimaryKeys;
|
|
88
|
+
if (primaryKeys.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Single primary key
|
|
92
|
+
if (primaryKeys.length === 1) {
|
|
93
|
+
const pkField = primaryKeys[0].Name;
|
|
94
|
+
const pkValue = record.record.primaryKey?.[pkField] || record.record.fields?.[pkField];
|
|
95
|
+
if (!pkValue) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return core_1.CompositeKey.FromID(pkValue.toString());
|
|
99
|
+
}
|
|
100
|
+
// Composite primary key
|
|
101
|
+
const keyPairs = [];
|
|
102
|
+
for (const pk of primaryKeys) {
|
|
103
|
+
const value = record.record.primaryKey?.[pk.Name] || record.record.fields?.[pk.Name];
|
|
104
|
+
if (!value) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
keyPairs.push({ FieldName: pk.Name, Value: value.toString() });
|
|
108
|
+
}
|
|
109
|
+
return core_1.CompositeKey.FromKeyValuePairs(keyPairs);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get primary key from a simple record (plain object from RunView with ResultType: 'simple')
|
|
113
|
+
*/
|
|
114
|
+
getPrimaryKeyFromSimpleRecord(record, entityName) {
|
|
115
|
+
const entityInfo = this.metadata.Entities.find(e => e.Name === entityName);
|
|
116
|
+
if (!entityInfo) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const primaryKeys = entityInfo.PrimaryKeys;
|
|
120
|
+
if (primaryKeys.length === 0) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
// Single primary key
|
|
124
|
+
if (primaryKeys.length === 1) {
|
|
125
|
+
const pkField = primaryKeys[0].Name;
|
|
126
|
+
const pkValue = record[pkField];
|
|
127
|
+
if (!pkValue) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return core_1.CompositeKey.FromID(pkValue.toString());
|
|
131
|
+
}
|
|
132
|
+
// Composite primary key
|
|
133
|
+
const keyPairs = [];
|
|
134
|
+
for (const pk of primaryKeys) {
|
|
135
|
+
const value = record[pk.Name];
|
|
136
|
+
if (!value) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
keyPairs.push({ FieldName: pk.Name, Value: value.toString() });
|
|
140
|
+
}
|
|
141
|
+
return core_1.CompositeKey.FromKeyValuePairs(keyPairs);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if a database record exists in metadata files
|
|
145
|
+
*/
|
|
146
|
+
checkIfInMetadata(entityName, primaryKey, records) {
|
|
147
|
+
const keyString = primaryKey.ToConcatenatedString();
|
|
148
|
+
for (const record of records) {
|
|
149
|
+
if (record.entityName !== entityName) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const recordKey = this.extractPrimaryKey(record);
|
|
153
|
+
if (recordKey && recordKey.ToConcatenatedString() === keyString) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get orphaned references (database-only records not in metadata)
|
|
161
|
+
* These will prevent deletion unless handled
|
|
162
|
+
*/
|
|
163
|
+
getOrphanedReferences(references) {
|
|
164
|
+
return references.filter(ref => !ref.existsInMetadata);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get metadata references (records in metadata that reference deletion targets)
|
|
168
|
+
* These should already be marked for deletion if the user set things up correctly
|
|
169
|
+
*/
|
|
170
|
+
getMetadataReferences(references) {
|
|
171
|
+
return references.filter(ref => ref.existsInMetadata);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
exports.DatabaseReferenceScanner = DatabaseReferenceScanner;
|
|
175
|
+
//# sourceMappingURL=database-reference-scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database-reference-scanner.js","sourceRoot":"","sources":["../../src/lib/database-reference-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["import { Metadata, RunView, UserInfo, CompositeKey } from '@memberjunction/core';\nimport { FlattenedRecord } from './record-dependency-analyzer';\nimport { ReverseFKInfo } from './entity-foreign-key-helper';\n\n/**\n * Represents a database record that references a record being deleted\n */\nexport interface DatabaseReference {\n entityName: string; // Entity containing the referencing record\n primaryKey: CompositeKey; // Primary key of the referencing record\n referencingField: string; // Foreign key field making the reference\n referencedEntity: string; // Entity being referenced\n referencedKey: CompositeKey; // Primary key of referenced record\n existsInMetadata: boolean; // Whether this record exists in metadata files\n}\n\n/**\n * Scans the database for existing records that reference records marked for deletion\n * This helps identify:\n * 1. Database-only records that will prevent deletion\n * 2. Records that should be included in the deletion plan\n */\nexport class DatabaseReferenceScanner {\n constructor(\n private metadata: Metadata,\n private contextUser: UserInfo\n ) {}\n\n /**\n * Scan database for records that reference records marked for deletion\n *\n * @param recordsToDelete Records that will be deleted\n * @param reverseFKMap Map of entity -> entities that reference it\n * @param allMetadataRecords All records from metadata (for checking if DB record exists in metadata)\n * @returns Array of database references found\n */\n async scanForReferences(\n recordsToDelete: FlattenedRecord[],\n reverseFKMap: Map<string, ReverseFKInfo[]>,\n allMetadataRecords: FlattenedRecord[]\n ): Promise<DatabaseReference[]> {\n const references: DatabaseReference[] = [];\n const rv = new RunView();\n\n for (const record of recordsToDelete) {\n const entityName = record.entityName;\n const primaryKey = this.extractPrimaryKey(record);\n\n if (!primaryKey) {\n console.warn(`Cannot scan references for ${entityName}: no primary key found`);\n continue;\n }\n\n // Find all entities that could reference this one\n const referencingEntities = reverseFKMap.get(entityName) || [];\n\n for (const refInfo of referencingEntities) {\n try {\n // Query database for existing references\n // Use the actual value from the primary key, not the concatenated string format\n const pkValue = primaryKey.KeyValuePairs.length === 1\n ? primaryKey.KeyValuePairs[0].Value\n : primaryKey.ToConcatenatedString();\n\n const filter = `${refInfo.fieldName} = '${pkValue}'`;\n\n const result = await rv.RunView({\n EntityName: refInfo.entityName,\n ExtraFilter: filter,\n ResultType: 'simple' // More efficient - we don't need entity objects\n }, this.contextUser);\n\n if (result.Success && result.Results && result.Results.length > 0) {\n // Found database records that reference this record\n for (const dbRecord of result.Results) {\n const refPrimaryKey = this.getPrimaryKeyFromSimpleRecord(\n dbRecord,\n refInfo.entityName\n );\n\n if (!refPrimaryKey) {\n console.warn(`Cannot get primary key for ${refInfo.entityName} record`);\n continue;\n }\n\n references.push({\n entityName: refInfo.entityName,\n primaryKey: refPrimaryKey,\n referencingField: refInfo.fieldName,\n referencedEntity: entityName,\n referencedKey: primaryKey,\n existsInMetadata: this.checkIfInMetadata(\n refInfo.entityName,\n refPrimaryKey,\n allMetadataRecords // Check against ALL metadata, not just deletes\n )\n });\n }\n }\n } catch (error) {\n console.error(\n `Error scanning references from ${refInfo.entityName}.${refInfo.fieldName} ` +\n `to ${entityName}:`,\n error\n );\n }\n }\n }\n\n return references;\n }\n\n /**\n * Extract primary key from a flattened record\n */\n private extractPrimaryKey(record: FlattenedRecord): CompositeKey | null {\n const entityInfo = this.metadata.Entities.find(e => e.Name === record.entityName);\n if (!entityInfo) {\n return null;\n }\n\n const primaryKeys = entityInfo.PrimaryKeys;\n if (primaryKeys.length === 0) {\n return null;\n }\n\n // Single primary key\n if (primaryKeys.length === 1) {\n const pkField = primaryKeys[0].Name;\n const pkValue = record.record.primaryKey?.[pkField] || record.record.fields?.[pkField];\n\n if (!pkValue) {\n return null;\n }\n\n return CompositeKey.FromID(pkValue.toString());\n }\n\n // Composite primary key\n const keyPairs: { FieldName: string; Value: string }[] = [];\n for (const pk of primaryKeys) {\n const value = record.record.primaryKey?.[pk.Name] || record.record.fields?.[pk.Name];\n if (!value) {\n return null;\n }\n keyPairs.push({ FieldName: pk.Name, Value: value.toString() });\n }\n\n return CompositeKey.FromKeyValuePairs(keyPairs);\n }\n\n /**\n * Get primary key from a simple record (plain object from RunView with ResultType: 'simple')\n */\n private getPrimaryKeyFromSimpleRecord(record: Record<string, unknown>, entityName: string): CompositeKey | null {\n const entityInfo = this.metadata.Entities.find(e => e.Name === entityName);\n if (!entityInfo) {\n return null;\n }\n\n const primaryKeys = entityInfo.PrimaryKeys;\n if (primaryKeys.length === 0) {\n return null;\n }\n\n // Single primary key\n if (primaryKeys.length === 1) {\n const pkField = primaryKeys[0].Name;\n const pkValue = record[pkField];\n\n if (!pkValue) {\n return null;\n }\n\n return CompositeKey.FromID(pkValue.toString());\n }\n\n // Composite primary key\n const keyPairs: { FieldName: string; Value: string }[] = [];\n for (const pk of primaryKeys) {\n const value = record[pk.Name];\n if (!value) {\n return null;\n }\n keyPairs.push({ FieldName: pk.Name, Value: value.toString() });\n }\n\n return CompositeKey.FromKeyValuePairs(keyPairs);\n }\n\n /**\n * Check if a database record exists in metadata files\n */\n private checkIfInMetadata(\n entityName: string,\n primaryKey: CompositeKey,\n records: FlattenedRecord[]\n ): boolean {\n const keyString = primaryKey.ToConcatenatedString();\n\n for (const record of records) {\n if (record.entityName !== entityName) {\n continue;\n }\n\n const recordKey = this.extractPrimaryKey(record);\n if (recordKey && recordKey.ToConcatenatedString() === keyString) {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Get orphaned references (database-only records not in metadata)\n * These will prevent deletion unless handled\n */\n getOrphanedReferences(references: DatabaseReference[]): DatabaseReference[] {\n return references.filter(ref => !ref.existsInMetadata);\n }\n\n /**\n * Get metadata references (records in metadata that reference deletion targets)\n * These should already be marked for deletion if the user set things up correctly\n */\n getMetadataReferences(references: DatabaseReference[]): DatabaseReference[] {\n return references.filter(ref => ref.existsInMetadata);\n }\n}\n"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Metadata, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import { FlattenedRecord, ReverseDependency } from './record-dependency-analyzer';
|
|
3
|
+
import { DatabaseReference } from './database-reference-scanner';
|
|
4
|
+
/**
|
|
5
|
+
* Complete audit result for deletion operations
|
|
6
|
+
*/
|
|
7
|
+
export interface DeletionAudit {
|
|
8
|
+
explicitDeletes: Map<string, FlattenedRecord>;
|
|
9
|
+
implicitDeletes: Map<string, FlattenedRecord>;
|
|
10
|
+
alreadyDeleted: Map<string, FlattenedRecord>;
|
|
11
|
+
databaseOnlyReferences: DatabaseReference[];
|
|
12
|
+
reverseDependencies: Map<string, ReverseDependency[]>;
|
|
13
|
+
deletionLevels: FlattenedRecord[][];
|
|
14
|
+
circularDependencies: string[][];
|
|
15
|
+
orphanedReferences: ReverseDependency[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Performs comprehensive deletion auditing across all metadata files
|
|
19
|
+
* Identifies all records that need to be deleted, in what order, and potential issues
|
|
20
|
+
*/
|
|
21
|
+
export declare class DeletionAuditor {
|
|
22
|
+
private metadata;
|
|
23
|
+
private contextUser;
|
|
24
|
+
constructor(metadata: Metadata, contextUser: UserInfo);
|
|
25
|
+
/**
|
|
26
|
+
* Perform comprehensive deletion audit across all metadata files
|
|
27
|
+
*
|
|
28
|
+
* This analyzes:
|
|
29
|
+
* 1. Which records are explicitly marked for deletion
|
|
30
|
+
* 2. Which records must be implicitly deleted (due to FK constraints)
|
|
31
|
+
* 3. Database-only references that will prevent deletion
|
|
32
|
+
* 4. Correct deletion order (reverse topological sort)
|
|
33
|
+
* 5. Circular dependencies
|
|
34
|
+
*
|
|
35
|
+
* @param allRecords All flattened records from all metadata files
|
|
36
|
+
* @returns Complete deletion audit
|
|
37
|
+
*/
|
|
38
|
+
auditDeletions(allRecords: FlattenedRecord[]): Promise<DeletionAudit>;
|
|
39
|
+
/**
|
|
40
|
+
* Find all records marked with delete: true
|
|
41
|
+
*/
|
|
42
|
+
private findExplicitDeletes;
|
|
43
|
+
/**
|
|
44
|
+
* Find all records that depend on records being deleted (implicit deletes)
|
|
45
|
+
* Uses BFS to find transitive dependents
|
|
46
|
+
*/
|
|
47
|
+
private findImplicitDeletes;
|
|
48
|
+
/**
|
|
49
|
+
* Check which records actually exist in the database
|
|
50
|
+
* Returns records separated into existing (need deletion) and already deleted
|
|
51
|
+
*/
|
|
52
|
+
private checkRecordExistence;
|
|
53
|
+
/**
|
|
54
|
+
* Find orphaned references (database records not in metadata files)
|
|
55
|
+
* These will prevent deletion unless handled
|
|
56
|
+
*/
|
|
57
|
+
private findOrphanedReferences;
|
|
58
|
+
/**
|
|
59
|
+
* Check for circular dependencies among records to delete
|
|
60
|
+
* This would prevent safe deletion order
|
|
61
|
+
*/
|
|
62
|
+
private findCircularDependencies;
|
|
63
|
+
/**
|
|
64
|
+
* Get a display name for a record (for error messages)
|
|
65
|
+
*/
|
|
66
|
+
private getRecordDisplayName;
|
|
67
|
+
/**
|
|
68
|
+
* Check if deletion audit has blocking issues
|
|
69
|
+
*/
|
|
70
|
+
isValid(audit: DeletionAudit): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Check if deletion audit requires user confirmation
|
|
73
|
+
* (due to implicit deletes)
|
|
74
|
+
*/
|
|
75
|
+
requiresConfirmation(audit: DeletionAudit): boolean;
|
|
76
|
+
}
|