@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.
- package/README.md +84 -2
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +9 -4
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +4 -0
- package/dist/Config/config.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +4 -3
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +72 -19
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +16 -8
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +19 -0
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +165 -35
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.d.ts.map +1 -1
- package/dist/Misc/advanced_generation.js +4 -13
- package/dist/Misc/advanced_generation.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +9 -4
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts +64 -0
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +128 -27
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/temp_batch_file.d.ts +34 -0
- package/dist/Misc/temp_batch_file.d.ts.map +1 -0
- package/dist/Misc/temp_batch_file.js +89 -0
- package/dist/Misc/temp_batch_file.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- 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
|
-
|
|
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
|
|
188
|
-
(
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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 (
|
|
509
|
-
|
|
510
|
-
|
|
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 (
|
|
539
|
-
|
|
540
|
-
|
|
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 ||
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|