@memberjunction/codegen-lib 2.104.0 → 2.106.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.
@@ -502,8 +502,10 @@ class SQLCodeGenBase {
502
502
  sRet += s + '\nGO\n';
503
503
  }
504
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
505
507
  if (!options.onlyPermissions &&
506
- (options.entity.BaseViewGenerated || (config_1.configInfo.forceRegeneration?.enabled && config_1.configInfo.forceRegeneration?.baseViews)) &&
508
+ options.entity.BaseViewGenerated &&
507
509
  !options.entity.VirtualEntity) {
508
510
  // generate the base view
509
511
  const s = this.generateSingleEntitySQLFileHeader(options.entity, options.entity.BaseView) + await this.generateBaseView(options.pool, options.entity);
@@ -532,9 +534,9 @@ class SQLCodeGenBase {
532
534
  // CREATE SP
533
535
  if (options.entity.AllowCreateAPI && !options.entity.VirtualEntity) {
534
536
  const spName = this.getSPName(options.entity, exports.SPType.Create);
535
- if (!options.onlyPermissions &&
536
- (options.entity.spCreateGenerated ||
537
- (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) {
538
540
  // generate the create SP
539
541
  const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPCreate(options.entity);
540
542
  if (options.writeFiles) {
@@ -562,9 +564,9 @@ class SQLCodeGenBase {
562
564
  // UPDATE SP
563
565
  if (options.entity.AllowUpdateAPI && !options.entity.VirtualEntity) {
564
566
  const spName = this.getSPName(options.entity, exports.SPType.Update);
565
- if (!options.onlyPermissions &&
566
- (options.entity.spUpdateGenerated ||
567
- (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) {
568
570
  // generate the update SP
569
571
  const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPUpdate(options.entity);
570
572
  if (options.writeFiles) {
@@ -592,9 +594,11 @@ class SQLCodeGenBase {
592
594
  // DELETE SP
593
595
  if (options.entity.AllowDeleteAPI && !options.entity.VirtualEntity) {
594
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
595
600
  if (!options.onlyPermissions &&
596
- (options.entity.spDeleteGenerated || // Generate if marked as generated (not custom)
597
- (config_1.configInfo.forceRegeneration?.enabled && (config_1.configInfo.forceRegeneration?.spDelete || config_1.configInfo.forceRegeneration?.allStoredProcedures)) ||
601
+ (options.entity.spDeleteGenerated ||
598
602
  this.entitiesNeedingDeleteSPRegeneration.has(options.entity.ID))) {
599
603
  // generate the delete SP
600
604
  if (this.entitiesNeedingDeleteSPRegeneration.has(options.entity.ID)) {
@@ -892,6 +896,77 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
892
896
  }
893
897
  return sOutput;
894
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
+ }
895
970
  async generateBaseView(pool, entity) {
896
971
  const viewName = entity.BaseView ? entity.BaseView : `vw${entity.CodeName}`;
897
972
  const classNameFirstChar = entity.ClassName.charAt(0).toLowerCase();
@@ -901,6 +976,10 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
901
976
  const whereClause = entity.DeleteType === 'Soft' ? `WHERE
902
977
  ${classNameFirstChar}.[${core_1.EntityInfo.DeletedAtFieldName}] IS NULL
903
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) : '';
904
983
  return `
905
984
  ------------------------------------------------------------
906
985
  ----- BASE VIEW FOR ENTITY: ${entity.Name}
@@ -908,15 +987,16 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
908
987
  ----- BASE TABLE: ${entity.BaseTable}
909
988
  ----- PRIMARY KEY: ${entity.PrimaryKeys.map(pk => pk.Name).join(', ')}
910
989
  ------------------------------------------------------------
911
- DROP VIEW IF EXISTS [${entity.SchemaName}].[${viewName}]
990
+ IF OBJECT_ID('[${entity.SchemaName}].[${viewName}]', 'V') IS NOT NULL
991
+ DROP VIEW [${entity.SchemaName}].[${viewName}];
912
992
  GO
913
993
 
914
994
  CREATE VIEW [${entity.SchemaName}].[${viewName}]
915
995
  AS
916
- SELECT
917
- ${classNameFirstChar}.*${relatedFieldsString.length > 0 ? ',' : ''}${relatedFieldsString}
996
+ ${cteClause}SELECT
997
+ ${classNameFirstChar}.*${relatedFieldsString.length > 0 ? ',' : ''}${relatedFieldsString}${rootFields}
918
998
  FROM
919
- [${entity.SchemaName}].[${entity.BaseTable}] AS ${classNameFirstChar}${relatedFieldsJoinString ? '\n' + relatedFieldsJoinString : ''}
999
+ [${entity.SchemaName}].[${entity.BaseTable}] AS ${classNameFirstChar}${relatedFieldsJoinString ? '\n' + relatedFieldsJoinString : ''}${this.generateRecursiveCTEJoins(recursiveFKs, classNameFirstChar, entity)}
920
1000
  ${whereClause}GO${permissions}
921
1001
  `;
922
1002
  }
@@ -1108,7 +1188,8 @@ ${whereClause}GO${permissions}
1108
1188
  ------------------------------------------------------------
1109
1189
  ----- CREATE PROCEDURE FOR ${entity.BaseTable}
1110
1190
  ------------------------------------------------------------
1111
- DROP PROCEDURE IF EXISTS [${entity.SchemaName}].[${spName}]
1191
+ IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
1192
+ DROP PROCEDURE [${entity.SchemaName}].[${spName}];
1112
1193
  GO
1113
1194
 
1114
1195
  CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
@@ -1140,7 +1221,8 @@ GO${permissions}
1140
1221
  ------------------------------------------------------------
1141
1222
  ----- TRIGGER FOR ${core_1.EntityInfo.UpdatedAtFieldName} field for the ${entity.BaseTable} table
1142
1223
  ------------------------------------------------------------
1143
- 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}];
1144
1226
  GO
1145
1227
  CREATE TRIGGER [${entity.SchemaName}].trgUpdate${entity.ClassName}
1146
1228
  ON [${entity.SchemaName}].[${entity.BaseTable}]
@@ -1178,7 +1260,8 @@ GO`;
1178
1260
  ------------------------------------------------------------
1179
1261
  ----- UPDATE PROCEDURE FOR ${entity.BaseTable}
1180
1262
  ------------------------------------------------------------
1181
- DROP PROCEDURE IF EXISTS [${entity.SchemaName}].[${spName}]
1263
+ IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
1264
+ DROP PROCEDURE [${entity.SchemaName}].[${spName}];
1182
1265
  GO
1183
1266
 
1184
1267
  CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
@@ -1207,7 +1290,52 @@ GO
1207
1290
  ${updatedAtTrigger}
1208
1291
  `;
1209
1292
  }
1210
- __specialUUIDValue = '00000000-0000-0000-0000-000000000000';
1293
+ /**
1294
+ * Formats a default value for use in SQL, handling special cases like SQL functions
1295
+ * @param defaultValue The default value from the database metadata
1296
+ * @param needsQuotes Whether the field type typically needs quotes
1297
+ * @returns Properly formatted default value for SQL
1298
+ */
1299
+ formatDefaultValue(defaultValue, needsQuotes) {
1300
+ if (!defaultValue || defaultValue.trim().length === 0) {
1301
+ return 'NULL';
1302
+ }
1303
+ let trimmedValue = defaultValue.trim();
1304
+ const lowerValue = trimmedValue.toLowerCase();
1305
+ // SQL functions that should not be quoted
1306
+ const sqlFunctions = [
1307
+ 'newid()',
1308
+ 'newsequentialid()',
1309
+ 'getdate()',
1310
+ 'getutcdate()',
1311
+ 'sysdatetime()',
1312
+ 'sysdatetimeoffset()',
1313
+ 'current_timestamp',
1314
+ 'user_name()',
1315
+ 'suser_name()',
1316
+ 'system_user'
1317
+ ];
1318
+ // Check if this is a SQL function
1319
+ for (const func of sqlFunctions) {
1320
+ if (lowerValue.includes(func)) {
1321
+ // Remove outer parentheses if they exist (e.g., "(getutcdate())" -> "getutcdate()")
1322
+ if (trimmedValue.startsWith('(') && trimmedValue.endsWith(')')) {
1323
+ trimmedValue = trimmedValue.substring(1, trimmedValue.length - 1);
1324
+ }
1325
+ return trimmedValue;
1326
+ }
1327
+ }
1328
+ // If the value already has quotes, remove them first
1329
+ let cleanValue = trimmedValue;
1330
+ if (cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
1331
+ cleanValue = cleanValue.substring(1, cleanValue.length - 1);
1332
+ }
1333
+ // Add quotes if needed
1334
+ if (needsQuotes) {
1335
+ return `'${cleanValue}'`;
1336
+ }
1337
+ return cleanValue;
1338
+ }
1211
1339
  createEntityFieldsParamString(entityFields, isUpdate) {
1212
1340
  let sOutput = '', isFirst = true;
1213
1341
  for (let i = 0; i < entityFields.length; ++i) {
@@ -1228,6 +1356,11 @@ ${updatedAtTrigger}
1228
1356
  // This allows callers to omit the PK and let the DB/sproc handle it
1229
1357
  defaultParamValue = ' = NULL';
1230
1358
  }
1359
+ else if (!isUpdate && ef.HasDefaultValue && !ef.AllowsNull) {
1360
+ // For non-nullable fields with database defaults, make the parameter optional
1361
+ // This allows callers to pass NULL and let the database default be used
1362
+ defaultParamValue = ' = NULL';
1363
+ }
1231
1364
  sOutput += `@${ef.CodeName} ${ef.SQLFullType}${defaultParamValue}`;
1232
1365
  }
1233
1366
  }
@@ -1238,7 +1371,6 @@ ${updatedAtTrigger}
1238
1371
  let sOutput = '', isFirst = true;
1239
1372
  for (let i = 0; i < entityFields.length; ++i) {
1240
1373
  const ef = entityFields[i];
1241
- const quotes = ef.NeedsQuotes ? "'" : "";
1242
1374
  // we only want fields that are (a) not primary keys, or if a pkey, not an auto-increment pkey and (b) not virtual fields and (c) updateable fields and (d) not auto-increment fields if they're not pkeys)
1243
1375
  // ALSO: if excludePrimaryKey is true, skip all primary key fields
1244
1376
  if ((excludePrimaryKey && ef.IsPrimaryKey) || (ef.IsPrimaryKey && autoGeneratedPrimaryKey) || ef.IsVirtual || !ef.AllowUpdateAPI || ef.AutoIncrement) {
@@ -1254,32 +1386,35 @@ ${updatedAtTrigger}
1254
1386
  else
1255
1387
  sOutput += `NULL`; // we don't set the deleted at field on an insert, only on a delete
1256
1388
  }
1257
- else if ((prefix && prefix !== '') && !ef.IsPrimaryKey && ef.IsUniqueIdentifier && ef.HasDefaultValue) {
1258
- // this is the VALUE side (prefix not null/blank), is NOT a primary key, and is a uniqueidentifier column, and has a default value specified
1259
- // in this situation we need to check if the value being passed in is the special value '00000000-0000-0000-0000-000000000000' (which is in __specialUUIDValue) if it is, we substitute it with the actual default value
1260
- // When the uniqueidentifier default value is set to NEWID() or NEWSEQUENTIALID(), we need to ensure that the value is not wrapped in quotes, so we check for that
1261
- // otherwise we use the value passed in
1262
- // next check to make sure ef.DefaultValue does not contain quotes around the value if it is a string type, if it does, we need to remove them
1263
- let defValue = ef.DefaultValue;
1264
- if (ef.TSType === core_1.EntityFieldTSType.String) {
1265
- if (defValue.startsWith("'") && defValue.endsWith("'")) {
1266
- defValue = defValue.substring(1, defValue.length - 1).trim(); // remove the quotes
1267
- }
1268
- }
1269
- const defValueLowered = defValue.toLowerCase().trim();
1270
- //If the default value is NEWID or NEWSEQUENTIALID, we will use the default value as is, without quotes.
1271
- //Otherwise, wrap the default value with the quotes variable value.
1272
- if (!defValueLowered.includes('newid()') && !defValueLowered.includes('newsequentialid()')) {
1273
- defValue = `${quotes}${defValue}${quotes}`;
1274
- }
1275
- sOutput += `CASE @${ef.CodeName} WHEN '${this.__specialUUIDValue}' THEN ${defValue} ELSE @${ef.CodeName} END`;
1389
+ else if ((prefix && prefix !== '') && !ef.IsPrimaryKey && ef.IsUniqueIdentifier && ef.HasDefaultValue && !ef.AllowsNull) {
1390
+ // this is the VALUE side (prefix not null/blank), is NOT a primary key, and is a uniqueidentifier column with a default value and does NOT allow NULL
1391
+ // We need to handle both NULL and the special value '00000000-0000-0000-0000-000000000000' for backward compatibility
1392
+ // Existing code uses the special value to indicate "use the default", so we preserve that behavior
1393
+ const formattedDefault = this.formatDefaultValue(ef.DefaultValue, ef.NeedsQuotes);
1394
+ sOutput += `CASE @${ef.CodeName} WHEN '00000000-0000-0000-0000-000000000000' THEN ${formattedDefault} ELSE ISNULL(@${ef.CodeName}, ${formattedDefault}) END`;
1276
1395
  }
1277
1396
  else {
1278
1397
  let sVal = '';
1279
- if (!prefix || prefix.length === 0)
1398
+ if (!prefix || prefix.length === 0) {
1399
+ // Column name side
1280
1400
  sVal = '[' + ef.Name + ']'; // always put field names in brackets so that if reserved words are being used for field names in a table like "USER" and so on, they still work
1281
- else
1401
+ }
1402
+ else {
1403
+ // Value/parameter side
1282
1404
  sVal = prefix + ef.CodeName;
1405
+ // If this field has a default value and doesn't allow NULL, wrap with ISNULL
1406
+ // For UniqueIdentifier fields, also handle the special value '00000000-0000-0000-0000-000000000000' for backward compatibility
1407
+ if (ef.HasDefaultValue && !ef.AllowsNull) {
1408
+ const formattedDefault = this.formatDefaultValue(ef.DefaultValue, ef.NeedsQuotes);
1409
+ if (ef.IsUniqueIdentifier) {
1410
+ // Handle both NULL and the special UUID value for backward compatibility with existing code
1411
+ sVal = `CASE ${sVal} WHEN '00000000-0000-0000-0000-000000000000' THEN ${formattedDefault} ELSE ISNULL(${sVal}, ${formattedDefault}) END`;
1412
+ }
1413
+ else {
1414
+ sVal = `ISNULL(${sVal}, ${formattedDefault})`;
1415
+ }
1416
+ }
1417
+ }
1283
1418
  sOutput += sVal;
1284
1419
  }
1285
1420
  }
@@ -1345,7 +1480,8 @@ ${deleteCode} AND ${core_1.EntityInfo.DeletedAtFieldName} IS NULL -- don'
1345
1480
  ------------------------------------------------------------
1346
1481
  ----- DELETE PROCEDURE FOR ${entity.BaseTable}
1347
1482
  ------------------------------------------------------------
1348
- DROP PROCEDURE IF EXISTS [${entity.SchemaName}].[${spName}]
1483
+ IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
1484
+ DROP PROCEDURE [${entity.SchemaName}].[${spName}];
1349
1485
  GO
1350
1486
 
1351
1487
  CREATE PROCEDURE [${entity.SchemaName}].[${spName}]