@memberjunction/metadata-sync 2.117.0 → 2.119.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.
Files changed (37) hide show
  1. package/README.md +24 -0
  2. package/dist/index.d.ts +9 -0
  3. package/dist/index.js +12 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/database-reference-scanner.d.ts +56 -0
  6. package/dist/lib/database-reference-scanner.js +175 -0
  7. package/dist/lib/database-reference-scanner.js.map +1 -0
  8. package/dist/lib/deletion-auditor.d.ts +76 -0
  9. package/dist/lib/deletion-auditor.js +219 -0
  10. package/dist/lib/deletion-auditor.js.map +1 -0
  11. package/dist/lib/deletion-report-generator.d.ts +58 -0
  12. package/dist/lib/deletion-report-generator.js +287 -0
  13. package/dist/lib/deletion-report-generator.js.map +1 -0
  14. package/dist/lib/entity-foreign-key-helper.d.ts +51 -0
  15. package/dist/lib/entity-foreign-key-helper.js +83 -0
  16. package/dist/lib/entity-foreign-key-helper.js.map +1 -0
  17. package/dist/lib/provider-utils.d.ts +9 -1
  18. package/dist/lib/provider-utils.js +42 -5
  19. package/dist/lib/provider-utils.js.map +1 -1
  20. package/dist/lib/record-dependency-analyzer.d.ts +44 -0
  21. package/dist/lib/record-dependency-analyzer.js +133 -0
  22. package/dist/lib/record-dependency-analyzer.js.map +1 -1
  23. package/dist/services/PullService.d.ts +2 -0
  24. package/dist/services/PullService.js +4 -0
  25. package/dist/services/PullService.js.map +1 -1
  26. package/dist/services/PushService.d.ts +42 -2
  27. package/dist/services/PushService.js +451 -109
  28. package/dist/services/PushService.js.map +1 -1
  29. package/dist/services/StatusService.d.ts +2 -0
  30. package/dist/services/StatusService.js +5 -1
  31. package/dist/services/StatusService.js.map +1 -1
  32. package/dist/services/ValidationService.d.ts +4 -0
  33. package/dist/services/ValidationService.js +32 -2
  34. package/dist/services/ValidationService.js.map +1 -1
  35. package/dist/types/validation.d.ts +2 -0
  36. package/dist/types/validation.js.map +1 -1
  37. 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
