@memberjunction/codegen-lib 5.1.0 → 5.3.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.
@@ -809,7 +809,7 @@ export class ManageMetadataBase {
809
809
  // Load VE EntityField rows from DB (we need the ID and auto-update flags)
810
810
  const schema = mj_core_schema();
811
811
  const fieldsSQL = `
812
- SELECT ID, Name, Category, AutoUpdateCategory, AutoUpdateDisplayName
812
+ SELECT ID, Name, Category, AutoUpdateCategory, AutoUpdateDisplayName, GeneratedFormSection, DisplayName, ExtendedType, CodeType
813
813
  FROM [${schema}].EntityField
814
814
  WHERE EntityID = '${entity.ID}'
815
815
  `;
@@ -830,7 +830,7 @@ export class ManageMetadataBase {
830
830
  if (fieldCategories.length === 0)
831
831
  return false;
832
832
  const existingCategories = this.buildExistingCategorySet(dbFields);
833
- await this.applyFieldCategories(pool, entity.ID, dbFields, fieldCategories, existingCategories);
833
+ await this.applyFieldCategories(pool, entity, dbFields, fieldCategories, existingCategories);
834
834
  // Apply entity icon if provided
835
835
  if (result.entityIcon) {
836
836
  await this.applyEntityIcon(pool, entity.ID, result.entityIcon);
@@ -1800,13 +1800,34 @@ export class ManageMetadataBase {
1800
1800
  * @returns {string} - The SQL statement to retrieve pending entity fields.
1801
1801
  */
1802
1802
  getPendingEntityFieldsSELECTSQL() {
1803
- const sSQL = `WITH MaxSequences AS (
1803
+ const schema = mj_core_schema();
1804
+ const sSQL = `
1805
+ -- Materialize system DMV views into temp tables so SQL Server gets real statistics
1806
+ -- instead of expanding nested view-on-view joins with bad cardinality estimates
1807
+ -- Drop first in case a prior run on this connection left them behind
1808
+ IF OBJECT_ID('tempdb..#__mj__CodeGen__vwForeignKeys') IS NOT NULL DROP TABLE #__mj__CodeGen__vwForeignKeys;
1809
+ IF OBJECT_ID('tempdb..#__mj__CodeGen__vwTablePrimaryKeys') IS NOT NULL DROP TABLE #__mj__CodeGen__vwTablePrimaryKeys;
1810
+ IF OBJECT_ID('tempdb..#__mj__CodeGen__vwTableUniqueKeys') IS NOT NULL DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
1811
+
1812
+ SELECT [column], [table], [schema_name], referenced_table, referenced_column, [referenced_schema]
1813
+ INTO #__mj__CodeGen__vwForeignKeys
1814
+ FROM [${schema}].vwForeignKeys;
1815
+
1816
+ SELECT TableName, ColumnName, SchemaName
1817
+ INTO #__mj__CodeGen__vwTablePrimaryKeys
1818
+ FROM [${schema}].vwTablePrimaryKeys;
1819
+
1820
+ SELECT TableName, ColumnName, SchemaName
1821
+ INTO #__mj__CodeGen__vwTableUniqueKeys
1822
+ FROM [${schema}].vwTableUniqueKeys;
1823
+
1824
+ WITH MaxSequences AS (
1804
1825
  -- Calculate the maximum existing sequence for each entity to avoid collisions
1805
1826
  SELECT
1806
1827
  EntityID,
1807
1828
  ISNULL(MAX(Sequence), 0) AS MaxSequence
1808
1829
  FROM
1809
- [${mj_core_schema()}].EntityField
1830
+ [${schema}].EntityField
1810
1831
  GROUP BY
1811
1832
  EntityID
1812
1833
  ),
@@ -1849,61 +1870,50 @@ NumberedRows AS (
1849
1870
  END,
1850
1871
  ROW_NUMBER() OVER (PARTITION BY sf.EntityID, sf.FieldName ORDER BY (SELECT NULL)) AS rn
1851
1872
  FROM
1852
- [${mj_core_schema()}].vwSQLColumnsAndEntityFields sf
1873
+ [${schema}].vwSQLColumnsAndEntityFields sf
1853
1874
  LEFT OUTER JOIN
1854
1875
  MaxSequences ms
1855
1876
  ON
1856
1877
  sf.EntityID = ms.EntityID
1857
1878
  LEFT OUTER JOIN
1858
- [${mj_core_schema()}].Entity e
1879
+ [${schema}].Entity e
1859
1880
  ON
1860
1881
  sf.EntityID = e.ID
1861
1882
  LEFT OUTER JOIN
1862
- [${mj_core_schema()}].vwForeignKeys fk
1883
+ #__mj__CodeGen__vwForeignKeys fk
1863
1884
  ON
1864
1885
  sf.FieldName = fk.[column] AND
1865
1886
  e.BaseTable = fk.[table] AND
1866
1887
  e.SchemaName = fk.[schema_name]
1867
1888
  LEFT OUTER JOIN
1868
- [${mj_core_schema()}].Entity re -- Related Entity
1889
+ [${schema}].Entity re -- Related Entity
1869
1890
  ON
1870
1891
  re.BaseTable = fk.referenced_table AND
1871
1892
  re.SchemaName = fk.[referenced_schema]
1872
1893
  LEFT OUTER JOIN
1873
- [${mj_core_schema()}].vwTablePrimaryKeys pk
1894
+ #__mj__CodeGen__vwTablePrimaryKeys pk
1874
1895
  ON
1875
1896
  e.BaseTable = pk.TableName AND
1876
1897
  sf.FieldName = pk.ColumnName AND
1877
1898
  e.SchemaName = pk.SchemaName
1878
1899
  LEFT OUTER JOIN
1879
- [${mj_core_schema()}].vwTableUniqueKeys uk
1900
+ #__mj__CodeGen__vwTableUniqueKeys uk
1880
1901
  ON
1881
1902
  e.BaseTable = uk.TableName AND
1882
1903
  sf.FieldName = uk.ColumnName AND
1883
1904
  e.SchemaName = uk.SchemaName
1884
1905
  WHERE
1885
1906
  EntityFieldID IS NULL -- only where we have NOT YET CREATED EntityField records\n${this.createExcludeTablesAndSchemasFilter('sf.')}
1886
- ),
1887
- FilteredRows AS ( -- filter rows to only include rn=1 OR where we have rows where the to/from fkey is the same so long as the field name <> the same
1888
- SELECT *
1889
- FROM NumberedRows
1890
- WHERE rn = 1
1891
- UNION ALL
1892
- SELECT nr.*
1893
- FROM NumberedRows nr
1894
- WHERE rn <> 1
1895
- AND NOT EXISTS (
1896
- SELECT 1
1897
- FROM NumberedRows nr1
1898
- WHERE nr1.rn = 1
1899
- AND nr1.EntityID = nr.EntityID
1900
- AND nr1.FieldName = nr.FieldName
1901
- )
1902
1907
  )
1903
1908
  SELECT *
1904
- FROM FilteredRows
1909
+ FROM NumberedRows
1910
+ WHERE rn = 1
1905
1911
  ORDER BY EntityID, Sequence;
1906
- `;
1912
+
1913
+ DROP TABLE #__mj__CodeGen__vwForeignKeys;
1914
+ DROP TABLE #__mj__CodeGen__vwTablePrimaryKeys;
1915
+ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
1916
+ `;
1907
1917
  return sSQL;
1908
1918
  }
1909
1919
  /**
@@ -2031,38 +2041,40 @@ NumberedRows AS (
2031
2041
  const sSQL = this.getPendingEntityFieldsSELECTSQL();
2032
2042
  const newEntityFieldsResult = await pool.request().query(sSQL);
2033
2043
  const newEntityFields = newEntityFieldsResult.recordset;
2034
- const transaction = new sql.Transaction(pool);
2035
- await transaction.begin();
2036
- try {
2037
- // wrap in a transaction so we get all of it or none of it
2038
- for (let i = 0; i < newEntityFields.length; ++i) {
2039
- const n = newEntityFields[i];
2040
- if (n.EntityID !== null && n.EntityID !== undefined && n.EntityID.length > 0) {
2041
- // need to check for null entity id = that is because the above query can return candidate Entity Fields but the entities may not have been created if the entities
2042
- // that would have been created violate rules - such as not having an ID column, etc.
2043
- const newEntityFieldUUID = this.createNewUUID();
2044
- const sSQLInsert = this.getPendingEntityFieldINSERTSQL(newEntityFieldUUID, n);
2045
- try {
2046
- await this.LogSQLAndExecute(pool, sSQLInsert, `SQL text to insert new entity field`);
2047
- // if we get here, we're okay, otherwise we have an exception, which we want as it blows up transaction
2048
- }
2049
- catch (e) {
2050
- // this is here so we can catch the error for debug. We want the transaction to die
2051
- logError(`Error inserting new entity field. SQL: \n${sSQLInsert}`);
2052
- throw e;
2044
+ if (newEntityFields.length > 0) {
2045
+ const transaction = new sql.Transaction(pool);
2046
+ await transaction.begin();
2047
+ try {
2048
+ // wrap in a transaction so we get all of it or none of it
2049
+ for (let i = 0; i < newEntityFields.length; ++i) {
2050
+ const n = newEntityFields[i];
2051
+ if (n.EntityID !== null && n.EntityID !== undefined && n.EntityID.length > 0) {
2052
+ // need to check for null entity id = that is because the above query can return candidate Entity Fields but the entities may not have been created if the entities
2053
+ // that would have been created violate rules - such as not having an ID column, etc.
2054
+ const newEntityFieldUUID = this.createNewUUID();
2055
+ const sSQLInsert = this.getPendingEntityFieldINSERTSQL(newEntityFieldUUID, n);
2056
+ try {
2057
+ await this.LogSQLAndExecute(pool, sSQLInsert, `SQL text to insert new entity field`);
2058
+ // if we get here, we're okay, otherwise we have an exception, which we want as it blows up transaction
2059
+ }
2060
+ catch (e) {
2061
+ // this is here so we can catch the error for debug. We want the transaction to die
2062
+ logError(`Error inserting new entity field. SQL: \n${sSQLInsert}`);
2063
+ throw e;
2064
+ }
2053
2065
  }
2054
2066
  }
2067
+ await transaction.commit();
2055
2068
  }
2056
- await transaction.commit();
2057
- }
2058
- catch (e) {
2059
- await transaction.rollback();
2060
- throw e;
2069
+ catch (e) {
2070
+ await transaction.rollback();
2071
+ throw e;
2072
+ }
2073
+ // if we get here now send a distinct list of the entities that had new fields to the modified entity list
2074
+ // column in the resultset is called EntityName, we dont have to dedupe them here because the method below
2075
+ // will do that for us
2076
+ ManageMetadataBase.addNewEntitiesToModifiedList(newEntityFields.map((f) => f.EntityName));
2061
2077
  }
2062
- // if we get here now send a distinct list of the entities that had new fields to the modified entity list
2063
- // column in the resultset is called EntityName, we dont have to dedupe them here because the method below
2064
- // will do that for us
2065
- ManageMetadataBase.addNewEntitiesToModifiedList(newEntityFields.map((f) => f.EntityName));
2066
2078
  return true;
2067
2079
  }
2068
2080
  catch (e) {
@@ -2559,7 +2571,6 @@ NumberedRows AS (
2559
2571
  * Checks if a table has a soft primary key defined in the additionalSchemaInfo JSON file (configured in mj.config.cjs)
2560
2572
  */
2561
2573
  hasSoftPrimaryKeyInConfig(schemaName, tableName) {
2562
- // Check if additionalSchemaInfo is configured
2563
2574
  if (!configInfo.additionalSchemaInfo) {
2564
2575
  return false;
2565
2576
  }
@@ -2570,15 +2581,20 @@ NumberedRows AS (
2570
2581
  }
2571
2582
  try {
2572
2583
  const config = ManageMetadataBase.getSoftPKFKConfig();
2573
- if (!config || !config.tables) {
2574
- logStatus(` [Soft PK Check] Config file found but no tables array`);
2584
+ if (!config) {
2585
+ logStatus(` [Soft PK Check] Config file found but could not be parsed`);
2575
2586
  return false;
2576
2587
  }
2577
- const tableConfig = config.tables.find((t) => t.schemaName?.toLowerCase() === schemaName?.toLowerCase() &&
2578
- t.tableName?.toLowerCase() === tableName?.toLowerCase());
2579
- const found = Boolean(tableConfig?.primaryKeys && tableConfig.primaryKeys.length > 0);
2588
+ const tables = this.extractTablesFromConfig(config);
2589
+ if (tables.length === 0) {
2590
+ logStatus(` [Soft PK Check] Config file found but no tables defined`);
2591
+ return false;
2592
+ }
2593
+ const tableConfig = tables.find((t) => t.SchemaName.toLowerCase() === schemaName?.toLowerCase() &&
2594
+ t.TableName.toLowerCase() === tableName?.toLowerCase());
2595
+ const found = Boolean(tableConfig?.PrimaryKey && tableConfig.PrimaryKey.length > 0);
2580
2596
  if (!found) {
2581
- logStatus(` [Soft PK Check] No config found for ${schemaName}.${tableName} (config has ${config.tables.length} tables)`);
2597
+ logStatus(` [Soft PK Check] No config found for ${schemaName}.${tableName} (config has ${tables.length} tables)`);
2582
2598
  }
2583
2599
  return found;
2584
2600
  }
@@ -2823,7 +2839,7 @@ NumberedRows AS (
2823
2839
  }
2824
2840
  /**
2825
2841
  * Creates a new application using direct SQL INSERT to ensure it's captured in SQL logging.
2826
- * The Path field is auto-generated from Name using the same slug logic as ApplicationEntityServerEntity.
2842
+ * The Path field is auto-generated from Name using the same slug logic as MJApplicationEntityServer.
2827
2843
  *
2828
2844
  * @param pool SQL connection pool
2829
2845
  * @param appID Pre-generated UUID for the application
@@ -3068,7 +3084,10 @@ NumberedRows AS (
3068
3084
  ef.EntityIDFieldName,
3069
3085
  ef.RelatedEntity,
3070
3086
  ef.IsVirtual,
3071
- ef.AllowUpdateAPI
3087
+ ef.AllowUpdateAPI,
3088
+ ef.IsNameField,
3089
+ ef.DefaultInView,
3090
+ ef.IncludeInUserSearchAPI
3072
3091
  FROM
3073
3092
  [${mj_core_schema()}].vwEntityFields ef
3074
3093
  WHERE
@@ -3154,40 +3173,45 @@ NumberedRows AS (
3154
3173
  * @param currentUser User context
3155
3174
  */
3156
3175
  async processEntityAdvancedGeneration(pool, entity, allFields, ag, currentUser) {
3157
- // Filter fields for this entity (client-side filtering)
3158
- const fields = allFields.filter((f) => f.EntityID === entity.ID);
3159
- // Determine if this is a new entity (for DefaultForNewUser decision)
3160
- const isNewEntity = ManageMetadataBase.newEntityList.includes(entity.Name);
3161
- // Smart Field Identification
3162
- // Only run if at least one field allows auto-update for any of the smart field properties
3163
- if (fields.some((f) => f.AutoUpdateIsNameField || f.AutoUpdateDefaultInView || f.AutoUpdateIncludeInUserSearchAPI)) {
3164
- const fieldAnalysis = await ag.identifyFields({
3165
- Name: entity.Name,
3166
- Description: entity.Description,
3167
- Fields: fields
3168
- }, currentUser);
3169
- if (fieldAnalysis) {
3170
- await this.applySmartFieldIdentification(pool, entity.ID, fields, fieldAnalysis);
3171
- }
3172
- }
3173
- // Form Layout Generation
3174
- // Only run if at least one field allows auto-update
3175
- const needsCategoryGeneration = fields.some((f) => f.AutoUpdateCategory && (!f.Category || f.Category.trim() === ''));
3176
- if (needsCategoryGeneration) {
3177
- // Build IS-A parent chain context if this entity has a parent
3178
- const parentChainContext = this.buildParentChainContext(entity, fields);
3179
- const layoutAnalysis = await ag.generateFormLayout({
3180
- Name: entity.Name,
3181
- Description: entity.Description,
3182
- SchemaName: entity.SchemaName,
3183
- Settings: entity.Settings,
3184
- Fields: fields,
3185
- ...parentChainContext
3186
- }, currentUser, isNewEntity);
3187
- if (layoutAnalysis) {
3188
- await this.applyFormLayout(pool, entity.ID, fields, layoutAnalysis, isNewEntity);
3189
- logStatus(` Applied form layout for ${entity.Name}`);
3190
- }
3176
+ try {
3177
+ // Filter fields for this entity (client-side filtering)
3178
+ const fields = allFields.filter((f) => f.EntityID === entity.ID);
3179
+ // Determine if this is a new entity (for DefaultForNewUser decision)
3180
+ const isNewEntity = ManageMetadataBase.newEntityList.includes(entity.Name);
3181
+ // Smart Field Identification
3182
+ // Only run if at least one field allows auto-update for any of the smart field properties
3183
+ if (fields.some((f) => f.AutoUpdateIsNameField || f.AutoUpdateDefaultInView || f.AutoUpdateIncludeInUserSearchAPI)) {
3184
+ const fieldAnalysis = await ag.identifyFields({
3185
+ Name: entity.Name,
3186
+ Description: entity.Description,
3187
+ Fields: fields
3188
+ }, currentUser);
3189
+ if (fieldAnalysis) {
3190
+ await this.applySmartFieldIdentification(pool, entity.ID, fields, fieldAnalysis);
3191
+ }
3192
+ }
3193
+ // Form Layout Generation
3194
+ // Only run if at least one field allows auto-update
3195
+ const needsCategoryGeneration = fields.some((f) => f.AutoUpdateCategory && (!f.Category || f.Category.trim() === ''));
3196
+ if (needsCategoryGeneration) {
3197
+ // Build IS-A parent chain context if this entity has a parent
3198
+ const parentChainContext = this.buildParentChainContext(entity, fields);
3199
+ const layoutAnalysis = await ag.generateFormLayout({
3200
+ Name: entity.Name,
3201
+ Description: entity.Description,
3202
+ SchemaName: entity.SchemaName,
3203
+ Settings: entity.Settings,
3204
+ Fields: fields,
3205
+ ...parentChainContext
3206
+ }, currentUser, isNewEntity);
3207
+ if (layoutAnalysis) {
3208
+ await this.applyFormLayout(pool, entity, fields, layoutAnalysis, isNewEntity);
3209
+ logStatus(` Applied form layout for ${entity.Name}`);
3210
+ }
3211
+ }
3212
+ }
3213
+ catch (ex) {
3214
+ logError('Error Processing Entity Advanced Generation', ex);
3191
3215
  }
3192
3216
  }
3193
3217
  /**
@@ -3258,7 +3282,7 @@ NumberedRows AS (
3258
3282
  const sqlStatements = [];
3259
3283
  // Find the name field (exactly one)
3260
3284
  const nameField = fields.find(f => f.Name === result.nameField);
3261
- if (nameField && nameField.AutoUpdateIsNameField && nameField.ID) {
3285
+ if (nameField && nameField.AutoUpdateIsNameField && nameField.ID && !nameField.IsNameField /*don't waste SQL to set the value if IsNameField already set */) {
3262
3286
  sqlStatements.push(`
3263
3287
  UPDATE [${mj_core_schema()}].EntityField
3264
3288
  SET IsNameField = 1
@@ -3278,12 +3302,15 @@ NumberedRows AS (
3278
3302
  }
3279
3303
  // Build update statements for all default in view fields
3280
3304
  for (const field of defaultInViewFields) {
3281
- sqlStatements.push(`
3282
- UPDATE [${mj_core_schema()}].EntityField
3283
- SET DefaultInView = 1
3284
- WHERE ID = '${field.ID}'
3285
- AND AutoUpdateDefaultInView = 1
3286
- `);
3305
+ if (!field.DefaultInView) {
3306
+ // only set these when DefaultInView not already on, otherwise wasteful
3307
+ sqlStatements.push(`
3308
+ UPDATE [${mj_core_schema()}].EntityField
3309
+ SET DefaultInView = 1
3310
+ WHERE ID = '${field.ID}'
3311
+ AND AutoUpdateDefaultInView = 1
3312
+ `);
3313
+ }
3287
3314
  }
3288
3315
  // Find all searchable fields (one or more) - for IncludeInUserSearchAPI
3289
3316
  if (result.searchableFields && result.searchableFields.length > 0) {
@@ -3295,18 +3322,26 @@ NumberedRows AS (
3295
3322
  }
3296
3323
  // Build update statements for all searchable fields
3297
3324
  for (const field of searchableFields) {
3298
- sqlStatements.push(`
3299
- UPDATE [${mj_core_schema()}].EntityField
3300
- SET IncludeInUserSearchAPI = 1
3301
- WHERE ID = '${field.ID}'
3302
- AND AutoUpdateIncludeInUserSearchAPI = 1
3303
- `);
3325
+ if (!field.IncludeInUserSearchAPI) {
3326
+ // only set this if IncludeInUserSearchAPI isn't already set
3327
+ sqlStatements.push(`
3328
+ UPDATE [${mj_core_schema()}].EntityField
3329
+ SET IncludeInUserSearchAPI = 1
3330
+ WHERE ID = '${field.ID}'
3331
+ AND AutoUpdateIncludeInUserSearchAPI = 1
3332
+ `);
3333
+ }
3304
3334
  }
3305
3335
  }
3306
3336
  // Execute all updates in one batch
3307
3337
  if (sqlStatements.length > 0) {
3308
3338
  const combinedSQL = sqlStatements.join('\n');
3309
- await this.LogSQLAndExecute(pool, combinedSQL, `Set field properties for entity`, false);
3339
+ try {
3340
+ await this.LogSQLAndExecute(pool, combinedSQL, `Set field properties for entity`, false);
3341
+ }
3342
+ catch (ex) {
3343
+ logError('Error executing combined smart field SQL: ', ex);
3344
+ }
3310
3345
  }
3311
3346
  }
3312
3347
  /**
@@ -3318,21 +3353,21 @@ NumberedRows AS (
3318
3353
  * @param result Form layout result from LLM
3319
3354
  * @param isNewEntity If true, apply entityImportance; if false, skip it
3320
3355
  */
3321
- async applyFormLayout(pool, entityId, fields, result, isNewEntity = false) {
3356
+ async applyFormLayout(pool, entity, fields, result, isNewEntity = false) {
3322
3357
  const existingCategories = this.buildExistingCategorySet(fields);
3323
- await this.applyFieldCategories(pool, entityId, fields, result.fieldCategories, existingCategories);
3358
+ await this.applyFieldCategories(pool, entity, fields, result.fieldCategories, existingCategories);
3324
3359
  if (result.entityIcon) {
3325
- await this.applyEntityIcon(pool, entityId, result.entityIcon);
3360
+ await this.applyEntityIcon(pool, entity.ID, result.entityIcon);
3326
3361
  }
3327
3362
  // Resolve categoryInfo from new or legacy format
3328
3363
  const categoryInfoToStore = result.categoryInfo ||
3329
3364
  (result.categoryIcons ?
3330
3365
  Object.fromEntries(Object.entries(result.categoryIcons).map(([cat, icon]) => [cat, { icon, description: '' }])) : null);
3331
3366
  if (categoryInfoToStore) {
3332
- await this.applyCategoryInfoSettings(pool, entityId, categoryInfoToStore);
3367
+ await this.applyCategoryInfoSettings(pool, entity.ID, categoryInfoToStore);
3333
3368
  }
3334
3369
  if (isNewEntity && result.entityImportance) {
3335
- await this.applyEntityImportance(pool, entityId, result.entityImportance);
3370
+ await this.applyEntityImportance(pool, entity.ID, result.entityImportance);
3336
3371
  }
3337
3372
  }
3338
3373
  // ─────────────────────────────────────────────────────────────────
@@ -3357,7 +3392,7 @@ NumberedRows AS (
3357
3392
  * Enforces stability rules: fields with existing categories cannot move to NEW categories.
3358
3393
  * All SQL updates are batched into a single execution for performance.
3359
3394
  */
3360
- async applyFieldCategories(pool, entityId, fields, fieldCategories, existingCategories) {
3395
+ async applyFieldCategories(pool, entity, fields, fieldCategories, existingCategories) {
3361
3396
  const sqlStatements = [];
3362
3397
  for (const fieldCategory of fieldCategories) {
3363
3398
  const field = fields.find(f => f.Name === fieldCategory.fieldName);
@@ -3374,32 +3409,44 @@ NumberedRows AS (
3374
3409
  logStatus(` Rejected category change for field '${field.Name}': cannot move from existing category '${field.Category}' to new category '${category}'. Keeping original category.`);
3375
3410
  category = field.Category;
3376
3411
  }
3377
- const setClauses = [
3378
- `Category = '${category.replace(/'/g, "''")}'`,
3379
- `GeneratedFormSection = 'Category'`
3380
- ];
3381
- if (fieldCategory.displayName && field.AutoUpdateDisplayName) {
3412
+ const setClauses = [];
3413
+ if (field.Category !== category) {
3414
+ setClauses.push(`Category = '${category.replace(/'/g, "''")}'`);
3415
+ }
3416
+ if (field.GeneratedFormSection !== 'Category') {
3417
+ setClauses.push(`GeneratedFormSection = 'Category'`);
3418
+ }
3419
+ if (fieldCategory.displayName && field.AutoUpdateDisplayName && field.DisplayName !== fieldCategory.displayName) {
3382
3420
  setClauses.push(`DisplayName = '${fieldCategory.displayName.replace(/'/g, "''")}'`);
3383
3421
  }
3384
- if (fieldCategory.extendedType !== undefined) {
3422
+ if (fieldCategory.extendedType !== undefined && field.ExtendedType !== fieldCategory.extendedType) {
3385
3423
  const extendedType = fieldCategory.extendedType === null ? 'NULL' : `'${String(fieldCategory.extendedType).replace(/'/g, "''")}'`;
3386
3424
  setClauses.push(`ExtendedType = ${extendedType}`);
3387
3425
  }
3388
- if (fieldCategory.codeType !== undefined) {
3426
+ if (fieldCategory.codeType !== undefined && field.CodeType !== fieldCategory.codeType) {
3389
3427
  const codeType = fieldCategory.codeType === null ? 'NULL' : `'${String(fieldCategory.codeType).replace(/'/g, "''")}'`;
3390
3428
  setClauses.push(`CodeType = ${codeType}`);
3391
3429
  }
3392
- sqlStatements.push(`UPDATE [${mj_core_schema()}].EntityField
3393
- SET ${setClauses.join(',\n ')}
3394
- WHERE ID = '${field.ID}'
3395
- AND AutoUpdateCategory = 1`);
3430
+ if (setClauses.length > 0) {
3431
+ // only generate an UPDATE if we have 1+ set clause
3432
+ sqlStatements.push(`\n-- UPDATE Entity Field Category Info ${entity.Name}.${field.Name} \nUPDATE [${mj_core_schema()}].EntityField
3433
+ SET
3434
+ ${setClauses.join(',\n ')}
3435
+ WHERE
3436
+ ID = '${field.ID}' AND AutoUpdateCategory = 1`);
3437
+ }
3396
3438
  }
3397
3439
  else if (!field) {
3398
3440
  logError(`Form layout returned invalid fieldName: '${fieldCategory.fieldName}' not found in entity`);
3399
3441
  }
3400
3442
  }
3401
3443
  if (sqlStatements.length > 0) {
3402
- await this.LogSQLAndExecute(pool, sqlStatements.join('\n'), `Set categories for ${sqlStatements.length} fields`, false);
3444
+ try {
3445
+ await this.LogSQLAndExecute(pool, sqlStatements.join('\n'), `Set categories for ${sqlStatements.length} fields`, false);
3446
+ }
3447
+ catch (ex) {
3448
+ logError('Error Applying Field Categories', ex);
3449
+ }
3403
3450
  }
3404
3451
  }
3405
3452
  /**
@@ -3419,8 +3466,13 @@ NumberedRows AS (
3419
3466
  SET Icon = '${escapedIcon}', __mj_UpdatedAt = GETUTCDATE()
3420
3467
  WHERE ID = '${entityId}'
3421
3468
  `;
3422
- await this.LogSQLAndExecute(pool, updateSQL, `Set entity icon to ${entityIcon}`, false);
3423
- logStatus(` Set entity icon: ${entityIcon}`);
3469
+ try {
3470
+ await this.LogSQLAndExecute(pool, updateSQL, `Set entity icon to ${entityIcon}`, false);
3471
+ logStatus(` Set entity icon: ${entityIcon}`);
3472
+ }
3473
+ catch (ex) {
3474
+ logError('Error Applying Entity Icon', ex);
3475
+ }
3424
3476
  }
3425
3477
  }
3426
3478
  }
@@ -3435,18 +3487,28 @@ NumberedRows AS (
3435
3487
  const checkNewSQL = `SELECT ID FROM [${mj_core_schema()}].EntitySetting WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryInfo'`;
3436
3488
  const existingNew = await pool.request().query(checkNewSQL);
3437
3489
  if (existingNew.recordset.length > 0) {
3438
- await this.LogSQLAndExecute(pool, `
3439
- UPDATE [${mj_core_schema()}].EntitySetting
3440
- SET Value = '${infoJSON}', __mj_UpdatedAt = GETUTCDATE()
3441
- WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryInfo'
3442
- `, `Update FieldCategoryInfo setting for entity`, false);
3490
+ try {
3491
+ await this.LogSQLAndExecute(pool, `
3492
+ UPDATE [${mj_core_schema()}].EntitySetting
3493
+ SET Value = '${infoJSON}', __mj_UpdatedAt = GETUTCDATE()
3494
+ WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryInfo'
3495
+ `, `Update FieldCategoryInfo setting for entity`, false);
3496
+ }
3497
+ catch (ex) {
3498
+ logError('Error Applying Category Info Settings: Part 1', ex);
3499
+ }
3443
3500
  }
3444
3501
  else {
3445
3502
  const newId = uuidv4();
3446
- await this.LogSQLAndExecute(pool, `
3447
- INSERT INTO [${mj_core_schema()}].EntitySetting (ID, EntityID, Name, Value, __mj_CreatedAt, __mj_UpdatedAt)
3448
- VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}', GETUTCDATE(), GETUTCDATE())
3449
- `, `Insert FieldCategoryInfo setting for entity`, false);
3503
+ try {
3504
+ await this.LogSQLAndExecute(pool, `
3505
+ INSERT INTO [${mj_core_schema()}].EntitySetting (ID, EntityID, Name, Value, __mj_CreatedAt, __mj_UpdatedAt)
3506
+ VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}', GETUTCDATE(), GETUTCDATE())
3507
+ `, `Insert FieldCategoryInfo setting for entity`, false);
3508
+ }
3509
+ catch (ex) {
3510
+ logError('Error Applying Category Info Settings: Part 2', ex);
3511
+ }
3450
3512
  }
3451
3513
  // Also upsert legacy FieldCategoryIcons for backwards compatibility
3452
3514
  const iconsOnly = {};
@@ -3459,22 +3521,32 @@ NumberedRows AS (
3459
3521
  const checkLegacySQL = `SELECT ID FROM [${mj_core_schema()}].EntitySetting WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryIcons'`;
3460
3522
  const existingLegacy = await pool.request().query(checkLegacySQL);
3461
3523
  if (existingLegacy.recordset.length > 0) {
3462
- await this.LogSQLAndExecute(pool, `
3463
- UPDATE [${mj_core_schema()}].EntitySetting
3464
- SET Value = '${iconsJSON}', __mj_UpdatedAt = GETUTCDATE()
3465
- WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryIcons'
3466
- `, `Update FieldCategoryIcons setting (legacy)`, false);
3524
+ try {
3525
+ await this.LogSQLAndExecute(pool, `
3526
+ UPDATE [${mj_core_schema()}].EntitySetting
3527
+ SET Value = '${iconsJSON}', __mj_UpdatedAt = GETUTCDATE()
3528
+ WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryIcons'
3529
+ `, `Update FieldCategoryIcons setting (legacy)`, false);
3530
+ }
3531
+ catch (ex) {
3532
+ logError('Error Applying Category Info Settings: Part 3', ex);
3533
+ }
3467
3534
  }
3468
3535
  else {
3469
3536
  const newId = uuidv4();
3470
- await this.LogSQLAndExecute(pool, `
3471
- INSERT INTO [${mj_core_schema()}].EntitySetting (ID, EntityID, Name, Value, __mj_CreatedAt, __mj_UpdatedAt)
3472
- VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}', GETUTCDATE(), GETUTCDATE())
3473
- `, `Insert FieldCategoryIcons setting (legacy)`, false);
3537
+ try {
3538
+ await this.LogSQLAndExecute(pool, `
3539
+ INSERT INTO [${mj_core_schema()}].EntitySetting (ID, EntityID, Name, Value, __mj_CreatedAt, __mj_UpdatedAt)
3540
+ VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}', GETUTCDATE(), GETUTCDATE())
3541
+ `, `Insert FieldCategoryIcons setting (legacy)`, false);
3542
+ }
3543
+ catch (ex) {
3544
+ logError('Error Applying Category Info Settings: Part 4', ex);
3545
+ }
3474
3546
  }
3475
3547
  }
3476
3548
  /**
3477
- * Applies entity importance analysis to ApplicationEntity records.
3549
+ * Applies entity importance analysis to MJApplicationEntity records.
3478
3550
  * Only called for NEW entities to set DefaultForNewUser.
3479
3551
  */
3480
3552
  async applyEntityImportance(pool, entityId, importance) {
@@ -3484,9 +3556,14 @@ NumberedRows AS (
3484
3556
  SET DefaultForNewUser = ${defaultForNewUser}, __mj_UpdatedAt = GETUTCDATE()
3485
3557
  WHERE EntityID = '${entityId}'
3486
3558
  `;
3487
- await this.LogSQLAndExecute(pool, updateSQL, `Set DefaultForNewUser=${defaultForNewUser} for NEW entity (category: ${importance.entityCategory}, confidence: ${importance.confidence})`, false);
3488
- logStatus(` Entity importance (NEW Entity): ${importance.entityCategory} (defaultForNewUser: ${importance.defaultForNewUser}, confidence: ${importance.confidence})`);
3489
- logStatus(` Reasoning: ${importance.reasoning}`);
3559
+ try {
3560
+ await this.LogSQLAndExecute(pool, updateSQL, `Set DefaultForNewUser=${defaultForNewUser} for NEW entity (category: ${importance.entityCategory}, confidence: ${importance.confidence})`, false);
3561
+ logStatus(` Entity importance (NEW Entity): ${importance.entityCategory} (defaultForNewUser: ${importance.defaultForNewUser}, confidence: ${importance.confidence})`);
3562
+ logStatus(` Reasoning: ${importance.reasoning}`);
3563
+ }
3564
+ catch (ex) {
3565
+ logError('Error Applying Entity Importance', ex);
3566
+ }
3490
3567
  }
3491
3568
  /**
3492
3569
  * Executes the given SQL query using the given ConnectionPool object.