@memberjunction/codegen-lib 2.103.0 → 2.105.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 (36) hide show
  1. package/README.md +84 -2
  2. package/dist/Angular/angular-codegen.d.ts.map +1 -1
  3. package/dist/Angular/angular-codegen.js +9 -4
  4. package/dist/Angular/angular-codegen.js.map +1 -1
  5. package/dist/Config/config.d.ts.map +1 -1
  6. package/dist/Config/config.js +4 -0
  7. package/dist/Config/config.js.map +1 -1
  8. package/dist/Database/manage-metadata.d.ts +4 -3
  9. package/dist/Database/manage-metadata.d.ts.map +1 -1
  10. package/dist/Database/manage-metadata.js +72 -19
  11. package/dist/Database/manage-metadata.js.map +1 -1
  12. package/dist/Database/sql.d.ts.map +1 -1
  13. package/dist/Database/sql.js +16 -8
  14. package/dist/Database/sql.js.map +1 -1
  15. package/dist/Database/sql_codegen.d.ts +19 -0
  16. package/dist/Database/sql_codegen.d.ts.map +1 -1
  17. package/dist/Database/sql_codegen.js +165 -35
  18. package/dist/Database/sql_codegen.js.map +1 -1
  19. package/dist/Misc/advanced_generation.d.ts.map +1 -1
  20. package/dist/Misc/advanced_generation.js +4 -13
  21. package/dist/Misc/advanced_generation.js.map +1 -1
  22. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  23. package/dist/Misc/entity_subclasses_codegen.js +9 -4
  24. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  25. package/dist/Misc/graphql_server_codegen.d.ts +64 -0
  26. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  27. package/dist/Misc/graphql_server_codegen.js +128 -27
  28. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  29. package/dist/Misc/temp_batch_file.d.ts +34 -0
  30. package/dist/Misc/temp_batch_file.d.ts.map +1 -0
  31. package/dist/Misc/temp_batch_file.js +89 -0
  32. package/dist/Misc/temp_batch_file.js.map +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -0
  35. package/dist/index.js.map +1 -1
  36. package/package.json +8 -11
