@memberjunction/metadata-sync 2.117.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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Metadata, EntityDependency } from '@memberjunction/core';
|
|
2
|
+
/**
|
|
3
|
+
* Information about a reverse foreign key relationship
|
|
4
|
+
* (which entities reference a given entity)
|
|
5
|
+
*/
|
|
6
|
+
export interface ReverseFKInfo {
|
|
7
|
+
entityName: string;
|
|
8
|
+
fieldName: string;
|
|
9
|
+
relatedFieldName: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Helper utility for working with entity foreign key relationships
|
|
13
|
+
* Provides methods for building reverse FK maps and querying dependencies
|
|
14
|
+
*/
|
|
15
|
+
export declare class EntityForeignKeyHelper {
|
|
16
|
+
/**
|
|
17
|
+
* Build a reverse foreign key map
|
|
18
|
+
* Maps: entity name -> list of {entity, field} pairs that reference it
|
|
19
|
+
*
|
|
20
|
+
* Example: "Users" -> [{ entityName: "Orders", fieldName: "UserID", relatedFieldName: "ID" }]
|
|
21
|
+
*
|
|
22
|
+
* @param metadata The metadata instance
|
|
23
|
+
* @returns Map of entity name to list of reverse FK references
|
|
24
|
+
*/
|
|
25
|
+
static buildReverseFKMap(metadata: Metadata): Map<string, ReverseFKInfo[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Get entity dependencies using the Metadata API
|
|
28
|
+
* Returns all entities that have foreign keys pointing to the specified entity
|
|
29
|
+
*
|
|
30
|
+
* @param metadata The metadata instance
|
|
31
|
+
* @param entityName The entity to check dependencies for
|
|
32
|
+
* @returns Array of entity dependencies
|
|
33
|
+
*/
|
|
34
|
+
static getEntityDependencies(metadata: Metadata, entityName: string): Promise<EntityDependency[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Check if an entity has any dependent entities
|
|
37
|
+
*
|
|
38
|
+
* @param metadata The metadata instance
|
|
39
|
+
* @param entityName The entity to check
|
|
40
|
+
* @returns True if other entities reference this entity
|
|
41
|
+
*/
|
|
42
|
+
static hasDependentEntities(metadata: Metadata, entityName: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Get all foreign key fields for an entity
|
|
45
|
+
*
|
|
46
|
+
* @param metadata The metadata instance
|
|
47
|
+
* @param entityName The entity name
|
|
48
|
+
* @returns Array of foreign key field names
|
|
49
|
+
*/
|
|
50
|
+
static getForeignKeyFields(metadata: Metadata, entityName: string): string[];
|
|
51
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EntityForeignKeyHelper = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Helper utility for working with entity foreign key relationships
|
|
6
|
+
* Provides methods for building reverse FK maps and querying dependencies
|
|
7
|
+
*/
|
|
8
|
+
class EntityForeignKeyHelper {
|
|
9
|
+
/**
|
|
10
|
+
* Build a reverse foreign key map
|
|
11
|
+
* Maps: entity name -> list of {entity, field} pairs that reference it
|
|
12
|
+
*
|
|
13
|
+
* Example: "Users" -> [{ entityName: "Orders", fieldName: "UserID", relatedFieldName: "ID" }]
|
|
14
|
+
*
|
|
15
|
+
* @param metadata The metadata instance
|
|
16
|
+
* @returns Map of entity name to list of reverse FK references
|
|
17
|
+
*/
|
|
18
|
+
static buildReverseFKMap(metadata) {
|
|
19
|
+
const reverseMap = new Map();
|
|
20
|
+
for (const entity of metadata.Entities) {
|
|
21
|
+
// Skip deprecated and disabled entities to avoid deprecation warnings during database scans
|
|
22
|
+
if (entity.Status === 'Deprecated' || entity.Status === 'Disabled') {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
// Get all foreign key fields in this entity
|
|
26
|
+
const foreignKeys = entity.ForeignKeys;
|
|
27
|
+
for (const field of foreignKeys) {
|
|
28
|
+
const targetEntity = field.RelatedEntity;
|
|
29
|
+
if (!targetEntity) {
|
|
30
|
+
continue; // Skip if no related entity
|
|
31
|
+
}
|
|
32
|
+
// Add this FK to the reverse map for the target entity
|
|
33
|
+
if (!reverseMap.has(targetEntity)) {
|
|
34
|
+
reverseMap.set(targetEntity, []);
|
|
35
|
+
}
|
|
36
|
+
reverseMap.get(targetEntity).push({
|
|
37
|
+
entityName: entity.Name,
|
|
38
|
+
fieldName: field.Name,
|
|
39
|
+
relatedFieldName: field.RelatedEntityFieldName || 'ID'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return reverseMap;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get entity dependencies using the Metadata API
|
|
47
|
+
* Returns all entities that have foreign keys pointing to the specified entity
|
|
48
|
+
*
|
|
49
|
+
* @param metadata The metadata instance
|
|
50
|
+
* @param entityName The entity to check dependencies for
|
|
51
|
+
* @returns Array of entity dependencies
|
|
52
|
+
*/
|
|
53
|
+
static async getEntityDependencies(metadata, entityName) {
|
|
54
|
+
return await metadata.GetEntityDependencies(entityName);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if an entity has any dependent entities
|
|
58
|
+
*
|
|
59
|
+
* @param metadata The metadata instance
|
|
60
|
+
* @param entityName The entity to check
|
|
61
|
+
* @returns True if other entities reference this entity
|
|
62
|
+
*/
|
|
63
|
+
static async hasDependentEntities(metadata, entityName) {
|
|
64
|
+
const deps = await metadata.GetEntityDependencies(entityName);
|
|
65
|
+
return deps.length > 0;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get all foreign key fields for an entity
|
|
69
|
+
*
|
|
70
|
+
* @param metadata The metadata instance
|
|
71
|
+
* @param entityName The entity name
|
|
72
|
+
* @returns Array of foreign key field names
|
|
73
|
+
*/
|
|
74
|
+
static getForeignKeyFields(metadata, entityName) {
|
|
75
|
+
const entity = metadata.Entities.find(e => e.Name === entityName);
|
|
76
|
+
if (!entity) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return entity.ForeignKeys.map(fk => fk.Name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.EntityForeignKeyHelper = EntityForeignKeyHelper;
|
|
83
|
+
//# sourceMappingURL=entity-foreign-key-helper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-foreign-key-helper.js","sourceRoot":"","sources":["../../src/lib/entity-foreign-key-helper.ts"],"names":[],"mappings":";;;AAYA;;;GAGG;AACH,MAAa,sBAAsB;IAC/B;;;;;;;;OAQG;IACH,MAAM,CAAC,iBAAiB,CAAC,QAAkB;QACvC,MAAM,UAAU,GAAG,IAAI,GAAG,EAA2B,CAAC;QAEtD,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACrC,4FAA4F;YAC5F,IAAI,MAAM,CAAC,MAAM,KAAK,YAAY,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;gBACjE,SAAS;YACb,CAAC;YAED,4CAA4C;YAC5C,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;YAEvC,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;gBAC9B,MAAM,YAAY,GAAG,KAAK,CAAC,aAAa,CAAC;gBAEzC,IAAI,CAAC,YAAY,EAAE,CAAC;oBAChB,SAAS,CAAC,4BAA4B;gBAC1C,CAAC;gBAED,uDAAuD;gBACvD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;oBAChC,UAAU,CAAC,GAAG,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;gBACrC,CAAC;gBAED,UAAU,CAAC,GAAG,CAAC,YAAY,CAAE,CAAC,IAAI,CAAC;oBAC/B,UAAU,EAAE,MAAM,CAAC,IAAI;oBACvB,SAAS,EAAE,KAAK,CAAC,IAAI;oBACrB,gBAAgB,EAAE,KAAK,CAAC,sBAAsB,IAAI,IAAI;iBACzD,CAAC,CAAC;YACP,CAAC;QACL,CAAC;QAED,OAAO,UAAU,CAAC;IACtB,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAC9B,QAAkB,EAClB,UAAkB;QAElB,OAAO,MAAM,QAAQ,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC;IAC5D,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAC7B,QAAkB,EAClB,UAAkB;QAElB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,mBAAmB,CAAC,QAAkB,EAAE,UAAkB;QAC7D,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;QAClE,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,OAAO,EAAE,CAAC;QACd,CAAC;QAED,OAAO,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;IACjD,CAAC;CACJ;AA1FD,wDA0FC","sourcesContent":["import { Metadata, EntityDependency } from '@memberjunction/core';\n\n/**\n * Information about a reverse foreign key relationship\n * (which entities reference a given entity)\n */\nexport interface ReverseFKInfo {\n entityName: string; // Entity that has the FK\n fieldName: string; // FK field name in that entity\n relatedFieldName: string; // Field in target entity (usually 'ID')\n}\n\n/**\n * Helper utility for working with entity foreign key relationships\n * Provides methods for building reverse FK maps and querying dependencies\n */\nexport class EntityForeignKeyHelper {\n /**\n * Build a reverse foreign key map\n * Maps: entity name -> list of {entity, field} pairs that reference it\n *\n * Example: \"Users\" -> [{ entityName: \"Orders\", fieldName: \"UserID\", relatedFieldName: \"ID\" }]\n *\n * @param metadata The metadata instance\n * @returns Map of entity name to list of reverse FK references\n */\n static buildReverseFKMap(metadata: Metadata): Map<string, ReverseFKInfo[]> {\n const reverseMap = new Map<string, ReverseFKInfo[]>();\n\n for (const entity of metadata.Entities) {\n // Skip deprecated and disabled entities to avoid deprecation warnings during database scans\n if (entity.Status === 'Deprecated' || entity.Status === 'Disabled') {\n continue;\n }\n\n // Get all foreign key fields in this entity\n const foreignKeys = entity.ForeignKeys;\n\n for (const field of foreignKeys) {\n const targetEntity = field.RelatedEntity;\n\n if (!targetEntity) {\n continue; // Skip if no related entity\n }\n\n // Add this FK to the reverse map for the target entity\n if (!reverseMap.has(targetEntity)) {\n reverseMap.set(targetEntity, []);\n }\n\n reverseMap.get(targetEntity)!.push({\n entityName: entity.Name,\n fieldName: field.Name,\n relatedFieldName: field.RelatedEntityFieldName || 'ID'\n });\n }\n }\n\n return reverseMap;\n }\n\n /**\n * Get entity dependencies using the Metadata API\n * Returns all entities that have foreign keys pointing to the specified entity\n *\n * @param metadata The metadata instance\n * @param entityName The entity to check dependencies for\n * @returns Array of entity dependencies\n */\n static async getEntityDependencies(\n metadata: Metadata,\n entityName: string\n ): Promise<EntityDependency[]> {\n return await metadata.GetEntityDependencies(entityName);\n }\n\n /**\n * Check if an entity has any dependent entities\n *\n * @param metadata The metadata instance\n * @param entityName The entity to check\n * @returns True if other entities reference this entity\n */\n static async hasDependentEntities(\n metadata: Metadata,\n entityName: string\n ): Promise<boolean> {\n const deps = await metadata.GetEntityDependencies(entityName);\n return deps.length > 0;\n }\n\n /**\n * Get all foreign key fields for an entity\n *\n * @param metadata The metadata instance\n * @param entityName The entity name\n * @returns Array of foreign key field names\n */\n static getForeignKeyFields(metadata: Metadata, entityName: string): string[] {\n const entity = metadata.Entities.find(e => e.Name === entityName);\n if (!entity) {\n return [];\n }\n\n return entity.ForeignKeys.map(fk => fk.Name);\n }\n}\n"]}
|
|
@@ -91,6 +91,8 @@ export declare function getDataProvider(): DatabaseProviderBase | null;
|
|
|
91
91
|
* @param specificDir - Optional specific subdirectory name to check
|
|
92
92
|
* @param directoryOrder - Optional array specifying the order directories should be processed
|
|
93
93
|
* @param ignoreDirectories - Optional array of directory patterns to ignore
|
|
94
|
+
* @param includeFilter - Optional array of directory patterns to include (whitelist)
|
|
95
|
+
* @param excludeFilter - Optional array of directory patterns to exclude (blacklist)
|
|
94
96
|
* @returns Array of absolute directory paths containing .mj-sync.json files, ordered according to directoryOrder
|
|
95
97
|
*
|
|
96
98
|
* @example
|
|
@@ -103,6 +105,12 @@ export declare function getDataProvider(): DatabaseProviderBase | null;
|
|
|
103
105
|
*
|
|
104
106
|
* // Find directories with custom ordering
|
|
105
107
|
* const dirs = findEntityDirectories(process.cwd(), undefined, ['prompts', 'agent-types']);
|
|
108
|
+
*
|
|
109
|
+
* // Filter with include patterns
|
|
110
|
+
* const dirs = findEntityDirectories(process.cwd(), undefined, undefined, undefined, ['prompts', 'agent-*']);
|
|
111
|
+
*
|
|
112
|
+
* // Filter with exclude patterns
|
|
113
|
+
* const dirs = findEntityDirectories(process.cwd(), undefined, undefined, undefined, undefined, ['*-test', 'temp']);
|
|
106
114
|
* ```
|
|
107
115
|
*/
|
|
108
|
-
export declare function findEntityDirectories(dir: string, specificDir?: string, directoryOrder?: string[], ignoreDirectories?: string[]): string[];
|
|
116
|
+
export declare function findEntityDirectories(dir: string, specificDir?: string, directoryOrder?: string[], ignoreDirectories?: string[], includeFilter?: string[], excludeFilter?: string[]): string[];
|
|
@@ -36,6 +36,7 @@ const sql = __importStar(require("mssql"));
|
|
|
36
36
|
const sqlserver_dataprovider_1 = require("@memberjunction/sqlserver-dataprovider");
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const minimatch_1 = require("minimatch");
|
|
39
40
|
/** Global ConnectionPool instance for connection lifecycle management */
|
|
40
41
|
let globalPool = null;
|
|
41
42
|
/** Global provider instance to ensure single initialization */
|
|
@@ -189,6 +190,8 @@ exports.getDataProvider = getDataProvider;
|
|
|
189
190
|
* @param specificDir - Optional specific subdirectory name to check
|
|
190
191
|
* @param directoryOrder - Optional array specifying the order directories should be processed
|
|
191
192
|
* @param ignoreDirectories - Optional array of directory patterns to ignore
|
|
193
|
+
* @param includeFilter - Optional array of directory patterns to include (whitelist)
|
|
194
|
+
* @param excludeFilter - Optional array of directory patterns to exclude (blacklist)
|
|
192
195
|
* @returns Array of absolute directory paths containing .mj-sync.json files, ordered according to directoryOrder
|
|
193
196
|
*
|
|
194
197
|
* @example
|
|
@@ -201,9 +204,15 @@ exports.getDataProvider = getDataProvider;
|
|
|
201
204
|
*
|
|
202
205
|
* // Find directories with custom ordering
|
|
203
206
|
* const dirs = findEntityDirectories(process.cwd(), undefined, ['prompts', 'agent-types']);
|
|
207
|
+
*
|
|
208
|
+
* // Filter with include patterns
|
|
209
|
+
* const dirs = findEntityDirectories(process.cwd(), undefined, undefined, undefined, ['prompts', 'agent-*']);
|
|
210
|
+
*
|
|
211
|
+
* // Filter with exclude patterns
|
|
212
|
+
* const dirs = findEntityDirectories(process.cwd(), undefined, undefined, undefined, undefined, ['*-test', 'temp']);
|
|
204
213
|
* ```
|
|
205
214
|
*/
|
|
206
|
-
function findEntityDirectories(dir, specificDir, directoryOrder, ignoreDirectories) {
|
|
215
|
+
function findEntityDirectories(dir, specificDir, directoryOrder, ignoreDirectories, includeFilter, excludeFilter) {
|
|
207
216
|
const results = [];
|
|
208
217
|
// If specific directory is provided, check if it's an entity directory or root config directory
|
|
209
218
|
if (specificDir) {
|
|
@@ -227,7 +236,7 @@ function findEntityDirectories(dir, specificDir, directoryOrder, ignoreDirectori
|
|
|
227
236
|
...(ignoreDirectories || []),
|
|
228
237
|
...(config.ignoreDirectories || [])
|
|
229
238
|
];
|
|
230
|
-
return findEntityDirectories(targetDir, undefined, config.directoryOrder, mergedIgnoreDirectories);
|
|
239
|
+
return findEntityDirectories(targetDir, undefined, config.directoryOrder, mergedIgnoreDirectories, includeFilter, excludeFilter);
|
|
231
240
|
}
|
|
232
241
|
}
|
|
233
242
|
catch (error) {
|
|
@@ -235,7 +244,7 @@ function findEntityDirectories(dir, specificDir, directoryOrder, ignoreDirectori
|
|
|
235
244
|
}
|
|
236
245
|
}
|
|
237
246
|
// Fallback: look for entity subdirectories in the target directory
|
|
238
|
-
return findEntityDirectories(targetDir, undefined, directoryOrder, ignoreDirectories);
|
|
247
|
+
return findEntityDirectories(targetDir, undefined, directoryOrder, ignoreDirectories, includeFilter, excludeFilter);
|
|
239
248
|
}
|
|
240
249
|
return results;
|
|
241
250
|
}
|
|
@@ -278,10 +287,38 @@ function findEntityDirectories(dir, specificDir, directoryOrder, ignoreDirectori
|
|
|
278
287
|
}
|
|
279
288
|
// Sort unordered directories alphabetically
|
|
280
289
|
unorderedDirs.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
|
|
281
|
-
|
|
290
|
+
const allDirs = [...orderedDirs, ...unorderedDirs];
|
|
291
|
+
return applyDirectoryFilters(allDirs, includeFilter, excludeFilter);
|
|
282
292
|
}
|
|
283
293
|
// No ordering specified, return in alphabetical order (existing behavior)
|
|
284
|
-
|
|
294
|
+
const sortedDirs = foundDirectories.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
|
|
295
|
+
return applyDirectoryFilters(sortedDirs, includeFilter, excludeFilter);
|
|
285
296
|
}
|
|
286
297
|
exports.findEntityDirectories = findEntityDirectories;
|
|
298
|
+
/**
|
|
299
|
+
* Apply include/exclude filters to a list of directories
|
|
300
|
+
*
|
|
301
|
+
* @param directories - Array of directory paths to filter
|
|
302
|
+
* @param includeFilter - Optional array of patterns to include (whitelist)
|
|
303
|
+
* @param excludeFilter - Optional array of patterns to exclude (blacklist)
|
|
304
|
+
* @returns Filtered array of directory paths
|
|
305
|
+
*/
|
|
306
|
+
function applyDirectoryFilters(directories, includeFilter, excludeFilter) {
|
|
307
|
+
let filteredDirs = directories;
|
|
308
|
+
// Apply include filter (whitelist)
|
|
309
|
+
if (includeFilter && includeFilter.length > 0) {
|
|
310
|
+
filteredDirs = directories.filter(dir => {
|
|
311
|
+
const dirName = path.basename(dir);
|
|
312
|
+
return includeFilter.some(pattern => (0, minimatch_1.minimatch)(dirName, pattern, { nocase: true }));
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// Apply exclude filter (blacklist)
|
|
316
|
+
if (excludeFilter && excludeFilter.length > 0) {
|
|
317
|
+
filteredDirs = filteredDirs.filter(dir => {
|
|
318
|
+
const dirName = path.basename(dir);
|
|
319
|
+
return !excludeFilter.some(pattern => (0, minimatch_1.minimatch)(dirName, pattern, { nocase: true }));
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return filteredDirs;
|
|
323
|
+
}
|
|
287
324
|
//# sourceMappingURL=provider-utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-utils.js","sourceRoot":"","sources":["../../src/lib/provider-utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Database provider utilities for MetadataSync\n * @module provider-utils\n * \n * This module provides utilities for initializing and managing the database\n * connection, accessing system users, and finding entity directories. It handles\n * the mssql ConnectionPool lifecycle and MemberJunction provider initialization.\n */\n\nimport * as sql from 'mssql';\nimport { SQLServerDataProvider, SQLServerProviderConfigData, UserCache, setupSQLServerClient } from '@memberjunction/sqlserver-dataprovider';\nimport type { MJConfig } from '../config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { DatabaseProviderBase, UserInfo } from '@memberjunction/core';\n\n/** Global ConnectionPool instance for connection lifecycle management */\nlet globalPool: sql.ConnectionPool | null = null;\n\n/** Global provider instance to ensure single initialization */\nlet globalProvider: SQLServerDataProvider | null = null;\n\n/** Promise to track ongoing initialization */\nlet initializationPromise: Promise<SQLServerDataProvider> | null = null;\n\n/**\n * Initialize a SQLServerDataProvider with the given configuration\n * \n * Creates and initializes a mssql ConnectionPool for SQL Server, then sets up\n * the MemberJunction SQLServerDataProvider. The connection is stored globally\n * for proper cleanup. Auto-detects Azure SQL databases for encryption settings.\n * \n * @param config - MemberJunction configuration with database connection details\n * @returns Promise resolving to initialized SQLServerDataProvider instance\n * @throws Error if database connection fails\n * \n * @example\n * ```typescript\n * const config = loadMJConfig();\n * const provider = await initializeProvider(config);\n * // Provider is ready for use\n * ```\n */\nexport async function initializeProvider(config: MJConfig): Promise<SQLServerDataProvider> {\n // Return existing provider if already initialized\n if (globalProvider) {\n return globalProvider;\n }\n \n // Return ongoing initialization if in progress\n if (initializationPromise) {\n return initializationPromise;\n }\n \n // Start new initialization\n initializationPromise = (async () => {\n // Create mssql config\n const poolConfig: sql.config = {\n server: config.dbHost,\n port: config.dbPort ? Number(config.dbPort) : 1433,\n database: config.dbDatabase,\n user: config.dbUsername,\n password: config.dbPassword,\n options: {\n encrypt: config.dbEncrypt === 'Y' || config.dbEncrypt === 'true' || \n config.dbHost.includes('.database.windows.net'), // Auto-detect Azure SQL\n trustServerCertificate: config.dbTrustServerCertificate === 'Y',\n instanceName: config.dbInstanceName,\n enableArithAbort: true\n }\n };\n \n // Create and connect pool\n const pool = new sql.ConnectionPool(poolConfig);\n await pool.connect();\n \n // Store for cleanup\n globalPool = pool;\n \n // Create provider config\n const providerConfig = new SQLServerProviderConfigData(\n pool,\n config.mjCoreSchema || '__mj' \n );\n \n // Use setupSQLServerClient to properly initialize\n globalProvider = await setupSQLServerClient(providerConfig);\n return globalProvider;\n })();\n \n return initializationPromise;\n}\n\n/**\n * Clean up the global database connection\n * \n * Closes the mssql ConnectionPool if it exists and is connected.\n * Should be called when the CLI command completes to ensure proper cleanup.\n * \n * @returns Promise that resolves when cleanup is complete\n * \n * @example\n * ```typescript\n * try {\n * // Do work with database\n * } finally {\n * await cleanupProvider();\n * }\n * ```\n */\nexport async function cleanupProvider(): Promise<void> {\n if (globalPool && globalPool.connected) {\n await globalPool.close();\n globalPool = null;\n }\n globalProvider = null;\n initializationPromise = null;\n}\n\n/**\n * Get the system user from the UserCache\n * \n * Retrieves the \"System\" user from MemberJunction's UserCache. This user is\n * typically used for CLI operations where no specific user context exists.\n * The System user must have the Developer role to perform metadata sync operations.\n * \n * @returns The System UserInfo object\n * @throws Error if System user is not found in the cache or doesn't have Developer role\n * \n * @example\n * ```typescript\n * const systemUser = getSystemUser();\n * const syncEngine = new SyncEngine(systemUser);\n * ```\n */\nexport function getSystemUser(): UserInfo {\n const sysUser = UserCache.Instance.UserByName(\"System\", false);\n if (!sysUser) {\n throw new Error(\"System user not found in cache. Ensure the system user exists in the database.\"); \n }\n \n // Check if the System user has the Developer role\n const hasDeveloperRole = sysUser.UserRoles && sysUser.UserRoles.some(\n userRole => userRole.Role.trim().toLowerCase() === 'developer'\n );\n \n if (!hasDeveloperRole) {\n throw new Error(\n \"System user does not have the 'Developer' role. \" +\n \"The Developer role is required for metadata sync operations. \" +\n \"Please ensure the System user is assigned the Developer role in the database:\\n\" +\n \"* Add a record to the __mj.UserRole table linking the System user to the Developer role\"\n );\n }\n \n return sysUser;\n}\n\n/**\n * Get the current data provider instance\n * \n * Returns the global SQLServerDataProvider instance that was initialized by\n * initializeProvider. This allows access to data provider features like SQL logging.\n * \n * @returns The global SQLServerDataProvider instance or null if not initialized\n * \n * @example\n * ```typescript\n * const provider = getDataProvider();\n * if (provider?.CreateSqlLogger) {\n * const logger = await provider.CreateSqlLogger('/path/to/log.sql');\n * }\n * ```\n */\nexport function getDataProvider(): DatabaseProviderBase | null {\n return globalProvider;\n}\n\n/**\n * Find entity directories at the immediate level only\n * \n * Searches for directories containing .mj-sync.json files, which indicate\n * entity data directories. Only searches immediate subdirectories, not recursive.\n * If a specific directory is provided, only checks that directory.\n * \n * @param dir - Base directory to search from\n * @param specificDir - Optional specific subdirectory name to check\n * @param directoryOrder - Optional array specifying the order directories should be processed\n * @param ignoreDirectories - Optional array of directory patterns to ignore\n * @returns Array of absolute directory paths containing .mj-sync.json files, ordered according to directoryOrder\n * \n * @example\n * ```typescript\n * // Find all entity directories\n * const dirs = findEntityDirectories(process.cwd());\n * \n * // Check specific directory\n * const dirs = findEntityDirectories(process.cwd(), 'ai-prompts');\n * \n * // Find directories with custom ordering\n * const dirs = findEntityDirectories(process.cwd(), undefined, ['prompts', 'agent-types']);\n * ```\n */\nexport function findEntityDirectories(dir: string, specificDir?: string, directoryOrder?: string[], ignoreDirectories?: string[]): string[] {\n const results: string[] = [];\n \n // If specific directory is provided, check if it's an entity directory or root config directory\n if (specificDir) {\n const targetDir = path.isAbsolute(specificDir) ? specificDir : path.join(dir, specificDir);\n if (fs.existsSync(targetDir)) {\n const syncConfigPath = path.join(targetDir, '.mj-sync.json');\n const hasSyncConfig = fs.existsSync(syncConfigPath);\n \n if (hasSyncConfig) {\n try {\n const config = JSON.parse(fs.readFileSync(syncConfigPath, 'utf8'));\n \n // If this config has an entity field, it's an entity directory\n if (config.entity) {\n results.push(targetDir);\n return results;\n }\n \n // If this config has directoryOrder but no entity, treat it as a root config\n // and look for entity directories in its subdirectories\n if (config.directoryOrder) {\n // Merge ignore directories from parent with current config\n const mergedIgnoreDirectories = [\n ...(ignoreDirectories || []),\n ...(config.ignoreDirectories || [])\n ];\n return findEntityDirectories(targetDir, undefined, config.directoryOrder, mergedIgnoreDirectories);\n }\n } catch (error) {\n // If we can't parse the config, treat it as a regular directory\n }\n }\n \n // Fallback: look for entity subdirectories in the target directory\n return findEntityDirectories(targetDir, undefined, directoryOrder, ignoreDirectories);\n }\n return results;\n }\n \n // Otherwise, find all immediate subdirectories with .mj-sync.json\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n const foundDirectories: string[] = [];\n \n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n // Check if this directory should be ignored\n if (ignoreDirectories && ignoreDirectories.some(pattern => {\n // Simple pattern matching: exact name or ends with pattern\n return entry.name === pattern || entry.name.endsWith(pattern);\n })) {\n continue;\n }\n \n const subDir = path.join(dir, entry.name);\n const hasSyncConfig = fs.existsSync(path.join(subDir, '.mj-sync.json'));\n \n if (hasSyncConfig) {\n foundDirectories.push(subDir);\n }\n }\n }\n \n // If directoryOrder is specified, sort directories according to it\n if (directoryOrder && directoryOrder.length > 0) {\n const orderedDirs: string[] = [];\n const unorderedDirs: string[] = [];\n \n // First, add directories in the specified order\n for (const dirName of directoryOrder) {\n const matchingDir = foundDirectories.find(fullPath => \n path.basename(fullPath) === dirName\n );\n if (matchingDir) {\n orderedDirs.push(matchingDir);\n }\n }\n \n // Then, add any remaining directories in alphabetical order\n for (const foundDir of foundDirectories) {\n const dirName = path.basename(foundDir);\n if (!directoryOrder.includes(dirName)) {\n unorderedDirs.push(foundDir);\n }\n }\n \n // Sort unordered directories alphabetically\n unorderedDirs.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));\n \n return [...orderedDirs, ...unorderedDirs];\n }\n \n // No ordering specified, return in alphabetical order (existing behavior)\n return foundDirectories.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));\n}"]}
|
|
1
|
+
{"version":3,"file":"provider-utils.js","sourceRoot":"","sources":["../../src/lib/provider-utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Database provider utilities for MetadataSync\n * @module provider-utils\n * \n * This module provides utilities for initializing and managing the database\n * connection, accessing system users, and finding entity directories. It handles\n * the mssql ConnectionPool lifecycle and MemberJunction provider initialization.\n */\n\nimport * as sql from 'mssql';\nimport { SQLServerDataProvider, SQLServerProviderConfigData, UserCache, setupSQLServerClient } from '@memberjunction/sqlserver-dataprovider';\nimport type { MJConfig } from '../config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { DatabaseProviderBase, UserInfo } from '@memberjunction/core';\nimport { minimatch } from 'minimatch';\n\n/** Global ConnectionPool instance for connection lifecycle management */\nlet globalPool: sql.ConnectionPool | null = null;\n\n/** Global provider instance to ensure single initialization */\nlet globalProvider: SQLServerDataProvider | null = null;\n\n/** Promise to track ongoing initialization */\nlet initializationPromise: Promise<SQLServerDataProvider> | null = null;\n\n/**\n * Initialize a SQLServerDataProvider with the given configuration\n * \n * Creates and initializes a mssql ConnectionPool for SQL Server, then sets up\n * the MemberJunction SQLServerDataProvider. The connection is stored globally\n * for proper cleanup. Auto-detects Azure SQL databases for encryption settings.\n * \n * @param config - MemberJunction configuration with database connection details\n * @returns Promise resolving to initialized SQLServerDataProvider instance\n * @throws Error if database connection fails\n * \n * @example\n * ```typescript\n * const config = loadMJConfig();\n * const provider = await initializeProvider(config);\n * // Provider is ready for use\n * ```\n */\nexport async function initializeProvider(config: MJConfig): Promise<SQLServerDataProvider> {\n // Return existing provider if already initialized\n if (globalProvider) {\n return globalProvider;\n }\n \n // Return ongoing initialization if in progress\n if (initializationPromise) {\n return initializationPromise;\n }\n \n // Start new initialization\n initializationPromise = (async () => {\n // Create mssql config\n const poolConfig: sql.config = {\n server: config.dbHost,\n port: config.dbPort ? Number(config.dbPort) : 1433,\n database: config.dbDatabase,\n user: config.dbUsername,\n password: config.dbPassword,\n options: {\n encrypt: config.dbEncrypt === 'Y' || config.dbEncrypt === 'true' || \n config.dbHost.includes('.database.windows.net'), // Auto-detect Azure SQL\n trustServerCertificate: config.dbTrustServerCertificate === 'Y',\n instanceName: config.dbInstanceName,\n enableArithAbort: true\n }\n };\n \n // Create and connect pool\n const pool = new sql.ConnectionPool(poolConfig);\n await pool.connect();\n \n // Store for cleanup\n globalPool = pool;\n \n // Create provider config\n const providerConfig = new SQLServerProviderConfigData(\n pool,\n config.mjCoreSchema || '__mj' \n );\n \n // Use setupSQLServerClient to properly initialize\n globalProvider = await setupSQLServerClient(providerConfig);\n return globalProvider;\n })();\n \n return initializationPromise;\n}\n\n/**\n * Clean up the global database connection\n * \n * Closes the mssql ConnectionPool if it exists and is connected.\n * Should be called when the CLI command completes to ensure proper cleanup.\n * \n * @returns Promise that resolves when cleanup is complete\n * \n * @example\n * ```typescript\n * try {\n * // Do work with database\n * } finally {\n * await cleanupProvider();\n * }\n * ```\n */\nexport async function cleanupProvider(): Promise<void> {\n if (globalPool && globalPool.connected) {\n await globalPool.close();\n globalPool = null;\n }\n globalProvider = null;\n initializationPromise = null;\n}\n\n/**\n * Get the system user from the UserCache\n * \n * Retrieves the \"System\" user from MemberJunction's UserCache. This user is\n * typically used for CLI operations where no specific user context exists.\n * The System user must have the Developer role to perform metadata sync operations.\n * \n * @returns The System UserInfo object\n * @throws Error if System user is not found in the cache or doesn't have Developer role\n * \n * @example\n * ```typescript\n * const systemUser = getSystemUser();\n * const syncEngine = new SyncEngine(systemUser);\n * ```\n */\nexport function getSystemUser(): UserInfo {\n const sysUser = UserCache.Instance.UserByName(\"System\", false);\n if (!sysUser) {\n throw new Error(\"System user not found in cache. Ensure the system user exists in the database.\"); \n }\n \n // Check if the System user has the Developer role\n const hasDeveloperRole = sysUser.UserRoles && sysUser.UserRoles.some(\n userRole => userRole.Role.trim().toLowerCase() === 'developer'\n );\n \n if (!hasDeveloperRole) {\n throw new Error(\n \"System user does not have the 'Developer' role. \" +\n \"The Developer role is required for metadata sync operations. \" +\n \"Please ensure the System user is assigned the Developer role in the database:\\n\" +\n \"* Add a record to the __mj.UserRole table linking the System user to the Developer role\"\n );\n }\n \n return sysUser;\n}\n\n/**\n * Get the current data provider instance\n * \n * Returns the global SQLServerDataProvider instance that was initialized by\n * initializeProvider. This allows access to data provider features like SQL logging.\n * \n * @returns The global SQLServerDataProvider instance or null if not initialized\n * \n * @example\n * ```typescript\n * const provider = getDataProvider();\n * if (provider?.CreateSqlLogger) {\n * const logger = await provider.CreateSqlLogger('/path/to/log.sql');\n * }\n * ```\n */\nexport function getDataProvider(): DatabaseProviderBase | null {\n return globalProvider;\n}\n\n/**\n * Find entity directories at the immediate level only\n *\n * Searches for directories containing .mj-sync.json files, which indicate\n * entity data directories. Only searches immediate subdirectories, not recursive.\n * If a specific directory is provided, only checks that directory.\n *\n * @param dir - Base directory to search from\n * @param specificDir - Optional specific subdirectory name to check\n * @param directoryOrder - Optional array specifying the order directories should be processed\n * @param ignoreDirectories - Optional array of directory patterns to ignore\n * @param includeFilter - Optional array of directory patterns to include (whitelist)\n * @param excludeFilter - Optional array of directory patterns to exclude (blacklist)\n * @returns Array of absolute directory paths containing .mj-sync.json files, ordered according to directoryOrder\n *\n * @example\n * ```typescript\n * // Find all entity directories\n * const dirs = findEntityDirectories(process.cwd());\n *\n * // Check specific directory\n * const dirs = findEntityDirectories(process.cwd(), 'ai-prompts');\n *\n * // Find directories with custom ordering\n * const dirs = findEntityDirectories(process.cwd(), undefined, ['prompts', 'agent-types']);\n *\n * // Filter with include patterns\n * const dirs = findEntityDirectories(process.cwd(), undefined, undefined, undefined, ['prompts', 'agent-*']);\n *\n * // Filter with exclude patterns\n * const dirs = findEntityDirectories(process.cwd(), undefined, undefined, undefined, undefined, ['*-test', 'temp']);\n * ```\n */\nexport function findEntityDirectories(\n dir: string,\n specificDir?: string,\n directoryOrder?: string[],\n ignoreDirectories?: string[],\n includeFilter?: string[],\n excludeFilter?: string[]\n): string[] {\n const results: string[] = [];\n \n // If specific directory is provided, check if it's an entity directory or root config directory\n if (specificDir) {\n const targetDir = path.isAbsolute(specificDir) ? specificDir : path.join(dir, specificDir);\n if (fs.existsSync(targetDir)) {\n const syncConfigPath = path.join(targetDir, '.mj-sync.json');\n const hasSyncConfig = fs.existsSync(syncConfigPath);\n \n if (hasSyncConfig) {\n try {\n const config = JSON.parse(fs.readFileSync(syncConfigPath, 'utf8'));\n \n // If this config has an entity field, it's an entity directory\n if (config.entity) {\n results.push(targetDir);\n return results;\n }\n \n // If this config has directoryOrder but no entity, treat it as a root config\n // and look for entity directories in its subdirectories\n if (config.directoryOrder) {\n // Merge ignore directories from parent with current config\n const mergedIgnoreDirectories = [\n ...(ignoreDirectories || []),\n ...(config.ignoreDirectories || [])\n ];\n return findEntityDirectories(\n targetDir,\n undefined,\n config.directoryOrder,\n mergedIgnoreDirectories,\n includeFilter,\n excludeFilter\n );\n }\n } catch (error) {\n // If we can't parse the config, treat it as a regular directory\n }\n }\n \n // Fallback: look for entity subdirectories in the target directory\n return findEntityDirectories(targetDir, undefined, directoryOrder, ignoreDirectories, includeFilter, excludeFilter);\n }\n return results;\n }\n \n // Otherwise, find all immediate subdirectories with .mj-sync.json\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n const foundDirectories: string[] = [];\n \n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n // Check if this directory should be ignored\n if (ignoreDirectories && ignoreDirectories.some(pattern => {\n // Simple pattern matching: exact name or ends with pattern\n return entry.name === pattern || entry.name.endsWith(pattern);\n })) {\n continue;\n }\n \n const subDir = path.join(dir, entry.name);\n const hasSyncConfig = fs.existsSync(path.join(subDir, '.mj-sync.json'));\n \n if (hasSyncConfig) {\n foundDirectories.push(subDir);\n }\n }\n }\n \n // If directoryOrder is specified, sort directories according to it\n if (directoryOrder && directoryOrder.length > 0) {\n const orderedDirs: string[] = [];\n const unorderedDirs: string[] = [];\n \n // First, add directories in the specified order\n for (const dirName of directoryOrder) {\n const matchingDir = foundDirectories.find(fullPath => \n path.basename(fullPath) === dirName\n );\n if (matchingDir) {\n orderedDirs.push(matchingDir);\n }\n }\n \n // Then, add any remaining directories in alphabetical order\n for (const foundDir of foundDirectories) {\n const dirName = path.basename(foundDir);\n if (!directoryOrder.includes(dirName)) {\n unorderedDirs.push(foundDir);\n }\n }\n \n // Sort unordered directories alphabetically\n unorderedDirs.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));\n\n const allDirs = [...orderedDirs, ...unorderedDirs];\n return applyDirectoryFilters(allDirs, includeFilter, excludeFilter);\n }\n\n // No ordering specified, return in alphabetical order (existing behavior)\n const sortedDirs = foundDirectories.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));\n return applyDirectoryFilters(sortedDirs, includeFilter, excludeFilter);\n}\n\n/**\n * Apply include/exclude filters to a list of directories\n *\n * @param directories - Array of directory paths to filter\n * @param includeFilter - Optional array of patterns to include (whitelist)\n * @param excludeFilter - Optional array of patterns to exclude (blacklist)\n * @returns Filtered array of directory paths\n */\nfunction applyDirectoryFilters(\n directories: string[],\n includeFilter?: string[],\n excludeFilter?: string[]\n): string[] {\n let filteredDirs = directories;\n\n // Apply include filter (whitelist)\n if (includeFilter && includeFilter.length > 0) {\n filteredDirs = directories.filter(dir => {\n const dirName = path.basename(dir);\n return includeFilter.some(pattern =>\n minimatch(dirName, pattern, { nocase: true })\n );\n });\n }\n\n // Apply exclude filter (blacklist)\n if (excludeFilter && excludeFilter.length > 0) {\n filteredDirs = filteredDirs.filter(dir => {\n const dirName = path.basename(dir);\n return !excludeFilter.some(pattern =>\n minimatch(dirName, pattern, { nocase: true })\n );\n });\n }\n\n return filteredDirs;\n}"]}
|
|
@@ -16,6 +16,17 @@ export interface FlattenedRecord {
|
|
|
16
16
|
id: string;
|
|
17
17
|
originalIndex: number;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Represents a reverse dependency relationship
|
|
21
|
+
* (which records depend on a given record)
|
|
22
|
+
*/
|
|
23
|
+
export interface ReverseDependency {
|
|
24
|
+
recordId: string;
|
|
25
|
+
dependentId: string;
|
|
26
|
+
entityName: string;
|
|
27
|
+
fieldName: string | null;
|
|
28
|
+
filePath: string;
|
|
29
|
+
}
|
|
19
30
|
/**
|
|
20
31
|
* Result of dependency analysis
|
|
21
32
|
*/
|
|
@@ -80,4 +91,37 @@ export declare class RecordDependencyAnalyzer {
|
|
|
80
91
|
* Records in the same level have no dependencies on each other and can be processed in parallel
|
|
81
92
|
*/
|
|
82
93
|
private groupByDependencyLevels;
|
|
94
|
+
/**
|
|
95
|
+
* Build reverse dependency map from forward dependencies
|
|
96
|
+
* Maps: record ID -> list of records that depend on it
|
|
97
|
+
*
|
|
98
|
+
* This is essential for deletion ordering - we need to know what depends on a record
|
|
99
|
+
* before we can safely delete it.
|
|
100
|
+
*/
|
|
101
|
+
buildReverseDependencyMap(records: FlattenedRecord[]): Map<string, ReverseDependency[]>;
|
|
102
|
+
/**
|
|
103
|
+
* Find the foreign key field that creates a dependency
|
|
104
|
+
* Used for better error reporting
|
|
105
|
+
*/
|
|
106
|
+
private findForeignKeyFieldForDependency;
|
|
107
|
+
/**
|
|
108
|
+
* Perform reverse topological sort for deletion order
|
|
109
|
+
* Returns records grouped by dependency level, with leaf nodes (highest dependency level) first
|
|
110
|
+
*
|
|
111
|
+
* For deletions, we want to delete in reverse order of creation:
|
|
112
|
+
* - Records at highest forward dependency levels (leaf nodes) delete FIRST
|
|
113
|
+
* - Records at level 0 (root nodes with no dependencies) delete LAST
|
|
114
|
+
*
|
|
115
|
+
* This is simply the reverse of the forward topological sort used for creates.
|
|
116
|
+
*/
|
|
117
|
+
reverseTopologicalSort(records: FlattenedRecord[], reverseDependencies: Map<string, ReverseDependency[]>): FlattenedRecord[][];
|
|
118
|
+
/**
|
|
119
|
+
* Find all transitive dependents of a set of records
|
|
120
|
+
* This is useful for finding all records that must be deleted when deleting a parent
|
|
121
|
+
*
|
|
122
|
+
* @param recordIds Set of record IDs to find dependents for
|
|
123
|
+
* @param reverseDependencies Reverse dependency map
|
|
124
|
+
* @returns Set of all record IDs that depend on the input records (transitively)
|
|
125
|
+
*/
|
|
126
|
+
findTransitiveDependents(recordIds: Set<string>, reverseDependencies: Map<string, ReverseDependency[]>): Set<string>;
|
|
83
127
|
}
|
|
@@ -454,6 +454,139 @@ class RecordDependencyAnalyzer {
|
|
|
454
454
|
}
|
|
455
455
|
return levels;
|
|
456
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Build reverse dependency map from forward dependencies
|
|
459
|
+
* Maps: record ID -> list of records that depend on it
|
|
460
|
+
*
|
|
461
|
+
* This is essential for deletion ordering - we need to know what depends on a record
|
|
462
|
+
* before we can safely delete it.
|
|
463
|
+
*/
|
|
464
|
+
buildReverseDependencyMap(records) {
|
|
465
|
+
const reverseMap = new Map();
|
|
466
|
+
for (const record of records) {
|
|
467
|
+
// For each dependency this record has...
|
|
468
|
+
for (const depId of record.dependencies) {
|
|
469
|
+
// Add this record as a dependent of that dependency
|
|
470
|
+
if (!reverseMap.has(depId)) {
|
|
471
|
+
reverseMap.set(depId, []);
|
|
472
|
+
}
|
|
473
|
+
reverseMap.get(depId).push({
|
|
474
|
+
recordId: depId,
|
|
475
|
+
dependentId: record.id,
|
|
476
|
+
entityName: record.entityName,
|
|
477
|
+
fieldName: this.findForeignKeyFieldForDependency(record, depId),
|
|
478
|
+
filePath: record.path
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return reverseMap;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Find the foreign key field that creates a dependency
|
|
486
|
+
* Used for better error reporting
|
|
487
|
+
*/
|
|
488
|
+
findForeignKeyFieldForDependency(record, dependencyId) {
|
|
489
|
+
const entityInfo = this.getEntityInfo(record.entityName);
|
|
490
|
+
if (!entityInfo)
|
|
491
|
+
return null;
|
|
492
|
+
const dependentRecord = this.recordIdMap.get(dependencyId);
|
|
493
|
+
if (!dependentRecord)
|
|
494
|
+
return null;
|
|
495
|
+
// Check all foreign key fields
|
|
496
|
+
for (const field of entityInfo.ForeignKeys) {
|
|
497
|
+
const fieldValue = record.record.fields?.[field.Name];
|
|
498
|
+
// Check if this field references the dependent record
|
|
499
|
+
if (fieldValue && typeof fieldValue === 'string') {
|
|
500
|
+
// Handle @lookup references
|
|
501
|
+
if (fieldValue.startsWith(metadata_keywords_1.METADATA_KEYWORDS.LOOKUP)) {
|
|
502
|
+
const resolvedDep = this.findLookupDependency(fieldValue, record);
|
|
503
|
+
if (resolvedDep === dependencyId) {
|
|
504
|
+
return field.Name;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// Handle direct foreign key values
|
|
508
|
+
else if (!fieldValue.startsWith('@')) {
|
|
509
|
+
const relatedEntityInfo = this.getEntityInfo(field.RelatedEntity);
|
|
510
|
+
if (relatedEntityInfo) {
|
|
511
|
+
const dep = this.findRecordByPrimaryKey(field.RelatedEntity, fieldValue, relatedEntityInfo);
|
|
512
|
+
if (dep === dependencyId) {
|
|
513
|
+
return field.Name;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Perform reverse topological sort for deletion order
|
|
523
|
+
* Returns records grouped by dependency level, with leaf nodes (highest dependency level) first
|
|
524
|
+
*
|
|
525
|
+
* For deletions, we want to delete in reverse order of creation:
|
|
526
|
+
* - Records at highest forward dependency levels (leaf nodes) delete FIRST
|
|
527
|
+
* - Records at level 0 (root nodes with no dependencies) delete LAST
|
|
528
|
+
*
|
|
529
|
+
* This is simply the reverse of the forward topological sort used for creates.
|
|
530
|
+
*/
|
|
531
|
+
reverseTopologicalSort(records, reverseDependencies) {
|
|
532
|
+
// Calculate forward dependency levels (same as creation order)
|
|
533
|
+
const recordLevels = new Map();
|
|
534
|
+
// Calculate the level for each record based on its FORWARD dependencies
|
|
535
|
+
for (const record of records) {
|
|
536
|
+
let maxDependencyLevel = -1;
|
|
537
|
+
// Find the maximum level of all dependencies (things this record depends ON)
|
|
538
|
+
for (const depId of record.dependencies) {
|
|
539
|
+
const depLevel = recordLevels.get(depId);
|
|
540
|
+
if (depLevel !== undefined && depLevel > maxDependencyLevel) {
|
|
541
|
+
maxDependencyLevel = depLevel;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// This record's level is one more than its highest dependency
|
|
545
|
+
const recordLevel = maxDependencyLevel + 1;
|
|
546
|
+
recordLevels.set(record.id, recordLevel);
|
|
547
|
+
}
|
|
548
|
+
// Group records by level
|
|
549
|
+
const forwardLevels = [];
|
|
550
|
+
for (const record of records) {
|
|
551
|
+
const level = recordLevels.get(record.id) || 0;
|
|
552
|
+
if (!forwardLevels[level]) {
|
|
553
|
+
forwardLevels[level] = [];
|
|
554
|
+
}
|
|
555
|
+
forwardLevels[level].push(record);
|
|
556
|
+
}
|
|
557
|
+
// Reverse the array for deletion order
|
|
558
|
+
// Forward level 0 (roots) becomes last to delete
|
|
559
|
+
// Forward level N (leaves) becomes first to delete
|
|
560
|
+
return forwardLevels.reverse();
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Find all transitive dependents of a set of records
|
|
564
|
+
* This is useful for finding all records that must be deleted when deleting a parent
|
|
565
|
+
*
|
|
566
|
+
* @param recordIds Set of record IDs to find dependents for
|
|
567
|
+
* @param reverseDependencies Reverse dependency map
|
|
568
|
+
* @returns Set of all record IDs that depend on the input records (transitively)
|
|
569
|
+
*/
|
|
570
|
+
findTransitiveDependents(recordIds, reverseDependencies) {
|
|
571
|
+
const dependents = new Set();
|
|
572
|
+
const visited = new Set();
|
|
573
|
+
// BFS to find all transitive dependents
|
|
574
|
+
const queue = Array.from(recordIds);
|
|
575
|
+
while (queue.length > 0) {
|
|
576
|
+
const recordId = queue.shift();
|
|
577
|
+
if (visited.has(recordId))
|
|
578
|
+
continue;
|
|
579
|
+
visited.add(recordId);
|
|
580
|
+
const deps = reverseDependencies.get(recordId) || [];
|
|
581
|
+
for (const dep of deps) {
|
|
582
|
+
// Add dependent to result set
|
|
583
|
+
dependents.add(dep.dependentId);
|
|
584
|
+
// Queue for processing its dependents
|
|
585
|
+
queue.push(dep.dependentId);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return dependents;
|
|
589
|
+
}
|
|
457
590
|
}
|
|
458
591
|
exports.RecordDependencyAnalyzer = RecordDependencyAnalyzer;
|
|
459
592
|
//# sourceMappingURL=record-dependency-analyzer.js.map
|