@memberjunction/codegen-lib 5.32.0 → 5.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/Config/config.d.ts +19 -8
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +18 -8
- package/dist/Config/config.js.map +1 -1
- package/dist/Config/db-connection.d.ts.map +1 -1
- package/dist/Config/db-connection.js +8 -3
- package/dist/Config/db-connection.js.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts +67 -0
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.js +82 -15
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +112 -4
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +347 -43
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.d.ts +4 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js +6 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +122 -16
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +605 -49
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/postgresql/viewFallback.d.ts +14 -0
- package/dist/Database/providers/postgresql/viewFallback.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/viewFallback.js +107 -13
- package/dist/Database/providers/postgresql/viewFallback.js.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.d.ts +4 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.d.ts.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js +6 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +5 -2
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +27 -7
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js +101 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js.map +1 -1
- package/dist/Database/sql.d.ts +8 -0
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +18 -14
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +19 -5
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +109 -55
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +1 -1
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/createNewUser.d.ts.map +1 -1
- package/dist/Misc/createNewUser.js +14 -3
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +23 -17
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts +11 -0
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +27 -40
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +22 -2
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/Misc/system_integrity.js +3 -3
- package/dist/Misc/system_integrity.js.map +1 -1
- package/dist/Misc/temp_batch_file.js +2 -2
- package/dist/Misc/temp_batch_file.js.map +1 -1
- package/dist/runCodeGen.d.ts +1 -1
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +51 -6
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +19 -18
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { CodeGenDatabaseProvider } from './codeGenDatabaseProvider.js';
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
// Side-effect import — registers `SQLServerCodeGenProvider` with `MJGlobal.ClassFactory`
|
|
3
|
+
// under the `'sqlserver'` key via its `@RegisterClass` decorator. Without this import,
|
|
4
|
+
// `ClassFactory.CreateInstance(CodeGenDatabaseProvider, 'sqlserver')` returns nothing
|
|
5
|
+
// and the SS code path silently fails.
|
|
6
|
+
import './providers/sqlserver/SQLServerCodeGenProvider.js';
|
|
7
|
+
import { configInfo, currentWorkingDirectory, dbPlatform, getSettingValue, mj_core_schema, outputDir } from '../Config/config.js';
|
|
4
8
|
import { EntityInfo, ExtractActualDefaultValue, LogError, LogStatus, Metadata, SeverityType } from "@memberjunction/core";
|
|
5
9
|
import { MJEntityFieldSchema } from "@memberjunction/core-entities";
|
|
6
10
|
import { logError, logMessage, logStatus, startSpinner, updateSpinner, succeedSpinner } from "../Misc/status_logging.js";
|
|
@@ -36,31 +40,35 @@ export class ManageMetadataBase {
|
|
|
36
40
|
constructor() {
|
|
37
41
|
// ─── Database Provider Infrastructure ─────────────────────────────
|
|
38
42
|
// All platform-specific SQL generation is delegated to the CodeGenDatabaseProvider.
|
|
39
|
-
// The provider is lazily initialized from the
|
|
43
|
+
// The provider is lazily initialized from the dbPlatform() config on first access.
|
|
40
44
|
this._dbProvider = null;
|
|
41
45
|
// ─── End Dialect Infrastructure ───────────────────────────────────
|
|
42
46
|
this._sqlUtilityObject = MJGlobal.Instance.ClassFactory.CreateInstance(SQLUtilityBase);
|
|
43
47
|
}
|
|
44
48
|
/**
|
|
45
49
|
* Returns the CodeGenDatabaseProvider for the current database platform.
|
|
46
|
-
* Lazily initialized from
|
|
50
|
+
* Lazily initialized from dbPlatform() configuration.
|
|
51
|
+
*
|
|
52
|
+
* Lookup goes through `MJGlobal.ClassFactory` keyed by the platform string,
|
|
53
|
+
* which matches the `@RegisterClass(CodeGenDatabaseProvider, '<platform>')`
|
|
54
|
+
* decorators on the concrete providers (`'sqlserver'`, `'postgresql'`).
|
|
55
|
+
* Mismatched keys silently fall back to the abstract base class — fail loud
|
|
56
|
+
* with an explicit error so a misconfigured platform doesn't ship as a
|
|
57
|
+
* runtime breakage in dialect-specific methods.
|
|
47
58
|
*/
|
|
48
59
|
get dbProvider() {
|
|
49
60
|
if (!this._dbProvider) {
|
|
50
|
-
const platform =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
else {
|
|
62
|
-
this._dbProvider = new SQLServerCodeGenProvider();
|
|
63
|
-
}
|
|
61
|
+
const platform = dbPlatform();
|
|
62
|
+
const provider = MJGlobal.Instance.ClassFactory.CreateInstance(CodeGenDatabaseProvider, platform);
|
|
63
|
+
// CreateInstance returns the abstract base class on a missed key (it
|
|
64
|
+
// doesn't throw), so disambiguate by constructor identity: anything
|
|
65
|
+
// that's still the bare base means lookup didn't resolve to a real
|
|
66
|
+
// dialect provider for this platform.
|
|
67
|
+
if (!provider || provider.constructor === CodeGenDatabaseProvider) {
|
|
68
|
+
throw new Error(`CodeGen provider for dbPlatform='${platform}' not found. Ensure the corresponding ` +
|
|
69
|
+
`provider package is installed and registered via @RegisterClass(CodeGenDatabaseProvider, '${platform}').`);
|
|
70
|
+
}
|
|
71
|
+
this._dbProvider = provider;
|
|
64
72
|
}
|
|
65
73
|
return this._dbProvider;
|
|
66
74
|
}
|
|
@@ -117,6 +125,49 @@ export class ManageMetadataBase {
|
|
|
117
125
|
async runQueryWithParams(pool, sql, params) {
|
|
118
126
|
return pool.queryWithParams(this.qsql(sql), params);
|
|
119
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Returns a sequence value safe to use for an EntityField INSERT under the given
|
|
130
|
+
* EntityID, defaulting to `candidate` when it doesn't collide. Otherwise returns
|
|
131
|
+
* `MAX(Sequence) + 1` for that entity.
|
|
132
|
+
*
|
|
133
|
+
* Why this exists: several insert paths (virtual-entity field sync, IS-A parent
|
|
134
|
+
* field sync, schema-derived field creation) compute a deterministic sequence
|
|
135
|
+
* up-front, but a partial prior run can leave rows at that exact ordinal under
|
|
136
|
+
* the target EntityID. SS hides the collision because retried runs typically
|
|
137
|
+
* succeed before failing; PG raises UQ_EntityField_EntityID_Sequence (~4 errors
|
|
138
|
+
* per advanced-generation run). The values are temporary anyway —
|
|
139
|
+
* spUpdateExistingEntityFieldsFromSchema renumbers them on the next pass.
|
|
140
|
+
*/
|
|
141
|
+
async nextAvailableEntityFieldSequence(pool, entityId, candidate) {
|
|
142
|
+
const schema = mj_core_schema();
|
|
143
|
+
const tbl = this.qs(schema, 'EntityField');
|
|
144
|
+
const seqCol = this.qi('Sequence');
|
|
145
|
+
const entityIdCol = this.qi('EntityID');
|
|
146
|
+
const sql = `SELECT
|
|
147
|
+
${this.coalesce(`MAX(${seqCol})`, '0')} AS ${this.qi('MaxSeq')},
|
|
148
|
+
${this.coalesce(`MAX(CASE WHEN ${seqCol} = @Candidate THEN 1 ELSE 0 END)`, '0')} AS ${this.qi('Hit')}
|
|
149
|
+
FROM ${tbl}
|
|
150
|
+
WHERE ${entityIdCol} = @EntityID`;
|
|
151
|
+
try {
|
|
152
|
+
const result = await this.runQueryWithParams(pool, sql, {
|
|
153
|
+
EntityID: entityId,
|
|
154
|
+
Candidate: candidate,
|
|
155
|
+
});
|
|
156
|
+
const row = result.recordset?.[0];
|
|
157
|
+
if (!row)
|
|
158
|
+
return candidate;
|
|
159
|
+
const hit = Number(row.Hit ?? 0) > 0;
|
|
160
|
+
if (!hit)
|
|
161
|
+
return candidate;
|
|
162
|
+
const maxSeq = Number(row.MaxSeq ?? 0);
|
|
163
|
+
return maxSeq + 1;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// If the lookup itself fails, fall back to the candidate. The downstream
|
|
167
|
+
// INSERT will surface the real error if there's a true collision.
|
|
168
|
+
return candidate;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
120
171
|
/**
|
|
121
172
|
* Wraps a SELECT query with a row limit.
|
|
122
173
|
* SQL Server: SELECT TOP N ... , PostgreSQL: SELECT ... LIMIT N
|
|
@@ -374,6 +425,49 @@ export class ManageMetadataBase {
|
|
|
374
425
|
}
|
|
375
426
|
return results;
|
|
376
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Extracts value-list field configurations from the additionalSchemaInfo config file.
|
|
430
|
+
* Walks schema → tables → table.Fields[] and returns a map keyed by "schema.table".
|
|
431
|
+
*/
|
|
432
|
+
extractFieldValueLists(config) {
|
|
433
|
+
const results = new Map();
|
|
434
|
+
// Check flat "Tables" array format
|
|
435
|
+
if (Array.isArray(config.Tables)) {
|
|
436
|
+
for (const table of config.Tables) {
|
|
437
|
+
this.extractFieldsFromTableEntry(table.SchemaName || 'dbo', table.TableName, table, results);
|
|
438
|
+
}
|
|
439
|
+
return results;
|
|
440
|
+
}
|
|
441
|
+
// Schema-as-key format
|
|
442
|
+
const metadataKeys = new Set(['$schema', 'description', 'version', 'VirtualEntities', 'ISARelationships', 'Entities', 'Tables', 'Schemas']);
|
|
443
|
+
for (const [key, value] of Object.entries(config)) {
|
|
444
|
+
if (metadataKeys.has(key))
|
|
445
|
+
continue;
|
|
446
|
+
if (!Array.isArray(value))
|
|
447
|
+
continue;
|
|
448
|
+
for (const table of value) {
|
|
449
|
+
this.extractFieldsFromTableEntry(key, table.TableName, table, results);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return results;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Extracts Fields[] entries from a single table config entry and adds them to the results map.
|
|
456
|
+
*/
|
|
457
|
+
extractFieldsFromTableEntry(schemaName, tableName, tableConfig, results) {
|
|
458
|
+
const fields = tableConfig.Fields;
|
|
459
|
+
if (!Array.isArray(fields) || fields.length === 0)
|
|
460
|
+
return;
|
|
461
|
+
const key = `${schemaName}.${tableName}`;
|
|
462
|
+
const fieldConfigs = fields.map((f) => ({
|
|
463
|
+
FieldName: f.FieldName,
|
|
464
|
+
ValueListType: f.ValueListType || 'List',
|
|
465
|
+
PossibleValues: f.PossibleValues || [],
|
|
466
|
+
Description: f.Description || undefined,
|
|
467
|
+
Confidence: f.Confidence || undefined,
|
|
468
|
+
}));
|
|
469
|
+
results.set(key, fieldConfigs);
|
|
470
|
+
}
|
|
377
471
|
/**
|
|
378
472
|
* Processes organic key configurations from additionalSchemaInfo.
|
|
379
473
|
* For each configured organic key:
|
|
@@ -954,6 +1048,11 @@ export class ManageMetadataBase {
|
|
|
954
1048
|
else {
|
|
955
1049
|
// 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
|
|
956
1050
|
newEntityFieldUUID = this.createNewUUID();
|
|
1051
|
+
// Compute a non-colliding sequence by querying MAX(Sequence) for this entity. The
|
|
1052
|
+
// ordinal `fieldSequence` (column index in the view) is correct for stable ordering
|
|
1053
|
+
// when nothing exists yet, but a partial prior run could have left rows at the same
|
|
1054
|
+
// ordinal under this EntityID — UQ_EntityField_EntityID_Sequence then fires on PG.
|
|
1055
|
+
const safeSequence = await this.nextAvailableEntityFieldSequence(pool, entity.ID, fieldSequence);
|
|
957
1056
|
const q = (n) => this.qi(n);
|
|
958
1057
|
const sqlAdd = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')} (
|
|
959
1058
|
${q('ID')}, ${q('EntityID')}, ${q('Name')}, ${q('Type')}, ${q('AllowsNull')},
|
|
@@ -962,7 +1061,7 @@ export class ManageMetadataBase {
|
|
|
962
1061
|
${q('__mj_CreatedAt')}, ${q('__mj_UpdatedAt')} )
|
|
963
1062
|
VALUES ( '${newEntityFieldUUID}', '${entity.ID}', '${veField.FieldName}', '${veField.Type}', ${this.boolLit(veField.AllowsNull)},
|
|
964
1063
|
${veField.Length}, ${veField.Precision}, ${veField.Scale},
|
|
965
|
-
${
|
|
1064
|
+
${safeSequence}, ${this.boolLit(makePrimaryKey)}, ${this.boolLit(makePrimaryKey)},
|
|
966
1065
|
${this.utcNow()}, ${this.utcNow()}
|
|
967
1066
|
)`;
|
|
968
1067
|
await this.LogSQLAndExecute(pool, sqlAdd, `SQL text to add virtual entity field ${veField.FieldName} for entity ${virtualEntity.Name}`);
|
|
@@ -1187,7 +1286,7 @@ export class ManageMetadataBase {
|
|
|
1187
1286
|
WHERE EntityID='${entity.ID}' AND Name='${pk}'`);
|
|
1188
1287
|
logStatus(` ✓ Set PK for ${entity.Name}.${pk} (LLM-identified)`);
|
|
1189
1288
|
}
|
|
1190
|
-
await this.
|
|
1289
|
+
await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set LLM-identified PKs for ${entity.Name}: ${validPKs.join(', ')}`);
|
|
1191
1290
|
return true;
|
|
1192
1291
|
}
|
|
1193
1292
|
/**
|
|
@@ -1232,7 +1331,7 @@ export class ManageMetadataBase {
|
|
|
1232
1331
|
if (sqlStatements.length === 0) {
|
|
1233
1332
|
return false;
|
|
1234
1333
|
}
|
|
1235
|
-
await this.
|
|
1334
|
+
await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set LLM-identified FKs for ${entity.Name}`);
|
|
1236
1335
|
return true;
|
|
1237
1336
|
}
|
|
1238
1337
|
/**
|
|
@@ -1269,7 +1368,7 @@ export class ManageMetadataBase {
|
|
|
1269
1368
|
if (sqlStatements.length === 0) {
|
|
1270
1369
|
return false;
|
|
1271
1370
|
}
|
|
1272
|
-
await this.
|
|
1371
|
+
await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set LLM-generated descriptions for ${entity.Name} (${sqlStatements.length} fields)`);
|
|
1273
1372
|
return true;
|
|
1274
1373
|
}
|
|
1275
1374
|
/**
|
|
@@ -1412,8 +1511,12 @@ export class ManageMetadataBase {
|
|
|
1412
1511
|
else {
|
|
1413
1512
|
// Create new virtual field record for this parent field
|
|
1414
1513
|
const newFieldID = this.createNewUUID();
|
|
1415
|
-
// Use high sequence — will be reordered by updateExistingEntityFieldsFromSchema
|
|
1416
|
-
|
|
1514
|
+
// Use high sequence — will be reordered by updateExistingEntityFieldsFromSchema.
|
|
1515
|
+
// Query MAX(Sequence) at insert time so we never collide with an existing row
|
|
1516
|
+
// (e.g. partial prior run left a record at the candidate ordinal under this
|
|
1517
|
+
// EntityID — UQ_EntityField_EntityID_Sequence then fires on PG).
|
|
1518
|
+
const candidate = 100000 + parentFields.indexOf(parentField);
|
|
1519
|
+
const sequence = await this.nextAvailableEntityFieldSequence(pool, childEntity.ID, candidate);
|
|
1417
1520
|
const q = (n) => this.qi(n);
|
|
1418
1521
|
const sqlInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')} (
|
|
1419
1522
|
${q('ID')}, ${q('EntityID')}, ${q('Name')}, ${q('Type')}, ${q('AllowsNull')},
|
|
@@ -1948,6 +2051,14 @@ export class ManageMetadataBase {
|
|
|
1948
2051
|
bSuccess = false;
|
|
1949
2052
|
}
|
|
1950
2053
|
logStatus(` Managed entity field values in ${(new Date().getTime() - step6StartTime.getTime()) / 1000} seconds`);
|
|
2054
|
+
// Apply soft value-list config from additionalSchemaInfo Fields[]
|
|
2055
|
+
// Runs AFTER CHECK constraint pass so CHECK-derived values are authoritative
|
|
2056
|
+
const stepValueListStartTime = new Date();
|
|
2057
|
+
if (!await this.applyValueListConfig(pool)) {
|
|
2058
|
+
logError('Error applying soft value-list configuration');
|
|
2059
|
+
// Non-fatal: soft value-lists are supplementary
|
|
2060
|
+
}
|
|
2061
|
+
logStatus(` Applied soft value-list configuration in ${(new Date().getTime() - stepValueListStartTime.getTime()) / 1000} seconds`);
|
|
1951
2062
|
}
|
|
1952
2063
|
// Advanced Generation - Smart field identification and form layout
|
|
1953
2064
|
if (!skipAdvancedGeneration) {
|
|
@@ -2098,6 +2209,98 @@ export class ManageMetadataBase {
|
|
|
2098
2209
|
return false;
|
|
2099
2210
|
}
|
|
2100
2211
|
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Applies soft value-list configuration from Fields[] entries in additionalSchemaInfo.json.
|
|
2214
|
+
* Runs AFTER manageEntityFieldValuesAndValidatorFunctions() so CHECK-constraint-derived
|
|
2215
|
+
* values are already in the database. If EntityFieldValue rows already exist for a field
|
|
2216
|
+
* (from CHECK constraints), the soft-enum pass skips that field — CHECK is authoritative.
|
|
2217
|
+
*/
|
|
2218
|
+
async applyValueListConfig(pool) {
|
|
2219
|
+
const config = ManageMetadataBase.getSoftPKFKConfig();
|
|
2220
|
+
if (!config)
|
|
2221
|
+
return true;
|
|
2222
|
+
const fieldLists = this.extractFieldValueLists(config);
|
|
2223
|
+
if (fieldLists.size === 0)
|
|
2224
|
+
return true;
|
|
2225
|
+
try {
|
|
2226
|
+
// Load all existing EntityFieldValue rows for dedup checking
|
|
2227
|
+
const efvSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence`;
|
|
2228
|
+
const allEFVResult = await this.runQuery(pool, efvSQL);
|
|
2229
|
+
const allEntityFieldValues = allEFVResult.recordset;
|
|
2230
|
+
let totalApplied = 0;
|
|
2231
|
+
let totalSkipped = 0;
|
|
2232
|
+
for (const [schemaTable, fields] of fieldLists) {
|
|
2233
|
+
const [schemaName, tableName] = schemaTable.split('.');
|
|
2234
|
+
const result = await this.applyValueListsForTable(pool, schemaName, tableName, fields, allEntityFieldValues);
|
|
2235
|
+
totalApplied += result.applied;
|
|
2236
|
+
totalSkipped += result.skipped;
|
|
2237
|
+
}
|
|
2238
|
+
if (totalApplied > 0 || totalSkipped > 0) {
|
|
2239
|
+
logStatus(` Applied ${totalApplied} soft value-list(s), skipped ${totalSkipped} (CHECK-authoritative)`);
|
|
2240
|
+
}
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
catch (e) {
|
|
2244
|
+
logError(`Error applying value-list configuration: ${e}`);
|
|
2245
|
+
return false;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Applies value-list fields for a single table. For each field, checks if
|
|
2250
|
+
* EntityFieldValue rows already exist (from CHECK constraints) and skips if so.
|
|
2251
|
+
*/
|
|
2252
|
+
async applyValueListsForTable(pool, schemaName, tableName, fields, allEntityFieldValues) {
|
|
2253
|
+
const schema = mj_core_schema();
|
|
2254
|
+
let applied = 0;
|
|
2255
|
+
let skipped = 0;
|
|
2256
|
+
// Look up entity ID
|
|
2257
|
+
const entityLookupSQL = `SELECT ID FROM ${this.qs(schema, 'Entity')} WHERE SchemaName = '${schemaName}' AND BaseTable = '${tableName}'`;
|
|
2258
|
+
const entityResult = await this.runQuery(pool, entityLookupSQL);
|
|
2259
|
+
if (entityResult.recordset.length === 0) {
|
|
2260
|
+
logStatus(` ⚠️ Entity not found for ${schemaName}.${tableName} — skipping soft value-lists`);
|
|
2261
|
+
return { applied, skipped };
|
|
2262
|
+
}
|
|
2263
|
+
const entityId = entityResult.recordset[0].ID;
|
|
2264
|
+
for (const fieldCfg of fields) {
|
|
2265
|
+
const fieldResult = await this.applyValueListForField(pool, schema, entityId, schemaName, tableName, fieldCfg, allEntityFieldValues);
|
|
2266
|
+
if (fieldResult === 'applied')
|
|
2267
|
+
applied++;
|
|
2268
|
+
else
|
|
2269
|
+
skipped++;
|
|
2270
|
+
}
|
|
2271
|
+
return { applied, skipped };
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Applies value-list config for a single field. Returns 'applied' or 'skipped'.
|
|
2275
|
+
*/
|
|
2276
|
+
async applyValueListForField(pool, schema, entityId, schemaName, tableName, fieldCfg, allEntityFieldValues) {
|
|
2277
|
+
// Look up EntityField ID
|
|
2278
|
+
const efSQL = `SELECT ID, ValueListType FROM ${this.qs(schema, 'EntityField')} WHERE EntityID = '${entityId}' AND Name = '${fieldCfg.FieldName}'`;
|
|
2279
|
+
const efResult = await this.runQuery(pool, efSQL);
|
|
2280
|
+
if (efResult.recordset.length === 0) {
|
|
2281
|
+
logStatus(` ⚠️ Field ${fieldCfg.FieldName} not found on ${schemaName}.${tableName} — skipping`);
|
|
2282
|
+
return 'skipped';
|
|
2283
|
+
}
|
|
2284
|
+
const efRecord = efResult.recordset[0];
|
|
2285
|
+
const entityFieldID = efRecord.ID;
|
|
2286
|
+
// CHECK constraint is authoritative — if EntityFieldValue rows already exist, skip
|
|
2287
|
+
const existing = allEntityFieldValues.filter((efv) => String(efv.EntityFieldID) === String(entityFieldID));
|
|
2288
|
+
const currentValueListType = (efRecord.ValueListType || '').trim().toLowerCase();
|
|
2289
|
+
if (existing.length > 0 && currentValueListType !== 'none' && currentValueListType !== '') {
|
|
2290
|
+
return 'skipped';
|
|
2291
|
+
}
|
|
2292
|
+
// Apply: sync values and set ValueListType
|
|
2293
|
+
const sortedValues = [...fieldCfg.PossibleValues].sort();
|
|
2294
|
+
await this.syncEntityFieldValues(pool, entityFieldID, sortedValues, allEntityFieldValues);
|
|
2295
|
+
// Update ValueListType
|
|
2296
|
+
const newType = fieldCfg.ValueListType;
|
|
2297
|
+
if (currentValueListType !== newType.toLowerCase()) {
|
|
2298
|
+
const updateSQL = `UPDATE ${this.qs(schema, 'EntityField')} SET ValueListType='${newType}' WHERE ID='${entityFieldID}'`;
|
|
2299
|
+
await this.LogSQLAndExecute(pool, updateSQL, `Set soft ValueListType='${newType}' for ${schemaName}.${tableName}.${fieldCfg.FieldName}`);
|
|
2300
|
+
}
|
|
2301
|
+
logStatus(` ✓ Applied soft value-list for ${tableName}.${fieldCfg.FieldName} (${sortedValues.length} values, type=${newType})`);
|
|
2302
|
+
return 'applied';
|
|
2303
|
+
}
|
|
2101
2304
|
/**
|
|
2102
2305
|
* This method ensures that the __mj_CreatedAt and __mj_UpdatedAt fields exist in each entity that has TrackRecordChanges set to true. If the fields do not exist, they are created.
|
|
2103
2306
|
* If the fields exist but have incorrect default values, the default values are updated. The default value that is to be used for these special fields is GETUTCDATE() which is the
|
|
@@ -2408,6 +2611,7 @@ export class ManageMetadataBase {
|
|
|
2408
2611
|
${this.qi('AutoIncrement')},
|
|
2409
2612
|
${this.qi('AllowUpdateAPI')},
|
|
2410
2613
|
${this.qi('IsVirtual')},
|
|
2614
|
+
${this.qi('IsComputed')},
|
|
2411
2615
|
${this.qi('RelatedEntityID')},
|
|
2412
2616
|
${this.qi('RelatedEntityFieldName')},
|
|
2413
2617
|
${this.qi('IsNameField')},
|
|
@@ -2437,6 +2641,7 @@ export class ManageMetadataBase {
|
|
|
2437
2641
|
${this.boolLit(n.AutoIncrement)},
|
|
2438
2642
|
${this.boolLit(n.AllowUpdateAPI)},
|
|
2439
2643
|
${this.boolLit(n.IsVirtual)},
|
|
2644
|
+
${this.boolLit(n.IsComputed)},
|
|
2440
2645
|
${n.RelatedEntityID && n.RelatedEntityID.length > 0 ? `'${n.RelatedEntityID}'` : 'NULL'},
|
|
2441
2646
|
${n.RelatedEntityFieldName && n.RelatedEntityFieldName.length > 0 ? `'${n.RelatedEntityFieldName}'` : 'NULL'},
|
|
2442
2647
|
${this.boolLit(n.IsNameField !== null ? n.IsNameField : false)},
|
|
@@ -3806,6 +4011,8 @@ export class ManageMetadataBase {
|
|
|
3806
4011
|
if (fieldAnalysis) {
|
|
3807
4012
|
await this.applySmartFieldIdentification(pool, {
|
|
3808
4013
|
ID: entity.ID,
|
|
4014
|
+
Name: entity.Name,
|
|
4015
|
+
SchemaName: entityRecord.SchemaName,
|
|
3809
4016
|
AllowUserSearchAPI: entityRecord.AllowUserSearchAPI,
|
|
3810
4017
|
AutoUpdateAllowUserSearchAPI: entityRecord.AutoUpdateAllowUserSearchAPI,
|
|
3811
4018
|
FullTextSearchEnabled: entityRecord.FullTextSearchEnabled,
|
|
@@ -3905,7 +4112,7 @@ export class ManageMetadataBase {
|
|
|
3905
4112
|
const sqlStatements = [];
|
|
3906
4113
|
this.applyNameFieldUpdates(sqlStatements, fields, result);
|
|
3907
4114
|
this.applyDefaultInViewUpdates(sqlStatements, fields, result);
|
|
3908
|
-
this.applySearchableFieldUpdates(sqlStatements, fields, result);
|
|
4115
|
+
this.applySearchableFieldUpdates(sqlStatements, fields, result, entity);
|
|
3909
4116
|
this.applySearchPredicateUpdates(sqlStatements, fields, result);
|
|
3910
4117
|
this.applyEntitySearchConfig(sqlStatements, entity, result);
|
|
3911
4118
|
if (configInfo.advancedGeneration?.allowFullTextSearchAutoUpdate) {
|
|
@@ -3913,9 +4120,8 @@ export class ManageMetadataBase {
|
|
|
3913
4120
|
}
|
|
3914
4121
|
// Execute all updates in one batch
|
|
3915
4122
|
if (sqlStatements.length > 0) {
|
|
3916
|
-
const combinedSQL = sqlStatements.join('\n');
|
|
3917
4123
|
try {
|
|
3918
|
-
await this.
|
|
4124
|
+
await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set field properties for entity`, false);
|
|
3919
4125
|
}
|
|
3920
4126
|
catch (ex) {
|
|
3921
4127
|
logError('Error executing combined smart field SQL: ', ex);
|
|
@@ -3963,9 +4169,17 @@ export class ManageMetadataBase {
|
|
|
3963
4169
|
}
|
|
3964
4170
|
}
|
|
3965
4171
|
/**
|
|
3966
|
-
* Generate SQL UPDATEs for IncludeInUserSearchAPI on the identified searchable fields
|
|
4172
|
+
* Generate SQL UPDATEs for IncludeInUserSearchAPI on the identified searchable fields.
|
|
4173
|
+
*
|
|
4174
|
+
* Guardrails: even if the SmartFieldIdentificationResult flags a field as searchable,
|
|
4175
|
+
* we refuse to enable IncludeInUserSearchAPI when the field is a primary key, a
|
|
4176
|
+
* non-text type (LIKE forces an implicit per-row CONVERT and never seeks an index),
|
|
4177
|
+
* or an unbounded text type whose parent entity is not FTX-enabled (the LIKE path
|
|
4178
|
+
* cannot seek MAX/ntext/text columns; FTX is the right tool there). These are
|
|
4179
|
+
* always-wrong cases — they cause the data provider to emit unindexed scans on
|
|
4180
|
+
* every search.
|
|
3967
4181
|
*/
|
|
3968
|
-
applySearchableFieldUpdates(sqlStatements, fields, result) {
|
|
4182
|
+
applySearchableFieldUpdates(sqlStatements, fields, result, entity) {
|
|
3969
4183
|
if (!result.searchableFields || result.searchableFields.length === 0) {
|
|
3970
4184
|
return;
|
|
3971
4185
|
}
|
|
@@ -3974,7 +4188,13 @@ export class ManageMetadataBase {
|
|
|
3974
4188
|
if (missingSearchableFields.length > 0) {
|
|
3975
4189
|
logError(`Smart field identification returned invalid searchableFields: ${missingSearchableFields.join(', ')} not found in entity`);
|
|
3976
4190
|
}
|
|
4191
|
+
const ftxEnabled = !!entity?.FullTextSearchEnabled;
|
|
3977
4192
|
for (const field of searchableFields) {
|
|
4193
|
+
if (!this.isFieldEligibleForUserSearch(field, ftxEnabled)) {
|
|
4194
|
+
// Silently skip — the LLM proposed a field we cannot honor without
|
|
4195
|
+
// creating an unindexed scan. This is a guardrail, not a bug.
|
|
4196
|
+
continue;
|
|
4197
|
+
}
|
|
3978
4198
|
if (!field.IncludeInUserSearchAPI) {
|
|
3979
4199
|
sqlStatements.push(`
|
|
3980
4200
|
UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
@@ -3985,6 +4205,25 @@ export class ManageMetadataBase {
|
|
|
3985
4205
|
}
|
|
3986
4206
|
}
|
|
3987
4207
|
}
|
|
4208
|
+
/**
|
|
4209
|
+
* Returns true if the field is a sensible target for LIKE-based user search.
|
|
4210
|
+
* Mirrors the runtime guard in GenericDatabaseProvider.isTextSearchableType /
|
|
4211
|
+
* the Phase 1 hygiene migration so CodeGen stops re-introducing invalid flags.
|
|
4212
|
+
*/
|
|
4213
|
+
isFieldEligibleForUserSearch(field, ftxEnabled) {
|
|
4214
|
+
if (field.IsPrimaryKey === true)
|
|
4215
|
+
return false;
|
|
4216
|
+
const type = (field.Type ?? '').toLowerCase();
|
|
4217
|
+
// Non-text types are never appropriate for substring search.
|
|
4218
|
+
const textTypes = new Set(['nvarchar', 'varchar', 'char', 'nchar']);
|
|
4219
|
+
if (!textTypes.has(type))
|
|
4220
|
+
return false;
|
|
4221
|
+
// Unbounded text on a non-FTX entity cannot be index-seeked.
|
|
4222
|
+
const length = field.Length;
|
|
4223
|
+
if (length === -1 && !ftxEnabled)
|
|
4224
|
+
return false;
|
|
4225
|
+
return true;
|
|
4226
|
+
}
|
|
3988
4227
|
/**
|
|
3989
4228
|
* Generate SQL UPDATEs for UserSearchPredicateAPI on each searchable field
|
|
3990
4229
|
*/
|
|
@@ -4013,7 +4252,14 @@ export class ManageMetadataBase {
|
|
|
4013
4252
|
}
|
|
4014
4253
|
}
|
|
4015
4254
|
/**
|
|
4016
|
-
* Generate SQL UPDATEs for entity-level AllowUserSearchAPI
|
|
4255
|
+
* Generate SQL UPDATEs for entity-level AllowUserSearchAPI.
|
|
4256
|
+
*
|
|
4257
|
+
* Guardrail: even if the LLM proposes enabling AllowUserSearchAPI on an entity
|
|
4258
|
+
* that looks like an audit/log/run-history table by name, refuse — those tables
|
|
4259
|
+
* grow unboundedly, the LIKE fan-out across them dominates global-search latency,
|
|
4260
|
+
* and they're virtually never the right target for a user-facing search box.
|
|
4261
|
+
* The team can still enable search on such an entity by setting
|
|
4262
|
+
* AutoUpdateAllowUserSearchAPI=0 and flipping the flag manually.
|
|
4017
4263
|
*/
|
|
4018
4264
|
applyEntitySearchConfig(sqlStatements, entity, result) {
|
|
4019
4265
|
if (result.allowUserSearch == null || !entity.AutoUpdateAllowUserSearchAPI) {
|
|
@@ -4021,6 +4267,11 @@ export class ManageMetadataBase {
|
|
|
4021
4267
|
}
|
|
4022
4268
|
const newValue = result.allowUserSearch;
|
|
4023
4269
|
const currentValue = !!entity.AllowUserSearchAPI;
|
|
4270
|
+
// If the LLM is proposing to ENABLE search on a log/audit/run-history-style
|
|
4271
|
+
// entity, drop the proposal. We never block a proposal to DISABLE search.
|
|
4272
|
+
if (newValue && !currentValue && this.isLikelyLogOrAuditEntity(entity.Name, entity.SchemaName)) {
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
4024
4275
|
if (newValue !== currentValue) {
|
|
4025
4276
|
sqlStatements.push(`
|
|
4026
4277
|
UPDATE ${this.qs(mj_core_schema(), 'Entity')}
|
|
@@ -4030,6 +4281,31 @@ export class ManageMetadataBase {
|
|
|
4030
4281
|
`);
|
|
4031
4282
|
}
|
|
4032
4283
|
}
|
|
4284
|
+
/**
|
|
4285
|
+
* Heuristic: does the entity name match the shape of an audit / log /
|
|
4286
|
+
* run-history / change-tracking table? These are the entities that drive
|
|
4287
|
+
* the most LIKE-scan time and almost never belong in global search.
|
|
4288
|
+
*/
|
|
4289
|
+
isLikelyLogOrAuditEntity(name, _schemaName) {
|
|
4290
|
+
if (!name)
|
|
4291
|
+
return false;
|
|
4292
|
+
const n = name.trim();
|
|
4293
|
+
// Common patterns: ends in "Logs"/"Log", ends in "Runs"/"Run History",
|
|
4294
|
+
// contains "Audit", or matches MJ's "Record Changes" naming.
|
|
4295
|
+
if (/\bLogs?$/i.test(n))
|
|
4296
|
+
return true;
|
|
4297
|
+
if (/\bAudit\b/i.test(n))
|
|
4298
|
+
return true;
|
|
4299
|
+
if (/\bRecord Changes?$/i.test(n))
|
|
4300
|
+
return true;
|
|
4301
|
+
if (/\bRuns?$/i.test(n) && !/Test Runs?$/i.test(n))
|
|
4302
|
+
return true;
|
|
4303
|
+
if (/\bRun (History|Steps|Messages)$/i.test(n))
|
|
4304
|
+
return true;
|
|
4305
|
+
if (/\bExecution Logs?$/i.test(n))
|
|
4306
|
+
return true;
|
|
4307
|
+
return false;
|
|
4308
|
+
}
|
|
4033
4309
|
/**
|
|
4034
4310
|
* Generate SQL UPDATEs for entity-level and field-level FullTextSearch configuration
|
|
4035
4311
|
*/
|
|
@@ -4238,7 +4514,7 @@ WHERE
|
|
|
4238
4514
|
}
|
|
4239
4515
|
if (sqlStatements.length > 0) {
|
|
4240
4516
|
try {
|
|
4241
|
-
await this.
|
|
4517
|
+
await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set categories for ${sqlStatements.length} fields`, false);
|
|
4242
4518
|
}
|
|
4243
4519
|
catch (ex) {
|
|
4244
4520
|
logError('Error Applying Field Categories', ex);
|
|
@@ -4259,8 +4535,8 @@ WHERE
|
|
|
4259
4535
|
const escapedIcon = entityIcon.replace(/'/g, "''");
|
|
4260
4536
|
const updateSQL = `
|
|
4261
4537
|
UPDATE ${this.qs(mj_core_schema(), 'Entity')}
|
|
4262
|
-
SET Icon = '${escapedIcon}',
|
|
4263
|
-
WHERE ID = '${entityId}'
|
|
4538
|
+
SET ${this.qi('Icon')} = '${escapedIcon}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
4539
|
+
WHERE ${this.qi('ID')} = '${entityId}'
|
|
4264
4540
|
`;
|
|
4265
4541
|
try {
|
|
4266
4542
|
await this.LogSQLAndExecute(pool, updateSQL, `Set entity icon to ${entityIcon}`, false);
|
|
@@ -4280,14 +4556,14 @@ WHERE
|
|
|
4280
4556
|
return;
|
|
4281
4557
|
const infoJSON = JSON.stringify(categoryInfo).replace(/'/g, "''");
|
|
4282
4558
|
// Upsert FieldCategoryInfo (new format)
|
|
4283
|
-
const checkNewSQL = `SELECT ID FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryInfo'`;
|
|
4559
|
+
const checkNewSQL = `SELECT ${this.qi('ID')} FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'`;
|
|
4284
4560
|
const existingNew = await this.runQuery(pool, checkNewSQL);
|
|
4285
4561
|
if (existingNew.recordset.length > 0) {
|
|
4286
4562
|
try {
|
|
4287
4563
|
await this.LogSQLAndExecute(pool, `
|
|
4288
4564
|
UPDATE ${this.qs(mj_core_schema(), 'EntitySetting')}
|
|
4289
|
-
SET Value = '${infoJSON}',
|
|
4290
|
-
WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryInfo'
|
|
4565
|
+
SET ${this.qi('Value')} = '${infoJSON}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
4566
|
+
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'
|
|
4291
4567
|
`, `Update FieldCategoryInfo setting for entity`, false);
|
|
4292
4568
|
}
|
|
4293
4569
|
catch (ex) {
|
|
@@ -4298,7 +4574,7 @@ WHERE
|
|
|
4298
4574
|
const newId = uuidv4();
|
|
4299
4575
|
try {
|
|
4300
4576
|
await this.LogSQLAndExecute(pool, `
|
|
4301
|
-
INSERT INTO ${this.qs(mj_core_schema(), 'EntitySetting')} (ID, EntityID, Name, Value,
|
|
4577
|
+
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)})
|
|
4302
4578
|
VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}', ${this.utcNow()}, ${this.utcNow()})
|
|
4303
4579
|
`, `Insert FieldCategoryInfo setting for entity`, false);
|
|
4304
4580
|
}
|
|
@@ -4314,14 +4590,14 @@ WHERE
|
|
|
4314
4590
|
}
|
|
4315
4591
|
}
|
|
4316
4592
|
const iconsJSON = JSON.stringify(iconsOnly).replace(/'/g, "''");
|
|
4317
|
-
const checkLegacySQL = `SELECT ID FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryIcons'`;
|
|
4593
|
+
const checkLegacySQL = `SELECT ${this.qi('ID')} FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'`;
|
|
4318
4594
|
const existingLegacy = await this.runQuery(pool, checkLegacySQL);
|
|
4319
4595
|
if (existingLegacy.recordset.length > 0) {
|
|
4320
4596
|
try {
|
|
4321
4597
|
await this.LogSQLAndExecute(pool, `
|
|
4322
4598
|
UPDATE ${this.qs(mj_core_schema(), 'EntitySetting')}
|
|
4323
|
-
SET Value = '${iconsJSON}',
|
|
4324
|
-
WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryIcons'
|
|
4599
|
+
SET ${this.qi('Value')} = '${iconsJSON}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
4600
|
+
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'
|
|
4325
4601
|
`, `Update FieldCategoryIcons setting (legacy)`, false);
|
|
4326
4602
|
}
|
|
4327
4603
|
catch (ex) {
|
|
@@ -4332,7 +4608,7 @@ WHERE
|
|
|
4332
4608
|
const newId = uuidv4();
|
|
4333
4609
|
try {
|
|
4334
4610
|
await this.LogSQLAndExecute(pool, `
|
|
4335
|
-
INSERT INTO ${this.qs(mj_core_schema(), 'EntitySetting')} (ID, EntityID, Name, Value,
|
|
4611
|
+
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)})
|
|
4336
4612
|
VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}', ${this.utcNow()}, ${this.utcNow()})
|
|
4337
4613
|
`, `Insert FieldCategoryIcons setting (legacy)`, false);
|
|
4338
4614
|
}
|
|
@@ -4348,8 +4624,8 @@ WHERE
|
|
|
4348
4624
|
async applyEntityImportance(pool, entityId, importance) {
|
|
4349
4625
|
const updateSQL = `
|
|
4350
4626
|
UPDATE ${this.qs(mj_core_schema(), 'ApplicationEntity')}
|
|
4351
|
-
SET DefaultForNewUser = ${this.boolLit(importance.defaultForNewUser)},
|
|
4352
|
-
WHERE EntityID = '${entityId}'
|
|
4627
|
+
SET ${this.qi('DefaultForNewUser')} = ${this.boolLit(importance.defaultForNewUser)}, ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
4628
|
+
WHERE ${this.qi('EntityID')} = '${entityId}'
|
|
4353
4629
|
`;
|
|
4354
4630
|
try {
|
|
4355
4631
|
await this.LogSQLAndExecute(pool, updateSQL, `Set DefaultForNewUser=${importance.defaultForNewUser} for NEW entity (category: ${importance.entityCategory}, confidence: ${importance.confidence})`, false);
|
|
@@ -4373,5 +4649,33 @@ WHERE
|
|
|
4373
4649
|
async LogSQLAndExecute(pool, query, description, isRecurringScript = false, includeBatchSeparator = false, batchSeparator = 'GO') {
|
|
4374
4650
|
return await SQLLogging.LogSQLAndExecute(pool, this.qsql(query), description, isRecurringScript, includeBatchSeparator, batchSeparator);
|
|
4375
4651
|
}
|
|
4652
|
+
/**
|
|
4653
|
+
* Logs and executes a sequence of SQL statements as a single batch.
|
|
4654
|
+
*
|
|
4655
|
+
* Each statement is dialect-quoted (via `qsql`), has its trailing
|
|
4656
|
+
* whitespace/`;` trimmed, then gets a single `;` re-appended. Statements
|
|
4657
|
+
* are joined with `\n` and sent through `LogSQLAndExecute` so PG's strict
|
|
4658
|
+
* parser sees properly-terminated boundaries between statements.
|
|
4659
|
+
*
|
|
4660
|
+
* Use this instead of `statements.join('\n')` followed by
|
|
4661
|
+
* `LogSQLAndExecute(...)`. SQL Server tolerated the unterminated form;
|
|
4662
|
+
* PG does not. SS still accepts the explicitly-terminated batch, so
|
|
4663
|
+
* this is a no-cost upgrade for both dialects.
|
|
4664
|
+
*
|
|
4665
|
+
* Empty / whitespace-only statements are filtered out. Returns
|
|
4666
|
+
* `undefined` (no execution) when nothing remains.
|
|
4667
|
+
*/
|
|
4668
|
+
async LogSQLBatchAndExecute(pool, statements, description, isRecurringScript = false, includeBatchSeparator = false, batchSeparator = 'GO') {
|
|
4669
|
+
const terminated = [];
|
|
4670
|
+
for (const s of statements) {
|
|
4671
|
+
const trimmed = (s ?? '').replace(/[\s;]+$/g, '');
|
|
4672
|
+
if (trimmed.length === 0)
|
|
4673
|
+
continue;
|
|
4674
|
+
terminated.push(`${trimmed};`);
|
|
4675
|
+
}
|
|
4676
|
+
if (terminated.length === 0)
|
|
4677
|
+
return undefined;
|
|
4678
|
+
return await this.LogSQLAndExecute(pool, terminated.join('\n'), description, isRecurringScript, includeBatchSeparator, batchSeparator);
|
|
4679
|
+
}
|
|
4376
4680
|
}
|
|
4377
4681
|
//# sourceMappingURL=manage-metadata.js.map
|