@memberjunction/codegen-lib 6.1.0-edge.5 → 6.1.0-edge.6
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 +20 -0
- package/dist/Angular/angular-codegen.d.ts +9 -6
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +80 -51
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/join-grid-related-entity-component.d.ts.map +1 -1
- package/dist/Angular/join-grid-related-entity-component.js +5 -2
- package/dist/Angular/join-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/related-entity-components.d.ts +11 -0
- package/dist/Angular/related-entity-components.d.ts.map +1 -1
- package/dist/Angular/related-entity-components.js +23 -0
- package/dist/Angular/related-entity-components.js.map +1 -1
- package/dist/Angular/timeline-related-entity-component.d.ts.map +1 -1
- package/dist/Angular/timeline-related-entity-component.js +3 -2
- package/dist/Angular/timeline-related-entity-component.js.map +1 -1
- package/dist/Config/config.d.ts +44 -2
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +31 -5
- package/dist/Config/config.js.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts +19 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.js +37 -3
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
- package/dist/Database/dbSchema.js +2 -1
- package/dist/Database/dbSchema.js.map +1 -1
- package/dist/Database/decision-metadata-format.d.ts +124 -0
- package/dist/Database/decision-metadata-format.d.ts.map +1 -0
- package/dist/Database/decision-metadata-format.js +279 -0
- package/dist/Database/decision-metadata-format.js.map +1 -0
- package/dist/Database/decision-metadata-writer.d.ts +48 -0
- package/dist/Database/decision-metadata-writer.d.ts.map +1 -0
- package/dist/Database/decision-metadata-writer.js +482 -0
- package/dist/Database/decision-metadata-writer.js.map +1 -0
- package/dist/Database/entity-field-change-tracking.d.ts +53 -0
- package/dist/Database/entity-field-change-tracking.d.ts.map +1 -0
- package/dist/Database/entity-field-change-tracking.js +120 -0
- package/dist/Database/entity-field-change-tracking.js.map +1 -0
- package/dist/Database/entity-level-tree-cycles.d.ts.map +1 -1
- package/dist/Database/entity-level-tree-cycles.js +3 -2
- package/dist/Database/entity-level-tree-cycles.js.map +1 -1
- package/dist/Database/field-metadata-lock.d.ts +47 -0
- package/dist/Database/field-metadata-lock.d.ts.map +1 -0
- package/dist/Database/field-metadata-lock.js +159 -0
- package/dist/Database/field-metadata-lock.js.map +1 -0
- package/dist/Database/manage-metadata.d.ts +100 -50
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +549 -292
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +2 -2
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +90 -41
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/postgresql/metadataSupportObjects.js +31 -8
- package/dist/Database/providers/postgresql/metadataSupportObjects.js.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +48 -23
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js +15 -2
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +9 -0
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +53 -14
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.js +3 -2
- package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +2 -2
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.d.ts +7 -5
- package/dist/Misc/advanced_generation.d.ts.map +1 -1
- package/dist/Misc/advanced_generation.js +34 -23
- package/dist/Misc/advanced_generation.js.map +1 -1
- package/dist/Misc/codegen-reporter.d.ts +2 -0
- package/dist/Misc/codegen-reporter.d.ts.map +1 -1
- package/dist/Misc/codegen-reporter.js +6 -2
- package/dist/Misc/codegen-reporter.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +30 -20
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +12 -5
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/remote_operations_codegen.d.ts.map +1 -1
- package/dist/Misc/remote_operations_codegen.js +4 -3
- package/dist/Misc/remote_operations_codegen.js.map +1 -1
- package/dist/Misc/schema-emit.d.ts.map +1 -1
- package/dist/Misc/schema-emit.js +2 -1
- package/dist/Misc/schema-emit.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +63 -8
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +169 -42
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/Misc/util.d.ts +12 -0
- package/dist/Misc/util.d.ts.map +1 -1
- package/dist/Misc/util.js +71 -8
- package/dist/Misc/util.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +28 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +40 -13
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +29 -29
|
@@ -11,7 +11,7 @@ import { QueryParameterProcessor } from '@memberjunction/query-processor';
|
|
|
11
11
|
// and the SS code path silently fails.
|
|
12
12
|
import './providers/sqlserver/SQLServerCodeGenProvider.js';
|
|
13
13
|
import { configInfo, currentWorkingDirectory, dbPlatform, getSettingValue, mj_core_schema, outputDir } from '../Config/config.js';
|
|
14
|
-
import { CodeNameFromString, EntityInfo, ExternalDataSourceReadRouter, ExtractActualDefaultValue, LogError, LogStatus, Metadata, RunQuerySQLFilterManager, SeverityType } from "@memberjunction/core";
|
|
14
|
+
import { CodeNameFromString, EntityFieldInfo, EntityInfo, ExternalDataSourceReadRouter, ExtractActualDefaultValue, LogError, LogStatus, Metadata, RunQuerySQLFilterManager, SeverityType } from "@memberjunction/core";
|
|
15
15
|
import { MJEntityFieldSchema } from "@memberjunction/core-entities";
|
|
16
16
|
import { logError, logMessage, logStatus, startSpinner, updateSpinner, succeedSpinner } from "../Misc/status_logging.js";
|
|
17
17
|
import { SQLUtilityBase } from "./sql.js";
|
|
@@ -23,10 +23,15 @@ import { applySearchableFieldsCap, entityLevelEnableBlockedReason, isNarrativeFi
|
|
|
23
23
|
import { mapExternalNativeTypeToMJ } from "../Misc/externalTypeMapping.js";
|
|
24
24
|
import { SQLParser } from "@memberjunction/sql-parser";
|
|
25
25
|
import { createDisplayName, generatePluralName, MJGlobal, ResolveSingleEntityResourceTarget, stripTrailingChars, UUIDsEqual } from "@memberjunction/global";
|
|
26
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
26
|
+
import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
|
|
27
27
|
import * as fs from 'fs';
|
|
28
28
|
import path from 'path';
|
|
29
|
+
import { canonicalJSONStringify, deepEqualJSON } from "../Misc/util.js";
|
|
29
30
|
import { SQLLogging } from "../Misc/sql_logging.js";
|
|
31
|
+
import { computeFieldMetadataUpdate } from "./field-metadata-lock.js";
|
|
32
|
+
import { DecisionMetadataWriter } from "./decision-metadata-writer.js";
|
|
33
|
+
import { TRACKED_FIELD_COLUMNS, DISPLAYNAME_REOPEN_REASONS, TYPE_REOPEN_REASONS, diffEntityFieldSnapshots, } from './entity-field-change-tracking.js';
|
|
34
|
+
export { TRACKED_FIELD_COLUMNS, DISPLAYNAME_REOPEN_REASONS, TYPE_REOPEN_REASONS, diffEntityFieldSnapshots, } from './entity-field-change-tracking.js';
|
|
30
35
|
export class ValidatorResult {
|
|
31
36
|
constructor() {
|
|
32
37
|
this.entityName = "";
|
|
@@ -161,47 +166,19 @@ export class ManageMetadataBase {
|
|
|
161
166
|
return pool.queryWithParams(this.qsql(sql), params);
|
|
162
167
|
}
|
|
163
168
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* `MAX(Sequence) + 1` for that entity.
|
|
169
|
+
* The `Sequence` expression for every EntityField INSERT CodeGen emits:
|
|
170
|
+
* `(SELECT COALESCE(MAX(Sequence), 0) + 1 FROM EntityField WHERE EntityID = '<id>')`.
|
|
167
171
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const tbl = this.qs(schema, 'EntityField');
|
|
179
|
-
const seqCol = this.qi('Sequence');
|
|
180
|
-
const entityIdCol = this.qi('EntityID');
|
|
181
|
-
const sql = `SELECT
|
|
182
|
-
${this.coalesce(`MAX(${seqCol})`, '0')} AS ${this.qi('MaxSeq')},
|
|
183
|
-
${this.coalesce(`MAX(CASE WHEN ${seqCol} = @Candidate THEN 1 ELSE 0 END)`, '0')} AS ${this.qi('Hit')}
|
|
184
|
-
FROM ${tbl}
|
|
185
|
-
WHERE ${entityIdCol} = @EntityID`;
|
|
186
|
-
try {
|
|
187
|
-
const result = await this.runQueryWithParams(pool, sql, {
|
|
188
|
-
EntityID: entityId,
|
|
189
|
-
Candidate: candidate,
|
|
190
|
-
});
|
|
191
|
-
const row = result.recordset?.[0];
|
|
192
|
-
if (!row)
|
|
193
|
-
return candidate;
|
|
194
|
-
const hit = Number(row.Hit ?? 0) > 0;
|
|
195
|
-
if (!hit)
|
|
196
|
-
return candidate;
|
|
197
|
-
const maxSeq = Number(row.MaxSeq ?? 0);
|
|
198
|
-
return maxSeq + 1;
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
// If the lookup itself fails, fall back to the candidate. The downstream
|
|
202
|
-
// INSERT will surface the real error if there's a true collision.
|
|
203
|
-
return candidate;
|
|
204
|
-
}
|
|
172
|
+
* Evaluated at APPLY time, never a literal. The value is disposable — the run's own
|
|
173
|
+
* spUpdateExistingEntityFieldsFromSchema pass and R__RefreshMetadata renumber every field from
|
|
174
|
+
* the schema — but it must be unique on ANY database in ANY order, because the INSERT is
|
|
175
|
+
* appended verbatim to a migration and Flyway runs every versioned migration before the
|
|
176
|
+
* repeatable renumber. Each INSERT re-evaluates MAX after the one before it, so a batch that
|
|
177
|
+
* executes sequentially in emission order rises in that order (#3670, #4202).
|
|
178
|
+
*/
|
|
179
|
+
applyTimeEntityFieldSequenceSQL(entityID) {
|
|
180
|
+
// COALESCE is valid on both platforms and is the form the docs and the gate describe.
|
|
181
|
+
return `(SELECT COALESCE(MAX(${this.qi('Sequence')}), 0) + 1 FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ${this.qi('EntityID')} = '${entityID}')`;
|
|
205
182
|
}
|
|
206
183
|
/**
|
|
207
184
|
* Wraps a SELECT query with a row limit.
|
|
@@ -276,6 +253,9 @@ export class ManageMetadataBase {
|
|
|
276
253
|
static get newEntityList() {
|
|
277
254
|
return this._newEntityList;
|
|
278
255
|
}
|
|
256
|
+
static set newEntityList(value) {
|
|
257
|
+
this._newEntityList = value;
|
|
258
|
+
}
|
|
279
259
|
static { this._modifiedEntityList = []; }
|
|
280
260
|
/**
|
|
281
261
|
* Globally scoped list of entities that have been modified during the metadata management process.
|
|
@@ -283,6 +263,108 @@ export class ManageMetadataBase {
|
|
|
283
263
|
static get modifiedEntityList() {
|
|
284
264
|
return this._modifiedEntityList;
|
|
285
265
|
}
|
|
266
|
+
static set modifiedEntityList(value) {
|
|
267
|
+
this._modifiedEntityList = value;
|
|
268
|
+
}
|
|
269
|
+
static { this._newFieldSet = new Set(); }
|
|
270
|
+
static { this._changedFields = new Map(); }
|
|
271
|
+
static { this._changedFieldReport = []; }
|
|
272
|
+
static fieldKey(entityID, name) {
|
|
273
|
+
const eid = (entityID ?? '').trim().toLowerCase();
|
|
274
|
+
const fld = (name ?? '').trim().toLowerCase();
|
|
275
|
+
if (!eid || !fld) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
return `${eid}:${fld}`;
|
|
279
|
+
}
|
|
280
|
+
static registerNewField(entityID, name) {
|
|
281
|
+
const key = this.fieldKey(entityID, name);
|
|
282
|
+
if (key) {
|
|
283
|
+
this._newFieldSet.add(key);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
static registerFieldChange(entityID, name, reasons) {
|
|
287
|
+
const key = this.fieldKey(entityID, name);
|
|
288
|
+
if (key) {
|
|
289
|
+
let set = this._changedFields.get(key);
|
|
290
|
+
if (!set) {
|
|
291
|
+
set = new Set();
|
|
292
|
+
this._changedFields.set(key, set);
|
|
293
|
+
}
|
|
294
|
+
for (const r of reasons) {
|
|
295
|
+
set.add(r);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
static isFieldNew(entityID, name) {
|
|
300
|
+
const key = this.fieldKey(entityID, name);
|
|
301
|
+
return key ? this._newFieldSet.has(key) : false;
|
|
302
|
+
}
|
|
303
|
+
static fieldChangeReasons(entityID, name) {
|
|
304
|
+
const key = this.fieldKey(entityID, name);
|
|
305
|
+
const set = key ? this._changedFields.get(key) : undefined;
|
|
306
|
+
return set ?? new Set();
|
|
307
|
+
}
|
|
308
|
+
static isDisplayNameReopened(entityID, name) {
|
|
309
|
+
const reasons = this.fieldChangeReasons(entityID, name);
|
|
310
|
+
for (const r of reasons) {
|
|
311
|
+
if (DISPLAYNAME_REOPEN_REASONS.has(r)) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
static isTypeReopened(entityID, name) {
|
|
318
|
+
const reasons = this.fieldChangeReasons(entityID, name);
|
|
319
|
+
for (const r of reasons) {
|
|
320
|
+
if (TYPE_REOPEN_REASONS.has(r)) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
static get newFieldCount() {
|
|
327
|
+
return this._newFieldSet.size;
|
|
328
|
+
}
|
|
329
|
+
static get changedFieldCount() {
|
|
330
|
+
return this._changedFields.size;
|
|
331
|
+
}
|
|
332
|
+
static get changedFieldReport() {
|
|
333
|
+
return this._changedFieldReport;
|
|
334
|
+
}
|
|
335
|
+
static clearFieldTracking() {
|
|
336
|
+
this._newFieldSet.clear();
|
|
337
|
+
this._changedFields.clear();
|
|
338
|
+
this._changedFieldReport = [];
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Takes a snapshot of EntityField rows from vwEntityFields for diffing.
|
|
342
|
+
* Only queries columns in TRACKED_FIELD_COLUMNS (Sequence is excluded).
|
|
343
|
+
*/
|
|
344
|
+
async snapshotEntityFields(pool, entityIDs) {
|
|
345
|
+
const isScoped = entityIDs !== undefined && entityIDs.length > 0;
|
|
346
|
+
const filter = isScoped
|
|
347
|
+
? `WHERE ${this.qi('EntityID')} IN (${entityIDs.map(id => `'${id}'`).join(',')})`
|
|
348
|
+
: '';
|
|
349
|
+
const cols = TRACKED_FIELD_COLUMNS.map(c => this.qi(c)).join(', ');
|
|
350
|
+
const sSQL = `SELECT ${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Entity')} AS ${this.qi('EntityName')}, ${this.qi('Name')}, ${cols}
|
|
351
|
+
FROM ${this.qs(mj_core_schema(), 'vwEntityFields')}
|
|
352
|
+
${filter}`;
|
|
353
|
+
const result = await this.runQuery(pool, sSQL);
|
|
354
|
+
const map = new Map();
|
|
355
|
+
if (result && result.recordset) {
|
|
356
|
+
for (const r of result.recordset) {
|
|
357
|
+
map.set(String(r.ID).toLowerCase(), r);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return map;
|
|
361
|
+
}
|
|
362
|
+
findFieldByName(fields, name) {
|
|
363
|
+
if (!name)
|
|
364
|
+
return undefined;
|
|
365
|
+
const target = name.trim().toLowerCase();
|
|
366
|
+
return fields.find(f => (f.Name ?? '').trim().toLowerCase() === target);
|
|
367
|
+
}
|
|
286
368
|
static { this._deletedEntitySchemaList = []; }
|
|
287
369
|
/**
|
|
288
370
|
* Schemas that lost an entity during this run.
|
|
@@ -939,29 +1021,63 @@ export class ManageMetadataBase {
|
|
|
939
1021
|
logStatus(` > Entities config: "${ec.SchemaName}.${ec.BaseTable}" has no attributes to set — skipping`);
|
|
940
1022
|
continue;
|
|
941
1023
|
}
|
|
942
|
-
// Look up the entity by BaseTable + SchemaName
|
|
1024
|
+
// Look up the entity by BaseTable + SchemaName, selecting current values for the named attributes
|
|
1025
|
+
const attrCols = attrs.map(([k]) => `${this.qi(k)}`).join(', ');
|
|
943
1026
|
const entityResult = await this.runQueryWithParams(pool, `
|
|
944
|
-
${this.selectTop(1,
|
|
1027
|
+
${this.selectTop(1, `ID, Name, ${attrCols}`, `FROM ${this.qs(schema, 'vwEntities')}
|
|
945
1028
|
WHERE BaseTable = @BaseTable AND SchemaName = @SchemaName`)}
|
|
946
1029
|
`, { 'BaseTable': ec.BaseTable, 'SchemaName': ec.SchemaName });
|
|
947
1030
|
if (entityResult.recordset.length === 0) {
|
|
948
1031
|
logError(` > Entities config: entity for "${ec.SchemaName}.${ec.BaseTable}" not found — skipping`);
|
|
949
1032
|
continue;
|
|
950
1033
|
}
|
|
951
|
-
const
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const
|
|
1034
|
+
const currentRow = entityResult.recordset[0];
|
|
1035
|
+
const entityId = String(currentRow.ID);
|
|
1036
|
+
const entityName = String(currentRow.Name);
|
|
1037
|
+
// Compare each attribute against the current database value; only include differing attributes
|
|
1038
|
+
const differingAttrs = [];
|
|
956
1039
|
for (const [key, value] of attrs) {
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1040
|
+
const curVal = currentRow[key];
|
|
1041
|
+
let differs = false;
|
|
1042
|
+
let litVal = 'NULL';
|
|
1043
|
+
if (typeof value === 'boolean') {
|
|
1044
|
+
const curBool = curVal === true || curVal === 1 || curVal === '1';
|
|
1045
|
+
differs = curBool !== value;
|
|
1046
|
+
litVal = this.boolLit(value);
|
|
1047
|
+
}
|
|
1048
|
+
else if (typeof value === 'number') {
|
|
1049
|
+
const curNum = curVal === null || curVal === undefined ? null : Number(curVal);
|
|
1050
|
+
differs = curNum !== value;
|
|
1051
|
+
litVal = String(value);
|
|
1052
|
+
}
|
|
1053
|
+
else if (typeof value === 'string') {
|
|
1054
|
+
const curStr = curVal === null || curVal === undefined ? '' : String(curVal);
|
|
1055
|
+
differs = curStr !== value;
|
|
1056
|
+
litVal = `'${value.replace(/'/g, "''")}'`;
|
|
1057
|
+
}
|
|
1058
|
+
else if (value === null) {
|
|
1059
|
+
differs = curVal !== null && curVal !== undefined;
|
|
1060
|
+
litVal = 'NULL';
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
differs = JSON.stringify(curVal) !== JSON.stringify(value);
|
|
1064
|
+
litVal = `'${JSON.stringify(value).replace(/'/g, "''")}'`;
|
|
1065
|
+
}
|
|
1066
|
+
if (differs) {
|
|
1067
|
+
differingAttrs.push({ key, literalValue: litVal, summary: `${key}=${value}` });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (differingAttrs.length === 0) {
|
|
1071
|
+
continue;
|
|
962
1072
|
}
|
|
963
|
-
|
|
964
|
-
const
|
|
1073
|
+
// Build an UPDATE with literal values and execute through LogSQLAndExecute for migration capture
|
|
1074
|
+
const setClauses = differingAttrs.map(a => `${this.qi(a.key)} = ${a.literalValue}`);
|
|
1075
|
+
setClauses.unshift(`${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}`);
|
|
1076
|
+
const updateSQL = `UPDATE ${this.qs(schema, 'Entity')}
|
|
1077
|
+
SET ${setClauses.join(', ')}
|
|
1078
|
+
WHERE ${this.qi('ID')} = '${entityId}'`;
|
|
1079
|
+
await this.LogSQLAndExecute(pool, updateSQL, `Update attributes on "${entityName}" from additionalSchemaInfo`);
|
|
1080
|
+
const attrSummary = differingAttrs.map(a => a.summary).join(', ');
|
|
965
1081
|
logStatus(` > Entities config: Set ${attrSummary} on "${entityName}"`);
|
|
966
1082
|
updatedCount++;
|
|
967
1083
|
}
|
|
@@ -1123,9 +1239,10 @@ export class ManageMetadataBase {
|
|
|
1123
1239
|
const tableSQL = this.dbProvider.generateMaterializedTableSQL(matSchema, tableName, columns);
|
|
1124
1240
|
const viewSQL = this.dbProvider.generateMaterializedWrapperViewSQL(matSchema, viewName, tableName);
|
|
1125
1241
|
// includeBatchSeparator: each is a single GO-free batch (executed via ds.query), but the
|
|
1126
|
-
// migration file needs a GO between statements for Flyway/sqlcmd.
|
|
1127
|
-
|
|
1128
|
-
await this.LogSQLAndExecute(pool,
|
|
1242
|
+
// migration file needs a GO between statements for Flyway/sqlcmd. Pass the provider's
|
|
1243
|
+
// separator, not the 'GO' default: on PostgreSQL it is '' and a literal GO breaks replay.
|
|
1244
|
+
await this.LogSQLAndExecute(pool, tableSQL, `Create materialized table for base-view materialization of entity ${entity.Name}`, false, true, this.dbProvider.BatchSeparator);
|
|
1245
|
+
await this.LogSQLAndExecute(pool, viewSQL, `Create wrapper view for base-view materialization of entity ${entity.Name}`, false, true, this.dbProvider.BatchSeparator);
|
|
1129
1246
|
// 4) Upsert the MJ: Materialized Results row, keyed on (SourceType, SourceEntityID).
|
|
1130
1247
|
const existing = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(coreSchema, 'MaterializedResult')} WHERE SourceType = @SourceType AND SourceEntityID = @EntityID`, { SourceType: 'EntityBaseView', EntityID: entity.ID });
|
|
1131
1248
|
if (existing.recordset.length > 0) {
|
|
@@ -1383,8 +1500,8 @@ export class ManageMetadataBase {
|
|
|
1383
1500
|
}
|
|
1384
1501
|
const tableSQL = this.dbProvider.generateMaterializedTableSQL(coreSchema, tableName, analysis.columns);
|
|
1385
1502
|
const viewSQL = this.dbProvider.generateMaterializedWrapperViewSQL(coreSchema, viewName, tableName);
|
|
1386
|
-
await this.LogSQLAndExecute(pool, tableSQL, `Create materialized table for query "${queryName}"`, false, true);
|
|
1387
|
-
await this.LogSQLAndExecute(pool, viewSQL, `Create wrapper view for query "${queryName}"`, false, true);
|
|
1503
|
+
await this.LogSQLAndExecute(pool, tableSQL, `Create materialized table for query "${queryName}"`, false, true, this.dbProvider.BatchSeparator);
|
|
1504
|
+
await this.LogSQLAndExecute(pool, viewSQL, `Create wrapper view for query "${queryName}"`, false, true, this.dbProvider.BatchSeparator);
|
|
1388
1505
|
// 3) Mint the read-only Virtual Entity over the wrapper view (idempotent by view).
|
|
1389
1506
|
const existingVE = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(coreSchema, 'vwEntities')} WHERE BaseView = @V AND SchemaName = @S`, { V: viewName, S: coreSchema });
|
|
1390
1507
|
let generatedEntityId = existingVE.recordset[0]?.ID;
|
|
@@ -1964,8 +2081,8 @@ export class ManageMetadataBase {
|
|
|
1964
2081
|
* INTEGRITY CHECK — in a well-formed entity every base (non-virtual) field sequences BEFORE the
|
|
1965
2082
|
* virtual/related fields, so the EntityField order matches the base view's `SELECT [base].*, <joins>`
|
|
1966
2083
|
* column output. The positional save-capture in the data providers (e.g. SQLServerDataProvider's
|
|
1967
|
-
* @ResultTable) relies on that alignment. CodeGen
|
|
1968
|
-
* `
|
|
2084
|
+
* @ResultTable) relies on that alignment. CodeGen inserts newly-discovered columns at an apply-time
|
|
2085
|
+
* `MAX(Sequence) + ordinal` placeholder that updateExistingEntityFieldsFromSchema is supposed to renumber; if
|
|
1969
2086
|
* a base column is left sequenced AFTER a virtual field, the save-capture would mis-route values by
|
|
1970
2087
|
* position. The providers now compensate (saves stay correct), but the metadata is still wrong — so we
|
|
1971
2088
|
* scan for it after every metadata pass and log a prominent warning to drive the root cause out over time.
|
|
@@ -1978,7 +2095,7 @@ export class ManageMetadataBase {
|
|
|
1978
2095
|
const sSQL = `SELECT ${this.qi('EntityID')}, ${this.qi('Entity')}, ${this.qi('Name')}, ${this.qi('Sequence')}, ${this.qi('IsVirtual')}
|
|
1979
2096
|
FROM ${this.qs(mj_core_schema(), 'vwEntityFields')}
|
|
1980
2097
|
${excl}
|
|
1981
|
-
ORDER BY ${this.qi('EntityID')}, ${this.qi('Sequence')}`;
|
|
2098
|
+
ORDER BY ${this.qi('EntityID')}, ${this.qi('Sequence')}, ${this.qi('Name')}`;
|
|
1982
2099
|
const result = await this.runQuery(pool, sSQL);
|
|
1983
2100
|
const rows = result.recordset;
|
|
1984
2101
|
const isV = (v) => v === true || v === 1 || v === '1';
|
|
@@ -2011,7 +2128,7 @@ export class ManageMetadataBase {
|
|
|
2011
2128
|
` Healthy entities sequence ALL base (non-virtual) fields before virtual/related fields, so EntityField\n` +
|
|
2012
2129
|
` order matches the base view ([base].* then joins) that the data-provider save-capture relies on. When a\n` +
|
|
2013
2130
|
` base column sequences after a virtual field, the capture re-orders defensively (saves stay correct), but\n` +
|
|
2014
|
-
` the metadata is wrong. Usual cause: a newly-added column's
|
|
2131
|
+
` the metadata is wrong. Usual cause: a newly-added column's apply-time MAX(Sequence)+ordinal placeholder was not\n` +
|
|
2015
2132
|
` renumbered. Fix the field's Sequence at the source so it sorts with the base columns.\n` +
|
|
2016
2133
|
` Affected (entity: base fields out of order):\n`;
|
|
2017
2134
|
for (const o of offenders)
|
|
@@ -2464,11 +2581,8 @@ export class ManageMetadataBase {
|
|
|
2464
2581
|
else {
|
|
2465
2582
|
// this means that we do NOT have a match so the field does not exist in the entity definition, so we need to add it
|
|
2466
2583
|
newEntityFieldUUID = this.createNewUUID();
|
|
2467
|
-
//
|
|
2468
|
-
//
|
|
2469
|
-
// when nothing exists yet, but a partial prior run could have left rows at the same
|
|
2470
|
-
// ordinal under this EntityID — UQ_EntityField_EntityID_Sequence then fires on PG.
|
|
2471
|
-
const safeSequence = await this.nextAvailableEntityFieldSequence(pool, entity.ID, fieldSequence);
|
|
2584
|
+
// Apply-time Sequence: see applyTimeEntityFieldSequenceSQL. A literal here would be
|
|
2585
|
+
// replayed verbatim from the CodeGen capture and collide on a fresh install.
|
|
2472
2586
|
const q = (n) => this.qi(n);
|
|
2473
2587
|
const sqlAdd = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')} (
|
|
2474
2588
|
${q('ID')}, ${q('EntityID')}, ${q('Name')}, ${q('Type')}, ${q('AllowsNull')},
|
|
@@ -2477,10 +2591,11 @@ export class ManageMetadataBase {
|
|
|
2477
2591
|
${q('__mj_CreatedAt')}, ${q('__mj_UpdatedAt')} )
|
|
2478
2592
|
VALUES ( '${newEntityFieldUUID}', '${entity.ID}', '${String(veField.FieldName).replace(/'/g, "''")}', '${veField.Type}', ${this.boolLit(veField.AllowsNull)},
|
|
2479
2593
|
${veField.Length}, ${veField.Precision}, ${veField.Scale},
|
|
2480
|
-
${
|
|
2594
|
+
${this.applyTimeEntityFieldSequenceSQL(entity.ID)}, ${this.boolLit(wantPrimaryKey)}, ${this.boolLit(wantUnique)},
|
|
2481
2595
|
${this.utcNow()}, ${this.utcNow()}
|
|
2482
2596
|
)`;
|
|
2483
2597
|
await this.LogSQLAndExecute(pool, sqlAdd, `SQL text to add virtual entity field ${veField.FieldName} for entity ${virtualEntity.Name}`);
|
|
2598
|
+
ManageMetadataBase.registerNewField(entity.ID, veField.FieldName);
|
|
2484
2599
|
didUpdate = true;
|
|
2485
2600
|
}
|
|
2486
2601
|
}
|
|
@@ -2502,14 +2617,16 @@ export class ManageMetadataBase {
|
|
|
2502
2617
|
if (virtualEntities.length === 0) {
|
|
2503
2618
|
return;
|
|
2504
2619
|
}
|
|
2505
|
-
// Pre-build available entities list once (shared across all virtual entity decorations)
|
|
2620
|
+
// Pre-build available entities list once (shared across all virtual entity decorations).
|
|
2621
|
+
// Only single-column keys are offered as FK targets: an MJ foreign key references exactly one
|
|
2622
|
+
// column (RelatedEntityFieldName), so a composite-key entity cannot be the target of a soft FK.
|
|
2506
2623
|
const availableEntities = md.Entities
|
|
2507
|
-
.filter(e => !e.VirtualEntity && e.PrimaryKeys.length
|
|
2624
|
+
.filter(e => !e.VirtualEntity && e.PrimaryKeys.length === 1)
|
|
2508
2625
|
.map(e => ({
|
|
2509
2626
|
Name: e.Name,
|
|
2510
2627
|
SchemaName: e.SchemaName,
|
|
2511
2628
|
BaseTable: e.BaseTable,
|
|
2512
|
-
PrimaryKeyField: e.
|
|
2629
|
+
PrimaryKeyField: e.FirstPrimaryKey.Name // first-pk-ok: filtered to PrimaryKeys.length === 1 above; this is the FK target column offered to the LLM
|
|
2513
2630
|
}));
|
|
2514
2631
|
logStatus(` Decorating virtual entity fields with LLM (${virtualEntities.length} entities)...`);
|
|
2515
2632
|
let decoratedCount = 0;
|
|
@@ -2641,7 +2758,7 @@ export class ManageMetadataBase {
|
|
|
2641
2758
|
// Load VE EntityField rows from DB (we need the ID and auto-update flags)
|
|
2642
2759
|
const schema = mj_core_schema();
|
|
2643
2760
|
const fieldsSQL = `
|
|
2644
|
-
SELECT ID, Name, Category, AutoUpdateCategory, AutoUpdateDisplayName, GeneratedFormSection, DisplayName, ExtendedType, CodeType
|
|
2761
|
+
SELECT ID, Name, Category, AutoUpdateCategory, AutoUpdateDisplayName, AutoUpdateExtendedType, GeneratedFormSection, DisplayName, ExtendedType, CodeType, ValueListType
|
|
2645
2762
|
FROM ${this.qs(schema, 'EntityField')}
|
|
2646
2763
|
WHERE EntityID = '${entity.ID}'
|
|
2647
2764
|
`;
|
|
@@ -2662,16 +2779,18 @@ export class ManageMetadataBase {
|
|
|
2662
2779
|
if (fieldCategories.length === 0)
|
|
2663
2780
|
return false;
|
|
2664
2781
|
const existingCategories = this.buildExistingCategorySet(dbFields);
|
|
2665
|
-
|
|
2782
|
+
const isNewEntity = ManageMetadataBase.newEntityList.includes(entity.Name);
|
|
2783
|
+
await this.applyFieldCategories(pool, entity, dbFields, fieldCategories, existingCategories, { isNewEntity });
|
|
2666
2784
|
// Apply entity icon if provided
|
|
2667
2785
|
if (result.entityIcon) {
|
|
2668
|
-
await this.applyEntityIcon(pool, entity.ID, result.entityIcon);
|
|
2786
|
+
await this.applyEntityIcon(pool, entity.ID, result.entityIcon, entity.Name);
|
|
2669
2787
|
}
|
|
2670
2788
|
// Apply category info settings if provided
|
|
2671
2789
|
if (result.categoryInfo && Object.keys(result.categoryInfo).length > 0) {
|
|
2672
|
-
await this.applyCategoryInfoSettings(pool, entity.ID, result.categoryInfo);
|
|
2790
|
+
await this.applyCategoryInfoSettings(pool, entity.ID, result.categoryInfo, entity.Name);
|
|
2673
2791
|
}
|
|
2674
2792
|
logStatus(` Applied categories for VE ${entity.Name} (${fieldCategories.length} fields)`);
|
|
2793
|
+
await DecisionMetadataWriter.Instance.flushEntity(entity);
|
|
2675
2794
|
return true;
|
|
2676
2795
|
}
|
|
2677
2796
|
/**
|
|
@@ -2769,9 +2888,12 @@ export class ManageMetadataBase {
|
|
|
2769
2888
|
continue;
|
|
2770
2889
|
}
|
|
2771
2890
|
const escapedDescription = fd.description.replace(/'/g, "''");
|
|
2772
|
-
let setClauses = `Description='${escapedDescription}'`;
|
|
2773
|
-
// Apply extended type if provided and
|
|
2774
|
-
|
|
2891
|
+
let setClauses = `Description='${escapedDescription}', ${this.qi('AutoUpdateDescription')} = ${this.boolLit(false)}`;
|
|
2892
|
+
// Apply extended type if provided, valid, and the field is not locked
|
|
2893
|
+
const isFieldNew = ManageMetadataBase.isFieldNew(entity.ID, field.Name);
|
|
2894
|
+
const isTypeReopened = ManageMetadataBase.isTypeReopened(entity.ID, field.Name);
|
|
2895
|
+
const isNewEntity = ManageMetadataBase.newEntityList.includes(entity.Name);
|
|
2896
|
+
if (fd.extendedType && field.AutoUpdateExtendedType && (isNewEntity || isFieldNew || isTypeReopened)) {
|
|
2775
2897
|
const validExtendedType = this.validateExtendedType(fd.extendedType);
|
|
2776
2898
|
if (validExtendedType) {
|
|
2777
2899
|
setClauses += `, ExtendedType='${validExtendedType}'`;
|
|
@@ -2788,12 +2910,9 @@ export class ManageMetadataBase {
|
|
|
2788
2910
|
return true;
|
|
2789
2911
|
}
|
|
2790
2912
|
/**
|
|
2791
|
-
* Valid values for EntityField.ExtendedType
|
|
2913
|
+
* Valid values for EntityField.ExtendedType. Domain lives on {@link EntityFieldInfo.ExtendedTypes}.
|
|
2792
2914
|
*/
|
|
2793
|
-
static { this.VALID_EXTENDED_TYPES = new Set(
|
|
2794
|
-
'Code', 'Email', 'FaceTime', 'Geo', 'GeoLatitude', 'GeoLongitude', 'GeoCountry', 'GeoStateProvince',
|
|
2795
|
-
'GeoCity', 'GeoPostalCode', 'GeoAddress', 'MSTeams', 'Other', 'SIP', 'SMS', 'Skype', 'Tel', 'URL', 'WhatsApp', 'ZoomMtg'
|
|
2796
|
-
]); }
|
|
2915
|
+
static { this.VALID_EXTENDED_TYPES = new Set(EntityFieldInfo.ExtendedTypes); }
|
|
2797
2916
|
static { this.EXTENDED_TYPE_ALIASES = {
|
|
2798
2917
|
'phone': 'Tel',
|
|
2799
2918
|
'telephone': 'Tel',
|
|
@@ -2823,6 +2942,17 @@ export class ManageMetadataBase {
|
|
|
2823
2942
|
'zoom': 'ZoomMtg',
|
|
2824
2943
|
'whatsapp': 'WhatsApp',
|
|
2825
2944
|
'skype': 'Skype',
|
|
2945
|
+
'image': 'Image',
|
|
2946
|
+
'photo': 'Image',
|
|
2947
|
+
'picture': 'Image',
|
|
2948
|
+
'logo': 'Image',
|
|
2949
|
+
'avatar': 'Image',
|
|
2950
|
+
'thumbnail': 'Image',
|
|
2951
|
+
'color': 'Color',
|
|
2952
|
+
'colour': 'Color',
|
|
2953
|
+
'hex': 'Color',
|
|
2954
|
+
'json': 'JSON',
|
|
2955
|
+
'jsonb': 'JSON',
|
|
2826
2956
|
}; }
|
|
2827
2957
|
/**
|
|
2828
2958
|
* Validates an LLM-suggested ExtendedType against the allowed values in EntityField.
|
|
@@ -2930,12 +3060,8 @@ export class ManageMetadataBase {
|
|
|
2930
3060
|
else {
|
|
2931
3061
|
// Create new virtual field record for this parent field
|
|
2932
3062
|
const newFieldID = this.createNewUUID();
|
|
2933
|
-
//
|
|
2934
|
-
//
|
|
2935
|
-
// (e.g. partial prior run left a record at the candidate ordinal under this
|
|
2936
|
-
// EntityID — UQ_EntityField_EntityID_Sequence then fires on PG).
|
|
2937
|
-
const candidate = 100000 + parentFields.indexOf(parentField);
|
|
2938
|
-
const sequence = await this.nextAvailableEntityFieldSequence(pool, childEntity.ID, candidate);
|
|
3063
|
+
// Apply-time Sequence (see applyTimeEntityFieldSequenceSQL); reordered by
|
|
3064
|
+
// updateExistingEntityFieldsFromSchema afterwards.
|
|
2939
3065
|
const q = (n) => this.qi(n);
|
|
2940
3066
|
const sqlInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')} (
|
|
2941
3067
|
${q('ID')}, ${q('EntityID')}, ${q('Name')}, ${q('Type')}, ${q('AllowsNull')},
|
|
@@ -2947,9 +3073,10 @@ export class ManageMetadataBase {
|
|
|
2947
3073
|
'${newFieldID}', '${childEntity.ID}', '${parentField.Name}',
|
|
2948
3074
|
'${parentField.Type}', ${this.boolLit(parentField.AllowsNull)},
|
|
2949
3075
|
${parentField.Length}, ${parentField.Precision}, ${parentField.Scale},
|
|
2950
|
-
${
|
|
3076
|
+
${this.applyTimeEntityFieldSequenceSQL(childEntity.ID)}, ${this.boolLit(true)}, ${this.boolLit(true)}, ${this.boolLit(false)}, ${this.boolLit(false)},
|
|
2951
3077
|
${this.utcNow()}, ${this.utcNow()})`;
|
|
2952
3078
|
await this.LogSQLAndExecute(pool, sqlInsert, `Create IS-A parent field ${parentField.Name} on ${childEntity.Name}`);
|
|
3079
|
+
ManageMetadataBase.registerNewField(childEntity.ID, parentField.Name);
|
|
2953
3080
|
bUpdated = true;
|
|
2954
3081
|
}
|
|
2955
3082
|
}
|
|
@@ -3022,7 +3149,7 @@ export class ManageMetadataBase {
|
|
|
3022
3149
|
RelatedEntityID IS NOT NULL AND
|
|
3023
3150
|
IsVirtual = ${this.boolLit(false)} AND
|
|
3024
3151
|
EntityID NOT IN (SELECT ID FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
|
|
3025
|
-
ORDER BY RelatedEntityID`;
|
|
3152
|
+
ORDER BY RelatedEntityID, EntityID, Sequence, ID`;
|
|
3026
3153
|
const entityFieldsResult = await this.runQuery(pool, sSQL);
|
|
3027
3154
|
const entityFields = entityFieldsResult.recordset;
|
|
3028
3155
|
// Get the relationship counts for each entity
|
|
@@ -3200,7 +3327,7 @@ export class ManageMetadataBase {
|
|
|
3200
3327
|
RelatedEntityID IS NOT NULL AND
|
|
3201
3328
|
IsVirtual = ${this.boolLit(false)} AND
|
|
3202
3329
|
EntityID NOT IN (SELECT ID FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
|
|
3203
|
-
ORDER BY RelatedEntityID`;
|
|
3330
|
+
ORDER BY RelatedEntityID, EntityID, Sequence, ID`;
|
|
3204
3331
|
const entityFieldsResult = await this.runQuery(pool, sSQL);
|
|
3205
3332
|
const entityFields = entityFieldsResult.recordset;
|
|
3206
3333
|
const sSQLRelationship = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityRelationship')}`;
|
|
@@ -3430,7 +3557,7 @@ export class ManageMetadataBase {
|
|
|
3430
3557
|
// AN: 14-June-2025 - See note below about the new order of these steps, this must
|
|
3431
3558
|
// happen before we update existing entity fields from schema.
|
|
3432
3559
|
const step2StartTime = new Date();
|
|
3433
|
-
if (!await this.createNewEntityFieldsFromSchema(pool, scopedEntityIDs)) {
|
|
3560
|
+
if (!await this.createNewEntityFieldsFromSchema(pool, scopedEntityIDs, excludeSchemas)) {
|
|
3434
3561
|
logError('Error creating new entity fields from schema');
|
|
3435
3562
|
bSuccess = false;
|
|
3436
3563
|
}
|
|
@@ -3447,7 +3574,7 @@ export class ManageMetadataBase {
|
|
|
3447
3574
|
}
|
|
3448
3575
|
// AN: 14-June-2025 - we are now running this AFTER we create new entity fields from schema
|
|
3449
3576
|
// which results in the same pattern of behavior as migrations where we first create new fields
|
|
3450
|
-
//
|
|
3577
|
+
// at an apply-time MAX(Sequence)+ordinal placeholder (above every existing row) and then
|
|
3451
3578
|
// we align them properly in sequential order from 1+ via this method below.
|
|
3452
3579
|
const step3StartTime = new Date();
|
|
3453
3580
|
if (!await this.updateExistingEntityFieldsFromSchema(pool, excludeSchemas, scopedEntityIDs)) {
|
|
@@ -3586,6 +3713,9 @@ export class ManageMetadataBase {
|
|
|
3586
3713
|
try {
|
|
3587
3714
|
logStatus(` Found ${configInfo.additionalSchemaInfo}, applying soft PK/FK configuration...`);
|
|
3588
3715
|
const config = ManageMetadataBase.getSoftPKFKConfig();
|
|
3716
|
+
if (!config) {
|
|
3717
|
+
return true;
|
|
3718
|
+
}
|
|
3589
3719
|
let totalPKs = 0;
|
|
3590
3720
|
let totalFKs = 0;
|
|
3591
3721
|
const schema = mj_core_schema();
|
|
@@ -3610,6 +3740,18 @@ export class ManageMetadataBase {
|
|
|
3610
3740
|
const primaryKeys = table.PrimaryKey || [];
|
|
3611
3741
|
if (primaryKeys.length > 0) {
|
|
3612
3742
|
for (const pk of primaryKeys) {
|
|
3743
|
+
const checkSQL = `SELECT ${this.qi('IsPrimaryKey')}, ${this.qi('IsSoftPrimaryKey')}
|
|
3744
|
+
FROM ${this.qs(schema, 'EntityField')}
|
|
3745
|
+
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = '${pk.FieldName}'`;
|
|
3746
|
+
const checkRes = await this.runQuery(pool, checkSQL);
|
|
3747
|
+
if (checkRes.recordset.length > 0) {
|
|
3748
|
+
const row = checkRes.recordset[0];
|
|
3749
|
+
const isPk = row.IsPrimaryKey === true || row.IsPrimaryKey === 1 || row.IsPrimaryKey === '1';
|
|
3750
|
+
const isSoftPk = row.IsSoftPrimaryKey === true || row.IsSoftPrimaryKey === 1 || row.IsSoftPrimaryKey === '1';
|
|
3751
|
+
if (isPk && isSoftPk) {
|
|
3752
|
+
continue; // Already correctly set, skip write
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3613
3755
|
const sSQL = `UPDATE ${this.qs(schema, 'EntityField')}
|
|
3614
3756
|
SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()},
|
|
3615
3757
|
${this.qi('IsPrimaryKey')} = ${this.boolLit(true)},
|
|
@@ -3635,11 +3777,29 @@ export class ManageMetadataBase {
|
|
|
3635
3777
|
continue;
|
|
3636
3778
|
}
|
|
3637
3779
|
const relatedEntityId = relatedEntityResult.recordset[0].ID;
|
|
3780
|
+
const checkSQL = `SELECT ${this.qi('RelatedEntityID')}, ${this.qi('RelatedEntityFieldName')}, ${this.qi('IsSoftForeignKey')}, ${this.qi('AutoUpdateRelatedEntityInfo')}
|
|
3781
|
+
FROM ${this.qs(schema, 'EntityField')}
|
|
3782
|
+
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = '${fk.FieldName}'`;
|
|
3783
|
+
const checkRes = await this.runQuery(pool, checkSQL);
|
|
3784
|
+
if (checkRes.recordset.length > 0) {
|
|
3785
|
+
const row = checkRes.recordset[0];
|
|
3786
|
+
const curRelId = String(row.RelatedEntityID ?? '').trim().toLowerCase();
|
|
3787
|
+
const curRelField = String(row.RelatedEntityFieldName ?? '').trim().toLowerCase();
|
|
3788
|
+
const curSoftFk = row.IsSoftForeignKey === true || row.IsSoftForeignKey === 1 || row.IsSoftForeignKey === '1';
|
|
3789
|
+
const curAutoUpdate = row.AutoUpdateRelatedEntityInfo === true || row.AutoUpdateRelatedEntityInfo === 1 || row.AutoUpdateRelatedEntityInfo === '1';
|
|
3790
|
+
if (curRelId === String(relatedEntityId).trim().toLowerCase() &&
|
|
3791
|
+
curRelField === String(fk.RelatedField).trim().toLowerCase() &&
|
|
3792
|
+
curSoftFk &&
|
|
3793
|
+
!curAutoUpdate) {
|
|
3794
|
+
continue; // Already correctly set, skip write
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3638
3797
|
const sSQL = `UPDATE ${this.qs(schema, 'EntityField')}
|
|
3639
3798
|
SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()},
|
|
3640
3799
|
${this.qi('RelatedEntityID')} = '${relatedEntityId}',
|
|
3641
3800
|
${this.qi('RelatedEntityFieldName')} = '${fk.RelatedField}',
|
|
3642
|
-
${this.qi('IsSoftForeignKey')} = ${this.boolLit(true)}
|
|
3801
|
+
${this.qi('IsSoftForeignKey')} = ${this.boolLit(true)},
|
|
3802
|
+
${this.qi('AutoUpdateRelatedEntityInfo')} = ${this.boolLit(false)}
|
|
3643
3803
|
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = '${fk.FieldName}'`;
|
|
3644
3804
|
const result = await this.LogSQLAndExecute(pool, sSQL, `Set soft FK for ${tableSchema}.${tableName}.${fk.FieldName} → ${fk.RelatedTable}.${fk.RelatedField}`);
|
|
3645
3805
|
if (result !== null) {
|
|
@@ -3672,7 +3832,7 @@ export class ManageMetadataBase {
|
|
|
3672
3832
|
return true;
|
|
3673
3833
|
try {
|
|
3674
3834
|
// Load all existing EntityFieldValue rows for dedup checking
|
|
3675
|
-
const efvSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence`;
|
|
3835
|
+
const efvSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence, Value, ID`;
|
|
3676
3836
|
const allEFVResult = await this.runQuery(pool, efvSQL);
|
|
3677
3837
|
const allEntityFieldValues = allEFVResult.recordset;
|
|
3678
3838
|
let totalApplied = 0;
|
|
@@ -3899,7 +4059,8 @@ export class ManageMetadataBase {
|
|
|
3899
4059
|
async dropExistingDefaultConstraint(pool, entity, fieldName) {
|
|
3900
4060
|
try {
|
|
3901
4061
|
const sqlDropDefaultConstraint = this.dbProvider.dropDefaultConstraintSQL(entity.SchemaName, entity.BaseTable, fieldName);
|
|
3902
|
-
|
|
4062
|
+
// DECLARE-bearing block: close its batch in the replayable log (see SQLLogging.appendToSQLLogFile).
|
|
4063
|
+
await this.LogSQLAndExecute(pool, sqlDropDefaultConstraint, `SQL text to drop default existing default constraints in entity ${entity.SchemaName}.${entity.BaseTable}`, false, true, this.dbProvider.BatchSeparator);
|
|
3903
4064
|
}
|
|
3904
4065
|
catch (e) {
|
|
3905
4066
|
logError(e);
|
|
@@ -3931,7 +4092,7 @@ export class ManageMetadataBase {
|
|
|
3931
4092
|
// Use new API to generate entity description
|
|
3932
4093
|
const result = await ag.generateEntityDescription(e, data[0].BaseTable, fields.map((f) => ({ Name: f.Name, Type: f.Type, IsNullable: f.AllowsNull, Description: f.Description })), currentUser);
|
|
3933
4094
|
if (result?.entityDescription && result.entityDescription.length > 0) {
|
|
3934
|
-
const sSQL = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET Description = '${esc(result.entityDescription)}' WHERE Name = '${esc(e)}'`;
|
|
4095
|
+
const sSQL = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET ${this.qi('Description')} = '${esc(result.entityDescription)}', ${this.qi('AutoUpdateDescription')} = ${this.boolLit(false)} WHERE ${this.qi('Name')} = '${esc(e)}'`;
|
|
3935
4096
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to update entity description for entity ${e}`);
|
|
3936
4097
|
}
|
|
3937
4098
|
else {
|
|
@@ -4015,9 +4176,9 @@ export class ManageMetadataBase {
|
|
|
4015
4176
|
*
|
|
4016
4177
|
* @returns {string} - The SQL statement to retrieve pending entity fields.
|
|
4017
4178
|
*/
|
|
4018
|
-
getPendingEntityFieldsSELECTSQL(entityIDs) {
|
|
4179
|
+
getPendingEntityFieldsSELECTSQL(entityIDs, excludeSchemas) {
|
|
4019
4180
|
const schema = mj_core_schema();
|
|
4020
|
-
return this.dbProvider.getPendingEntityFieldsSQL(schema, entityIDs);
|
|
4181
|
+
return this.dbProvider.getPendingEntityFieldsSQL(schema, entityIDs, excludeSchemas);
|
|
4021
4182
|
}
|
|
4022
4183
|
/**
|
|
4023
4184
|
* This method builds a SQL Statement that will insert a row into the EntityField table with information about a new field.
|
|
@@ -4030,7 +4191,11 @@ export class ManageMetadataBase {
|
|
|
4030
4191
|
const isPrimaryKey = n.FieldName?.trim().toLowerCase() === 'id';
|
|
4031
4192
|
const isForeignKey = n.RelatedEntityID && n.RelatedEntityID.length > 0; // Foreign keys have RelatedEntityID set
|
|
4032
4193
|
const isNameField = n.FieldName?.trim().toLowerCase() === 'name' || n.IsNameField;
|
|
4033
|
-
|
|
4194
|
+
// The field's position in the schema (SourceOrdinal / column_id). `n.Sequence` is NOT that:
|
|
4195
|
+
// the pending-fields SELECT computes it as MAX+100000+ordinal, so comparing it against
|
|
4196
|
+
// IncludeFirstNFieldsAsDefaultInView could never be true and the setting was dead.
|
|
4197
|
+
const sourceOrdinal = typeof n.SourceOrdinal === 'number' && n.SourceOrdinal > 0 ? n.SourceOrdinal : 1;
|
|
4198
|
+
const isEarlySequence = sourceOrdinal <= configInfo.newEntityDefaults?.IncludeFirstNFieldsAsDefaultInView;
|
|
4034
4199
|
const bDefaultInView = (isNameField || isEarlySequence) && !isPrimaryKey && !isForeignKey;
|
|
4035
4200
|
const escapedDescription = n.Description ? `'${n.Description.replace(/'/g, "''")}'` : 'NULL';
|
|
4036
4201
|
let fieldDisplayName = '';
|
|
@@ -4057,17 +4222,10 @@ export class ManageMetadataBase {
|
|
|
4057
4222
|
: (parsedDefaultValue.trim().toLowerCase() === 'null' ? 'NULL' : `'${escapedParsedDefault}'`);
|
|
4058
4223
|
const conflictCheck = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ID = '${newEntityFieldUUID}' OR (EntityID = '${n.EntityID}' AND Name = '${n.FieldName}')`;
|
|
4059
4224
|
const guard = this.dbProvider.wrapInsertWithConflictGuard(conflictCheck);
|
|
4060
|
-
// Sequence is
|
|
4061
|
-
//
|
|
4062
|
-
//
|
|
4063
|
-
|
|
4064
|
-
// batch, manageEntityFields calls spUpdateExistingEntityFieldsFromSchema
|
|
4065
|
-
// which rewrites EVERY field on the entity from the live view — including
|
|
4066
|
-
// parked rows. That proc must run AFTER views are current (CodeGen Pass 2,
|
|
4067
|
-
// after SQL generation). Pass 1 still emits this SQL against whatever the
|
|
4068
|
-
// view is at that moment; Pass 2 is the one that matches the finished BaseView.
|
|
4069
|
-
const sourceOrdinal = typeof n.SourceOrdinal === 'number' && n.SourceOrdinal > 0 ? n.SourceOrdinal : 1;
|
|
4070
|
-
const sequenceExpr = String(sourceOrdinal);
|
|
4225
|
+
// Sequence is an apply-time expression, never a literal: see applyTimeEntityFieldSequenceSQL.
|
|
4226
|
+
// `n.Sequence` (MAX+100000+ordinal from the pending SELECT) is only that query's ORDER BY key
|
|
4227
|
+
// and must never be written; `sourceOrdinal` above feeds DefaultInView only.
|
|
4228
|
+
const sequenceExpr = this.applyTimeEntityFieldSequenceSQL(n.EntityID);
|
|
4071
4229
|
return `
|
|
4072
4230
|
${guard.prefix}
|
|
4073
4231
|
INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')}
|
|
@@ -4121,7 +4279,7 @@ export class ManageMetadataBase {
|
|
|
4121
4279
|
${n.RelatedEntityID && n.RelatedEntityID.length > 0 ? `'${n.RelatedEntityID}'` : 'NULL'},
|
|
4122
4280
|
${n.RelatedEntityFieldName && n.RelatedEntityFieldName.length > 0 ? `'${n.RelatedEntityFieldName}'` : 'NULL'},
|
|
4123
4281
|
${this.boolLit(n.IsNameField !== null ? n.IsNameField : false)},
|
|
4124
|
-
${this.boolLit(n.
|
|
4282
|
+
${this.boolLit(Boolean(n.IsNameField))},
|
|
4125
4283
|
${this.boolLit(n.RelatedEntityID && n.RelatedEntityID.length > 0)},
|
|
4126
4284
|
${this.boolLit(bDefaultInView)},
|
|
4127
4285
|
${this.boolLit(n.IsPrimaryKey)},
|
|
@@ -4141,45 +4299,20 @@ export class ManageMetadataBase {
|
|
|
4141
4299
|
* @param sqlDefaultValue
|
|
4142
4300
|
* @returns
|
|
4143
4301
|
*/
|
|
4144
|
-
/**
|
|
4145
|
-
* Park existing EntityField.Sequence values out of the 1..N catalog range so a
|
|
4146
|
-
* following INSERT can use the real BaseView column_id without colliding on
|
|
4147
|
-
* UQ_EntityField_EntityID_Sequence. The +100000 band is unique-safe; the
|
|
4148
|
-
* subsequent spUpdateExistingEntityFieldsFromSchema rewrite brings every row
|
|
4149
|
-
* (parked and new) back to live catalog order.
|
|
4150
|
-
*
|
|
4151
|
-
* Parks ONLY when nothing on the entity is parked yet, which is what makes a second
|
|
4152
|
-
* emission for the same entity in the same run a no-op. `Sequence < 100000` alone does
|
|
4153
|
-
* not achieve that: after the first park the rows below the band are precisely the ones
|
|
4154
|
-
* the first pass just INSERTED at their catalog ordinals, so a second park lifts THOSE
|
|
4155
|
-
* into the band — onto the row the first park moved from the same ordinal, and the
|
|
4156
|
-
* migration dies on UQ_EntityField_EntityID_Sequence with a duplicate at 100000+ordinal.
|
|
4157
|
-
* Reachable whenever an entity gains fields in both CodeGen passes: pass 1 for the real
|
|
4158
|
-
* columns, pass 2 for the denormalized name column a new foreign key introduces.
|
|
4159
|
-
*/
|
|
4160
|
-
parkEntityFieldSequencesSQL(entityID) {
|
|
4161
|
-
const table = this.qs(mj_core_schema(), 'EntityField');
|
|
4162
|
-
return `UPDATE ${table}
|
|
4163
|
-
SET ${this.qi('Sequence')} = ${this.qi('Sequence')} + 100000
|
|
4164
|
-
WHERE ${this.qi('EntityID')} = '${entityID}'
|
|
4165
|
-
AND ${this.qi('Sequence')} < 100000
|
|
4166
|
-
AND NOT EXISTS (
|
|
4167
|
-
SELECT 1 FROM ${table}
|
|
4168
|
-
WHERE ${this.qi('EntityID')} = '${entityID}'
|
|
4169
|
-
AND ${this.qi('Sequence')} >= 100000
|
|
4170
|
-
);`;
|
|
4171
|
-
}
|
|
4172
4302
|
parseDefaultValue(sqlDefaultValue) {
|
|
4173
4303
|
if (sqlDefaultValue === null || sqlDefaultValue === undefined) {
|
|
4174
4304
|
return null;
|
|
4175
4305
|
}
|
|
4176
4306
|
return this.dbProvider.parseColumnDefaultValue(sqlDefaultValue) ?? null;
|
|
4177
4307
|
}
|
|
4178
|
-
async createNewEntityFieldsFromSchema(pool, entityIDs) {
|
|
4308
|
+
async createNewEntityFieldsFromSchema(pool, entityIDs, excludeSchemas) {
|
|
4179
4309
|
try {
|
|
4180
4310
|
// entityIDs flows down to the provider's WHERE clause so the inline SELECT
|
|
4181
|
-
// narrows to changed entities only when scoped.
|
|
4182
|
-
|
|
4311
|
+
// narrows to changed entities only when scoped. excludeSchemas is the compiled
|
|
4312
|
+
// includeSchemas scope (out-of-allow-list schemas). Without it, Pass 1 inserts
|
|
4313
|
+
// pending fields for every schema in the database (Forms CodeGen emitted Common
|
|
4314
|
+
// Activity Files EntityField rows).
|
|
4315
|
+
const sSQL = this.getPendingEntityFieldsSELECTSQL(entityIDs, excludeSchemas);
|
|
4183
4316
|
const newEntityFieldsResult = await this.runQuery(pool, sSQL);
|
|
4184
4317
|
const newEntityFields = newEntityFieldsResult.recordset;
|
|
4185
4318
|
if (newEntityFields.length > 0) {
|
|
@@ -4197,18 +4330,14 @@ export class ManageMetadataBase {
|
|
|
4197
4330
|
// Batch size is configurable via `metadataInsertBatchSize` (default 250).
|
|
4198
4331
|
const CHUNK_SIZE = configInfo.metadataInsertBatchSize ?? 250;
|
|
4199
4332
|
const inserts = [];
|
|
4200
|
-
const parkedEntityIDs = new Set();
|
|
4201
4333
|
for (let i = 0; i < newEntityFields.length; ++i) {
|
|
4202
4334
|
const n = newEntityFields[i];
|
|
4203
4335
|
if (n.EntityID !== null && n.EntityID !== undefined && n.EntityID.length > 0) {
|
|
4204
4336
|
// 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
|
|
4205
4337
|
// that would have been created violate rules - such as not having an ID column, etc.
|
|
4206
|
-
if (!parkedEntityIDs.has(n.EntityID)) {
|
|
4207
|
-
inserts.push(this.parkEntityFieldSequencesSQL(n.EntityID));
|
|
4208
|
-
parkedEntityIDs.add(n.EntityID);
|
|
4209
|
-
}
|
|
4210
4338
|
const newEntityFieldUUID = this.createNewUUID();
|
|
4211
4339
|
inserts.push(this.getPendingEntityFieldINSERTSQL(newEntityFieldUUID, n));
|
|
4340
|
+
ManageMetadataBase.registerNewField(n.EntityID, n.FieldName);
|
|
4212
4341
|
}
|
|
4213
4342
|
}
|
|
4214
4343
|
for (let i = 0; i < inserts.length; i += CHUNK_SIZE) {
|
|
@@ -4304,11 +4433,20 @@ export class ManageMetadataBase {
|
|
|
4304
4433
|
const label = isScoped
|
|
4305
4434
|
? `SQL text to update existing entity fields from schema (${entityIDs.length} scoped entities)`
|
|
4306
4435
|
: `SQL text to update existing entity fields from schema`;
|
|
4307
|
-
const
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4436
|
+
const before = await this.snapshotEntityFields(pool, entityIDs);
|
|
4437
|
+
await this.LogSQLAndExecute(pool, sSQL, label, true);
|
|
4438
|
+
const after = await this.snapshotEntityFields(pool, entityIDs);
|
|
4439
|
+
const changes = diffEntityFieldSnapshots(before, after, (e, n) => ManageMetadataBase.isFieldNew(e, n));
|
|
4440
|
+
if (changes.length > 0) {
|
|
4441
|
+
ManageMetadataBase.addNewEntitiesToModifiedList(changes.map(c => c.entityName));
|
|
4442
|
+
for (const c of changes) {
|
|
4443
|
+
ManageMetadataBase.registerFieldChange(c.entityID, c.fieldName, c.reasons);
|
|
4444
|
+
ManageMetadataBase._changedFieldReport.push({
|
|
4445
|
+
entityName: c.entityName,
|
|
4446
|
+
fieldName: c.fieldName,
|
|
4447
|
+
reasons: [...c.reasons],
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
4312
4450
|
}
|
|
4313
4451
|
return true;
|
|
4314
4452
|
}
|
|
@@ -4461,13 +4599,22 @@ export class ManageMetadataBase {
|
|
|
4461
4599
|
const sSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntityFieldsWithCheckConstraints')}${filter}`;
|
|
4462
4600
|
const resultResult = await this.runQuery(pool, sSQL);
|
|
4463
4601
|
const result = resultResult.recordset;
|
|
4464
|
-
const efvSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence`;
|
|
4602
|
+
const efvSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence, Value, ID`;
|
|
4465
4603
|
const allEntityFieldValuesResult = await this.runQuery(pool, efvSQL);
|
|
4466
4604
|
const allEntityFieldValues = allEntityFieldValuesResult.recordset;
|
|
4467
|
-
const efSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntityFields')} ORDER BY EntityID, Sequence`;
|
|
4605
|
+
const efSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntityFields')} ORDER BY EntityID, Sequence, Name, ID`;
|
|
4468
4606
|
const allEntityFieldsResult = await this.runQuery(pool, efSQL);
|
|
4469
4607
|
const allEntityFields = allEntityFieldsResult.recordset;
|
|
4470
4608
|
const generationPromises = [];
|
|
4609
|
+
const ag = new AdvancedGeneration();
|
|
4610
|
+
// `skipDBUpdate` means load-only: `runValidationGeneration` is called below with
|
|
4611
|
+
// `generateNewCode = false`, and `generateValidatorFunctionFromCheckConstraint` only reaches an
|
|
4612
|
+
// LLM when that flag is true. Reading a validator back out of an Approved `GeneratedCode` record
|
|
4613
|
+
// is therefore a plain database read — gating it on the AI feature flag is what made
|
|
4614
|
+
// `mj codegen --no-ai` DELETE every committed `Validate()` override rather than preserve it, and
|
|
4615
|
+
// the `codegen-drift` gate (which runs `--no-ai`) then demanded that lossy output. Generation
|
|
4616
|
+
// stays gated; only the read is unconditional.
|
|
4617
|
+
const emitValidators = skipDBUpdate || ag.featureEnabled('ParseCheckConstraints');
|
|
4471
4618
|
const columnLevelResults = result.filter((r) => r.EntityFieldID); // get the column level constraints
|
|
4472
4619
|
const tableLevelResults = result.filter((r) => !r.EntityFieldID); // get the table level constraints
|
|
4473
4620
|
for (const r of columnLevelResults) {
|
|
@@ -4501,9 +4648,8 @@ export class ManageMetadataBase {
|
|
|
4501
4648
|
else {
|
|
4502
4649
|
// if we get here that means we don't have a simple condition in the check constraint that the RegEx could parse. If Advanced Generation is enabled, we will
|
|
4503
4650
|
// attempt to use an LLM to do things fancier now
|
|
4504
|
-
if (
|
|
4505
|
-
|
|
4506
|
-
// the user has the feature turned on, let's generate a description of the constraint and then build a Validate function for the constraint
|
|
4651
|
+
if (emitValidators) {
|
|
4652
|
+
// either we are loading persisted validators, or the feature is on and we may generate new ones
|
|
4507
4653
|
// run this in parallel
|
|
4508
4654
|
generationPromises.push(this.runValidationGeneration(r, allEntityFields, !skipDBUpdate, currentUser));
|
|
4509
4655
|
}
|
|
@@ -4512,9 +4658,8 @@ export class ManageMetadataBase {
|
|
|
4512
4658
|
}
|
|
4513
4659
|
// now for the table level constraints run the process for advanced generation
|
|
4514
4660
|
for (const r of tableLevelResults) {
|
|
4515
|
-
if (
|
|
4516
|
-
|
|
4517
|
-
// the user has the feature turned on, let's generate a description of the constraint and then build a Validate function for the constraint
|
|
4661
|
+
if (emitValidators) {
|
|
4662
|
+
// either we are loading persisted validators, or the feature is on and we may generate new ones
|
|
4518
4663
|
// run this in parallel
|
|
4519
4664
|
generationPromises.push(this.runValidationGeneration(r, allEntityFields, !skipDBUpdate, currentUser));
|
|
4520
4665
|
}
|
|
@@ -4585,9 +4730,9 @@ export class ManageMetadataBase {
|
|
|
4585
4730
|
}
|
|
4586
4731
|
}
|
|
4587
4732
|
try {
|
|
4588
|
-
|
|
4733
|
+
const ag = new AdvancedGeneration();
|
|
4734
|
+
if (generateNewCode && ag.featureEnabled('ParseCheckConstraints')) {
|
|
4589
4735
|
// feature is enabled, so let's call the AI to generate a function for us
|
|
4590
|
-
const ag = new AdvancedGeneration();
|
|
4591
4736
|
const entityFieldListInfo = allEntityFields.filter(item => item.Entity.trim().toLowerCase() === data.EntityName.trim().toLowerCase()).map(item => ` * ${item.Name} - ${item.Type}${item.AllowsNull ? ' (nullable)' : ' (not null)'}`).join('\n');
|
|
4592
4737
|
// Use new API to parse check constraint
|
|
4593
4738
|
const result = await ag.parseCheckConstraint(constraintDefinition, entityFieldListInfo, generatedValidationFunctionName, currentUser);
|
|
@@ -5775,7 +5920,8 @@ export class ManageMetadataBase {
|
|
|
5775
5920
|
WHERE
|
|
5776
5921
|
${whereClause}
|
|
5777
5922
|
ORDER BY
|
|
5778
|
-
e.Name
|
|
5923
|
+
e.Name,
|
|
5924
|
+
e.ID
|
|
5779
5925
|
`;
|
|
5780
5926
|
const entitiesResult = await this.runQuery(pool, entitiesSQL);
|
|
5781
5927
|
const entities = entitiesResult.recordset;
|
|
@@ -5801,11 +5947,16 @@ export class ManageMetadataBase {
|
|
|
5801
5947
|
ef.AutoUpdateIncludeInUserSearchAPI,
|
|
5802
5948
|
ef.AutoUpdateCategory,
|
|
5803
5949
|
ef.AutoUpdateDisplayName,
|
|
5950
|
+
ef.AutoUpdateExtendedType,
|
|
5951
|
+
ef.ExtendedType,
|
|
5952
|
+
ef.CodeType,
|
|
5953
|
+
ef.GeneratedFormSection,
|
|
5804
5954
|
ef.EntityIDFieldName,
|
|
5805
5955
|
ef.RelatedEntity,
|
|
5806
5956
|
ef.IsVirtual,
|
|
5807
5957
|
ef.AllowUpdateAPI,
|
|
5808
5958
|
ef.IsNameField,
|
|
5959
|
+
ef.ValueListType,
|
|
5809
5960
|
ef.DefaultInView,
|
|
5810
5961
|
ef.IncludeInUserSearchAPI,
|
|
5811
5962
|
ef.UserSearchPredicateAPI,
|
|
@@ -5819,7 +5970,9 @@ export class ManageMetadataBase {
|
|
|
5819
5970
|
ef.EntityID IN (${entityIds})
|
|
5820
5971
|
ORDER BY
|
|
5821
5972
|
ef.EntityID,
|
|
5822
|
-
ef.Sequence
|
|
5973
|
+
ef.Sequence,
|
|
5974
|
+
ef.Name,
|
|
5975
|
+
ef.ID
|
|
5823
5976
|
`;
|
|
5824
5977
|
const fieldsResult = await this.runQuery(pool, fieldsSQL);
|
|
5825
5978
|
const allFields = fieldsResult.recordset;
|
|
@@ -5834,6 +5987,9 @@ export class ManageMetadataBase {
|
|
|
5834
5987
|
WHERE
|
|
5835
5988
|
es.EntityID IN (${entityIds})
|
|
5836
5989
|
AND es.Name = 'FieldCategoryInfo'
|
|
5990
|
+
ORDER BY
|
|
5991
|
+
es.EntityID,
|
|
5992
|
+
es.Name
|
|
5837
5993
|
`;
|
|
5838
5994
|
const settingsResult = await this.runQuery(pool, settingsSQL);
|
|
5839
5995
|
const allSettings = settingsResult.recordset;
|
|
@@ -5931,16 +6087,26 @@ export class ManageMetadataBase {
|
|
|
5931
6087
|
// Fields for this entity — O(1) lookup into the pre-grouped map (keyed on the normalized
|
|
5932
6088
|
// EntityID) instead of a linear scan of the full pooled array.
|
|
5933
6089
|
const fields = fieldsByEntity.get(String(entity.ID ?? '').trim().toLowerCase()) ?? [];
|
|
6090
|
+
// Decorate fields with change state from field tracking (C1/C3)
|
|
6091
|
+
for (const f of fields) {
|
|
6092
|
+
f.IsNew = ManageMetadataBase.isFieldNew(entity.ID, f.Name);
|
|
6093
|
+
f.DescriptionReopened = ManageMetadataBase.isDisplayNameReopened(entity.ID, f.Name);
|
|
6094
|
+
f.TypeReopened = ManageMetadataBase.isTypeReopened(entity.ID, f.Name);
|
|
6095
|
+
}
|
|
6096
|
+
const newFieldNames = new Set(fields.filter((f) => f.IsNew).map((f) => String(f.Name)));
|
|
6097
|
+
const typeReopenedNames = new Set(fields.filter((f) => f.TypeReopened).map((f) => String(f.Name)));
|
|
6098
|
+
const hasNewFields = newFieldNames.size > 0;
|
|
6099
|
+
const hasTypeReopened = typeReopenedNames.size > 0;
|
|
6100
|
+
const hasDescriptionReopened = fields.some((f) => f.DescriptionReopened);
|
|
5934
6101
|
// Determine if this is a new entity (for DefaultForNewUser decision)
|
|
5935
6102
|
const isNewEntity = ManageMetadataBase.newEntityList.includes(entity.Name);
|
|
5936
|
-
// Smart Field Identification
|
|
5937
|
-
|
|
5938
|
-
// or if entity-level search/FTS auto-update flags are set
|
|
5939
|
-
const needsFieldAnalysis = fields.some((f) => f.AutoUpdateIsNameField || f.AutoUpdateDefaultInView || f.AutoUpdateIncludeInUserSearchAPI || f.AutoUpdateUserSearchPredicate || f.AutoUpdateFullTextSearch);
|
|
6103
|
+
// Smart Field Identification — FM4. Runs only when there is something new to decide about.
|
|
6104
|
+
const needsFieldAnalysis = (isNewEntity || hasNewFields || hasTypeReopened) && fields.some((f) => f.AutoUpdateIsNameField || f.AutoUpdateDefaultInView || f.AutoUpdateIncludeInUserSearchAPI || f.AutoUpdateUserSearchPredicate || f.AutoUpdateFullTextSearch);
|
|
5940
6105
|
// entity-level auto-update flags come from the raw SQL query, not EntityInfo
|
|
5941
6106
|
const entityRecord = entity;
|
|
5942
|
-
const needsEntitySearchConfig = entityRecord.AutoUpdateAllowUserSearchAPI || entityRecord.AutoUpdateFullTextSearch;
|
|
6107
|
+
const needsEntitySearchConfig = isNewEntity && (Boolean(entityRecord.AutoUpdateAllowUserSearchAPI) || Boolean(entityRecord.AutoUpdateFullTextSearch));
|
|
5943
6108
|
if (needsFieldAnalysis || needsEntitySearchConfig) {
|
|
6109
|
+
CodeGenReporter.Instance.counter('ai.smartFieldCalls');
|
|
5944
6110
|
const fieldAnalysis = await ag.identifyFields({
|
|
5945
6111
|
Name: entity.Name,
|
|
5946
6112
|
Description: entity.Description,
|
|
@@ -5955,13 +6121,17 @@ export class ManageMetadataBase {
|
|
|
5955
6121
|
AutoUpdateAllowUserSearchAPI: entityRecord.AutoUpdateAllowUserSearchAPI,
|
|
5956
6122
|
FullTextSearchEnabled: entityRecord.FullTextSearchEnabled,
|
|
5957
6123
|
AutoUpdateFullTextSearch: entityRecord.AutoUpdateFullTextSearch,
|
|
5958
|
-
}, fields, fieldAnalysis);
|
|
6124
|
+
}, fields, fieldAnalysis, { isNewEntity, newFieldNames, typeReopenedNames });
|
|
5959
6125
|
}
|
|
5960
6126
|
}
|
|
5961
6127
|
// Form Layout Generation
|
|
5962
|
-
//
|
|
5963
|
-
|
|
6128
|
+
// Fires only on change events (new entity, new fields, reopened description/type) AND when there is work to do
|
|
6129
|
+
// (blank categories with AutoUpdateCategory=1, or reopened fields to review). Gated on events so omitted blank fields don't loop (§3.4).
|
|
6130
|
+
const hasCategoryNeeded = fields.some((f) => Boolean(f.AutoUpdateCategory) && (!f.Category || String(f.Category).trim() === ''));
|
|
6131
|
+
const needsCategoryGeneration = (isNewEntity || hasNewFields || hasDescriptionReopened || hasTypeReopened)
|
|
6132
|
+
&& (hasCategoryNeeded || hasDescriptionReopened || hasTypeReopened);
|
|
5964
6133
|
if (needsCategoryGeneration) {
|
|
6134
|
+
CodeGenReporter.Instance.counter('ai.formLayoutCalls');
|
|
5965
6135
|
// Build IS-A parent chain context if this entity has a parent
|
|
5966
6136
|
const parentChainContext = this.buildParentChainContext(entity, fields);
|
|
5967
6137
|
const layoutAnalysis = await ag.generateFormLayout({
|
|
@@ -5977,6 +6147,8 @@ export class ManageMetadataBase {
|
|
|
5977
6147
|
logStatus(` Applied form layout for ${entity.Name}`);
|
|
5978
6148
|
}
|
|
5979
6149
|
}
|
|
6150
|
+
// Flush decision metadata for this entity
|
|
6151
|
+
await DecisionMetadataWriter.Instance.flushEntity(entity);
|
|
5980
6152
|
}
|
|
5981
6153
|
catch (ex) {
|
|
5982
6154
|
logError('Error Processing Entity Advanced Generation', ex);
|
|
@@ -6046,19 +6218,21 @@ export class ManageMetadataBase {
|
|
|
6046
6218
|
/**
|
|
6047
6219
|
* Apply smart field identification results to entity fields and entity-level search configuration
|
|
6048
6220
|
*/
|
|
6049
|
-
async applySmartFieldIdentification(pool, entity, fields, result) {
|
|
6221
|
+
async applySmartFieldIdentification(pool, entity, fields, result, ctx) {
|
|
6050
6222
|
// Run the guardrail pipeline once up front so every downstream applier
|
|
6051
6223
|
// sees the same already-normalized result. See search-guardrails.ts for
|
|
6052
6224
|
// the rules. Mutates result in place — see normalizeSearchFlagsInPlace.
|
|
6053
6225
|
this.normalizeSearchFlagsInPlace(entity, fields, result);
|
|
6054
6226
|
const sqlStatements = [];
|
|
6055
|
-
this.applyNameFieldUpdates(sqlStatements, fields, result);
|
|
6056
|
-
this.applyDefaultInViewUpdates(sqlStatements, fields, result);
|
|
6057
|
-
this.applySearchableFieldUpdates(sqlStatements, fields, result, entity);
|
|
6058
|
-
this.applySearchPredicateUpdates(sqlStatements, fields, result);
|
|
6059
|
-
|
|
6227
|
+
this.applyNameFieldUpdates(sqlStatements, fields, result, entity);
|
|
6228
|
+
this.applyDefaultInViewUpdates(sqlStatements, fields, result, entity, ctx);
|
|
6229
|
+
this.applySearchableFieldUpdates(sqlStatements, fields, result, entity, ctx);
|
|
6230
|
+
this.applySearchPredicateUpdates(sqlStatements, fields, result, entity, ctx);
|
|
6231
|
+
if (!ctx || ctx.isNewEntity) {
|
|
6232
|
+
this.applyEntitySearchConfig(sqlStatements, entity, result);
|
|
6233
|
+
}
|
|
6060
6234
|
if (configInfo.advancedGeneration?.allowFullTextSearchAutoUpdate) {
|
|
6061
|
-
this.applyFullTextSearchUpdates(sqlStatements, entity, fields, result);
|
|
6235
|
+
this.applyFullTextSearchUpdates(sqlStatements, entity, fields, result, ctx);
|
|
6062
6236
|
}
|
|
6063
6237
|
// Execute all updates in one batch
|
|
6064
6238
|
if (sqlStatements.length > 0) {
|
|
@@ -6197,11 +6371,11 @@ export class ManageMetadataBase {
|
|
|
6197
6371
|
* `AutoUpdateIsNameField=0` are never touched in either direction — the user owns
|
|
6198
6372
|
* them, and `EntityInfo.NameField`'s literal-`Name` preference arbitrates at runtime.
|
|
6199
6373
|
*/
|
|
6200
|
-
applyNameFieldUpdates(sqlStatements, fields, result) {
|
|
6374
|
+
applyNameFieldUpdates(sqlStatements, fields, result, entity) {
|
|
6201
6375
|
const nameFieldNames = result.nameFields ?? [];
|
|
6202
6376
|
// Surface LLM candidates that don't exist on the entity (same diagnostics as before).
|
|
6203
6377
|
for (const nfName of nameFieldNames) {
|
|
6204
|
-
if (!
|
|
6378
|
+
if (!this.findFieldByName(fields, nfName)) {
|
|
6205
6379
|
logError(`Smart field identification returned invalid nameField: '${nfName}' not found in entity fields`);
|
|
6206
6380
|
}
|
|
6207
6381
|
}
|
|
@@ -6214,6 +6388,9 @@ export class ManageMetadataBase {
|
|
|
6214
6388
|
WHERE ID = '${winner.ID}'
|
|
6215
6389
|
AND AutoUpdateIsNameField = ${this.boolLit(true)}
|
|
6216
6390
|
`);
|
|
6391
|
+
if (entity?.Name) {
|
|
6392
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, String(winner.Name), 'IsNameField', true);
|
|
6393
|
+
}
|
|
6217
6394
|
}
|
|
6218
6395
|
// Clear every other flagged field that allows auto-update — single winner, always.
|
|
6219
6396
|
for (const f of fields) {
|
|
@@ -6226,6 +6403,9 @@ export class ManageMetadataBase {
|
|
|
6226
6403
|
WHERE ID = '${f.ID}'
|
|
6227
6404
|
AND AutoUpdateIsNameField = ${this.boolLit(true)}
|
|
6228
6405
|
`);
|
|
6406
|
+
if (entity?.Name) {
|
|
6407
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, String(f.Name), 'IsNameField', false);
|
|
6408
|
+
}
|
|
6229
6409
|
}
|
|
6230
6410
|
}
|
|
6231
6411
|
/**
|
|
@@ -6247,7 +6427,7 @@ export class ManageMetadataBase {
|
|
|
6247
6427
|
return this.preferLiteralNameField(eligibleFlagged);
|
|
6248
6428
|
}
|
|
6249
6429
|
for (const candidateName of rankedCandidates) {
|
|
6250
|
-
const candidate =
|
|
6430
|
+
const candidate = this.findFieldByName(fields, candidateName);
|
|
6251
6431
|
if (candidate && this.isFieldEligibleForNameField(candidate)) {
|
|
6252
6432
|
return candidate;
|
|
6253
6433
|
}
|
|
@@ -6355,12 +6535,15 @@ export class ManageMetadataBase {
|
|
|
6355
6535
|
/**
|
|
6356
6536
|
* Generate SQL UPDATEs for DefaultInView on the identified default view fields
|
|
6357
6537
|
*/
|
|
6358
|
-
applyDefaultInViewUpdates(sqlStatements, fields, result) {
|
|
6538
|
+
applyDefaultInViewUpdates(sqlStatements, fields, result, entityOrCtx, maybeCtx) {
|
|
6539
|
+
const entity = (entityOrCtx && 'Name' in entityOrCtx) ? entityOrCtx : undefined;
|
|
6540
|
+
const ctx = (entityOrCtx && 'newFieldNames' in entityOrCtx) ? entityOrCtx : maybeCtx;
|
|
6541
|
+
const inScope = (f) => !ctx || ctx.isNewEntity || ctx.newFieldNames.has(String(f.Name)) || ctx.typeReopenedNames.has(String(f.Name));
|
|
6359
6542
|
// Guard against a malformed LLM result that omits the array entirely.
|
|
6360
6543
|
// Mirrors the defensive handling in the sibling apply* methods.
|
|
6361
6544
|
const defaultInView = result.defaultInView ?? [];
|
|
6362
|
-
const defaultInViewFields = fields.filter(f => defaultInView.
|
|
6363
|
-
const missingFields = defaultInView.filter(name => !
|
|
6545
|
+
const defaultInViewFields = fields.filter(f => defaultInView.some(n => n.toLowerCase() === String(f.Name ?? '').toLowerCase()) && f.AutoUpdateDefaultInView && f.ID && inScope(f));
|
|
6546
|
+
const missingFields = defaultInView.filter(name => !this.findFieldByName(fields, name));
|
|
6364
6547
|
if (missingFields.length > 0) {
|
|
6365
6548
|
logError(`Smart field identification returned invalid defaultInView fields: ${missingFields.join(', ')} not found in entity`);
|
|
6366
6549
|
}
|
|
@@ -6372,6 +6555,9 @@ export class ManageMetadataBase {
|
|
|
6372
6555
|
WHERE ID = '${field.ID}'
|
|
6373
6556
|
AND AutoUpdateDefaultInView = ${this.boolLit(true)}
|
|
6374
6557
|
`);
|
|
6558
|
+
if (entity?.Name) {
|
|
6559
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, String(field.Name), 'DefaultInView', true);
|
|
6560
|
+
}
|
|
6375
6561
|
}
|
|
6376
6562
|
}
|
|
6377
6563
|
}
|
|
@@ -6386,12 +6572,13 @@ export class ManageMetadataBase {
|
|
|
6386
6572
|
* always-wrong cases — they cause the data provider to emit unindexed scans on
|
|
6387
6573
|
* every search.
|
|
6388
6574
|
*/
|
|
6389
|
-
applySearchableFieldUpdates(sqlStatements, fields, result, entity) {
|
|
6575
|
+
applySearchableFieldUpdates(sqlStatements, fields, result, entity, ctx) {
|
|
6576
|
+
const inScope = (f) => !ctx || ctx.isNewEntity || ctx.newFieldNames.has(String(f.Name)) || ctx.typeReopenedNames.has(String(f.Name));
|
|
6390
6577
|
if (!result.searchableFields || result.searchableFields.length === 0) {
|
|
6391
6578
|
return;
|
|
6392
6579
|
}
|
|
6393
|
-
const searchableFields = fields.filter(f => result.searchableFields.
|
|
6394
|
-
const missingSearchableFields = result.searchableFields.filter(name => !
|
|
6580
|
+
const searchableFields = fields.filter(f => result.searchableFields.some(n => n.toLowerCase() === String(f.Name ?? '').toLowerCase()) && f.AutoUpdateIncludeInUserSearchAPI && f.ID && inScope(f));
|
|
6581
|
+
const missingSearchableFields = result.searchableFields.filter(name => !this.findFieldByName(fields, name));
|
|
6395
6582
|
if (missingSearchableFields.length > 0) {
|
|
6396
6583
|
logError(`Smart field identification returned invalid searchableFields: ${missingSearchableFields.join(', ')} not found in entity`);
|
|
6397
6584
|
}
|
|
@@ -6409,6 +6596,9 @@ export class ManageMetadataBase {
|
|
|
6409
6596
|
WHERE ID = '${field.ID}'
|
|
6410
6597
|
AND AutoUpdateIncludeInUserSearchAPI = ${this.boolLit(true)}
|
|
6411
6598
|
`);
|
|
6599
|
+
if (entity?.Name) {
|
|
6600
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, String(field.Name), 'IncludeInUserSearchAPI', true);
|
|
6601
|
+
}
|
|
6412
6602
|
}
|
|
6413
6603
|
}
|
|
6414
6604
|
}
|
|
@@ -6426,7 +6616,7 @@ export class ManageMetadataBase {
|
|
|
6426
6616
|
if (!textTypes.has(type))
|
|
6427
6617
|
return false;
|
|
6428
6618
|
// Unbounded text on a non-FTX entity cannot be index-seeked.
|
|
6429
|
-
const length = field.Length;
|
|
6619
|
+
const length = (field.Length ?? field.MaxLength);
|
|
6430
6620
|
if (length === -1 && !ftxEnabled)
|
|
6431
6621
|
return false;
|
|
6432
6622
|
return true;
|
|
@@ -6434,17 +6624,20 @@ export class ManageMetadataBase {
|
|
|
6434
6624
|
/**
|
|
6435
6625
|
* Generate SQL UPDATEs for UserSearchPredicateAPI on each searchable field
|
|
6436
6626
|
*/
|
|
6437
|
-
applySearchPredicateUpdates(sqlStatements, fields, result) {
|
|
6627
|
+
applySearchPredicateUpdates(sqlStatements, fields, result, entityOrCtx, maybeCtx) {
|
|
6628
|
+
const entity = (entityOrCtx && 'Name' in entityOrCtx) ? entityOrCtx : undefined;
|
|
6629
|
+
const ctx = (entityOrCtx && 'newFieldNames' in entityOrCtx) ? entityOrCtx : maybeCtx;
|
|
6630
|
+
const inScope = (f) => !ctx || ctx.isNewEntity || ctx.newFieldNames.has(String(f.Name)) || ctx.typeReopenedNames.has(String(f.Name));
|
|
6438
6631
|
if (!result.searchPredicates || result.searchPredicates.length === 0) {
|
|
6439
6632
|
return;
|
|
6440
6633
|
}
|
|
6441
6634
|
for (const sp of result.searchPredicates) {
|
|
6442
|
-
const field =
|
|
6635
|
+
const field = this.findFieldByName(fields, sp.field);
|
|
6443
6636
|
if (!field) {
|
|
6444
6637
|
logError(`Smart field identification returned invalid searchPredicate field: '${sp.field}' not found in entity fields`);
|
|
6445
6638
|
continue;
|
|
6446
6639
|
}
|
|
6447
|
-
if (!field.AutoUpdateUserSearchPredicate || !field.ID) {
|
|
6640
|
+
if (!field.AutoUpdateUserSearchPredicate || !field.ID || !inScope(field)) {
|
|
6448
6641
|
continue;
|
|
6449
6642
|
}
|
|
6450
6643
|
// Only update if the current value differs from the recommended predicate
|
|
@@ -6455,6 +6648,9 @@ export class ManageMetadataBase {
|
|
|
6455
6648
|
WHERE ID = '${field.ID}'
|
|
6456
6649
|
AND AutoUpdateUserSearchPredicate = ${this.boolLit(true)}
|
|
6457
6650
|
`);
|
|
6651
|
+
if (entity?.Name) {
|
|
6652
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, String(field.Name), 'UserSearchPredicateAPI', sp.predicate);
|
|
6653
|
+
}
|
|
6458
6654
|
}
|
|
6459
6655
|
}
|
|
6460
6656
|
}
|
|
@@ -6502,6 +6698,9 @@ export class ManageMetadataBase {
|
|
|
6502
6698
|
WHERE ID = '${entity.ID}'
|
|
6503
6699
|
AND AutoUpdateAllowUserSearchAPI = ${this.boolLit(true)}
|
|
6504
6700
|
`);
|
|
6701
|
+
if (entity.Name) {
|
|
6702
|
+
DecisionMetadataWriter.Instance.recordEntityDecision(entity.Name, 'AllowUserSearchAPI', newValue);
|
|
6703
|
+
}
|
|
6505
6704
|
}
|
|
6506
6705
|
}
|
|
6507
6706
|
/**
|
|
@@ -6532,9 +6731,10 @@ export class ManageMetadataBase {
|
|
|
6532
6731
|
/**
|
|
6533
6732
|
* Generate SQL UPDATEs for entity-level and field-level FullTextSearch configuration
|
|
6534
6733
|
*/
|
|
6535
|
-
applyFullTextSearchUpdates(sqlStatements, entity, fields, result) {
|
|
6536
|
-
|
|
6537
|
-
|
|
6734
|
+
applyFullTextSearchUpdates(sqlStatements, entity, fields, result, ctx) {
|
|
6735
|
+
const inScope = (f) => !ctx || ctx.isNewEntity || ctx.newFieldNames.has(String(f.Name)) || ctx.typeReopenedNames.has(String(f.Name));
|
|
6736
|
+
// Entity-level FullTextSearchEnabled (only for new entities)
|
|
6737
|
+
if ((!ctx || ctx.isNewEntity) && result.enableFullTextSearch != null && entity.AutoUpdateFullTextSearch) {
|
|
6538
6738
|
const newValue = result.enableFullTextSearch;
|
|
6539
6739
|
const currentValue = !!entity.FullTextSearchEnabled;
|
|
6540
6740
|
if (newValue !== currentValue) {
|
|
@@ -6544,6 +6744,9 @@ export class ManageMetadataBase {
|
|
|
6544
6744
|
WHERE ID = '${entity.ID}'
|
|
6545
6745
|
AND AutoUpdateFullTextSearch = ${this.boolLit(true)}
|
|
6546
6746
|
`);
|
|
6747
|
+
if (entity.Name) {
|
|
6748
|
+
DecisionMetadataWriter.Instance.recordEntityDecision(entity.Name, 'FullTextSearchEnabled', newValue);
|
|
6749
|
+
}
|
|
6547
6750
|
}
|
|
6548
6751
|
}
|
|
6549
6752
|
// Field-level FullTextSearchEnabled
|
|
@@ -6551,12 +6754,12 @@ export class ManageMetadataBase {
|
|
|
6551
6754
|
return;
|
|
6552
6755
|
}
|
|
6553
6756
|
for (const ftsFieldName of result.fullTextSearchFields) {
|
|
6554
|
-
const field =
|
|
6757
|
+
const field = this.findFieldByName(fields, ftsFieldName);
|
|
6555
6758
|
if (!field) {
|
|
6556
6759
|
logError(`Smart field identification returned invalid fullTextSearchField: '${ftsFieldName}' not found in entity fields`);
|
|
6557
6760
|
continue;
|
|
6558
6761
|
}
|
|
6559
|
-
if (!field.AutoUpdateFullTextSearch || !field.ID) {
|
|
6762
|
+
if (!field.AutoUpdateFullTextSearch || !field.ID || !inScope(field)) {
|
|
6560
6763
|
continue;
|
|
6561
6764
|
}
|
|
6562
6765
|
if (!field.FullTextSearchEnabled) {
|
|
@@ -6566,6 +6769,9 @@ export class ManageMetadataBase {
|
|
|
6566
6769
|
WHERE ID = '${field.ID}'
|
|
6567
6770
|
AND AutoUpdateFullTextSearch = ${this.boolLit(true)}
|
|
6568
6771
|
`);
|
|
6772
|
+
if (entity.Name) {
|
|
6773
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, String(field.Name), 'FullTextSearchEnabled', true);
|
|
6774
|
+
}
|
|
6569
6775
|
}
|
|
6570
6776
|
}
|
|
6571
6777
|
}
|
|
@@ -6583,29 +6789,29 @@ export class ManageMetadataBase {
|
|
|
6583
6789
|
// Defensive: a malformed LLM result can leave fieldCategories undefined or
|
|
6584
6790
|
// non-array; both consumers below iterate it, so coerce to a safe array.
|
|
6585
6791
|
const fieldCategories = Array.isArray(result.fieldCategories) ? result.fieldCategories : [];
|
|
6586
|
-
await this.applyFieldCategories(pool, entity, fields, fieldCategories, existingCategories);
|
|
6792
|
+
await this.applyFieldCategories(pool, entity, fields, fieldCategories, existingCategories, { isNewEntity });
|
|
6587
6793
|
// Auto-detect geo-capable entities and set SupportsGeoCoding
|
|
6588
|
-
await this.detectAndSetGeoCodingSupport(pool, entity
|
|
6794
|
+
await this.detectAndSetGeoCodingSupport(pool, entity);
|
|
6589
6795
|
if (result.entityIcon) {
|
|
6590
|
-
await this.applyEntityIcon(pool, entity.ID, result.entityIcon);
|
|
6796
|
+
await this.applyEntityIcon(pool, entity.ID, result.entityIcon, entity.Name);
|
|
6591
6797
|
}
|
|
6592
6798
|
// Resolve categoryInfo from new or legacy format
|
|
6593
6799
|
const categoryInfoToStore = result.categoryInfo ||
|
|
6594
6800
|
(result.categoryIcons ?
|
|
6595
6801
|
Object.fromEntries(Object.entries(result.categoryIcons).map(([cat, icon]) => [cat, { icon, description: '' }])) : null);
|
|
6596
6802
|
if (categoryInfoToStore) {
|
|
6597
|
-
await this.applyCategoryInfoSettings(pool, entity.ID, categoryInfoToStore);
|
|
6803
|
+
await this.applyCategoryInfoSettings(pool, entity.ID, categoryInfoToStore, entity.Name);
|
|
6598
6804
|
}
|
|
6599
6805
|
if (isNewEntity && result.entityImportance) {
|
|
6600
|
-
await this.applyEntityImportance(pool, entity.ID, result.entityImportance);
|
|
6806
|
+
await this.applyEntityImportance(pool, entity.ID, result.entityImportance, entity.Name);
|
|
6601
6807
|
}
|
|
6602
6808
|
}
|
|
6603
6809
|
/**
|
|
6604
|
-
* Detect if an entity has geo-capable fields based on
|
|
6810
|
+
* Detect if an entity has geo-capable fields based on persisted ExtendedType values in the database.
|
|
6605
6811
|
* If any field has a Geo* ExtendedType, set Entity.SupportsGeoCoding = 1.
|
|
6606
6812
|
* Respects the AutoUpdateSupportsGeoCoding flag — if 0, the value is locked.
|
|
6607
6813
|
*/
|
|
6608
|
-
async detectAndSetGeoCodingSupport(pool, entity
|
|
6814
|
+
async detectAndSetGeoCodingSupport(pool, entity) {
|
|
6609
6815
|
const schema = mj_core_schema();
|
|
6610
6816
|
// Check if the entity's AutoUpdateSupportsGeoCoding flag allows us to modify it
|
|
6611
6817
|
const autoUpdateResult = await pool.query(`
|
|
@@ -6617,14 +6823,8 @@ export class ManageMetadataBase {
|
|
|
6617
6823
|
if (!row || !row.AutoUpdateSupportsGeoCoding) {
|
|
6618
6824
|
return; // Flag is locked, don't modify
|
|
6619
6825
|
}
|
|
6620
|
-
//
|
|
6621
|
-
|
|
6622
|
-
'Geo', 'GeoLatitude', 'GeoLongitude', 'GeoCountry', 'GeoStateProvince',
|
|
6623
|
-
'GeoCity', 'GeoPostalCode', 'GeoAddress'
|
|
6624
|
-
]);
|
|
6625
|
-
const hasGeoFields = fieldCategories.some(fc => fc.extendedType != null && geoExtendedTypes.has(fc.extendedType));
|
|
6626
|
-
// Also check existing fields in the database for Geo* ExtendedTypes
|
|
6627
|
-
// (in case the LLM didn't re-assign them this run but they were previously set)
|
|
6826
|
+
// Check existing fields in the database for Geo* ExtendedTypes
|
|
6827
|
+
// (relies on persisted state after applyFieldCategories has run)
|
|
6628
6828
|
const existingGeoResult = await pool.query(`
|
|
6629
6829
|
SELECT COUNT(*) AS ${this.qi('GeoFieldCount')}
|
|
6630
6830
|
FROM ${this.qs(schema, 'EntityField')}
|
|
@@ -6632,7 +6832,7 @@ export class ManageMetadataBase {
|
|
|
6632
6832
|
AND ${this.qi('ExtendedType')} IN ('Geo', 'GeoLatitude', 'GeoLongitude', 'GeoCountry', 'GeoStateProvince', 'GeoCity', 'GeoPostalCode', 'GeoAddress')
|
|
6633
6833
|
`);
|
|
6634
6834
|
const existingGeoCount = existingGeoResult?.recordset?.[0]?.GeoFieldCount ?? 0;
|
|
6635
|
-
const shouldSupportGeo =
|
|
6835
|
+
const shouldSupportGeo = existingGeoCount > 0;
|
|
6636
6836
|
const currentValue = row.SupportsGeoCoding ? true : false;
|
|
6637
6837
|
if (shouldSupportGeo !== currentValue) {
|
|
6638
6838
|
await this.LogSQLAndExecute(pool, `
|
|
@@ -6640,7 +6840,8 @@ export class ManageMetadataBase {
|
|
|
6640
6840
|
SET ${this.qi('SupportsGeoCoding')} = ${this.boolLit(shouldSupportGeo)}
|
|
6641
6841
|
WHERE ${this.qi('ID')} = '${entity.ID}' AND ${this.qi('AutoUpdateSupportsGeoCoding')} = ${this.boolLit(true)}
|
|
6642
6842
|
`, `Set SupportsGeoCoding = ${shouldSupportGeo} for ${entity.Name}`);
|
|
6643
|
-
logStatus(` Entity ${entity.Name}: SupportsGeoCoding = ${shouldSupportGeo ? 1 : 0} (auto-detected from
|
|
6843
|
+
logStatus(` Entity ${entity.Name}: SupportsGeoCoding = ${shouldSupportGeo ? 1 : 0} (auto-detected from persisted geo fields)`);
|
|
6844
|
+
DecisionMetadataWriter.Instance.recordEntityDecision(entity.Name, 'SupportsGeoCoding', shouldSupportGeo);
|
|
6644
6845
|
// Queue for late-phase view regeneration — the view was already generated
|
|
6645
6846
|
// before this flag was set, so it needs to be regenerated with the geo JOIN
|
|
6646
6847
|
ManageMetadataBase.AddEntityRequiringViewRegen(entity.Name, 'Geocoding');
|
|
@@ -6687,55 +6888,72 @@ export class ManageMetadataBase {
|
|
|
6687
6888
|
* Enforces stability rules: fields with existing categories cannot move to NEW categories.
|
|
6688
6889
|
* All SQL updates are batched into a single execution for performance.
|
|
6689
6890
|
*/
|
|
6690
|
-
async applyFieldCategories(pool, entity, fields, fieldCategories, existingCategories) {
|
|
6891
|
+
async applyFieldCategories(pool, entity, fields, fieldCategories, existingCategories, ctx) {
|
|
6691
6892
|
const sqlStatements = [];
|
|
6893
|
+
const reporter = CodeGenReporter.Instance;
|
|
6894
|
+
// Count fields with blank Category that the LLM omitted (FM2 re-trigger loop tracking)
|
|
6895
|
+
const proposedNames = new Set(fieldCategories.map(fc => (fc.fieldName ?? '').trim().toLowerCase()));
|
|
6896
|
+
let omittedBlankCount = 0;
|
|
6897
|
+
for (const field of fields) {
|
|
6898
|
+
const isBlankCategory = !field.Category || field.Category.trim() === '';
|
|
6899
|
+
if (isBlankCategory && field.AutoUpdateCategory && !proposedNames.has(field.Name.trim().toLowerCase())) {
|
|
6900
|
+
omittedBlankCount++;
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
if (omittedBlankCount > 0) {
|
|
6904
|
+
reporter.counter('ai.fieldsOmittedByLLM', omittedBlankCount);
|
|
6905
|
+
}
|
|
6692
6906
|
for (const fieldCategory of fieldCategories) {
|
|
6693
|
-
const field =
|
|
6694
|
-
if (field
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
setClauses.push(`Category = '${category.replace(/'/g, "''")}'`);
|
|
6710
|
-
}
|
|
6711
|
-
if (field.GeneratedFormSection !== 'Category') {
|
|
6712
|
-
setClauses.push(`GeneratedFormSection = 'Category'`);
|
|
6713
|
-
}
|
|
6714
|
-
if (fieldCategory.displayName && field.AutoUpdateDisplayName && field.DisplayName !== fieldCategory.displayName) {
|
|
6715
|
-
setClauses.push(`DisplayName = '${fieldCategory.displayName.replace(/'/g, "''")}'`);
|
|
6716
|
-
}
|
|
6717
|
-
if (fieldCategory.extendedType !== undefined && field.ExtendedType !== fieldCategory.extendedType) {
|
|
6718
|
-
const extendedType = fieldCategory.extendedType === null ? 'NULL' : `'${String(fieldCategory.extendedType).replace(/'/g, "''")}'`;
|
|
6719
|
-
setClauses.push(`ExtendedType = ${extendedType}`);
|
|
6907
|
+
const field = this.findFieldByName(fields, fieldCategory.fieldName);
|
|
6908
|
+
if (!field) {
|
|
6909
|
+
logError(`Form layout returned invalid fieldName: '${fieldCategory.fieldName}' not found in entity`);
|
|
6910
|
+
continue;
|
|
6911
|
+
}
|
|
6912
|
+
const lockCtx = {
|
|
6913
|
+
isNewEntity: ctx.isNewEntity,
|
|
6914
|
+
isNewField: ManageMetadataBase.isFieldNew(entity.ID, field.Name),
|
|
6915
|
+
descriptionReopened: ManageMetadataBase.isDisplayNameReopened(entity.ID, field.Name),
|
|
6916
|
+
typeReopened: ManageMetadataBase.isTypeReopened(entity.ID, field.Name),
|
|
6917
|
+
existingCategories,
|
|
6918
|
+
};
|
|
6919
|
+
const update = computeFieldMetadataUpdate(field, fieldCategory, lockCtx, this.validateExtendedType.bind(this), (val, fn) => this.sanitizeCodeType(val, fn ?? field.Name, entity.Name));
|
|
6920
|
+
for (const skip of update.skipped) {
|
|
6921
|
+
if (skip.reason === 'locked') {
|
|
6922
|
+
reporter.counter('ai.fieldsLocked');
|
|
6720
6923
|
}
|
|
6721
|
-
if (
|
|
6722
|
-
|
|
6723
|
-
if (field.CodeType !== sanitized) {
|
|
6724
|
-
const codeType = sanitized == null ? 'NULL' : `'${sanitized.replace(/'/g, "''")}'`;
|
|
6725
|
-
setClauses.push(`CodeType = ${codeType}`);
|
|
6726
|
-
}
|
|
6924
|
+
else if (skip.reason === 'invalid') {
|
|
6925
|
+
reporter.counter('ai.fieldsInvalidProposal');
|
|
6727
6926
|
}
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6927
|
+
}
|
|
6928
|
+
const setClauses = [];
|
|
6929
|
+
if (update.Category !== undefined) {
|
|
6930
|
+
setClauses.push(`Category = '${update.Category.replace(/'/g, "''")}'`);
|
|
6931
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, field.Name, 'Category', update.Category);
|
|
6932
|
+
}
|
|
6933
|
+
if (update.GeneratedFormSection !== undefined) {
|
|
6934
|
+
setClauses.push(`GeneratedFormSection = 'Category'`);
|
|
6935
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, field.Name, 'GeneratedFormSection', 'Category');
|
|
6936
|
+
}
|
|
6937
|
+
if (update.DisplayName !== undefined) {
|
|
6938
|
+
setClauses.push(`DisplayName = '${update.DisplayName.replace(/'/g, "''")}'`);
|
|
6939
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, field.Name, 'DisplayName', update.DisplayName);
|
|
6940
|
+
}
|
|
6941
|
+
if (update.ExtendedType !== undefined) {
|
|
6942
|
+
const extVal = update.ExtendedType === null ? 'NULL' : `'${update.ExtendedType.replace(/'/g, "''")}'`;
|
|
6943
|
+
setClauses.push(`ExtendedType = ${extVal}`);
|
|
6944
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, field.Name, 'ExtendedType', update.ExtendedType);
|
|
6945
|
+
}
|
|
6946
|
+
if (update.CodeType !== undefined) {
|
|
6947
|
+
const codeVal = update.CodeType === null ? 'NULL' : `'${update.CodeType.replace(/'/g, "''")}'`;
|
|
6948
|
+
setClauses.push(`CodeType = ${codeVal}`);
|
|
6949
|
+
DecisionMetadataWriter.Instance.recordFieldDecision(entity.Name, field.Name, 'CodeType', update.CodeType);
|
|
6950
|
+
}
|
|
6951
|
+
if (setClauses.length > 0) {
|
|
6952
|
+
sqlStatements.push(`\n-- UPDATE Entity Field Category Info ${entity.Name}.${field.Name} \nUPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
6731
6953
|
SET
|
|
6732
6954
|
${setClauses.join(',\n ')}
|
|
6733
6955
|
WHERE
|
|
6734
|
-
ID = '${field.ID}'
|
|
6735
|
-
}
|
|
6736
|
-
}
|
|
6737
|
-
else if (!field) {
|
|
6738
|
-
logError(`Form layout returned invalid fieldName: '${fieldCategory.fieldName}' not found in entity`);
|
|
6956
|
+
ID = '${field.ID}'`);
|
|
6739
6957
|
}
|
|
6740
6958
|
}
|
|
6741
6959
|
if (sqlStatements.length > 0) {
|
|
@@ -6750,7 +6968,7 @@ WHERE
|
|
|
6750
6968
|
/**
|
|
6751
6969
|
* Sets the entity icon if the entity doesn't already have one.
|
|
6752
6970
|
*/
|
|
6753
|
-
async applyEntityIcon(pool, entityId, entityIcon) {
|
|
6971
|
+
async applyEntityIcon(pool, entityId, entityIcon, entityName) {
|
|
6754
6972
|
if (!entityIcon || entityIcon.trim().length === 0)
|
|
6755
6973
|
return;
|
|
6756
6974
|
const checkSQL = `SELECT Icon FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE ID = '${entityId}'`;
|
|
@@ -6767,6 +6985,9 @@ WHERE
|
|
|
6767
6985
|
try {
|
|
6768
6986
|
await this.LogSQLAndExecute(pool, updateSQL, `Set entity icon to ${entityIcon}`, false);
|
|
6769
6987
|
logStatus(` Set entity icon: ${entityIcon}`);
|
|
6988
|
+
if (entityName) {
|
|
6989
|
+
DecisionMetadataWriter.Instance.recordEntityDecision(entityName, 'Icon', entityIcon);
|
|
6990
|
+
}
|
|
6770
6991
|
}
|
|
6771
6992
|
catch (ex) {
|
|
6772
6993
|
logError('Error Applying Entity Icon', ex);
|
|
@@ -6777,32 +6998,49 @@ WHERE
|
|
|
6777
6998
|
/**
|
|
6778
6999
|
* Upserts FieldCategoryInfo (new format) and FieldCategoryIcons (legacy format) in EntitySetting.
|
|
6779
7000
|
*/
|
|
6780
|
-
async applyCategoryInfoSettings(pool, entityId, categoryInfo) {
|
|
7001
|
+
async applyCategoryInfoSettings(pool, entityId, categoryInfo, entityName) {
|
|
6781
7002
|
if (!categoryInfo || Object.keys(categoryInfo).length === 0)
|
|
6782
7003
|
return;
|
|
6783
|
-
|
|
7004
|
+
if (entityName) {
|
|
7005
|
+
DecisionMetadataWriter.Instance.recordEntitySetting(entityName, 'FieldCategoryInfo', categoryInfo);
|
|
7006
|
+
}
|
|
7007
|
+
const canonicalInfo = canonicalJSONStringify(categoryInfo, 2);
|
|
7008
|
+
const infoJSON = canonicalInfo.replace(/'/g, "''");
|
|
7009
|
+
const ENTITY_SETTING_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
6784
7010
|
// Upsert FieldCategoryInfo (new format)
|
|
6785
|
-
const checkNewSQL = `SELECT ${this.qi('ID')} FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'`;
|
|
7011
|
+
const checkNewSQL = `SELECT ${this.qi('ID')}, ${this.qi('Value')} FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'`;
|
|
6786
7012
|
const existingNew = await this.runQuery(pool, checkNewSQL);
|
|
6787
7013
|
if (existingNew.recordset.length > 0) {
|
|
7014
|
+
let isMatch = false;
|
|
6788
7015
|
try {
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'
|
|
6793
|
-
`, `Update FieldCategoryInfo setting for entity`, false);
|
|
7016
|
+
const rawVal = existingNew.recordset[0].Value;
|
|
7017
|
+
const parsed = typeof rawVal === 'string' ? JSON.parse(rawVal) : rawVal;
|
|
7018
|
+
isMatch = deepEqualJSON(parsed, categoryInfo);
|
|
6794
7019
|
}
|
|
6795
|
-
catch
|
|
6796
|
-
|
|
7020
|
+
catch {
|
|
7021
|
+
isMatch = false;
|
|
7022
|
+
}
|
|
7023
|
+
if (!isMatch) {
|
|
7024
|
+
try {
|
|
7025
|
+
await this.LogSQLAndExecute(pool, `
|
|
7026
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntitySetting')}
|
|
7027
|
+
SET ${this.qi('Value')} = '${infoJSON}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
7028
|
+
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'
|
|
7029
|
+
`, `Update FieldCategoryInfo setting for entity`, false);
|
|
7030
|
+
}
|
|
7031
|
+
catch (ex) {
|
|
7032
|
+
logError('Error Applying Category Info Settings: Part 1', ex);
|
|
7033
|
+
}
|
|
6797
7034
|
}
|
|
6798
7035
|
}
|
|
6799
7036
|
else {
|
|
6800
|
-
const newId =
|
|
7037
|
+
const newId = uuidv5(`${entityId}|FieldCategoryInfo`, ENTITY_SETTING_NAMESPACE);
|
|
6801
7038
|
try {
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}', ${this.utcNow()}, ${this.utcNow()})
|
|
6805
|
-
|
|
7039
|
+
const checkQuery = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'`;
|
|
7040
|
+
const insertSQL = `INSERT INTO ${this.qs(mj_core_schema(), 'EntitySetting')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Name')}, ${this.qi('Value')}, ${this.qi(EntityInfo.CreatedAtFieldName)}, ${this.qi(EntityInfo.UpdatedAtFieldName)})
|
|
7041
|
+
VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}', ${this.utcNow()}, ${this.utcNow()})`;
|
|
7042
|
+
const sSQL = this.conditionalInsert(checkQuery, insertSQL);
|
|
7043
|
+
await this.LogSQLAndExecute(pool, sSQL, `Insert FieldCategoryInfo setting for entity`, false);
|
|
6806
7044
|
}
|
|
6807
7045
|
catch (ex) {
|
|
6808
7046
|
logError('Error Applying Category Info Settings: Part 2', ex);
|
|
@@ -6815,28 +7053,44 @@ WHERE
|
|
|
6815
7053
|
iconsOnly[category] = info.icon;
|
|
6816
7054
|
}
|
|
6817
7055
|
}
|
|
6818
|
-
|
|
6819
|
-
|
|
7056
|
+
if (entityName) {
|
|
7057
|
+
DecisionMetadataWriter.Instance.recordEntitySetting(entityName, 'FieldCategoryIcons', iconsOnly);
|
|
7058
|
+
}
|
|
7059
|
+
const canonicalIcons = canonicalJSONStringify(iconsOnly, 2);
|
|
7060
|
+
const iconsJSON = canonicalIcons.replace(/'/g, "''");
|
|
7061
|
+
const checkLegacySQL = `SELECT ${this.qi('ID')}, ${this.qi('Value')} FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'`;
|
|
6820
7062
|
const existingLegacy = await this.runQuery(pool, checkLegacySQL);
|
|
6821
7063
|
if (existingLegacy.recordset.length > 0) {
|
|
7064
|
+
let isMatch = false;
|
|
6822
7065
|
try {
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'
|
|
6827
|
-
`, `Update FieldCategoryIcons setting (legacy)`, false);
|
|
7066
|
+
const rawVal = existingLegacy.recordset[0].Value;
|
|
7067
|
+
const parsed = typeof rawVal === 'string' ? JSON.parse(rawVal) : rawVal;
|
|
7068
|
+
isMatch = deepEqualJSON(parsed, iconsOnly);
|
|
6828
7069
|
}
|
|
6829
|
-
catch
|
|
6830
|
-
|
|
7070
|
+
catch {
|
|
7071
|
+
isMatch = false;
|
|
7072
|
+
}
|
|
7073
|
+
if (!isMatch) {
|
|
7074
|
+
try {
|
|
7075
|
+
await this.LogSQLAndExecute(pool, `
|
|
7076
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntitySetting')}
|
|
7077
|
+
SET ${this.qi('Value')} = '${iconsJSON}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
7078
|
+
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'
|
|
7079
|
+
`, `Update FieldCategoryIcons setting (legacy)`, false);
|
|
7080
|
+
}
|
|
7081
|
+
catch (ex) {
|
|
7082
|
+
logError('Error Applying Category Info Settings: Part 3', ex);
|
|
7083
|
+
}
|
|
6831
7084
|
}
|
|
6832
7085
|
}
|
|
6833
7086
|
else {
|
|
6834
|
-
const newId =
|
|
7087
|
+
const newId = uuidv5(`${entityId}|FieldCategoryIcons`, ENTITY_SETTING_NAMESPACE);
|
|
6835
7088
|
try {
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}', ${this.utcNow()}, ${this.utcNow()})
|
|
6839
|
-
|
|
7089
|
+
const checkQuery = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'`;
|
|
7090
|
+
const insertSQL = `INSERT INTO ${this.qs(mj_core_schema(), 'EntitySetting')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Name')}, ${this.qi('Value')}, ${this.qi(EntityInfo.CreatedAtFieldName)}, ${this.qi(EntityInfo.UpdatedAtFieldName)})
|
|
7091
|
+
VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}', ${this.utcNow()}, ${this.utcNow()})`;
|
|
7092
|
+
const sSQL = this.conditionalInsert(checkQuery, insertSQL);
|
|
7093
|
+
await this.LogSQLAndExecute(pool, sSQL, `Insert FieldCategoryIcons setting (legacy)`, false);
|
|
6840
7094
|
}
|
|
6841
7095
|
catch (ex) {
|
|
6842
7096
|
logError('Error Applying Category Info Settings: Part 4', ex);
|
|
@@ -6847,7 +7101,7 @@ WHERE
|
|
|
6847
7101
|
* Applies entity importance analysis to MJApplicationEntity records.
|
|
6848
7102
|
* Only called for NEW entities to set DefaultForNewUser.
|
|
6849
7103
|
*/
|
|
6850
|
-
async applyEntityImportance(pool, entityId, importance) {
|
|
7104
|
+
async applyEntityImportance(pool, entityId, importance, entityName) {
|
|
6851
7105
|
const updateSQL = `
|
|
6852
7106
|
UPDATE ${this.qs(mj_core_schema(), 'ApplicationEntity')}
|
|
6853
7107
|
SET ${this.qi('DefaultForNewUser')} = ${this.boolLit(importance.defaultForNewUser)}, ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
@@ -6857,6 +7111,9 @@ WHERE
|
|
|
6857
7111
|
await this.LogSQLAndExecute(pool, updateSQL, `Set DefaultForNewUser=${importance.defaultForNewUser} for NEW entity (category: ${importance.entityCategory}, confidence: ${importance.confidence})`, false);
|
|
6858
7112
|
logStatus(` Entity importance (NEW Entity): ${importance.entityCategory} (defaultForNewUser: ${importance.defaultForNewUser}, confidence: ${importance.confidence})`);
|
|
6859
7113
|
logStatus(` Reasoning: ${importance.reasoning}`);
|
|
7114
|
+
if (entityName) {
|
|
7115
|
+
DecisionMetadataWriter.Instance.recordApplicationEntityDecision(entityName, '', 'DefaultForNewUser', importance.defaultForNewUser);
|
|
7116
|
+
}
|
|
6860
7117
|
}
|
|
6861
7118
|
catch (ex) {
|
|
6862
7119
|
logError('Error Applying Entity Importance', ex);
|