- return [...orderedDirs, ...unorderedDirs];
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
- return foundDirectories.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
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":";AAAA;;;;;;;GAOG;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,2CAA6B;AAC7B,mFAA6I;AAE7I,uCAAyB;AACzB,2CAA6B;AAG7B,yEAAyE;AACzE,IAAI,UAAU,GAA8B,IAAI,CAAC;AAEjD,+DAA+D;AAC/D,IAAI,cAAc,GAAiC,IAAI,CAAC;AAExD,8CAA8C;AAC9C,IAAI,qBAAqB,GAA0C,IAAI,CAAC;AAExE;;;;;;;;;;;;;;;;;GAiBG;AACI,KAAK,UAAU,kBAAkB,CAAC,MAAgB;IACvD,kDAAkD;IAClD,IAAI,cAAc,EAAE,CAAC;QACnB,OAAO,cAAc,CAAC;IACxB,CAAC;IAED,+CAA+C;IAC/C,IAAI,qBAAqB,EAAE,CAAC;QAC1B,OAAO,qBAAqB,CAAC;IAC/B,CAAC;IAED,2BAA2B;IAC3B,qBAAqB,GAAG,CAAC,KAAK,IAAI,EAAE;QAClC,sBAAsB;QACtB,MAAM,UAAU,GAAe;YAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI;YAClD,QAAQ,EAAE,MAAM,CAAC,UAAU;YAC3B,IAAI,EAAE,MAAM,CAAC,UAAU;YACvB,QAAQ,EAAE,MAAM,CAAC,UAAU;YAC3B,OAAO,EAAE;gBACP,OAAO,EAAE,MAAM,CAAC,SAAS,KAAK,GAAG,IAAI,MAAM,CAAC,SAAS,KAAK,MAAM;oBACvD,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,wBAAwB;gBAClF,sBAAsB,EAAE,MAAM,CAAC,wBAAwB,KAAK,GAAG;gBAC/D,YAAY,EAAE,MAAM,CAAC,cAAc;gBACnC,gBAAgB,EAAE,IAAI;aACvB;SACF,CAAC;QAEF,0BAA0B;QAC1B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;QAChD,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QAErB,oBAAoB;QACpB,UAAU,GAAG,IAAI,CAAC;QAElB,yBAAyB;QACzB,MAAM,cAAc,GAAG,IAAI,oDAA2B,CACpD,IAAI,EACJ,MAAM,CAAC,YAAY,IAAI,MAAM,CAC9B,CAAC;QAEF,kDAAkD;QAClD,cAAc,GAAG,MAAM,IAAA,6CAAoB,EAAC,cAAc,CAAC,CAAC;QAC5D,OAAO,cAAc,CAAC;IACxB,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO,qBAAqB,CAAC;AAC/B,CAAC;AAhDD,gDAgDC;AAED;;;;;;;;;;;;;;;;GAgBG;AACI,KAAK,UAAU,eAAe;IACnC,IAAI,UAAU,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;QACvC,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC;QACzB,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC;IACD,cAAc,GAAG,IAAI,CAAC;IACtB,qBAAqB,GAAG,IAAI,CAAC;AAC/B,CAAC;AAPD,0CAOC;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,aAAa;IAC3B,MAAM,OAAO,GAAG,kCAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC/D,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,gFAAgF,CAAC,CAAC;IACpG,CAAC;IAED,kDAAkD;IAClD,MAAM,gBAAgB,GAAG,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,IAAI,CAClE,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,WAAW,CAC/D,CAAC;IAEF,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,kDAAkD;YAClD,+DAA+D;YAC/D,iFAAiF;YACjF,yFAAyF,CAC1F,CAAC;IACJ,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AArBD,sCAqBC;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,eAAe;IAC7B,OAAO,cAAc,CAAC;AACxB,CAAC;AAFD,0CAEC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,SAAgB,qBAAqB,CAAC,GAAW,EAAE,WAAoB,EAAE,cAAyB,EAAE,iBAA4B;IAC9H,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,gGAAgG;IAChG,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAC3F,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7B,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;YAC7D,MAAM,aAAa,GAAG,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;YAEpD,IAAI,aAAa,EAAE,CAAC;gBAClB,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC;oBAEnE,+DAA+D;oBAC/D,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;wBAClB,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;wBACxB,OAAO,OAAO,CAAC;oBACjB,CAAC;oBAED,6EAA6E;oBAC7E,wDAAwD;oBACxD,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;wBAC1B,2DAA2D;wBAC3D,MAAM,uBAAuB,GAAG;4BAC9B,GAAG,CAAC,iBAAiB,IAAI,EAAE,CAAC;4BAC5B,GAAG,CAAC,MAAM,CAAC,iBAAiB,IAAI,EAAE,CAAC;yBACpC,CAAC;wBACF,OAAO,qBAAqB,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE,uBAAuB,CAAC,CAAC;oBACrG,CAAC;gBACH,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,gEAAgE;gBAClE,CAAC;YACH,CAAC;YAED,mEAAmE;YACnE,OAAO,qBAAqB,CAAC,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,iBAAiB,CAAC,CAAC;QACxF,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,kEAAkE;IAClE,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,MAAM,gBAAgB,GAAa,EAAE,CAAC;IAEtC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACvD,4CAA4C;YAC5C,IAAI,iBAAiB,IAAI,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;gBACxD,2DAA2D;gBAC3D,OAAO,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YAChE,CAAC,CAAC,EAAE,CAAC;gBACH,SAAS;YACX,CAAC;YAED,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAC1C,MAAM,aAAa,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;YAExE,IAAI,aAAa,EAAE,CAAC;gBAClB,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAChC,CAAC;QACH,CAAC;IACH,CAAC;IAED,mEAAmE;IACnE,IAAI,cAAc,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChD,MAAM,WAAW,GAAa,EAAE,CAAC;QACjC,MAAM,aAAa,GAAa,EAAE,CAAC;QAEnC,gDAAgD;QAChD,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;YACrC,MAAM,WAAW,GAAG,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CACnD,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,OAAO,CACpC,CAAC;YACF,IAAI,WAAW,EAAE,CAAC;gBAChB,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAChC,CAAC;QACH,CAAC;QAED,4DAA4D;QAC5D,KAAK,MAAM,QAAQ,IAAI,gBAAgB,EAAE,CAAC;YACxC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACxC,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBACtC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;QAED,4CAA4C;QAC5C,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAE/E,OAAO,CAAC,GAAG,WAAW,EAAE,GAAG,aAAa,CAAC,CAAC;IAC5C,CAAC;IAED,0EAA0E;IAC1E,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3F,CAAC;AA/FD,sDA+FC","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