@memberjunction/codegen-lib 5.24.0 → 5.26.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/dist/Config/config.d.ts +279 -0
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +59 -1
- package/dist/Config/config.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +85 -12
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +297 -54
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +20 -0
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +130 -1
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.d.ts +15 -1
- package/dist/Misc/advanced_generation.d.ts.map +1 -1
- package/dist/Misc/advanced_generation.js +2 -1
- 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.map +1 -1
- package/package.json +18 -18
|
@@ -2,7 +2,7 @@ import { CodeGenDatabaseProvider } from './codeGenDatabaseProvider.js';
|
|
|
2
2
|
import { SQLServerCodeGenProvider } from './providers/sqlserver/SQLServerCodeGenProvider.js';
|
|
3
3
|
import { configInfo, currentWorkingDirectory, dbType, getSettingValue, mj_core_schema, outputDir } from '../Config/config.js';
|
|
4
4
|
import { EntityInfo, ExtractActualDefaultValue, LogError, LogStatus, Metadata, SeverityType } from "@memberjunction/core";
|
|
5
|
-
import { logError, logMessage, logStatus } from "../Misc/status_logging.js";
|
|
5
|
+
import { logError, logMessage, logStatus, startSpinner, updateSpinner, succeedSpinner } from "../Misc/status_logging.js";
|
|
6
6
|
import { SQLUtilityBase } from "./sql.js";
|
|
7
7
|
import { AdvancedGeneration } from "../Misc/advanced_generation.js";
|
|
8
8
|
import { SQLParser } from "@memberjunction/sql-parser";
|
|
@@ -186,6 +186,20 @@ export class ManageMetadataBase {
|
|
|
186
186
|
static get modifiedEntityList() {
|
|
187
187
|
return this._modifiedEntityList;
|
|
188
188
|
}
|
|
189
|
+
static { this._entitiesRequiringViewRegen = []; }
|
|
190
|
+
/**
|
|
191
|
+
* Entities that had late-phase changes requiring their base views to be
|
|
192
|
+
* regenerated after the main SQL generation pass. Each entry includes
|
|
193
|
+
* the reason for regen so downstream logic can apply reason-specific fixups.
|
|
194
|
+
*/
|
|
195
|
+
static get EntitiesRequiringViewRegen() {
|
|
196
|
+
return this._entitiesRequiringViewRegen;
|
|
197
|
+
}
|
|
198
|
+
static AddEntityRequiringViewRegen(entityName, reason) {
|
|
199
|
+
if (!this._entitiesRequiringViewRegen.some(e => e.EntityName === entityName && e.Reason === reason)) {
|
|
200
|
+
this._entitiesRequiringViewRegen.push({ EntityName: entityName, Reason: reason });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
189
203
|
static { this._generatedValidators = []; }
|
|
190
204
|
/**
|
|
191
205
|
* Globally scoped list of validators that have been generated during the metadata management process.
|
|
@@ -1260,7 +1274,8 @@ export class ManageMetadataBase {
|
|
|
1260
1274
|
* Valid values for EntityField.ExtendedType, plus common LLM aliases mapped to valid values.
|
|
1261
1275
|
*/
|
|
1262
1276
|
static { this.VALID_EXTENDED_TYPES = new Set([
|
|
1263
|
-
'Code', 'Email', 'FaceTime', 'Geo', '
|
|
1277
|
+
'Code', 'Email', 'FaceTime', 'Geo', 'GeoLatitude', 'GeoLongitude', 'GeoCountry', 'GeoStateProvince',
|
|
1278
|
+
'GeoCity', 'GeoPostalCode', 'GeoAddress', 'MSTeams', 'Other', 'SIP', 'SMS', 'Skype', 'Tel', 'URL', 'WhatsApp', 'ZoomMtg'
|
|
1264
1279
|
]); }
|
|
1265
1280
|
static { this.EXTENDED_TYPE_ALIASES = {
|
|
1266
1281
|
'phone': 'Tel',
|
|
@@ -1273,6 +1288,19 @@ export class ManageMetadataBase {
|
|
|
1273
1288
|
'text': 'SMS',
|
|
1274
1289
|
'location': 'Geo',
|
|
1275
1290
|
'address': 'Geo',
|
|
1291
|
+
'latitude': 'GeoLatitude',
|
|
1292
|
+
'lat': 'GeoLatitude',
|
|
1293
|
+
'longitude': 'GeoLongitude',
|
|
1294
|
+
'lng': 'GeoLongitude',
|
|
1295
|
+
'lon': 'GeoLongitude',
|
|
1296
|
+
'country': 'GeoCountry',
|
|
1297
|
+
'state': 'GeoStateProvince',
|
|
1298
|
+
'province': 'GeoStateProvince',
|
|
1299
|
+
'city': 'GeoCity',
|
|
1300
|
+
'postalcode': 'GeoPostalCode',
|
|
1301
|
+
'zipcode': 'GeoPostalCode',
|
|
1302
|
+
'zip': 'GeoPostalCode',
|
|
1303
|
+
'streetaddress': 'GeoAddress',
|
|
1276
1304
|
'teams': 'MSTeams',
|
|
1277
1305
|
'facetime': 'FaceTime',
|
|
1278
1306
|
'zoom': 'ZoomMtg',
|
|
@@ -1866,11 +1894,13 @@ export class ManageMetadataBase {
|
|
|
1866
1894
|
// Advanced Generation - Smart field identification and form layout
|
|
1867
1895
|
if (!skipAdvancedGeneration) {
|
|
1868
1896
|
const step7StartTime = new Date();
|
|
1897
|
+
startSpinner('Applying AI-powered advanced generation (smart fields, form layout)...');
|
|
1869
1898
|
if (!await this.applyAdvancedGeneration(pool, excludeSchemas, currentUser)) {
|
|
1870
1899
|
logError('Error applying advanced generation features');
|
|
1871
1900
|
// Don't fail the entire process - advanced generation is optional
|
|
1872
1901
|
}
|
|
1873
|
-
|
|
1902
|
+
const step7Elapsed = ((new Date().getTime() - step7StartTime.getTime()) / 1000).toFixed(1);
|
|
1903
|
+
succeedSpinner(`Advanced generation completed (${step7Elapsed}s)`);
|
|
1874
1904
|
}
|
|
1875
1905
|
logStatus(` Total time to manage entity fields: ${(new Date().getTime() - startTime.getTime()) / 1000} seconds`);
|
|
1876
1906
|
return bSuccess;
|
|
@@ -3296,6 +3326,8 @@ export class ManageMetadataBase {
|
|
|
3296
3326
|
VALUES ('${appID}', '${appName}', 'Generated for schema', '${schemaName}', '${path}', 1)`;
|
|
3297
3327
|
await this.LogSQLAndExecute(pool, sSQL, `SQL generated to create new application ${appName}`);
|
|
3298
3328
|
LogStatus(`Created new application ${appName} with Path: ${path}`);
|
|
3329
|
+
// Auto-assign default roles to the new application
|
|
3330
|
+
await this.addDefaultRolesForApplication(pool, appID, appName);
|
|
3299
3331
|
return appID;
|
|
3300
3332
|
}
|
|
3301
3333
|
catch (e) {
|
|
@@ -3303,6 +3335,29 @@ export class ManageMetadataBase {
|
|
|
3303
3335
|
return null;
|
|
3304
3336
|
}
|
|
3305
3337
|
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Adds default ApplicationRole records for a newly created application based on config settings.
|
|
3340
|
+
* This grants configured roles access to the new application automatically.
|
|
3341
|
+
*/
|
|
3342
|
+
async addDefaultRolesForApplication(pool, appId, appName) {
|
|
3343
|
+
const defaults = configInfo.newSchemaDefaults.ApplicationRoleDefaults;
|
|
3344
|
+
if (!defaults?.AutoAddRolesForNewApplications) {
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
const md = new Metadata();
|
|
3348
|
+
for (const roleDef of defaults.Roles) {
|
|
3349
|
+
const role = md.Roles.find(r => r.Name.trim().toLowerCase() === roleDef.RoleName.trim().toLowerCase());
|
|
3350
|
+
if (role) {
|
|
3351
|
+
const sSQLInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'ApplicationRole')}
|
|
3352
|
+
(${this.qi('ApplicationID')}, ${this.qi('RoleID')}, ${this.qi('CanAccess')}, ${this.qi('CanAdmin')}) VALUES
|
|
3353
|
+
('${appId}', '${role.ID}', ${roleDef.CanAccess ? 1 : 0}, ${roleDef.CanAdmin ? 1 : 0})`;
|
|
3354
|
+
await this.LogSQLAndExecute(pool, sSQLInsert, `Adding role ${roleDef.RoleName} to application ${appName}`);
|
|
3355
|
+
}
|
|
3356
|
+
else {
|
|
3357
|
+
LogError(`Unable to find Role '${roleDef.RoleName}' for application ${appName}`);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3306
3361
|
async applicationExists(pool, applicationName) {
|
|
3307
3362
|
const sSQL = `SELECT ID FROM ${this.qs(mj_core_schema(), 'Application')} WHERE Name = '${applicationName}'`;
|
|
3308
3363
|
const resultResult = await this.runQuery(pool, sSQL);
|
|
@@ -3390,6 +3445,7 @@ export class ManageMetadataBase {
|
|
|
3390
3445
|
createNewEntityInsertSQL(newEntityUUID, newEntityName, newEntity, newEntitySuffix, newEntityDisplayName) {
|
|
3391
3446
|
const newEntityDefaults = configInfo.newEntityDefaults;
|
|
3392
3447
|
const newEntityDescriptionEscaped = newEntity.EntityDescription ? `'${newEntity.EntityDescription.replace(/'/g, "''")}'` : null;
|
|
3448
|
+
const allowCaching = this.resolveAllowCachingForSchema(newEntity.SchemaName);
|
|
3393
3449
|
const q = (name) => this.qi(name);
|
|
3394
3450
|
const sSQLInsert = `
|
|
3395
3451
|
INSERT INTO ${this.qs(mj_core_schema(), 'Entity')} (
|
|
@@ -3402,7 +3458,8 @@ export class ManageMetadataBase {
|
|
|
3402
3458
|
${q('BaseView')},
|
|
3403
3459
|
${q('SchemaName')},
|
|
3404
3460
|
${q('IncludeInAPI')},
|
|
3405
|
-
${q('AllowUserSearchAPI')}
|
|
3461
|
+
${q('AllowUserSearchAPI')},
|
|
3462
|
+
${q('AllowCaching')}
|
|
3406
3463
|
${newEntityDefaults.TrackRecordChanges === undefined ? '' : ', ' + q('TrackRecordChanges')}
|
|
3407
3464
|
${newEntityDefaults.AuditRecordAccess === undefined ? '' : ', ' + q('AuditRecordAccess')}
|
|
3408
3465
|
${newEntityDefaults.AuditViewRuns === undefined ? '' : ', ' + q('AuditViewRuns')}
|
|
@@ -3424,7 +3481,8 @@ export class ManageMetadataBase {
|
|
|
3424
3481
|
'vw${generatePluralName(newEntity.TableName, { capitalizeFirstLetterOnly: true }) + (newEntitySuffix && newEntitySuffix.length > 0 ? newEntitySuffix : '')}',
|
|
3425
3482
|
'${newEntity.SchemaName}',
|
|
3426
3483
|
1,
|
|
3427
|
-
${newEntityDefaults.AllowUserSearchAPI === undefined ? 1 : newEntityDefaults.AllowUserSearchAPI ? 1 : 0}
|
|
3484
|
+
${newEntityDefaults.AllowUserSearchAPI === undefined ? 1 : newEntityDefaults.AllowUserSearchAPI ? 1 : 0},
|
|
3485
|
+
${allowCaching ? 1 : 0}
|
|
3428
3486
|
${newEntityDefaults.TrackRecordChanges === undefined ? '' : ', ' + (newEntityDefaults.TrackRecordChanges ? '1' : '0')}
|
|
3429
3487
|
${newEntityDefaults.AuditRecordAccess === undefined ? '' : ', ' + (newEntityDefaults.AuditRecordAccess ? '1' : '0')}
|
|
3430
3488
|
${newEntityDefaults.AuditViewRuns === undefined ? '' : ', ' + (newEntityDefaults.AuditViewRuns ? '1' : '0')}
|
|
@@ -3439,6 +3497,26 @@ export class ManageMetadataBase {
|
|
|
3439
3497
|
`;
|
|
3440
3498
|
return sSQLInsert;
|
|
3441
3499
|
}
|
|
3500
|
+
/**
|
|
3501
|
+
* Resolves the AllowCaching default for a new entity in the given schema.
|
|
3502
|
+
* AllowCachingBySchema entries override the global AllowCaching default. The
|
|
3503
|
+
* `${mj_core_schema}` placeholder is expanded so core-schema rules apply
|
|
3504
|
+
* regardless of how the core schema is named in this deployment.
|
|
3505
|
+
*/
|
|
3506
|
+
resolveAllowCachingForSchema(schemaName) {
|
|
3507
|
+
const defaults = configInfo.newEntityDefaults;
|
|
3508
|
+
const overrides = defaults.AllowCachingBySchema ?? [];
|
|
3509
|
+
const match = overrides.find(entry => {
|
|
3510
|
+
let candidate = entry.SchemaName;
|
|
3511
|
+
if (candidate?.trim().toLowerCase() === '${mj_core_schema}') {
|
|
3512
|
+
candidate = mj_core_schema();
|
|
3513
|
+
}
|
|
3514
|
+
return candidate.trim().toLowerCase() === schemaName.trim().toLowerCase();
|
|
3515
|
+
});
|
|
3516
|
+
if (match)
|
|
3517
|
+
return match.AllowCaching;
|
|
3518
|
+
return defaults.AllowCaching ?? false;
|
|
3519
|
+
}
|
|
3442
3520
|
/**
|
|
3443
3521
|
* Apply Advanced Generation features - Smart Field Identification and Form Layout Generation
|
|
3444
3522
|
*/
|
|
@@ -3486,7 +3564,11 @@ export class ManageMetadataBase {
|
|
|
3486
3564
|
e.Description,
|
|
3487
3565
|
e.SchemaName,
|
|
3488
3566
|
e.BaseTable,
|
|
3489
|
-
e.ParentID
|
|
3567
|
+
e.ParentID,
|
|
3568
|
+
e.AllowUserSearchAPI,
|
|
3569
|
+
e.AutoUpdateAllowUserSearchAPI,
|
|
3570
|
+
e.FullTextSearchEnabled,
|
|
3571
|
+
e.AutoUpdateFullTextSearch
|
|
3490
3572
|
FROM
|
|
3491
3573
|
${this.qs(mj_core_schema(), 'vwEntities')} e
|
|
3492
3574
|
WHERE
|
|
@@ -3524,7 +3606,12 @@ export class ManageMetadataBase {
|
|
|
3524
3606
|
ef.AllowUpdateAPI,
|
|
3525
3607
|
ef.IsNameField,
|
|
3526
3608
|
ef.DefaultInView,
|
|
3527
|
-
ef.IncludeInUserSearchAPI
|
|
3609
|
+
ef.IncludeInUserSearchAPI,
|
|
3610
|
+
ef.UserSearchPredicateAPI,
|
|
3611
|
+
ef.AutoUpdateUserSearchPredicate,
|
|
3612
|
+
ef.FullTextSearchEnabled,
|
|
3613
|
+
ef.AutoUpdateFullTextSearch,
|
|
3614
|
+
ef.Length as MaxLength
|
|
3528
3615
|
FROM
|
|
3529
3616
|
${this.qs(mj_core_schema(), 'vwEntityFields')} ef
|
|
3530
3617
|
WHERE
|
|
@@ -3570,19 +3657,16 @@ export class ManageMetadataBase {
|
|
|
3570
3657
|
}
|
|
3571
3658
|
}
|
|
3572
3659
|
/**
|
|
3573
|
-
* Process entities in batches with parallel execution
|
|
3574
|
-
*
|
|
3575
|
-
* @param entities Entities to process
|
|
3576
|
-
* @param allFields All fields for all entities (will be filtered per entity)
|
|
3577
|
-
* @param ag AdvancedGeneration instance
|
|
3578
|
-
* @param currentUser User context
|
|
3579
|
-
* @param batchSize Number of entities to process in parallel (default 5)
|
|
3660
|
+
* Process entities in batches with parallel execution.
|
|
3661
|
+
* Batch size is configurable via advancedGeneration.batchSize in mj.config.cjs (default: 5).
|
|
3580
3662
|
*/
|
|
3581
|
-
async processEntitiesBatched(pool, entities, allFields, ag, currentUser
|
|
3663
|
+
async processEntitiesBatched(pool, entities, allFields, ag, currentUser) {
|
|
3664
|
+
const batchSize = configInfo.advancedGeneration?.batchSize ?? 5;
|
|
3582
3665
|
let processedCount = 0;
|
|
3583
3666
|
let errorCount = 0;
|
|
3667
|
+
const total = entities.length;
|
|
3584
3668
|
// Process in batches
|
|
3585
|
-
for (let i = 0; i <
|
|
3669
|
+
for (let i = 0; i < total; i += batchSize) {
|
|
3586
3670
|
const batch = entities.slice(i, i + batchSize);
|
|
3587
3671
|
// Process batch in parallel
|
|
3588
3672
|
const batchResults = await Promise.allSettled(batch.map(entity => this.processEntityAdvancedGeneration(pool, entity, allFields, ag, currentUser)));
|
|
@@ -3596,9 +3680,9 @@ export class ManageMetadataBase {
|
|
|
3596
3680
|
logError(` Error processing entity: ${result.reason}`);
|
|
3597
3681
|
}
|
|
3598
3682
|
}
|
|
3599
|
-
|
|
3683
|
+
const pct = Math.round((processedCount / total) * 100);
|
|
3684
|
+
updateSpinner(`Advanced generation: ${processedCount}/${total} entities (${pct}%)${errorCount > 0 ? ` — ${errorCount} error(s)` : ''}`);
|
|
3600
3685
|
}
|
|
3601
|
-
logStatus(` Advanced Generation complete: ${processedCount} entities processed, ${errorCount} errors`);
|
|
3602
3686
|
return errorCount === 0;
|
|
3603
3687
|
}
|
|
3604
3688
|
/**
|
|
@@ -3616,15 +3700,26 @@ export class ManageMetadataBase {
|
|
|
3616
3700
|
// Determine if this is a new entity (for DefaultForNewUser decision)
|
|
3617
3701
|
const isNewEntity = ManageMetadataBase.newEntityList.includes(entity.Name);
|
|
3618
3702
|
// Smart Field Identification
|
|
3619
|
-
// Only run if at least one field allows auto-update for any of the smart field properties
|
|
3620
|
-
|
|
3703
|
+
// Only run if at least one field allows auto-update for any of the smart field properties,
|
|
3704
|
+
// or if entity-level search/FTS auto-update flags are set
|
|
3705
|
+
const needsFieldAnalysis = fields.some((f) => f.AutoUpdateIsNameField || f.AutoUpdateDefaultInView || f.AutoUpdateIncludeInUserSearchAPI || f.AutoUpdateUserSearchPredicate || f.AutoUpdateFullTextSearch);
|
|
3706
|
+
// entity-level auto-update flags come from the raw SQL query, not EntityInfo
|
|
3707
|
+
const entityRecord = entity;
|
|
3708
|
+
const needsEntitySearchConfig = entityRecord.AutoUpdateAllowUserSearchAPI || entityRecord.AutoUpdateFullTextSearch;
|
|
3709
|
+
if (needsFieldAnalysis || needsEntitySearchConfig) {
|
|
3621
3710
|
const fieldAnalysis = await ag.identifyFields({
|
|
3622
3711
|
Name: entity.Name,
|
|
3623
3712
|
Description: entity.Description,
|
|
3624
3713
|
Fields: fields
|
|
3625
3714
|
}, currentUser);
|
|
3626
3715
|
if (fieldAnalysis) {
|
|
3627
|
-
await this.applySmartFieldIdentification(pool,
|
|
3716
|
+
await this.applySmartFieldIdentification(pool, {
|
|
3717
|
+
ID: entity.ID,
|
|
3718
|
+
AllowUserSearchAPI: entityRecord.AllowUserSearchAPI,
|
|
3719
|
+
AutoUpdateAllowUserSearchAPI: entityRecord.AutoUpdateAllowUserSearchAPI,
|
|
3720
|
+
FullTextSearchEnabled: entityRecord.FullTextSearchEnabled,
|
|
3721
|
+
AutoUpdateFullTextSearch: entityRecord.AutoUpdateFullTextSearch,
|
|
3722
|
+
}, fields, fieldAnalysis);
|
|
3628
3723
|
}
|
|
3629
3724
|
}
|
|
3630
3725
|
// Form Layout Generation
|
|
@@ -3713,14 +3808,36 @@ export class ManageMetadataBase {
|
|
|
3713
3808
|
return null;
|
|
3714
3809
|
}
|
|
3715
3810
|
/**
|
|
3716
|
-
* Apply smart field identification results to entity fields
|
|
3811
|
+
* Apply smart field identification results to entity fields and entity-level search configuration
|
|
3717
3812
|
*/
|
|
3718
|
-
async applySmartFieldIdentification(pool,
|
|
3813
|
+
async applySmartFieldIdentification(pool, entity, fields, result) {
|
|
3719
3814
|
const sqlStatements = [];
|
|
3720
|
-
|
|
3815
|
+
this.applyNameFieldUpdates(sqlStatements, fields, result);
|
|
3816
|
+
this.applyDefaultInViewUpdates(sqlStatements, fields, result);
|
|
3817
|
+
this.applySearchableFieldUpdates(sqlStatements, fields, result);
|
|
3818
|
+
this.applySearchPredicateUpdates(sqlStatements, fields, result);
|
|
3819
|
+
this.applyEntitySearchConfig(sqlStatements, entity, result);
|
|
3820
|
+
if (configInfo.advancedGeneration?.allowFullTextSearchAutoUpdate) {
|
|
3821
|
+
this.applyFullTextSearchUpdates(sqlStatements, entity, fields, result);
|
|
3822
|
+
}
|
|
3823
|
+
// Execute all updates in one batch
|
|
3824
|
+
if (sqlStatements.length > 0) {
|
|
3825
|
+
const combinedSQL = sqlStatements.join('\n');
|
|
3826
|
+
try {
|
|
3827
|
+
await this.LogSQLAndExecute(pool, combinedSQL, `Set field properties for entity`, false);
|
|
3828
|
+
}
|
|
3829
|
+
catch (ex) {
|
|
3830
|
+
logError('Error executing combined smart field SQL: ', ex);
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Generate SQL UPDATEs for IsNameField on the identified name fields
|
|
3836
|
+
*/
|
|
3837
|
+
applyNameFieldUpdates(sqlStatements, fields, result) {
|
|
3721
3838
|
const nameFieldNames = result.nameFields ?? [];
|
|
3722
3839
|
for (const nfName of nameFieldNames) {
|
|
3723
|
-
const nameField = fields.find(
|
|
3840
|
+
const nameField = fields.find(f => f.Name === nfName);
|
|
3724
3841
|
if (nameField && nameField.AutoUpdateIsNameField && nameField.ID && !nameField.IsNameField) {
|
|
3725
3842
|
sqlStatements.push(`
|
|
3726
3843
|
UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
@@ -3733,17 +3850,18 @@ export class ManageMetadataBase {
|
|
|
3733
3850
|
logError(`Smart field identification returned invalid nameField: '${nfName}' not found in entity fields`);
|
|
3734
3851
|
}
|
|
3735
3852
|
}
|
|
3736
|
-
|
|
3853
|
+
}
|
|
3854
|
+
/**
|
|
3855
|
+
* Generate SQL UPDATEs for DefaultInView on the identified default view fields
|
|
3856
|
+
*/
|
|
3857
|
+
applyDefaultInViewUpdates(sqlStatements, fields, result) {
|
|
3737
3858
|
const defaultInViewFields = fields.filter(f => result.defaultInView.includes(f.Name) && f.AutoUpdateDefaultInView && f.ID);
|
|
3738
|
-
// Warn about any fields that weren't found
|
|
3739
3859
|
const missingFields = result.defaultInView.filter(name => !fields.some(f => f.Name === name));
|
|
3740
3860
|
if (missingFields.length > 0) {
|
|
3741
3861
|
logError(`Smart field identification returned invalid defaultInView fields: ${missingFields.join(', ')} not found in entity`);
|
|
3742
3862
|
}
|
|
3743
|
-
// Build update statements for all default in view fields
|
|
3744
3863
|
for (const field of defaultInViewFields) {
|
|
3745
3864
|
if (!field.DefaultInView) {
|
|
3746
|
-
// only set these when DefaultInView not already on, otherwise wasteful
|
|
3747
3865
|
sqlStatements.push(`
|
|
3748
3866
|
UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
3749
3867
|
SET DefaultInView = 1
|
|
@@ -3752,35 +3870,112 @@ export class ManageMetadataBase {
|
|
|
3752
3870
|
`);
|
|
3753
3871
|
}
|
|
3754
3872
|
}
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3873
|
+
}
|
|
3874
|
+
/**
|
|
3875
|
+
* Generate SQL UPDATEs for IncludeInUserSearchAPI on the identified searchable fields
|
|
3876
|
+
*/
|
|
3877
|
+
applySearchableFieldUpdates(sqlStatements, fields, result) {
|
|
3878
|
+
if (!result.searchableFields || result.searchableFields.length === 0) {
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
const searchableFields = fields.filter(f => result.searchableFields.includes(f.Name) && f.AutoUpdateIncludeInUserSearchAPI && f.ID);
|
|
3882
|
+
const missingSearchableFields = result.searchableFields.filter(name => !fields.some(f => f.Name === name));
|
|
3883
|
+
if (missingSearchableFields.length > 0) {
|
|
3884
|
+
logError(`Smart field identification returned invalid searchableFields: ${missingSearchableFields.join(', ')} not found in entity`);
|
|
3885
|
+
}
|
|
3886
|
+
for (const field of searchableFields) {
|
|
3887
|
+
if (!field.IncludeInUserSearchAPI) {
|
|
3888
|
+
sqlStatements.push(`
|
|
3889
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
3890
|
+
SET IncludeInUserSearchAPI = 1
|
|
3891
|
+
WHERE ID = '${field.ID}'
|
|
3892
|
+
AND AutoUpdateIncludeInUserSearchAPI = 1
|
|
3893
|
+
`);
|
|
3762
3894
|
}
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Generate SQL UPDATEs for UserSearchPredicateAPI on each searchable field
|
|
3899
|
+
*/
|
|
3900
|
+
applySearchPredicateUpdates(sqlStatements, fields, result) {
|
|
3901
|
+
if (!result.searchPredicates || result.searchPredicates.length === 0) {
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
for (const sp of result.searchPredicates) {
|
|
3905
|
+
const field = fields.find(f => f.Name === sp.field);
|
|
3906
|
+
if (!field) {
|
|
3907
|
+
logError(`Smart field identification returned invalid searchPredicate field: '${sp.field}' not found in entity fields`);
|
|
3908
|
+
continue;
|
|
3909
|
+
}
|
|
3910
|
+
if (!field.AutoUpdateUserSearchPredicate || !field.ID) {
|
|
3911
|
+
continue;
|
|
3912
|
+
}
|
|
3913
|
+
// Only update if the current value differs from the recommended predicate
|
|
3914
|
+
if (field.UserSearchPredicateAPI !== sp.predicate) {
|
|
3915
|
+
sqlStatements.push(`
|
|
3916
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
3917
|
+
SET UserSearchPredicateAPI = '${sp.predicate}'
|
|
3918
|
+
WHERE ID = '${field.ID}'
|
|
3919
|
+
AND AutoUpdateUserSearchPredicate = 1
|
|
3920
|
+
`);
|
|
3774
3921
|
}
|
|
3775
3922
|
}
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3923
|
+
}
|
|
3924
|
+
/**
|
|
3925
|
+
* Generate SQL UPDATEs for entity-level AllowUserSearchAPI
|
|
3926
|
+
*/
|
|
3927
|
+
applyEntitySearchConfig(sqlStatements, entity, result) {
|
|
3928
|
+
if (result.allowUserSearch == null || !entity.AutoUpdateAllowUserSearchAPI) {
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
const newValue = result.allowUserSearch ? 1 : 0;
|
|
3932
|
+
const currentValue = entity.AllowUserSearchAPI ? 1 : 0;
|
|
3933
|
+
if (newValue !== currentValue) {
|
|
3934
|
+
sqlStatements.push(`
|
|
3935
|
+
UPDATE ${this.qs(mj_core_schema(), 'Entity')}
|
|
3936
|
+
SET AllowUserSearchAPI = ${newValue}
|
|
3937
|
+
WHERE ID = '${entity.ID}'
|
|
3938
|
+
AND AutoUpdateAllowUserSearchAPI = 1
|
|
3939
|
+
`);
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
/**
|
|
3943
|
+
* Generate SQL UPDATEs for entity-level and field-level FullTextSearch configuration
|
|
3944
|
+
*/
|
|
3945
|
+
applyFullTextSearchUpdates(sqlStatements, entity, fields, result) {
|
|
3946
|
+
// Entity-level FullTextSearchEnabled
|
|
3947
|
+
if (result.enableFullTextSearch != null && entity.AutoUpdateFullTextSearch) {
|
|
3948
|
+
const newValue = result.enableFullTextSearch ? 1 : 0;
|
|
3949
|
+
const currentValue = entity.FullTextSearchEnabled ? 1 : 0;
|
|
3950
|
+
if (newValue !== currentValue) {
|
|
3951
|
+
sqlStatements.push(`
|
|
3952
|
+
UPDATE ${this.qs(mj_core_schema(), 'Entity')}
|
|
3953
|
+
SET FullTextSearchEnabled = ${newValue}
|
|
3954
|
+
WHERE ID = '${entity.ID}'
|
|
3955
|
+
AND AutoUpdateFullTextSearch = 1
|
|
3956
|
+
`);
|
|
3781
3957
|
}
|
|
3782
|
-
|
|
3783
|
-
|
|
3958
|
+
}
|
|
3959
|
+
// Field-level FullTextSearchEnabled
|
|
3960
|
+
if (!result.fullTextSearchFields || result.fullTextSearchFields.length === 0) {
|
|
3961
|
+
return;
|
|
3962
|
+
}
|
|
3963
|
+
for (const ftsFieldName of result.fullTextSearchFields) {
|
|
3964
|
+
const field = fields.find(f => f.Name === ftsFieldName);
|
|
3965
|
+
if (!field) {
|
|
3966
|
+
logError(`Smart field identification returned invalid fullTextSearchField: '${ftsFieldName}' not found in entity fields`);
|
|
3967
|
+
continue;
|
|
3968
|
+
}
|
|
3969
|
+
if (!field.AutoUpdateFullTextSearch || !field.ID) {
|
|
3970
|
+
continue;
|
|
3971
|
+
}
|
|
3972
|
+
if (!field.FullTextSearchEnabled) {
|
|
3973
|
+
sqlStatements.push(`
|
|
3974
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
3975
|
+
SET FullTextSearchEnabled = 1
|
|
3976
|
+
WHERE ID = '${field.ID}'
|
|
3977
|
+
AND AutoUpdateFullTextSearch = 1
|
|
3978
|
+
`);
|
|
3784
3979
|
}
|
|
3785
3980
|
}
|
|
3786
3981
|
}
|
|
@@ -3796,6 +3991,8 @@ export class ManageMetadataBase {
|
|
|
3796
3991
|
async applyFormLayout(pool, entity, fields, result, isNewEntity = false) {
|
|
3797
3992
|
const existingCategories = this.buildExistingCategorySet(fields);
|
|
3798
3993
|
await this.applyFieldCategories(pool, entity, fields, result.fieldCategories, existingCategories);
|
|
3994
|
+
// Auto-detect geo-capable entities and set SupportsGeoCoding
|
|
3995
|
+
await this.detectAndSetGeoCodingSupport(pool, entity, result.fieldCategories);
|
|
3799
3996
|
if (result.entityIcon) {
|
|
3800
3997
|
await this.applyEntityIcon(pool, entity.ID, result.entityIcon);
|
|
3801
3998
|
}
|
|
@@ -3810,6 +4007,52 @@ export class ManageMetadataBase {
|
|
|
3810
4007
|
await this.applyEntityImportance(pool, entity.ID, result.entityImportance);
|
|
3811
4008
|
}
|
|
3812
4009
|
}
|
|
4010
|
+
/**
|
|
4011
|
+
* Detect if an entity has geo-capable fields based on LLM-assigned ExtendedType values.
|
|
4012
|
+
* If any field has a Geo* ExtendedType, set Entity.SupportsGeoCoding = 1.
|
|
4013
|
+
* Respects the AutoUpdateSupportsGeoCoding flag — if 0, the value is locked.
|
|
4014
|
+
*/
|
|
4015
|
+
async detectAndSetGeoCodingSupport(pool, entity, fieldCategories) {
|
|
4016
|
+
const schema = mj_core_schema();
|
|
4017
|
+
// Check if the entity's AutoUpdateSupportsGeoCoding flag allows us to modify it
|
|
4018
|
+
const autoUpdateResult = await pool.query(`
|
|
4019
|
+
SELECT ${this.qi('AutoUpdateSupportsGeoCoding')}, ${this.qi('SupportsGeoCoding')}
|
|
4020
|
+
FROM ${this.qs(schema, 'Entity')}
|
|
4021
|
+
WHERE ${this.qi('ID')} = '${entity.ID}'
|
|
4022
|
+
`);
|
|
4023
|
+
const row = autoUpdateResult?.recordset?.[0];
|
|
4024
|
+
if (!row || !row.AutoUpdateSupportsGeoCoding) {
|
|
4025
|
+
return; // Flag is locked, don't modify
|
|
4026
|
+
}
|
|
4027
|
+
// Detect geo-capable fields from the LLM results
|
|
4028
|
+
const geoExtendedTypes = new Set([
|
|
4029
|
+
'Geo', 'GeoLatitude', 'GeoLongitude', 'GeoCountry', 'GeoStateProvince',
|
|
4030
|
+
'GeoCity', 'GeoPostalCode', 'GeoAddress'
|
|
4031
|
+
]);
|
|
4032
|
+
const hasGeoFields = fieldCategories.some(fc => fc.extendedType != null && geoExtendedTypes.has(fc.extendedType));
|
|
4033
|
+
// Also check existing fields in the database for Geo* ExtendedTypes
|
|
4034
|
+
// (in case the LLM didn't re-assign them this run but they were previously set)
|
|
4035
|
+
const existingGeoResult = await pool.query(`
|
|
4036
|
+
SELECT COUNT(*) AS ${this.qi('GeoFieldCount')}
|
|
4037
|
+
FROM ${this.qs(schema, 'EntityField')}
|
|
4038
|
+
WHERE ${this.qi('EntityID')} = '${entity.ID}'
|
|
4039
|
+
AND ${this.qi('ExtendedType')} IN ('Geo', 'GeoLatitude', 'GeoLongitude', 'GeoCountry', 'GeoStateProvince', 'GeoCity', 'GeoPostalCode', 'GeoAddress')
|
|
4040
|
+
`);
|
|
4041
|
+
const existingGeoCount = existingGeoResult?.recordset?.[0]?.GeoFieldCount ?? 0;
|
|
4042
|
+
const shouldSupportGeo = hasGeoFields || existingGeoCount > 0;
|
|
4043
|
+
const currentValue = row.SupportsGeoCoding ? true : false;
|
|
4044
|
+
if (shouldSupportGeo !== currentValue) {
|
|
4045
|
+
await this.LogSQLAndExecute(pool, `
|
|
4046
|
+
UPDATE ${this.qs(schema, 'Entity')}
|
|
4047
|
+
SET ${this.qi('SupportsGeoCoding')} = ${shouldSupportGeo ? 1 : 0}
|
|
4048
|
+
WHERE ${this.qi('ID')} = '${entity.ID}' AND ${this.qi('AutoUpdateSupportsGeoCoding')} = 1
|
|
4049
|
+
`, `Set SupportsGeoCoding = ${shouldSupportGeo ? 1 : 0} for ${entity.Name}`);
|
|
4050
|
+
logStatus(` Entity ${entity.Name}: SupportsGeoCoding = ${shouldSupportGeo ? 1 : 0} (auto-detected from ${hasGeoFields ? 'LLM' : 'existing'} geo fields)`);
|
|
4051
|
+
// Queue for late-phase view regeneration — the view was already generated
|
|
4052
|
+
// before this flag was set, so it needs to be regenerated with the geo JOIN
|
|
4053
|
+
ManageMetadataBase.AddEntityRequiringViewRegen(entity.Name, 'Geocoding');
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
3813
4056
|
// ─────────────────────────────────────────────────────────────────
|
|
3814
4057
|
// Shared category / icon / settings persistence methods
|
|
3815
4058
|
// Used by both the regular entity pipeline and VE decoration pipeline
|