@@ -38,6 +38,7 @@ const sqlserver_dataprovider_1 = require("@memberjunction/sqlserver-dataprovider
38
38
  const util_1 = require("../Misc/util");
39
39
  const global_1 = require("@memberjunction/global");
40
40
  const sql_logging_1 = require("../Misc/sql_logging");
41
+ const temp_batch_file_1 = require("../Misc/temp_batch_file");
41
42
  exports.SPType = {
42
43
  Create: 'Create',
43
44
  Update: 'Update',
@@ -110,9 +111,15 @@ class SQLCodeGenBase {
110
111
  }
111
112
  (0, status_logging_1.succeedSpinner)(`Custom SQL scripts completed (${(new Date().getTime() - startTime.getTime()) / 1000}s)`);
112
113
  // ALWAYS use the first filter where we only include entities that have IncludeInAPI = 1
113
- const baselineEntities = entities.filter(e => e.IncludeInAPI);
114
+ // Sort entities by name for deterministic processing order (workaround until MJCore fix in issue #1436)
115
+ const sortedEntities = entities.sort((a, b) => a.Name.localeCompare(b.Name));
116
+ const baselineEntities = sortedEntities.filter(e => e.IncludeInAPI);
114
117
  const includedEntities = baselineEntities.filter(e => config_1.configInfo.excludeSchemas.find(s => s.toLowerCase() === e.SchemaName.toLowerCase()) === undefined); //only include entities that are NOT in the excludeSchemas list
115
118
  const excludedEntities = baselineEntities.filter(e => config_1.configInfo.excludeSchemas.find(s => s.toLowerCase() === e.SchemaName.toLowerCase()) !== undefined); //only include entities that ARE in the excludeSchemas list in this array
119
+ // Initialize temp batch files for each schema
120
+ // These will be populated as SQL is generated and will be used for actual execution
121
+ const schemas = Array.from(new Set(baselineEntities.map(e => e.SchemaName)));
122
+ temp_batch_file_1.TempBatchFile.initialize(directory, schemas);
116
123
  // STEP 1.5 - Check for cascade delete dependencies that require regeneration
117
124
  (0, status_logging_1.startSpinner)('Analyzing cascade delete dependencies...');
118
125
  await this.markEntitiesForCascadeDeleteRegeneration(pool, includedEntities);
@@ -184,11 +191,27 @@ class SQLCodeGenBase {
184
191
  (0, status_logging_1.startSpinner)('Creating combined SQL files...');
185
192
  const allEntityFiles = this.createCombinedEntitySQLFiles(directory, baselineEntities);
186
193
  (0, status_logging_1.succeedSpinner)(`Created combined SQL files for ${allEntityFiles.length} schemas`);
187
- // STEP 2(e) ---- FINALLY, we now execute all the combined files by schema;
188
- (0, status_logging_1.startSpinner)('Executing combined entity SQL files...');
194
+ // STEP 2(e) ---- FINALLY, we execute SQL in proper dependency order
195
+ // Use temp batch files (which maintain CodeGen log order) if available, otherwise fall back to combined files
196
+ (0, status_logging_1.startSpinner)('Executing entity SQL files...');
189
197
  const step2eStartTime = new Date();
190
- if (!await this.SQLUtilityObject.executeSQLFiles(allEntityFiles, config_1.configInfo?.verboseOutput ?? false)) {
191
- (0, status_logging_1.failSpinner)('Failed to execute combined entity SQL files');
198
+ let executionSuccess = false;
199
+ if (temp_batch_file_1.TempBatchFile.hasContent()) {
200
+ // Execute temp batch files in dependency order (matches CodeGen run log)
201
+ const tempFiles = temp_batch_file_1.TempBatchFile.getTempFilePaths();
202
+ (0, util_1.logIf)(config_1.configInfo?.verboseOutput ?? false, `Executing ${tempFiles.length} temp batch file(s) in dependency order`);
203
+ executionSuccess = await this.SQLUtilityObject.executeSQLFiles(tempFiles, config_1.configInfo?.verboseOutput ?? false);
204
+ // Clean up temp files after execution
205
+ temp_batch_file_1.TempBatchFile.cleanup();
206
+ }
207
+ else {
208
+ // Fall back to combined files (for backward compatibility or if temp files weren't created)
209
+ (0, util_1.logIf)(config_1.configInfo?.verboseOutput ?? false, `Executing ${allEntityFiles.length} combined file(s)`);
210
+ executionSuccess = await this.SQLUtilityObject.executeSQLFiles(allEntityFiles, config_1.configInfo?.verboseOutput ?? false);
211
+ }
212
+ if (!executionSuccess) {
213
+ (0, status_logging_1.failSpinner)('Failed to execute entity SQL files');
214
+ temp_batch_file_1.TempBatchFile.cleanup(); // Cleanup on error
192
215
  return false;
193
216
  }
194
217
  const step2eEndTime = new Date();
@@ -227,6 +250,8 @@ class SQLCodeGenBase {
227
250
  }
228
251
  catch (err) {
229
252
  (0, status_logging_1.logError)(err);
253
+ // Clean up temp batch files on error
254
+ temp_batch_file_1.TempBatchFile.cleanup();
230
255
  return false;
231
256
  }
232
257
  }
@@ -446,6 +471,8 @@ class SQLCodeGenBase {
446
471
  }
447
472
  if (shouldLog) {
448
473
  sql_logging_1.SQLLogging.appendToSQLLogFile(sql, description);
474
+ // Also write to temp batch file for actual execution (matches CodeGen log order)
475
+ temp_batch_file_1.TempBatchFile.appendToTempBatchFile(sql, entity.SchemaName);
449
476
  }
450
477
  (0, util_1.logIf)(config_1.configInfo.verboseOutput, `SQL Generated for ${entity.Name}: ${description}`);
451
478
  }
@@ -475,8 +502,10 @@ class SQLCodeGenBase {
475
502
  sRet += s + '\nGO\n';
476
503
  }
477
504
  // BASE VIEW
505
+ // Only generate if BaseViewGenerated is true (respects custom views where it's false)
506
+ // forceRegeneration.baseViews only forces regeneration of views where BaseViewGenerated=true
478
507
  if (!options.onlyPermissions &&
479
- (options.entity.BaseViewGenerated || (config_1.configInfo.forceRegeneration?.enabled && config_1.configInfo.forceRegeneration?.baseViews)) &&
508
+ options.entity.BaseViewGenerated &&
480
509
  !options.entity.VirtualEntity) {
481
510
  // generate the base view
482
511
  const s = this.generateSingleEntitySQLFileHeader(options.entity, options.entity.BaseView) + await this.generateBaseView(options.pool, options.entity);
@@ -505,9 +534,9 @@ class SQLCodeGenBase {
505
534
  // CREATE SP
506
535
  if (options.entity.AllowCreateAPI && !options.entity.VirtualEntity) {
507
536
  const spName = this.getSPName(options.entity, exports.SPType.Create);
508
- if (!options.onlyPermissions &&
509
- (options.entity.spCreateGenerated ||
510
- (config_1.configInfo.forceRegeneration?.enabled && (config_1.configInfo.forceRegeneration?.spCreate || config_1.configInfo.forceRegeneration?.allStoredProcedures)))) {
537
+ // Only generate if spCreateGenerated is true (respects custom SPs where it's false)
538
+ // forceRegeneration only forces regeneration of SPs where spCreateGenerated=true
539
+ if (!options.onlyPermissions && options.entity.spCreateGenerated) {
511
540
  // generate the create SP
512
541
  const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPCreate(options.entity);
513
542
  if (options.writeFiles) {
@@ -535,9 +564,9 @@ class SQLCodeGenBase {
535
564
  // UPDATE SP
536
565
  if (options.entity.AllowUpdateAPI && !options.entity.VirtualEntity) {
537
566
  const spName = this.getSPName(options.entity, exports.SPType.Update);
538
- if (!options.onlyPermissions &&
539
- (options.entity.spUpdateGenerated ||
540
- (config_1.configInfo.forceRegeneration?.enabled && (config_1.configInfo.forceRegeneration?.spUpdate || config_1.configInfo.forceRegeneration?.allStoredProcedures)))) {
567
+ // Only generate if spUpdateGenerated is true (respects custom SPs where it's false)
568
+ // forceRegeneration only forces regeneration of SPs where spUpdateGenerated=true
569
+ if (!options.onlyPermissions && options.entity.spUpdateGenerated) {
541
570
  // generate the update SP
542
571
  const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPUpdate(options.entity);
543
572
  if (options.writeFiles) {
@@ -565,9 +594,11 @@ class SQLCodeGenBase {
565
594
  // DELETE SP
566
595
  if (options.entity.AllowDeleteAPI && !options.entity.VirtualEntity) {
567
596
  const spName = this.getSPName(options.entity, exports.SPType.Delete);
597
+ // Only generate if spDeleteGenerated is true (respects custom SPs where it's false)
598
+ // OR if this entity has cascade delete dependencies that require regeneration
599
+ // forceRegeneration only forces regeneration of SPs where spDeleteGenerated=true
568
600
  if (!options.onlyPermissions &&
569
- (options.entity.spDeleteGenerated || // Generate if marked as generated (not custom)
570
- (config_1.configInfo.forceRegeneration?.enabled && (config_1.configInfo.forceRegeneration?.spDelete || config_1.configInfo.forceRegeneration?.allStoredProcedures)) ||
601
+ (options.entity.spDeleteGenerated ||
571
602
  this.entitiesNeedingDeleteSPRegeneration.has(options.entity.ID))) {
572
603
  // generate the delete SP
573
604
  if (this.entitiesNeedingDeleteSPRegeneration.has(options.entity.ID)) {
@@ -634,11 +665,11 @@ class SQLCodeGenBase {
634
665
  getSPName(entity, type) {
635
666
  switch (type) {
636
667
  case exports.SPType.Create:
637
- return entity.spCreate && entity.spCreate.length > 0 ? entity.spCreate : 'spCreate' + entity.ClassName;
668
+ return entity.spCreate && entity.spCreate.length > 0 ? entity.spCreate : 'spCreate' + entity.BaseTableCodeName;
638
669
  case exports.SPType.Update:
639
- return entity.spUpdate && entity.spUpdate.length > 0 ? entity.spUpdate : 'spUpdate' + entity.ClassName;
670
+ return entity.spUpdate && entity.spUpdate.length > 0 ? entity.spUpdate : 'spUpdate' + entity.BaseTableCodeName;
640
671
  case exports.SPType.Delete:
641
- return entity.spDelete && entity.spDelete.length > 0 ? entity.spDelete : 'spDelete' + entity.ClassName;
672
+ return entity.spDelete && entity.spDelete.length > 0 ? entity.spDelete : 'spDelete' + entity.BaseTableCodeName;
642
673
  }
643
674
  }
644
675
  getEntityPermissionFileNames(entity) {
@@ -865,6 +896,77 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
865
896
  }
866
897
  return sOutput;
867
898
  }
899
+ /**
900
+ * Detects self-referential foreign keys in an entity (e.g., ParentTaskID pointing back to Task table)
901
+ * Returns array of field info objects representing recursive relationships
902
+ */
903
+ detectRecursiveForeignKeys(entity) {
904
+ return entity.Fields.filter(field => field.RelatedEntityID != null &&
905
+ field.RelatedEntityID === entity.ID);
906
+ }
907
+ /**
908
+ * Generates the WITH clause containing recursive CTEs for root ID calculation
909
+ * Each recursive FK gets its own CTE that traverses the hierarchy to find the root
910
+ */
911
+ generateRecursiveCTEs(entity, recursiveFKs) {
912
+ const primaryKey = entity.FirstPrimaryKey.Name;
913
+ const schemaName = entity.SchemaName;
914
+ const tableName = entity.BaseTable;
915
+ const classNameFirstChar = entity.ClassName.charAt(0).toLowerCase();
916
+ const ctes = recursiveFKs.map(field => {
917
+ const fieldName = field.Name;
918
+ const rootFieldName = `Root${fieldName}`;
919
+ const cteName = `CTE_${rootFieldName}`;
920
+ return ` ${cteName} AS (
921
+ -- Anchor: rows with no parent (root nodes)
922
+ SELECT
923
+ [${primaryKey}],
924
+ [${primaryKey}] AS [${rootFieldName}]
925
+ FROM
926
+ [${schemaName}].[${tableName}]
927
+ WHERE
928
+ [${fieldName}] IS NULL
929
+
930
+ UNION ALL
931
+
932
+ -- Recursive: traverse up the hierarchy
933
+ SELECT
934
+ child.[${primaryKey}],
935
+ parent.[${rootFieldName}]
936
+ FROM
937
+ [${schemaName}].[${tableName}] child
938
+ INNER JOIN
939
+ ${cteName} parent ON child.[${fieldName}] = parent.[${primaryKey}]
940
+ )`;
941
+ }).join(',\n');
942
+ return `WITH\n${ctes}\n`;
943
+ }
944
+ /**
945
+ * Generates the SELECT clause additions for root fields
946
+ * Example: , cte_root.[RootParentTaskID]
947
+ */
948
+ generateRootFieldSelects(recursiveFKs, classNameFirstChar) {
949
+ return recursiveFKs.map(field => {
950
+ const rootFieldName = `Root${field.Name}`;
951
+ const cteName = `CTE_${rootFieldName}`;
952
+ return `,\n ${cteName}.[${rootFieldName}]`;
953
+ }).join('');
954
+ }
955
+ /**
956
+ * Generates LEFT OUTER JOINs to the recursive CTEs
957
+ */
958
+ generateRecursiveCTEJoins(recursiveFKs, classNameFirstChar, entity) {
959
+ if (recursiveFKs.length === 0) {
960
+ return '';
961
+ }
962
+ const primaryKey = entity.FirstPrimaryKey.Name;
963
+ const joins = recursiveFKs.map(field => {
964
+ const rootFieldName = `Root${field.Name}`;
965
+ const cteName = `CTE_${rootFieldName}`;
966
+ return `LEFT OUTER JOIN\n ${cteName}\n ON\n [${classNameFirstChar}].[${primaryKey}] = ${cteName}.[${primaryKey}]`;
967
+ }).join('\n');
968
+ return '\n' + joins;
969
+ }
868
970
  async generateBaseView(pool, entity) {
869
971
  const viewName = entity.BaseView ? entity.BaseView : `vw${entity.CodeName}`;
870
972
  const classNameFirstChar = entity.ClassName.charAt(0).toLowerCase();
@@ -874,6 +976,10 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
874
976
  const whereClause = entity.DeleteType === 'Soft' ? `WHERE
875
977
  ${classNameFirstChar}.[${core_1.EntityInfo.DeletedAtFieldName}] IS NULL
876
978
  ` : '';
979
+ // Detect recursive foreign keys and generate CTEs
980
+ const recursiveFKs = this.detectRecursiveForeignKeys(entity);
981
+ const cteClause = recursiveFKs.length > 0 ? this.generateRecursiveCTEs(entity, recursiveFKs) : '';
982
+ const rootFields = recursiveFKs.length > 0 ? this.generateRootFieldSelects(recursiveFKs, classNameFirstChar) : '';
877
983
  return `
878
984
  ------------------------------------------------------------
879
985
  ----- BASE VIEW FOR ENTITY: ${entity.Name}
@@ -881,15 +987,16 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
881
987
  ----- BASE TABLE: ${entity.BaseTable}
882
988
  ----- PRIMARY KEY: ${entity.PrimaryKeys.map(pk => pk.Name).join(', ')}
883
989
  ------------------------------------------------------------
884
- DROP VIEW IF EXISTS [${entity.SchemaName}].[${viewName}]
990
+ IF OBJECT_ID('[${entity.SchemaName}].[${viewName}]', 'V') IS NOT NULL
991
+ DROP VIEW [${entity.SchemaName}].[${viewName}];
885
992
  GO
886
993
 
887
994
  CREATE VIEW [${entity.SchemaName}].[${viewName}]
888
995
  AS
889
- SELECT
890
- ${classNameFirstChar}.*${relatedFieldsString.length > 0 ? ',' : ''}${relatedFieldsString}
996
+ ${cteClause}SELECT
997
+ ${classNameFirstChar}.*${relatedFieldsString.length > 0 ? ',' : ''}${relatedFieldsString}${rootFields}
891
998
  FROM
892
- [${entity.SchemaName}].[${entity.BaseTable}] AS ${classNameFirstChar}${relatedFieldsJoinString ? '\n' + relatedFieldsJoinString : ''}
999
+ [${entity.SchemaName}].[${entity.BaseTable}] AS ${classNameFirstChar}${relatedFieldsJoinString ? '\n' + relatedFieldsJoinString : ''}${this.generateRecursiveCTEJoins(recursiveFKs, classNameFirstChar, entity)}
893
1000
  ${whereClause}GO${permissions}
894
1001
  `;
895
1002
  }
@@ -999,7 +1106,7 @@ ${whereClause}GO${permissions}
999
1106
  return (sOutput == '' ? '' : '\n') + sOutput;
1000
1107
  }
1001
1108
  generateSPCreate(entity) {
1002
- const spName = entity.spCreate ? entity.spCreate : `spCreate${entity.ClassName}`;
1109
+ const spName = entity.spCreate ? entity.spCreate : `spCreate${entity.BaseTableCodeName}`;
1003
1110
  const firstKey = entity.FirstPrimaryKey;
1004
1111
  //double exclamations used on the firstKey.DefaultValue property otherwise the type of this variable is 'number | ""';
1005
1112
  const primaryKeyAutomatic = firstKey.AutoIncrement; // Only exclude auto-increment fields, allow manual override for all other PKs including UUIDs with defaults
@@ -1081,7 +1188,8 @@ ${whereClause}GO${permissions}
1081
1188
  ------------------------------------------------------------
1082
1189
  ----- CREATE PROCEDURE FOR ${entity.BaseTable}
1083
1190
  ------------------------------------------------------------
1084
- DROP PROCEDURE IF EXISTS [${entity.SchemaName}].[${spName}]
1191
+ IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
1192
+ DROP PROCEDURE [${entity.SchemaName}].[${spName}];
1085
1193
  GO
1086
1194
 
1087
1195
  CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
@@ -1113,7 +1221,8 @@ GO${permissions}
1113
1221
  ------------------------------------------------------------
1114
1222
  ----- TRIGGER FOR ${core_1.EntityInfo.UpdatedAtFieldName} field for the ${entity.BaseTable} table
1115
1223
  ------------------------------------------------------------
1116
- DROP TRIGGER IF EXISTS [${entity.SchemaName}].trgUpdate${entity.ClassName}
1224
+ IF OBJECT_ID('[${entity.SchemaName}].[trgUpdate${entity.ClassName}]', 'TR') IS NOT NULL
1225
+ DROP TRIGGER [${entity.SchemaName}].[trgUpdate${entity.ClassName}];
1117
1226
  GO
1118
1227
  CREATE TRIGGER [${entity.SchemaName}].trgUpdate${entity.ClassName}
1119
1228
  ON [${entity.SchemaName}].[${entity.BaseTable}]
@@ -1135,7 +1244,7 @@ GO`;
1135
1244
  return triggerStatement;
1136
1245
  }
1137
1246
  generateSPUpdate(entity) {
1138
- const spName = entity.spUpdate ? entity.spUpdate : `spUpdate${entity.ClassName}`;
1247
+ const spName = entity.spUpdate ? entity.spUpdate : `spUpdate${entity.BaseTableCodeName}`;
1139
1248
  const efParamString = this.createEntityFieldsParamString(entity.Fields, true);
1140
1249
  const permissions = this.generateSPPermissions(entity, spName, exports.SPType.Update);
1141
1250
  const hasUpdatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() === core_1.EntityInfo.UpdatedAtFieldName.trim().toLowerCase()) !== undefined;
@@ -1151,7 +1260,8 @@ GO`;
1151
1260
  ------------------------------------------------------------
1152
1261
  ----- UPDATE PROCEDURE FOR ${entity.BaseTable}
1153
1262
  ------------------------------------------------------------
1154
- DROP PROCEDURE IF EXISTS [${entity.SchemaName}].[${spName}]
1263
+ IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
1264
+ DROP PROCEDURE [${entity.SchemaName}].[${spName}];
1155
1265
  GO
1156
1266
 
1157
1267
  CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
@@ -1277,7 +1387,7 @@ ${updatedAtTrigger}
1277
1387
  return sOutput;
1278
1388
  }
1279
1389
  generateSPDelete(entity) {
1280
- const spName = entity.spDelete ? entity.spDelete : `spDelete${entity.ClassName}`;
1390
+ const spName = entity.spDelete ? entity.spDelete : `spDelete${entity.BaseTableCodeName}`;
1281
1391
  const sCascadeDeletes = this.generateCascadeDeletes(entity);
1282
1392
  const permissions = this.generateSPPermissions(entity, spName, exports.SPType.Delete);
1283
1393
  let sVariables = '';
@@ -1318,7 +1428,8 @@ ${deleteCode} AND ${core_1.EntityInfo.DeletedAtFieldName} IS NULL -- don'
1318
1428
  ------------------------------------------------------------
1319
1429
  ----- DELETE PROCEDURE FOR ${entity.BaseTable}
1320
1430
  ------------------------------------------------------------
1321
- DROP PROCEDURE IF EXISTS [${entity.SchemaName}].[${spName}]
1431
+ IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
1432
+ DROP PROCEDURE [${entity.SchemaName}].[${spName}];
1322
1433
  GO
1323
1434
 
1324
1435
  CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
@@ -1370,7 +1481,7 @@ GO${permissions}
1370
1481
  else if (fkField.AllowsNull && !relatedEntity.AllowUpdateAPI) {
1371
1482
  // Nullable FK but no update API - this is a configuration error
1372
1483
  const sqlComment = `WARNING: ${relatedEntity.BaseTable} has nullable FK to ${parentEntity.BaseTable} but doesn't allow update API - cascade operation will fail`;
1373
- const consoleMsg = `WARNING in spDelete${parentEntity.ClassName} generation: ${relatedEntity.BaseTable} has nullable FK to ${parentEntity.BaseTable} but doesn't allow update API - cascade operation will fail`;
1484
+ const consoleMsg = `WARNING in spDelete${parentEntity.BaseTableCodeName} generation: ${relatedEntity.BaseTable} has nullable FK to ${parentEntity.BaseTable} but doesn't allow update API - cascade operation will fail`;
1374
1485
  (0, status_logging_1.logWarning)(consoleMsg);
1375
1486
  return `
1376
1487
  -- ${sqlComment}
@@ -1379,7 +1490,7 @@ GO${permissions}
1379
1490
  else if (!relatedEntity.AllowDeleteAPI) {
1380
1491
  // Entity doesn't allow delete API, so we can't cascade delete
1381
1492
  const sqlComment = `WARNING: ${relatedEntity.BaseTable} has non-nullable FK to ${parentEntity.BaseTable} but doesn't allow delete API - cascade operation will fail`;
1382
- const consoleMsg = `WARNING in spDelete${parentEntity.ClassName} generation: ${relatedEntity.BaseTable} has non-nullable FK to ${parentEntity.BaseTable} but doesn't allow delete API - cascade operation will fail`;
1493
+ const consoleMsg = `WARNING in spDelete${parentEntity.BaseTableCodeName} generation: ${relatedEntity.BaseTable} has non-nullable FK to ${parentEntity.BaseTable} but doesn't allow delete API - cascade operation will fail`;
1383
1494
  (0, status_logging_1.logWarning)(consoleMsg);
1384
1495
  return `
1385
1496
  -- ${sqlComment}
@@ -1519,6 +1630,11 @@ GO${permissions}
1519
1630
  for (const e of md.Entities) {
1520
1631
  for (const ef of e.Fields) {
1521
1632
  if (ef.RelatedEntityID === entity.ID && ef.IsVirtual === false) {
1633
+ // Skip self-referential foreign keys (e.g., ParentID pointing to same entity)
1634
+ // These don't create inter-entity dependencies for ordering purposes
1635
+ if (e.ID === entity.ID) {
1636
+ continue;
1637
+ }
1522
1638
  // Check if this would generate a cascade operation
1523
1639
  const wouldGenerateOperation = (ef.AllowsNull === false && e.AllowDeleteAPI) || // Non-nullable FK: cascade delete
1524
1640
  (ef.AllowsNull && e.AllowUpdateAPI); // Nullable FK: cascade update
@@ -1699,23 +1815,27 @@ GO${permissions}
1699
1815
  }
1700
1816
  }
1701
1817
  }
1702
- // Topological sort using DFS
1818
+ // Topological sort using DFS with circular dependency handling
1819
+ const circularDeps = new Set();
1703
1820
  const visit = (entityId) => {
1704
1821
  if (visited.has(entityId)) {
1705
1822
  return true;
1706
1823
  }
1707
1824
  if (visiting.has(entityId)) {
1708
- // Circular dependency detected
1825
+ // Circular dependency detected - mark it but don't fail
1709
1826
  const entity = entities.find(e => e.ID === entityId);
1710
1827
  (0, status_logging_1.logStatus)(`Warning: Circular cascade delete dependency detected involving ${entity?.Name || entityId}`);
1711
- return false;
1828
+ circularDeps.add(entityId);
1829
+ return false; // Signal circular dependency but continue processing
1712
1830
  }
1713
1831
  visiting.add(entityId);
1714
1832
  // Visit dependencies first
1715
1833
  const dependencies = reverseMap.get(entityId) || new Set();
1716
1834
  for (const depId of dependencies) {
1717
1835
  if (!visit(depId)) {
1718
- return false;
1836
+ // If dependency visit failed (circular), skip this dependency edge
1837
+ // but continue processing other dependencies
1838
+ continue;
1719
1839
  }
1720
1840
  }
1721
1841
  visiting.delete(entityId);
@@ -1726,9 +1846,19 @@ GO${permissions}
1726
1846
  // Visit all entities that need ordering
1727
1847
  for (const entityId of entityIdsToOrder) {
1728
1848
  if (!visited.has(entityId)) {
1729
- visit(entityId);
1849
+ const success = visit(entityId);
1850
+ if (!success && circularDeps.has(entityId)) {
1851
+ // Entity is part of circular dependency - add it anyway in arbitrary order
1852
+ // The SQL will still be generated, just not in perfect dependency order
1853
+ (0, status_logging_1.logStatus)(` - Adding ${entities.find(e => e.ID === entityId)?.Name || entityId} despite circular dependency`);
1854
+ visited.add(entityId);
1855
+ ordered.push(entityId);
1856
+ }
1730
1857
  }
1731
1858
  }
1859
+ if (circularDeps.size > 0) {
1860
+ (0, status_logging_1.logStatus)(`Note: ${circularDeps.size} entities have circular cascade delete dependencies and will be regenerated in arbitrary order.`);
1861
+ }
1732
1862
  return ordered;
1733
1863
  }
1734
1864
  }