@memberjunction/codegen-lib 6.2.0-edge.0 → 6.2.0-edge.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Angular/angular-codegen.d.ts +6 -0
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +26 -14
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Config/config.d.ts +45 -2
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +116 -39
- package/dist/Config/config.js.map +1 -1
- package/dist/Config/db-connection.d.ts +4 -0
- package/dist/Config/db-connection.d.ts.map +1 -1
- package/dist/Config/db-connection.js +11 -3
- package/dist/Config/db-connection.js.map +1 -1
- package/dist/Config/pg-connection.d.ts +2 -0
- package/dist/Config/pg-connection.d.ts.map +1 -1
- package/dist/Config/pg-connection.js +5 -1
- package/dist/Config/pg-connection.js.map +1 -1
- package/dist/Config/schema-output.d.ts +8 -0
- package/dist/Config/schema-output.d.ts.map +1 -1
- package/dist/Config/schema-output.js +19 -7
- package/dist/Config/schema-output.js.map +1 -1
- package/dist/Database/__tests__/exclude-schema-permissions.test.js +3 -3
- package/dist/Database/codeGenDatabaseProvider.d.ts +16 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.js +37 -9
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
- package/dist/Database/dbSchema.d.ts +4 -0
- package/dist/Database/dbSchema.d.ts.map +1 -1
- package/dist/Database/dbSchema.js +17 -9
- package/dist/Database/dbSchema.js.map +1 -1
- package/dist/Database/entity-field-change-tracking.d.ts +4 -0
- package/dist/Database/entity-field-change-tracking.d.ts.map +1 -1
- package/dist/Database/entity-field-change-tracking.js +12 -4
- package/dist/Database/entity-field-change-tracking.js.map +1 -1
- package/dist/Database/exclude-tables.d.ts +2 -0
- package/dist/Database/exclude-tables.d.ts.map +1 -1
- package/dist/Database/exclude-tables.js +5 -1
- package/dist/Database/exclude-tables.js.map +1 -1
- package/dist/Database/field-metadata-lock.d.ts +8 -6
- package/dist/Database/field-metadata-lock.d.ts.map +1 -1
- package/dist/Database/field-metadata-lock.js +29 -25
- package/dist/Database/field-metadata-lock.js.map +1 -1
- package/dist/Database/heal-schema-params.d.ts +14 -2
- package/dist/Database/heal-schema-params.d.ts.map +1 -1
- package/dist/Database/heal-schema-params.js +21 -5
- package/dist/Database/heal-schema-params.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +119 -3
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +529 -364
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/materializationAnalysis.d.ts +61 -2
- package/dist/Database/materializationAnalysis.d.ts.map +1 -1
- package/dist/Database/materializationAnalysis.js +43 -23
- package/dist/Database/materializationAnalysis.js.map +1 -1
- package/dist/Database/materializationBroadRender.d.ts +5 -3
- package/dist/Database/materializationBroadRender.d.ts.map +1 -1
- package/dist/Database/materializationBroadRender.js +17 -13
- package/dist/Database/materializationBroadRender.js.map +1 -1
- package/dist/Database/materializationDrift.d.ts +2 -0
- package/dist/Database/materializationDrift.d.ts.map +1 -1
- package/dist/Database/materializationDrift.js +5 -1
- package/dist/Database/materializationDrift.js.map +1 -1
- package/dist/Database/materializationParamClassifier.d.ts +17 -2
- package/dist/Database/materializationParamClassifier.d.ts.map +1 -1
- package/dist/Database/materializationParamClassifier.js +22 -10
- package/dist/Database/materializationParamClassifier.js.map +1 -1
- package/dist/Database/materializationParamVerifier.d.ts +2 -0
- package/dist/Database/materializationParamVerifier.d.ts.map +1 -1
- package/dist/Database/materializationParamVerifier.js +18 -14
- package/dist/Database/materializationParamVerifier.js.map +1 -1
- package/dist/Database/materializationSqlAst.d.ts +19 -1
- package/dist/Database/materializationSqlAst.d.ts.map +1 -1
- package/dist/Database/materializationSqlAst.js +53 -17
- package/dist/Database/materializationSqlAst.js.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +30 -30
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/postgresql/metadataSupportObjects.d.ts +2 -0
- package/dist/Database/providers/postgresql/metadataSupportObjects.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/metadataSupportObjects.js +5 -1
- package/dist/Database/providers/postgresql/metadataSupportObjects.js.map +1 -1
- package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts +10 -0
- package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/viewDependencyCapture.js +25 -5
- package/dist/Database/providers/postgresql/viewDependencyCapture.js.map +1 -1
- package/dist/Database/providers/postgresql/viewFallback.d.ts +11 -7
- package/dist/Database/providers/postgresql/viewFallback.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/viewFallback.js +18 -12
- package/dist/Database/providers/postgresql/viewFallback.js.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +13 -13
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
- package/dist/Database/reconcileFieldLevelSecurity.d.ts +2 -0
- package/dist/Database/reconcileFieldLevelSecurity.d.ts.map +1 -1
- package/dist/Database/reconcileFieldLevelSecurity.js +5 -1
- package/dist/Database/reconcileFieldLevelSecurity.js.map +1 -1
- package/dist/Database/reorder-columns.d.ts +2 -0
- package/dist/Database/reorder-columns.d.ts.map +1 -1
- package/dist/Database/reorder-columns.js +5 -1
- package/dist/Database/reorder-columns.js.map +1 -1
- package/dist/Database/schema-filters.d.ts +6 -0
- package/dist/Database/schema-filters.d.ts.map +1 -1
- package/dist/Database/schema-filters.js +17 -5
- package/dist/Database/schema-filters.js.map +1 -1
- package/dist/Database/schema-scope.d.ts +4 -0
- package/dist/Database/schema-scope.d.ts.map +1 -1
- package/dist/Database/schema-scope.js +13 -5
- package/dist/Database/schema-scope.js.map +1 -1
- package/dist/Database/search-guardrails.d.ts +42 -0
- package/dist/Database/search-guardrails.d.ts.map +1 -1
- package/dist/Database/search-guardrails.js +57 -17
- package/dist/Database/search-guardrails.js.map +1 -1
- package/dist/Database/sql.d.ts +20 -0
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +72 -32
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +92 -0
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +197 -113
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/EntityNameScanner/EntityNameScanner.d.ts +16 -0
- package/dist/EntityNameScanner/EntityNameScanner.d.ts.map +1 -1
- package/dist/EntityNameScanner/EntityNameScanner.js +47 -15
- package/dist/EntityNameScanner/EntityNameScanner.js.map +1 -1
- package/dist/EntityNameScanner/HtmlEntityNameScanner.d.ts +6 -0
- package/dist/EntityNameScanner/HtmlEntityNameScanner.d.ts.map +1 -1
- package/dist/EntityNameScanner/HtmlEntityNameScanner.js +19 -7
- package/dist/EntityNameScanner/HtmlEntityNameScanner.js.map +1 -1
- package/dist/EntityNameScanner/MetadataNameScanner.d.ts +6 -0
- package/dist/EntityNameScanner/MetadataNameScanner.d.ts.map +1 -1
- package/dist/EntityNameScanner/MetadataNameScanner.js +19 -7
- package/dist/EntityNameScanner/MetadataNameScanner.js.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +6 -0
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.js +16 -4
- package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.d.ts +4 -0
- package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +11 -3
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.d.ts +48 -2
- package/dist/Misc/advanced_generation.d.ts.map +1 -1
- package/dist/Misc/advanced_generation.js +53 -15
- package/dist/Misc/advanced_generation.js.map +1 -1
- package/dist/Misc/codegen-reporter.d.ts +53 -0
- package/dist/Misc/codegen-reporter.d.ts.map +1 -1
- package/dist/Misc/codegen-reporter.js +106 -22
- package/dist/Misc/codegen-reporter.js.map +1 -1
- package/dist/Misc/createNewUser.d.ts +6 -0
- package/dist/Misc/createNewUser.d.ts.map +1 -1
- package/dist/Misc/createNewUser.js +5 -1
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts +8 -0
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +52 -36
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/externalTypeMapping.d.ts +2 -0
- package/dist/Misc/externalTypeMapping.d.ts.map +1 -1
- package/dist/Misc/externalTypeMapping.js +5 -1
- package/dist/Misc/externalTypeMapping.js.map +1 -1
- package/dist/Misc/file-write.d.ts +2 -0
- package/dist/Misc/file-write.d.ts.map +1 -1
- package/dist/Misc/file-write.js +5 -1
- package/dist/Misc/file-write.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts +10 -0
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +46 -26
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/remote_operations_codegen.d.ts +12 -0
- package/dist/Misc/remote_operations_codegen.d.ts.map +1 -1
- package/dist/Misc/remote_operations_codegen.js +21 -5
- package/dist/Misc/remote_operations_codegen.js.map +1 -1
- package/dist/Misc/runCommand.d.ts +6 -0
- package/dist/Misc/runCommand.d.ts.map +1 -1
- package/dist/Misc/runCommand.js +16 -4
- package/dist/Misc/runCommand.js.map +1 -1
- package/dist/Misc/schema-emit.d.ts +28 -0
- package/dist/Misc/schema-emit.d.ts.map +1 -1
- package/dist/Misc/schema-emit.js +75 -23
- package/dist/Misc/schema-emit.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +31 -9
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +95 -51
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/Misc/sql_text.d.ts +4 -0
- package/dist/Misc/sql_text.d.ts.map +1 -1
- package/dist/Misc/sql_text.js +10 -2
- package/dist/Misc/sql_text.js.map +1 -1
- package/dist/Misc/status_logging.d.ts +36 -0
- package/dist/Misc/status_logging.d.ts.map +1 -1
- package/dist/Misc/status_logging.js +95 -23
- package/dist/Misc/status_logging.js.map +1 -1
- package/dist/Misc/system_integrity.js +4 -4
- package/dist/Misc/system_integrity.js.map +1 -1
- package/dist/Misc/temp_batch_file.d.ts +12 -0
- package/dist/Misc/temp_batch_file.d.ts.map +1 -1
- package/dist/Misc/temp_batch_file.js +35 -11
- package/dist/Misc/temp_batch_file.js.map +1 -1
- package/dist/Misc/util.d.ts +33 -0
- package/dist/Misc/util.d.ts.map +1 -1
- package/dist/Misc/util.js +53 -13
- package/dist/Misc/util.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +11 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +9 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/runCodeGen.d.ts +6 -0
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +150 -138
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +29 -29
|
@@ -1,54 +1,54 @@
|
|
|
1
1
|
import { SQLServerDialect, PostgreSQLDialect } from '@memberjunction/sql-dialect';
|
|
2
2
|
import { CodeGenDatabaseProvider } from './codeGenDatabaseProvider.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { AnalyzeQueryForMaterialization, DetectAggregationKeyColumns, DetectAdditiveMeasures, MATERIALIZATION_SURROGATE_COLUMN } from './materializationAnalysis.js';
|
|
4
|
+
import { EvaluateMaterializationDrift } from './materializationDrift.js';
|
|
5
|
+
import { ClassifyQueryParameters, BuildHeldValues } from './materializationParamClassifier.js';
|
|
6
|
+
import { BuildBroadRowFilterSQL } from './materializationBroadRender.js';
|
|
7
7
|
import { QueryParameterProcessor } from '@memberjunction/query-processor';
|
|
8
8
|
// Side-effect import — registers `SQLServerCodeGenProvider` with `MJGlobal.ClassFactory`
|
|
9
9
|
// under the `'sqlserver'` key via its `@RegisterClass` decorator. Without this import,
|
|
10
10
|
// `ClassFactory.CreateInstance(CodeGenDatabaseProvider, 'sqlserver')` returns nothing
|
|
11
11
|
// and the SS code path silently fails.
|
|
12
12
|
import './providers/sqlserver/SQLServerCodeGenProvider.js';
|
|
13
|
-
import { configInfo, currentWorkingDirectory,
|
|
13
|
+
import { configInfo, currentWorkingDirectory, DbPlatform, GetSettingValue, MjCoreSchema, OutputDir } from '../Config/config.js';
|
|
14
14
|
import { CodeNameFromString, EntityFieldInfo, EntityInfo, ExternalDataSourceReadRouter, ExtractActualDefaultValue, LogError, LogStatus, Metadata, RunQuerySQLFilterManager, SeverityType } from "@memberjunction/core";
|
|
15
15
|
import { MJEntityFieldSchema } from "@memberjunction/core-entities";
|
|
16
|
-
import { logError,
|
|
16
|
+
import { logError, LogMessage, logStatus, StartSpinner, UpdateSpinner, SucceedSpinner } from "../Misc/status_logging.js";
|
|
17
17
|
import { SQLUtilityBase } from "./sql.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { AdvancedGeneration,
|
|
18
|
+
import { ApplyIncludeSchemaScope } from "./schema-scope.js";
|
|
19
|
+
import { BuildHealSchemaRoutineParams, GetAuthoredExcludeSchemas, SnapshotAuthoredExcludeSchemas } from "./heal-schema-params.js";
|
|
20
|
+
import { AdvancedGeneration, IsPlausibleEntityName } from "../Misc/advanced_generation.js";
|
|
21
21
|
import { CodeGenReporter } from "../Misc/codegen-reporter.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { ApplySearchableFieldsCap, DefaultPredicateFor, EntityLevelEnableBlockedReason, IsNarrativeFieldName, MAX_SEARCHABLE_FIELDS_PER_ENTITY, NAME_LIKE_FIELD_NAMES, NormalizePredicate, NormalizeSmartFieldResultShape, } from "./search-guardrails.js";
|
|
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
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 {
|
|
30
|
-
import {
|
|
29
|
+
import { CanonicalJSONStringify, DeepEqualJSON } from "../Misc/util.js";
|
|
30
|
+
import { TrimTrailingStatementTerminators } from "../Misc/sql_text.js";
|
|
31
31
|
import { SQLLogging } from "../Misc/sql_logging.js";
|
|
32
|
-
import {
|
|
32
|
+
import { ComputeFieldMetadataUpdate } from "./field-metadata-lock.js";
|
|
33
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';
|
|
34
|
+
export { TRACKED_FIELD_COLUMNS, DISPLAYNAME_REOPEN_REASONS, TYPE_REOPEN_REASONS, DiffEntityFieldSnapshots, diffEntityFieldSnapshots, } from './entity-field-change-tracking.js';
|
|
35
35
|
export class ValidatorResult {
|
|
36
36
|
constructor() {
|
|
37
|
-
this.entityName = "";
|
|
38
|
-
this.sourceCheckConstraint = "";
|
|
39
|
-
this.functionText = "";
|
|
40
|
-
this.functionName = "";
|
|
41
|
-
this.functionDescription = "";
|
|
37
|
+
this.entityName = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
|
|
38
|
+
this.sourceCheckConstraint = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
|
|
39
|
+
this.functionText = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
|
|
40
|
+
this.functionName = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
|
|
41
|
+
this.functionDescription = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
|
|
42
42
|
/**
|
|
43
43
|
* The ID value in the Generated Codes entity that was created for this validator.
|
|
44
44
|
*/
|
|
45
|
-
this.generatedCodeId = "";
|
|
45
|
+
this.generatedCodeId = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
|
|
46
46
|
/**
|
|
47
47
|
* The ID for the AI Model that was used to generate the code
|
|
48
48
|
*/
|
|
49
|
-
this.aiModelID = "";
|
|
50
|
-
this.wasGenerated = true;
|
|
51
|
-
this.success = false;
|
|
49
|
+
this.aiModelID = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
|
|
50
|
+
this.wasGenerated = true; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
|
|
51
|
+
this.success = false; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
export class ManageMetadataBase {
|
|
@@ -98,7 +98,7 @@ export class ManageMetadataBase {
|
|
|
98
98
|
*/
|
|
99
99
|
get dbProvider() {
|
|
100
100
|
if (!this._dbProvider) {
|
|
101
|
-
const platform =
|
|
101
|
+
const platform = DbPlatform();
|
|
102
102
|
const provider = MJGlobal.Instance.ClassFactory.CreateInstance(CodeGenDatabaseProvider, platform);
|
|
103
103
|
// CreateInstance returns the abstract base class on a missed key (it
|
|
104
104
|
// doesn't throw), so disambiguate by constructor identity: anything
|
|
@@ -191,7 +191,7 @@ export class ManageMetadataBase {
|
|
|
191
191
|
*/
|
|
192
192
|
applyTimeEntityFieldSequenceSQL(entityID) {
|
|
193
193
|
// COALESCE is valid on both platforms and is the form the docs and the gate describe.
|
|
194
|
-
return `(SELECT COALESCE(MAX(${this.qi('Sequence')}), 0) + 1 FROM ${this.qs(
|
|
194
|
+
return `(SELECT COALESCE(MAX(${this.qi('Sequence')}), 0) + 1 FROM ${this.qs(MjCoreSchema(), 'EntityField')} WHERE ${this.qi('EntityID')} = '${entityID}')`;
|
|
195
195
|
}
|
|
196
196
|
/**
|
|
197
197
|
* Wraps a SELECT query with a row limit.
|
|
@@ -213,7 +213,7 @@ export class ManageMetadataBase {
|
|
|
213
213
|
* (avoids hardcoding runtime UUIDs that may differ on a fresh database).
|
|
214
214
|
*/
|
|
215
215
|
entityIdSubquery(tableName, schemaName) {
|
|
216
|
-
const schema =
|
|
216
|
+
const schema = MjCoreSchema();
|
|
217
217
|
return `(${this.selectTop(1, 'ID', `FROM ${this.qs(schema, 'vwEntities')} WHERE BaseTable = '${tableName}' AND SchemaName = '${schemaName}'`)})`;
|
|
218
218
|
}
|
|
219
219
|
/**
|
|
@@ -263,22 +263,38 @@ export class ManageMetadataBase {
|
|
|
263
263
|
/**
|
|
264
264
|
* Globally scoped list of entities that have been created during the metadata management process.
|
|
265
265
|
*/
|
|
266
|
-
static get
|
|
266
|
+
static get NewEntityList() {
|
|
267
267
|
return this._newEntityList;
|
|
268
268
|
}
|
|
269
|
-
static set
|
|
269
|
+
static set NewEntityList(value) {
|
|
270
270
|
this._newEntityList = value;
|
|
271
271
|
}
|
|
272
|
+
/** @deprecated Use {@link NewEntityList}. */
|
|
273
|
+
static get newEntityList() {
|
|
274
|
+
return this.NewEntityList;
|
|
275
|
+
}
|
|
276
|
+
/** @deprecated Use {@link NewEntityList}. */
|
|
277
|
+
static set newEntityList(value) {
|
|
278
|
+
this.NewEntityList = value;
|
|
279
|
+
}
|
|
272
280
|
static { this._modifiedEntityList = []; }
|
|
273
281
|
/**
|
|
274
282
|
* Globally scoped list of entities that have been modified during the metadata management process.
|
|
275
283
|
*/
|
|
276
|
-
static get
|
|
284
|
+
static get ModifiedEntityList() {
|
|
277
285
|
return this._modifiedEntityList;
|
|
278
286
|
}
|
|
279
|
-
static set
|
|
287
|
+
static set ModifiedEntityList(value) {
|
|
280
288
|
this._modifiedEntityList = value;
|
|
281
289
|
}
|
|
290
|
+
/** @deprecated Use {@link ModifiedEntityList}. */
|
|
291
|
+
static get modifiedEntityList() {
|
|
292
|
+
return this.ModifiedEntityList;
|
|
293
|
+
}
|
|
294
|
+
/** @deprecated Use {@link ModifiedEntityList}. */
|
|
295
|
+
static set modifiedEntityList(value) {
|
|
296
|
+
this.ModifiedEntityList = value;
|
|
297
|
+
}
|
|
282
298
|
static { this._newFieldSet = new Set(); }
|
|
283
299
|
static { this._changedFields = new Map(); }
|
|
284
300
|
static { this._changedFieldReport = []; }
|
|
@@ -290,13 +306,17 @@ export class ManageMetadataBase {
|
|
|
290
306
|
}
|
|
291
307
|
return `${eid}:${fld}`;
|
|
292
308
|
}
|
|
293
|
-
static
|
|
309
|
+
static RegisterNewField(entityID, name) {
|
|
294
310
|
const key = this.fieldKey(entityID, name);
|
|
295
311
|
if (key) {
|
|
296
312
|
this._newFieldSet.add(key);
|
|
297
313
|
}
|
|
298
314
|
}
|
|
299
|
-
|
|
315
|
+
/** @deprecated Use {@link RegisterNewField}. */
|
|
316
|
+
static registerNewField(entityID, name) {
|
|
317
|
+
return this.RegisterNewField(entityID, name);
|
|
318
|
+
}
|
|
319
|
+
static RegisterFieldChange(entityID, name, reasons) {
|
|
300
320
|
const key = this.fieldKey(entityID, name);
|
|
301
321
|
if (key) {
|
|
302
322
|
let set = this._changedFields.get(key);
|
|
@@ -309,17 +329,29 @@ export class ManageMetadataBase {
|
|
|
309
329
|
}
|
|
310
330
|
}
|
|
311
331
|
}
|
|
312
|
-
|
|
332
|
+
/** @deprecated Use {@link RegisterFieldChange}. */
|
|
333
|
+
static registerFieldChange(entityID, name, reasons) {
|
|
334
|
+
return this.RegisterFieldChange(entityID, name, reasons);
|
|
335
|
+
}
|
|
336
|
+
static IsFieldNew(entityID, name) {
|
|
313
337
|
const key = this.fieldKey(entityID, name);
|
|
314
338
|
return key ? this._newFieldSet.has(key) : false;
|
|
315
339
|
}
|
|
316
|
-
|
|
340
|
+
/** @deprecated Use {@link IsFieldNew}. */
|
|
341
|
+
static isFieldNew(entityID, name) {
|
|
342
|
+
return this.IsFieldNew(entityID, name);
|
|
343
|
+
}
|
|
344
|
+
static FieldChangeReasons(entityID, name) {
|
|
317
345
|
const key = this.fieldKey(entityID, name);
|
|
318
346
|
const set = key ? this._changedFields.get(key) : undefined;
|
|
319
347
|
return set ?? new Set();
|
|
320
348
|
}
|
|
321
|
-
|
|
322
|
-
|
|
349
|
+
/** @deprecated Use {@link FieldChangeReasons}. */
|
|
350
|
+
static fieldChangeReasons(entityID, name) {
|
|
351
|
+
return this.FieldChangeReasons(entityID, name);
|
|
352
|
+
}
|
|
353
|
+
static IsDisplayNameReopened(entityID, name) {
|
|
354
|
+
const reasons = this.FieldChangeReasons(entityID, name);
|
|
323
355
|
for (const r of reasons) {
|
|
324
356
|
if (DISPLAYNAME_REOPEN_REASONS.has(r)) {
|
|
325
357
|
return true;
|
|
@@ -327,8 +359,12 @@ export class ManageMetadataBase {
|
|
|
327
359
|
}
|
|
328
360
|
return false;
|
|
329
361
|
}
|
|
330
|
-
|
|
331
|
-
|
|
362
|
+
/** @deprecated Use {@link IsDisplayNameReopened}. */
|
|
363
|
+
static isDisplayNameReopened(entityID, name) {
|
|
364
|
+
return this.IsDisplayNameReopened(entityID, name);
|
|
365
|
+
}
|
|
366
|
+
static IsTypeReopened(entityID, name) {
|
|
367
|
+
const reasons = this.FieldChangeReasons(entityID, name);
|
|
332
368
|
for (const r of reasons) {
|
|
333
369
|
if (TYPE_REOPEN_REASONS.has(r)) {
|
|
334
370
|
return true;
|
|
@@ -336,20 +372,40 @@ export class ManageMetadataBase {
|
|
|
336
372
|
}
|
|
337
373
|
return false;
|
|
338
374
|
}
|
|
339
|
-
|
|
375
|
+
/** @deprecated Use {@link IsTypeReopened}. */
|
|
376
|
+
static isTypeReopened(entityID, name) {
|
|
377
|
+
return this.IsTypeReopened(entityID, name);
|
|
378
|
+
}
|
|
379
|
+
static get NewFieldCount() {
|
|
340
380
|
return this._newFieldSet.size;
|
|
341
381
|
}
|
|
342
|
-
|
|
382
|
+
/** @deprecated Use {@link NewFieldCount}. */
|
|
383
|
+
static get newFieldCount() {
|
|
384
|
+
return this.NewFieldCount;
|
|
385
|
+
}
|
|
386
|
+
static get ChangedFieldCount() {
|
|
343
387
|
return this._changedFields.size;
|
|
344
388
|
}
|
|
345
|
-
|
|
389
|
+
/** @deprecated Use {@link ChangedFieldCount}. */
|
|
390
|
+
static get changedFieldCount() {
|
|
391
|
+
return this.ChangedFieldCount;
|
|
392
|
+
}
|
|
393
|
+
static get ChangedFieldReport() {
|
|
346
394
|
return this._changedFieldReport;
|
|
347
395
|
}
|
|
348
|
-
|
|
396
|
+
/** @deprecated Use {@link ChangedFieldReport}. */
|
|
397
|
+
static get changedFieldReport() {
|
|
398
|
+
return this.ChangedFieldReport;
|
|
399
|
+
}
|
|
400
|
+
static ClearFieldTracking() {
|
|
349
401
|
this._newFieldSet.clear();
|
|
350
402
|
this._changedFields.clear();
|
|
351
403
|
this._changedFieldReport = [];
|
|
352
404
|
}
|
|
405
|
+
/** @deprecated Use {@link ClearFieldTracking}. */
|
|
406
|
+
static clearFieldTracking() {
|
|
407
|
+
return this.ClearFieldTracking();
|
|
408
|
+
}
|
|
353
409
|
/**
|
|
354
410
|
* Takes a snapshot of EntityField rows from vwEntityFields for diffing.
|
|
355
411
|
* Only queries columns in TRACKED_FIELD_COLUMNS (Sequence is excluded).
|
|
@@ -361,7 +417,7 @@ export class ManageMetadataBase {
|
|
|
361
417
|
: '';
|
|
362
418
|
const cols = TRACKED_FIELD_COLUMNS.map(c => this.qi(c)).join(', ');
|
|
363
419
|
const sSQL = `SELECT ${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Entity')} AS ${this.qi('EntityName')}, ${this.qi('Name')}, ${cols}
|
|
364
|
-
FROM ${this.qs(
|
|
420
|
+
FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')}
|
|
365
421
|
${filter}`;
|
|
366
422
|
const result = await this.runQuery(pool, sSQL);
|
|
367
423
|
const map = new Map();
|
|
@@ -391,9 +447,13 @@ export class ManageMetadataBase {
|
|
|
391
447
|
* the dead class keeps a live `@RegisterClass` registration on disk. The old monolith could
|
|
392
448
|
* not drift this way: it was rewritten in full every run.
|
|
393
449
|
*/
|
|
394
|
-
static get
|
|
450
|
+
static get DeletedEntitySchemaList() {
|
|
395
451
|
return this._deletedEntitySchemaList;
|
|
396
452
|
}
|
|
453
|
+
/** @deprecated Use {@link DeletedEntitySchemaList}. */
|
|
454
|
+
static get deletedEntitySchemaList() {
|
|
455
|
+
return this.DeletedEntitySchemaList;
|
|
456
|
+
}
|
|
397
457
|
static { this._entitiesRequiringViewRegen = []; }
|
|
398
458
|
/**
|
|
399
459
|
* Entities that had late-phase changes requiring their base views to be
|
|
@@ -412,9 +472,13 @@ export class ManageMetadataBase {
|
|
|
412
472
|
/**
|
|
413
473
|
* Globally scoped list of validators that have been generated during the metadata management process.
|
|
414
474
|
*/
|
|
415
|
-
static get
|
|
475
|
+
static get GeneratedValidators() {
|
|
416
476
|
return this._generatedValidators;
|
|
417
477
|
}
|
|
478
|
+
/** @deprecated Use {@link GeneratedValidators}. */
|
|
479
|
+
static get generatedValidators() {
|
|
480
|
+
return this.GeneratedValidators;
|
|
481
|
+
}
|
|
418
482
|
static { this._softPKFKConfigCache = null; }
|
|
419
483
|
static { this._softPKFKConfigPath = ''; }
|
|
420
484
|
/**
|
|
@@ -429,16 +493,20 @@ export class ManageMetadataBase {
|
|
|
429
493
|
* Deterministic + event-driven (no mtime/TOCTOU race). The CLI `Run()` path does not call this, so its
|
|
430
494
|
* load-once-per-process behavior is unchanged.
|
|
431
495
|
*/
|
|
432
|
-
static
|
|
496
|
+
static InvalidateSoftPKFKConfigCache() {
|
|
433
497
|
ManageMetadataBase._softPKFKConfigCache = null;
|
|
434
498
|
ManageMetadataBase._softPKFKConfigPath = '';
|
|
435
499
|
}
|
|
500
|
+
/** @deprecated Use {@link InvalidateSoftPKFKConfigCache}. */
|
|
501
|
+
static invalidateSoftPKFKConfigCache() {
|
|
502
|
+
return this.InvalidateSoftPKFKConfigCache();
|
|
503
|
+
}
|
|
436
504
|
/**
|
|
437
505
|
* Loads and caches the soft PK/FK configuration from the additionalSchemaInfo file.
|
|
438
506
|
* Cached per process to avoid repeated I/O within a CodeGen run; the in-process (RSU) path calls
|
|
439
507
|
* {@link invalidateSoftPKFKConfigCache} at the start of each run so a rewritten file is picked up.
|
|
440
508
|
*/
|
|
441
|
-
static
|
|
509
|
+
static GetSoftPKFKConfig() {
|
|
442
510
|
// Return cached config if path hasn't changed
|
|
443
511
|
const configPath = configInfo.additionalSchemaInfo
|
|
444
512
|
? path.join(currentWorkingDirectory, configInfo.additionalSchemaInfo)
|
|
@@ -464,6 +532,10 @@ export class ManageMetadataBase {
|
|
|
464
532
|
return null;
|
|
465
533
|
}
|
|
466
534
|
}
|
|
535
|
+
/** @deprecated Use {@link GetSoftPKFKConfig}. */
|
|
536
|
+
static getSoftPKFKConfig() {
|
|
537
|
+
return this.GetSoftPKFKConfig();
|
|
538
|
+
}
|
|
467
539
|
/**
|
|
468
540
|
* Extracts a flat array of table configs from the config file, handling both formats:
|
|
469
541
|
* 1. Schema-as-key (template format): { "dbo": [{ "TableName": "Orders", ... }] }
|
|
@@ -673,7 +745,7 @@ export class ManageMetadataBase {
|
|
|
673
745
|
const allOrganicKeys = this.extractOrganicKeysFromConfig(config);
|
|
674
746
|
if (allOrganicKeys.length === 0)
|
|
675
747
|
return { success: true, createdCount: 0, updatedCount: 0, failedCount: 0 };
|
|
676
|
-
const schema =
|
|
748
|
+
const schema = MjCoreSchema();
|
|
677
749
|
let createdCount = 0;
|
|
678
750
|
let updatedCount = 0;
|
|
679
751
|
let failedCount = 0;
|
|
@@ -709,7 +781,7 @@ export class ManageMetadataBase {
|
|
|
709
781
|
// well as last — and the metadata DML logged just before it (entity-config UPDATEs, the
|
|
710
782
|
// previous key's INSERTs) has no trailing GO. requiresOwnBatch puts the provider's
|
|
711
783
|
// separator on both sides in the migration file ('' on PostgreSQL: nothing is added).
|
|
712
|
-
await this.
|
|
784
|
+
await this.logSQLAndExecute(pool, viewSQL, `Create transitive bridge view ${viewFullName} for organic key "${okConfig.Name}" on ${ownerEntityName}`, false, true, this.dbProvider.BatchSeparator, true);
|
|
713
785
|
// Auto-populate TransitiveObject from the view definition
|
|
714
786
|
re.TransitiveObject = viewFullName;
|
|
715
787
|
}
|
|
@@ -729,7 +801,7 @@ export class ManageMetadataBase {
|
|
|
729
801
|
Sequence = ${okConfig.Sequence ?? 0},
|
|
730
802
|
Status = 'Active'
|
|
731
803
|
WHERE ID = '${organicKeyId}'`;
|
|
732
|
-
await this.
|
|
804
|
+
await this.logSQLAndExecute(pool, updateSQL, `Update organic key "${okConfig.Name}" on ${ownerEntityName}`);
|
|
733
805
|
updatedCount++;
|
|
734
806
|
logStatus(` > Organic key: Updated "${okConfig.Name}" on ${ownerEntityName}`);
|
|
735
807
|
}
|
|
@@ -739,7 +811,7 @@ export class ManageMetadataBase {
|
|
|
739
811
|
const insertSQL = `INSERT INTO ${this.qs(schema, 'EntityOrganicKey')}
|
|
740
812
|
(EntityID, Name, ${okConfig.Description ? 'Description, ' : ''}MatchFieldNames, NormalizationStrategy, ${okConfig.CustomNormalizationExpression ? 'CustomNormalizationExpression, ' : ''}Sequence, Status)
|
|
741
813
|
VALUES (${ownerSubquery}, '${okConfig.Name}', ${okConfig.Description ? `'${okConfig.Description.replace(/'/g, "''")}', ` : ''}'${matchFieldNames}', '${okConfig.NormalizationStrategy || 'LowerCaseTrim'}', ${okConfig.CustomNormalizationExpression ? `'${okConfig.CustomNormalizationExpression.replace(/'/g, "''")}', ` : ''}${okConfig.Sequence ?? 0}, 'Active')`;
|
|
742
|
-
await this.
|
|
814
|
+
await this.logSQLAndExecute(pool, insertSQL, `Insert organic key "${okConfig.Name}" on ${ownerEntityName}`);
|
|
743
815
|
createdCount++;
|
|
744
816
|
logStatus(` > Organic key: Created "${okConfig.Name}" on ${ownerEntityName}`);
|
|
745
817
|
// Fetch the new ID
|
|
@@ -769,7 +841,7 @@ export class ManageMetadataBase {
|
|
|
769
841
|
DisplayLocation = '${reConfig.DisplayLocation || 'After Field Tabs'}',
|
|
770
842
|
Sequence = ${reConfig.Sequence ?? 0}
|
|
771
843
|
WHERE ID = '${relId}'`;
|
|
772
|
-
await this.
|
|
844
|
+
await this.logSQLAndExecute(pool, updateRelSQL, `Update organic key related entity: "${okConfig.Name}" → ${relEntityName}`);
|
|
773
845
|
}
|
|
774
846
|
else {
|
|
775
847
|
// Use subqueries for FK references so the logged SQL is portable across databases
|
|
@@ -789,7 +861,7 @@ export class ManageMetadataBase {
|
|
|
789
861
|
${reConfig.DisplayName ? `'${reConfig.DisplayName.replace(/'/g, "''")}'` : 'NULL'},
|
|
790
862
|
'${reConfig.DisplayLocation || 'After Field Tabs'}',
|
|
791
863
|
${reConfig.Sequence ?? 0})`;
|
|
792
|
-
await this.
|
|
864
|
+
await this.logSQLAndExecute(pool, insertRelSQL, `Insert organic key related entity: "${okConfig.Name}" → ${relEntityName}`);
|
|
793
865
|
}
|
|
794
866
|
logStatus(` > Organic key "${okConfig.Name}": ${existingRel.recordset.length > 0 ? 'updated' : 'created'} → ${relEntityName} (${isDirect ? 'direct' : 'transitive'})`);
|
|
795
867
|
}
|
|
@@ -812,7 +884,7 @@ export class ManageMetadataBase {
|
|
|
812
884
|
*/
|
|
813
885
|
async findOrganicKeyEntity(pool, schemaName, tableName) {
|
|
814
886
|
const result = await this.runQueryWithParams(pool, `
|
|
815
|
-
${this.selectTop(1, 'ID, Name', `FROM ${this.qs(
|
|
887
|
+
${this.selectTop(1, 'ID, Name', `FROM ${this.qs(MjCoreSchema(), 'vwEntities')}
|
|
816
888
|
WHERE (BaseTable = @TableName AND SchemaName = @SchemaName)
|
|
817
889
|
OR Name = @TableName`, 'CASE WHEN BaseTable = @TableName AND SchemaName = @SchemaName THEN 0 ELSE 1 END')}
|
|
818
890
|
`, { 'TableName': tableName, 'SchemaName': schemaName });
|
|
@@ -881,7 +953,7 @@ export class ManageMetadataBase {
|
|
|
881
953
|
if (relationships.length === 0)
|
|
882
954
|
return { success: true, updatedCount: 0 };
|
|
883
955
|
let updatedCount = 0;
|
|
884
|
-
const schema =
|
|
956
|
+
const schema = MjCoreSchema();
|
|
885
957
|
for (const rel of relationships) {
|
|
886
958
|
try {
|
|
887
959
|
// Look up the parent entity — try by Name first, then by BaseTable within the given schema
|
|
@@ -925,7 +997,7 @@ export class ManageMetadataBase {
|
|
|
925
997
|
SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()},
|
|
926
998
|
${this.qi('ParentID')} = '${parentId}'
|
|
927
999
|
WHERE ${this.qi('ID')} = '${childId}'`;
|
|
928
|
-
await this.
|
|
1000
|
+
await this.logSQLAndExecute(pool, updateSQL, `Set IS-A ParentID for "${childName}" → "${parentName}"`);
|
|
929
1001
|
if (existingParentId) {
|
|
930
1002
|
logStatus(` > IS-A: Updated "${childName}" ParentID from previous value to "${parentName}"`);
|
|
931
1003
|
}
|
|
@@ -973,7 +1045,7 @@ export class ManageMetadataBase {
|
|
|
973
1045
|
* so a broken declaration fails before it produces generated code.
|
|
974
1046
|
*/
|
|
975
1047
|
async validateISARelationships(pool) {
|
|
976
|
-
const schema =
|
|
1048
|
+
const schema = MjCoreSchema();
|
|
977
1049
|
let errorCount = 0;
|
|
978
1050
|
try {
|
|
979
1051
|
const results = await this.runQuery(pool, this.buildISAValidationSQL(schema));
|
|
@@ -1064,7 +1136,7 @@ export class ManageMetadataBase {
|
|
|
1064
1136
|
if (entityConfigs.length === 0)
|
|
1065
1137
|
return { success: true, updatedCount: 0 };
|
|
1066
1138
|
let updatedCount = 0;
|
|
1067
|
-
const schema =
|
|
1139
|
+
const schema = MjCoreSchema();
|
|
1068
1140
|
const reservedKeys = new Set(['BaseTable', 'SchemaName']);
|
|
1069
1141
|
for (const ec of entityConfigs) {
|
|
1070
1142
|
try {
|
|
@@ -1129,7 +1201,7 @@ export class ManageMetadataBase {
|
|
|
1129
1201
|
const updateSQL = `UPDATE ${this.qs(schema, 'Entity')}
|
|
1130
1202
|
SET ${setClauses.join(', ')}
|
|
1131
1203
|
WHERE ${this.qi('ID')} = '${entityId}'`;
|
|
1132
|
-
await this.
|
|
1204
|
+
await this.logSQLAndExecute(pool, updateSQL, `Update attributes on "${entityName}" from additionalSchemaInfo`);
|
|
1133
1205
|
const attrSummary = differingAttrs.map(a => a.summary).join(', ');
|
|
1134
1206
|
logStatus(` > Entities config: Set ${attrSummary} on "${entityName}"`);
|
|
1135
1207
|
updatedCount++;
|
|
@@ -1155,7 +1227,7 @@ export class ManageMetadataBase {
|
|
|
1155
1227
|
if (virtualEntities.length === 0)
|
|
1156
1228
|
return { success: true, createdCount: 0 };
|
|
1157
1229
|
let createdCount = 0;
|
|
1158
|
-
const schema =
|
|
1230
|
+
const schema = MjCoreSchema();
|
|
1159
1231
|
for (const ve of virtualEntities) {
|
|
1160
1232
|
const viewSchema = ve.SchemaName || schema;
|
|
1161
1233
|
const viewName = ve.ViewName;
|
|
@@ -1231,7 +1303,7 @@ export class ManageMetadataBase {
|
|
|
1231
1303
|
// Refresh so entity.Fields reflect this run's field sync before we snapshot the shape.
|
|
1232
1304
|
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
1233
1305
|
await md.Refresh();
|
|
1234
|
-
const coreSchema =
|
|
1306
|
+
const coreSchema = MjCoreSchema();
|
|
1235
1307
|
const esc = (s) => s.replace(/'/g, "''");
|
|
1236
1308
|
const lit = (s) => (s ? `'${esc(s)}'` : 'NULL');
|
|
1237
1309
|
let processedCount = 0;
|
|
@@ -1294,8 +1366,8 @@ export class ManageMetadataBase {
|
|
|
1294
1366
|
// includeBatchSeparator: each is a single GO-free batch (executed via ds.query), but the
|
|
1295
1367
|
// migration file needs a GO between statements for Flyway/sqlcmd. Pass the provider's
|
|
1296
1368
|
// separator, not the 'GO' default: on PostgreSQL it is '' and a literal GO breaks replay.
|
|
1297
|
-
await this.
|
|
1298
|
-
await this.
|
|
1369
|
+
await this.logSQLAndExecute(pool, tableSQL, `Create materialized table for base-view materialization of entity ${entity.Name}`, false, true, this.dbProvider.BatchSeparator);
|
|
1370
|
+
await this.logSQLAndExecute(pool, viewSQL, `Create wrapper view for base-view materialization of entity ${entity.Name}`, false, true, this.dbProvider.BatchSeparator);
|
|
1299
1371
|
// 4) Upsert the MJ: Materialized Results row, keyed on (SourceType, SourceEntityID).
|
|
1300
1372
|
const existing = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(coreSchema, 'MaterializedResult')} WHERE SourceType = @SourceType AND SourceEntityID = @EntityID`, { SourceType: 'EntityBaseView', EntityID: entity.ID });
|
|
1301
1373
|
if (existing.recordset.length > 0) {
|
|
@@ -1303,7 +1375,7 @@ export class ManageMetadataBase {
|
|
|
1303
1375
|
SET SchemaName='${esc(matSchema)}', TableName='${esc(tableName)}', ViewName='${esc(viewName)}',
|
|
1304
1376
|
RefreshSchedule=${lit(decl.RefreshSchedule)}, IntendedWorkload=${lit(decl.IntendedWorkload)}
|
|
1305
1377
|
WHERE ID='${existing.recordset[0].ID}'`;
|
|
1306
|
-
await this.
|
|
1378
|
+
await this.logSQLAndExecute(pool, sqlUpd, `Update MJ: Materialized Results for base-view materialization of ${entity.Name}`);
|
|
1307
1379
|
}
|
|
1308
1380
|
else {
|
|
1309
1381
|
const newId = this.createNewUUID();
|
|
@@ -1315,7 +1387,7 @@ export class ManageMetadataBase {
|
|
|
1315
1387
|
VALUES ( '${newId}', 'EntityBaseView', '${entity.ID}', '${esc(matSchema)}', '${esc(tableName)}', '${esc(viewName)}',
|
|
1316
1388
|
'None', 'FullRebuild', ${lit(decl.RefreshSchedule)}, 'Building', ${lit(decl.IntendedWorkload)},
|
|
1317
1389
|
${this.utcNow()}, ${this.utcNow()} )`;
|
|
1318
|
-
await this.
|
|
1390
|
+
await this.logSQLAndExecute(pool, sqlIns, `Insert MJ: Materialized Results for base-view materialization of ${entity.Name}`);
|
|
1319
1391
|
}
|
|
1320
1392
|
processedCount++;
|
|
1321
1393
|
logStatus(` > Base-view materialization ready for entity "${entity.Name}" → [${matSchema}].[${viewName}]`);
|
|
@@ -1329,7 +1401,7 @@ export class ManageMetadataBase {
|
|
|
1329
1401
|
* fields, or a precise refusal (per-parameter reasons) so non-qualifying queries are skipped loudly.
|
|
1330
1402
|
*/
|
|
1331
1403
|
async classifyParameterizedQueryForMaterialization(pool, queryId, queryName, outputColumns, currentUser) {
|
|
1332
|
-
const coreSchema =
|
|
1404
|
+
const coreSchema = MjCoreSchema();
|
|
1333
1405
|
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
1334
1406
|
// Load the query template + parameter definitions (typed entities for the renderer).
|
|
1335
1407
|
const qRes = await this.runQueryWithParams(pool, `SELECT SQL, UsesTemplate FROM ${this.qs(coreSchema, 'Query')} WHERE ID = @QID`, { QID: queryId });
|
|
@@ -1361,31 +1433,31 @@ export class ManageMetadataBase {
|
|
|
1361
1433
|
// Phase 2 is shipped: enable Bucket-1 row-filter broad materialization. The provider now auto-injects
|
|
1362
1434
|
// the read-time predicate from the persisted ReadFilterSpec (bound params), so a RowFilterBroad
|
|
1363
1435
|
// materialization is safe to mint. `allowRowFilterBroad` remains the build-level kill switch.
|
|
1364
|
-
const classification =
|
|
1365
|
-
if (!classification.
|
|
1366
|
-
const detail = classification.
|
|
1367
|
-
return { qualifies: false, reason: `${classification.
|
|
1436
|
+
const classification = ClassifyQueryParameters({ queryName, params: paramDefs, outputColumns, dialect, render, allowRowFilterBroad: true });
|
|
1437
|
+
if (!classification.Qualification.qualifies) {
|
|
1438
|
+
const detail = classification.PerParam.map((pp) => `${pp.name}: ${pp.verdict.reason}`).join('; ');
|
|
1439
|
+
return { qualifies: false, reason: `${classification.Qualification.reason ?? 'parameters not materializable'}${detail ? ` — [${detail}]` : ''}` };
|
|
1368
1440
|
}
|
|
1369
|
-
if (classification.
|
|
1370
|
-
return { qualifies: false, reason: `parameterization mode '${classification.
|
|
1441
|
+
if (classification.Qualification.paramMode !== 'RowFilterBroad') {
|
|
1442
|
+
return { qualifies: false, reason: `parameterization mode '${classification.Qualification.paramMode}' is not supported for materialization in v1 (only RowFilterBroad)` };
|
|
1371
1443
|
}
|
|
1372
1444
|
// Build the broad source SQL: render a concrete instance (held values) then strip the row-filter predicates.
|
|
1373
1445
|
let renderedHeld;
|
|
1374
1446
|
try {
|
|
1375
|
-
renderedHeld = render(
|
|
1447
|
+
renderedHeld = render(BuildHeldValues(paramDefs));
|
|
1376
1448
|
}
|
|
1377
1449
|
catch (e) {
|
|
1378
1450
|
return { qualifies: false, reason: `could not render the query to build broad SQL: ${e instanceof Error ? e.message : String(e)}` };
|
|
1379
1451
|
}
|
|
1380
|
-
const expectedRemovals = classification.
|
|
1381
|
-
const broad =
|
|
1382
|
-
if (broad.
|
|
1383
|
-
return { qualifies: false, reason: `expected to strip row-filter predicate(s) on [${classification.
|
|
1452
|
+
const expectedRemovals = classification.Qualification.rowFilterColumns.length;
|
|
1453
|
+
const broad = BuildBroadRowFilterSQL(renderedHeld, classification.Qualification.rowFilterColumns, dialect, expectedRemovals);
|
|
1454
|
+
if (broad.RemovedCount === 0) {
|
|
1455
|
+
return { qualifies: false, reason: `expected to strip row-filter predicate(s) on [${classification.Qualification.rowFilterColumns.join(', ')}] but none were removed from the rendered SQL — refusing to avoid a wrongly-filtered materialization` };
|
|
1384
1456
|
}
|
|
1385
|
-
if (broad.
|
|
1386
|
-
return { qualifies: false, reason: `expected to strip exactly ${expectedRemovals} row-filter parameter predicate(s) on [${classification.
|
|
1457
|
+
if (broad.Ambiguous) {
|
|
1458
|
+
return { qualifies: false, reason: `expected to strip exactly ${expectedRemovals} row-filter parameter predicate(s) on [${classification.Qualification.rowFilterColumns.join(', ')}] but matched ${broad.RemovedCount} — the parameter predicate(s) cannot be cleanly isolated from other static or same-named predicates on those columns, so a broad materialization would include or exclude rows the live query never would. Refusing (query stays live-only).` };
|
|
1387
1459
|
}
|
|
1388
|
-
return { qualifies: true, rowFilterColumns: classification.
|
|
1460
|
+
return { qualifies: true, rowFilterColumns: classification.Qualification.rowFilterColumns, broadSQL: broad.Sql, readFilterSpec: classification.Qualification.readFilterSpec };
|
|
1389
1461
|
}
|
|
1390
1462
|
/**
|
|
1391
1463
|
* Processes query materialization (CodeGen materialization phase, sub-step C — plan §4.2).
|
|
@@ -1404,7 +1476,7 @@ export class ManageMetadataBase {
|
|
|
1404
1476
|
* Parameterized queries are skipped with a log (deferred to Phase 2).
|
|
1405
1477
|
*/
|
|
1406
1478
|
async processQueryMaterializations(pool, currentUser) {
|
|
1407
|
-
const coreSchema =
|
|
1479
|
+
const coreSchema = MjCoreSchema();
|
|
1408
1480
|
// Gate the whole method on the IsMaterialized column existing — the flagged-query SELECT below filters on it,
|
|
1409
1481
|
// and it (like the MaterializedResult table) is added by the materialization Foundation migration. On a DB
|
|
1410
1482
|
// where that migration hasn't run yet the SELECT would throw and abort the entire codegen pass; with no such
|
|
@@ -1468,7 +1540,7 @@ export class ManageMetadataBase {
|
|
|
1468
1540
|
// Detected BEFORE the shape analysis so the surrogate column can be typed for the keyed case.
|
|
1469
1541
|
const detectSQL = paramMode === 'RowFilterBroad' ? (broadSQL ?? '') : querySQL;
|
|
1470
1542
|
const matDialect = this.dbProvider.PlatformKey === 'postgresql' ? new PostgreSQLDialect() : new SQLServerDialect();
|
|
1471
|
-
const detectedKeyColumns = detectSQL ?
|
|
1543
|
+
const detectedKeyColumns = detectSQL ? DetectAggregationKeyColumns({ sql: detectSQL, dialect: matDialect, fields }) : null;
|
|
1472
1544
|
// An EXTERNAL query refreshes via rebuildFromExternalQuery, which fetches all rows through the EDS
|
|
1473
1545
|
// driver and writes a SYNTHETIC row-index surrogate — it cannot compute the per-group combined-key
|
|
1474
1546
|
// hash and ignores KeyColumns entirely. So minting a keyed (hash-surrogate) entity for an external
|
|
@@ -1494,7 +1566,7 @@ export class ManageMetadataBase {
|
|
|
1494
1566
|
// The materialized table's column shape is the query's output columns — independent of
|
|
1495
1567
|
// parameterization (a row-filter query is materialized broad over the same columns). Pass
|
|
1496
1568
|
// isParameterized:false so the analyzer yields the shape; the param gate above already ran.
|
|
1497
|
-
const analysis =
|
|
1569
|
+
const analysis = AnalyzeQueryForMaterialization({ queryName, isParameterized: false, fields, surrogateSQLType });
|
|
1498
1570
|
if (!analysis.qualifies) {
|
|
1499
1571
|
logStatus(` > Skipping materialization of query "${queryName}": ${analysis.reason}`);
|
|
1500
1572
|
continue;
|
|
@@ -1512,7 +1584,7 @@ export class ManageMetadataBase {
|
|
|
1512
1584
|
const isKeyedSingleSource = isKeyed && !!detectSQL && SQLParser.ExtractTableRefs(detectSQL, matDialect).length === 1;
|
|
1513
1585
|
const refreshStrategy = !isKeyedSingleSource
|
|
1514
1586
|
? 'FullRebuild'
|
|
1515
|
-
:
|
|
1587
|
+
: DetectAdditiveMeasures(detectSQL)
|
|
1516
1588
|
? 'Incremental'
|
|
1517
1589
|
: 'DirtyGroupRecompute';
|
|
1518
1590
|
// RLS downgrade gate (§6.2, ships day one) — a materialized query entity does NOT inherit its
|
|
@@ -1551,10 +1623,10 @@ export class ManageMetadataBase {
|
|
|
1551
1623
|
logError(` > REFUSING to materialize query "${queryName}": its derived table [${coreSchema}].[${tableName}] (from CodeName "${codeName}") is already owned by a different materialization (SourceType=${foreignOwner.SourceType}, QueryID=${foreignOwner.QueryID ?? 'n/a'}). Two sources whose names normalize to the same CodeName would clobber each other's snapshot — rename one so their CodeNames differ. Skipping.`);
|
|
1552
1624
|
continue;
|
|
1553
1625
|
}
|
|
1554
|
-
const tableSQL = this.dbProvider.generateMaterializedTableSQL(coreSchema, tableName, analysis.
|
|
1626
|
+
const tableSQL = this.dbProvider.generateMaterializedTableSQL(coreSchema, tableName, analysis.Columns);
|
|
1555
1627
|
const viewSQL = this.dbProvider.generateMaterializedWrapperViewSQL(coreSchema, viewName, tableName);
|
|
1556
|
-
await this.
|
|
1557
|
-
await this.
|
|
1628
|
+
await this.logSQLAndExecute(pool, tableSQL, `Create materialized table for query "${queryName}"`, false, true, this.dbProvider.BatchSeparator);
|
|
1629
|
+
await this.logSQLAndExecute(pool, viewSQL, `Create wrapper view for query "${queryName}"`, false, true, this.dbProvider.BatchSeparator);
|
|
1558
1630
|
// 3) Mint the read-only Virtual Entity over the wrapper view (idempotent by view).
|
|
1559
1631
|
const existingVE = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(coreSchema, 'vwEntities')} WHERE BaseView = @V AND SchemaName = @S`, { V: viewName, S: coreSchema });
|
|
1560
1632
|
let generatedEntityId = existingVE.recordset[0]?.ID;
|
|
@@ -1564,7 +1636,7 @@ export class ManageMetadataBase {
|
|
|
1564
1636
|
Name: queryName,
|
|
1565
1637
|
BaseView: viewName,
|
|
1566
1638
|
SchemaName: coreSchema,
|
|
1567
|
-
PrimaryKeyFieldName: analysis.
|
|
1639
|
+
PrimaryKeyFieldName: analysis.SurrogateColumnName,
|
|
1568
1640
|
Description: `Materialized result of query "${queryName}".`,
|
|
1569
1641
|
});
|
|
1570
1642
|
generatedEntityId = createRes.recordset?.[0]?.[''] || createRes.recordset?.[0]?.ID || createRes.recordset?.[0]?.Column0;
|
|
@@ -1603,7 +1675,7 @@ export class ManageMetadataBase {
|
|
|
1603
1675
|
SET GeneratedEntityID=${idLit(generatedEntityId)}, SchemaName='${esc(coreSchema)}', TableName='${esc(tableName)}', ViewName='${esc(viewName)}',
|
|
1604
1676
|
${this.qi('ParamMode')}='${paramMode}', ${this.qi('RowFilterColumns')}=${rowFilterColumnsLit}, ${this.qi('BroadSQL')}=${broadSQLLit}, ${this.qi('ReadFilterSpec')}=${readFilterSpecLit}, ${this.qi('KeyColumns')}=${keyColumnsLit}, ${this.qi('RefreshStrategy')}='${refreshStrategy}'
|
|
1605
1677
|
WHERE ID='${matResultId}'`;
|
|
1606
|
-
await this.
|
|
1678
|
+
await this.logSQLAndExecute(pool, sqlUpd, `Update MJ: Materialized Results for query "${queryName}"`);
|
|
1607
1679
|
}
|
|
1608
1680
|
else {
|
|
1609
1681
|
matResultId = this.createNewUUID();
|
|
@@ -1613,13 +1685,13 @@ export class ManageMetadataBase {
|
|
|
1613
1685
|
${c('ParamMode')}, ${c('RowFilterColumns')}, ${c('BroadSQL')}, ${c('ReadFilterSpec')}, ${c('KeyColumns')}, ${c('RefreshStrategy')}, ${c('Status')}, ${c('__mj_CreatedAt')}, ${c('__mj_UpdatedAt')} )
|
|
1614
1686
|
VALUES ( '${matResultId}', 'Query', ${idLit(generatedEntityId)}, '${esc(coreSchema)}', '${esc(tableName)}', '${esc(viewName)}',
|
|
1615
1687
|
'${paramMode}', ${rowFilterColumnsLit}, ${broadSQLLit}, ${readFilterSpecLit}, ${keyColumnsLit}, '${refreshStrategy}', 'Building', ${this.utcNow()}, ${this.utcNow()} )`;
|
|
1616
|
-
await this.
|
|
1688
|
+
await this.logSQLAndExecute(pool, sqlIns, `Insert MJ: Materialized Results for query "${queryName}"`);
|
|
1617
1689
|
// Link the new materialization to its source Query via the join table.
|
|
1618
1690
|
const joinId = this.createNewUUID();
|
|
1619
1691
|
const sqlJoinIns = `INSERT INTO ${this.qs(coreSchema, 'MaterializedResultQuery')} (
|
|
1620
1692
|
${c('ID')}, ${c('MaterializedResultID')}, ${c('QueryID')}, ${c('__mj_CreatedAt')}, ${c('__mj_UpdatedAt')} )
|
|
1621
1693
|
VALUES ( '${joinId}', '${matResultId}', '${queryId}', ${this.utcNow()}, ${this.utcNow()} )`;
|
|
1622
|
-
await this.
|
|
1694
|
+
await this.logSQLAndExecute(pool, sqlJoinIns, `Link MJ: Materialized Results to Query "${queryName}" via join row`);
|
|
1623
1695
|
}
|
|
1624
1696
|
processedCount++;
|
|
1625
1697
|
}
|
|
@@ -1636,7 +1708,7 @@ export class ManageMetadataBase {
|
|
|
1636
1708
|
* auto-rebuilds. Already-held (`DriftHold`) and `Disabled` rows are skipped.
|
|
1637
1709
|
*/
|
|
1638
1710
|
async detectMaterializationDrift(pool) {
|
|
1639
|
-
const coreSchema =
|
|
1711
|
+
const coreSchema = MjCoreSchema();
|
|
1640
1712
|
// Gate on MaterializedResult existing — the SELECT below reads it. Absent (materialization migration not yet
|
|
1641
1713
|
// applied on this DB) ⇒ no materializations exist to check for drift; skip cleanly rather than throwing and
|
|
1642
1714
|
// aborting the codegen pass.
|
|
@@ -1741,14 +1813,14 @@ export class ManageMetadataBase {
|
|
|
1741
1813
|
// would only stop FUTURE refreshes while leaving the already-populated rows queryable via raw SQL
|
|
1742
1814
|
// over the wrapper view. Emptying (not dropping) removes the leaked data without breaking any object
|
|
1743
1815
|
// dependency; the wrapper view remains but returns nothing. Then DriftHold so refresh never refills it.
|
|
1744
|
-
await this.
|
|
1745
|
-
await this.
|
|
1816
|
+
await this.logSQLAndExecute(pool, `DELETE FROM ${this.qs(r.SchemaName, r.TableName)}`, `Empty external RLS base-view mirror "${r.TableName}" (leak guard — mirror exposed RLS-refused rows)`);
|
|
1817
|
+
await this.logSQLAndExecute(pool, `UPDATE ${this.qs(coreSchema, 'MaterializedResult')} SET ${this.qi('Status')}='DriftHold' WHERE ID='${r.ID}'`, `Flag external RLS base-view materialization "${r.TableName}" as DriftHold (leak guard)`);
|
|
1746
1818
|
logError(` > RLS LEAK GUARD: base-view materialization "${r.TableName}" mirrors an EXTERNAL row-restricted entity ("${bvEntity.Name}" — role RLS and/or an API-key row filter) → mirror EMPTIED + DriftHold (refresh stopped). It exposed rows the live path refuses under RLS.`);
|
|
1747
1819
|
return true;
|
|
1748
1820
|
}
|
|
1749
1821
|
}
|
|
1750
1822
|
const facts = await this.gatherDriftFacts(pool, md, r, coreSchema);
|
|
1751
|
-
const verdict =
|
|
1823
|
+
const verdict = EvaluateMaterializationDrift(facts);
|
|
1752
1824
|
if (verdict.drift) {
|
|
1753
1825
|
// Fail-closed on ANY hold of a QUERY materialization: once held, the row drops out of
|
|
1754
1826
|
// detectMaterializationDrift's `Status NOT IN ('DriftHold', ...)` scan, so the C1 RLS re-check and the
|
|
@@ -1760,7 +1832,7 @@ export class ManageMetadataBase {
|
|
|
1760
1832
|
if (r.SourceType === 'Query' && r.GeneratedEntityID) {
|
|
1761
1833
|
await this.revokeMaterializedEntityReadAccess(pool, r.GeneratedEntityID, r.TableName, `drift hold — ${verdict.reason ?? 'shape/provenance drift'}`);
|
|
1762
1834
|
}
|
|
1763
|
-
await this.
|
|
1835
|
+
await this.logSQLAndExecute(pool, `UPDATE ${this.qs(coreSchema, 'MaterializedResult')} SET ${this.qi('Status')}='DriftHold' WHERE ID='${r.ID}'`, `Flag materialization "${r.TableName}" as DriftHold`);
|
|
1764
1836
|
logStatus(` > DRIFT: materialization "${r.TableName}" → DriftHold — ${verdict.reason}`);
|
|
1765
1837
|
return true;
|
|
1766
1838
|
}
|
|
@@ -1794,7 +1866,7 @@ export class ManageMetadataBase {
|
|
|
1794
1866
|
if (r.GeneratedEntityID) {
|
|
1795
1867
|
await this.revokeMaterializedEntityReadAccess(pool, r.GeneratedEntityID, r.TableName, reason);
|
|
1796
1868
|
}
|
|
1797
|
-
await this.
|
|
1869
|
+
await this.logSQLAndExecute(pool, `UPDATE ${this.qs(coreSchema, 'MaterializedResult')} SET ${this.qi('Status')}='DriftHold' WHERE ID='${r.ID}'`, `Flag materialization "${r.TableName}" as DriftHold (${label.toLowerCase()})`);
|
|
1798
1870
|
logError(` > ${label}: materialization "${r.TableName}" → read access revoked + DriftHold — ${reason}`);
|
|
1799
1871
|
}
|
|
1800
1872
|
/** Gathers the drift-relevant existence facts for one materialization against current metadata. */
|
|
@@ -1905,7 +1977,7 @@ export class ManageMetadataBase {
|
|
|
1905
1977
|
* @param pool - the ConnectionPool object to use for querying and updating the database
|
|
1906
1978
|
* @returns
|
|
1907
1979
|
*/
|
|
1908
|
-
async
|
|
1980
|
+
async ManageMetadata(pool, currentUser) {
|
|
1909
1981
|
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
1910
1982
|
// Auto-exclude platform-specific system schemas.
|
|
1911
1983
|
// We mutate configInfo.excludeSchemas directly so that all downstream code
|
|
@@ -1926,7 +1998,7 @@ export class ManageMetadataBase {
|
|
|
1926
1998
|
// machinery — when the provider supplies DDL (PostgreSQL), we install it
|
|
1927
1999
|
// idempotently here rather than depending on migrations to have shipped
|
|
1928
2000
|
// it. Nothing below can work without them, so a failure here is fatal.
|
|
1929
|
-
const supportObjectsSQL = this.dbProvider.getMetadataSupportObjectsSQL(
|
|
2001
|
+
const supportObjectsSQL = this.dbProvider.getMetadataSupportObjectsSQL(MjCoreSchema());
|
|
1930
2002
|
if (supportObjectsSQL) {
|
|
1931
2003
|
try {
|
|
1932
2004
|
await pool.query(supportObjectsSQL);
|
|
@@ -1940,7 +2012,7 @@ export class ManageMetadataBase {
|
|
|
1940
2012
|
// Authored exclude list (sys, staging, …) must be captured BEFORE includeSchemas is
|
|
1941
2013
|
// compiled into excludeSchemas. Heal EXEC statements use that original list plus
|
|
1942
2014
|
// @IncludedSchemaNames — never the sibling snapshot of this machine's database.
|
|
1943
|
-
|
|
2015
|
+
SnapshotAuthoredExcludeSchemas(configInfo.excludeSchemas);
|
|
1944
2016
|
// Resolve the opt-in `includeSchemas` positive scope into excludeSchemas, BEFORE the exclude
|
|
1945
2017
|
// snapshot below and before createNewEntities() runs. The universe is queried from the DATABASE
|
|
1946
2018
|
// rather than taken from loaded metadata on purpose: createNewEntities() discovers tables with no
|
|
@@ -1950,12 +2022,12 @@ export class ManageMetadataBase {
|
|
|
1950
2022
|
// every run after, orphaning its entity records). No-op when includeSchemas is unset.
|
|
1951
2023
|
if (configInfo.includeSchemas && configInfo.includeSchemas.length > 0) {
|
|
1952
2024
|
try {
|
|
1953
|
-
const schemaSQL = `SELECT DISTINCT ${this.qi('SchemaName')} FROM ${this.qs(
|
|
2025
|
+
const schemaSQL = `SELECT DISTINCT ${this.qi('SchemaName')} FROM ${this.qs(MjCoreSchema(), 'vwSQLTablesAndEntities')}`;
|
|
1954
2026
|
const schemaResult = await this.runQuery(pool, schemaSQL);
|
|
1955
2027
|
const allSchemas = (schemaResult.recordset ?? [])
|
|
1956
2028
|
.map((r) => r.SchemaName)
|
|
1957
2029
|
.filter((s) => !!s);
|
|
1958
|
-
const newlyExcluded =
|
|
2030
|
+
const newlyExcluded = ApplyIncludeSchemaScope(allSchemas, configInfo);
|
|
1959
2031
|
logStatus(` Applied includeSchemas scope [${configInfo.includeSchemas.join(', ')}] — excluded ${newlyExcluded.length} other schema(s) present in the database`);
|
|
1960
2032
|
}
|
|
1961
2033
|
catch (e) {
|
|
@@ -2005,12 +2077,12 @@ export class ManageMetadataBase {
|
|
|
2005
2077
|
start = new Date();
|
|
2006
2078
|
logStatus(' Recompiling base views...');
|
|
2007
2079
|
const sqlUtility = MJGlobal.Instance.ClassFactory.CreateInstance(SQLUtilityBase);
|
|
2008
|
-
const adminSchema =
|
|
2009
|
-
const schemasToExclude =
|
|
2080
|
+
const adminSchema = GetSettingValue('mj_core_schema', '__mj');
|
|
2081
|
+
const schemasToExclude = GetSettingValue('recompile_mj_views', true)
|
|
2010
2082
|
? excludeSchemas.filter((s) => s !== adminSchema)
|
|
2011
2083
|
: excludeSchemas;
|
|
2012
2084
|
if (!await sqlUtility.recompileAllBaseViews(pool, schemasToExclude, true, ManageMetadataBase._newEntityList /*exclude the newly created entities from the above step the first time we run as those views don't exist yet*/)) {
|
|
2013
|
-
|
|
2085
|
+
LogMessage(' Warning: Non-Fatal error recompiling base views', SeverityType.Warning, false);
|
|
2014
2086
|
// many times the former versions of base views will NOT succesfully recompile, so don't consider that scenario to be a
|
|
2015
2087
|
// failure for this entire function
|
|
2016
2088
|
}
|
|
@@ -2021,7 +2093,7 @@ export class ManageMetadataBase {
|
|
|
2021
2093
|
// Also skip deleting unneeded fields on this first pass — base views haven't been regenerated yet,
|
|
2022
2094
|
// so virtual fields (which come from view JOINs) would be incorrectly identified as orphaned and deleted.
|
|
2023
2095
|
// Deletion runs on the second pass (in sql_codegen.ts) after views are current.
|
|
2024
|
-
if (!await this.
|
|
2096
|
+
if (!await this.ManageEntityFields(pool, excludeSchemas, false, false, currentUser, true, true)) {
|
|
2025
2097
|
logError(' Error managing entity fields');
|
|
2026
2098
|
bSuccess = false;
|
|
2027
2099
|
}
|
|
@@ -2134,6 +2206,10 @@ export class ManageMetadataBase {
|
|
|
2134
2206
|
await this.checkEntityFieldSequenceIntegrity(pool, excludeSchemas);
|
|
2135
2207
|
return bSuccess;
|
|
2136
2208
|
}
|
|
2209
|
+
/** @deprecated Use {@link ManageMetadata}. */
|
|
2210
|
+
async manageMetadata(pool, currentUser) {
|
|
2211
|
+
return this.ManageMetadata(pool, currentUser);
|
|
2212
|
+
}
|
|
2137
2213
|
/**
|
|
2138
2214
|
* INTEGRITY CHECK — in a well-formed entity every base (non-virtual) field sequences BEFORE the
|
|
2139
2215
|
* virtual/related fields, so the EntityField order matches the base view's `SELECT [base].*, <joins>`
|
|
@@ -2147,10 +2223,10 @@ export class ManageMetadataBase {
|
|
|
2147
2223
|
async checkEntityFieldSequenceIntegrity(pool, excludeSchemas) {
|
|
2148
2224
|
try {
|
|
2149
2225
|
const excl = excludeSchemas.length > 0
|
|
2150
|
-
? `WHERE ${this.qi('EntityID')} NOT IN (SELECT ${this.qi('ID')} FROM ${this.qs(
|
|
2226
|
+
? `WHERE ${this.qi('EntityID')} NOT IN (SELECT ${this.qi('ID')} FROM ${this.qs(MjCoreSchema(), 'Entity')} WHERE ${this.qi('SchemaName')} IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))`
|
|
2151
2227
|
: '';
|
|
2152
2228
|
const sSQL = `SELECT ${this.qi('EntityID')}, ${this.qi('Entity')}, ${this.qi('Name')}, ${this.qi('Sequence')}, ${this.qi('IsVirtual')}
|
|
2153
|
-
FROM ${this.qs(
|
|
2229
|
+
FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')}
|
|
2154
2230
|
${excl}
|
|
2155
2231
|
ORDER BY ${this.qi('EntityID')}, ${this.qi('Sequence')}, ${this.qi('Name')}`;
|
|
2156
2232
|
const result = await this.runQuery(pool, sSQL);
|
|
@@ -2202,7 +2278,7 @@ export class ManageMetadataBase {
|
|
|
2202
2278
|
// virtual entities are records defined in the entity metadata and do NOT define a distinct base table
|
|
2203
2279
|
// but they do specify a base view. We DO NOT generate a base view for a virtual entity, we simply use it to figure
|
|
2204
2280
|
// out the fields that should be in the entity definition and add/update/delete the entity definition to match what's in the view when this runs
|
|
2205
|
-
const sql = `SELECT * FROM ${this.qs(
|
|
2281
|
+
const sql = `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntities')} WHERE VirtualEntity = ${this.boolLit(true)}`;
|
|
2206
2282
|
const virtualEntitiesResult = await this.runQuery(pool, sql);
|
|
2207
2283
|
const virtualEntities = virtualEntitiesResult.recordset;
|
|
2208
2284
|
let anyUpdates = false;
|
|
@@ -2262,7 +2338,7 @@ export class ManageMetadataBase {
|
|
|
2262
2338
|
async materializedResultTableExists(pool) {
|
|
2263
2339
|
if (this._materializedResultTableExists === null) {
|
|
2264
2340
|
const sql = `SELECT COUNT(*) AS TblExists FROM INFORMATION_SCHEMA.TABLES ` +
|
|
2265
|
-
`WHERE TABLE_SCHEMA = '${
|
|
2341
|
+
`WHERE TABLE_SCHEMA = '${MjCoreSchema()}' AND TABLE_NAME = 'MaterializedResult'`;
|
|
2266
2342
|
const result = await this.runQuery(pool, sql);
|
|
2267
2343
|
const row = (result.recordset?.[0] ?? {});
|
|
2268
2344
|
const cnt = row.TblExists ?? row.tblexists ?? 0;
|
|
@@ -2280,7 +2356,7 @@ export class ManageMetadataBase {
|
|
|
2280
2356
|
if (!(await this.entityHasExternalDataSourceColumn(pool))) {
|
|
2281
2357
|
return { success: true, anyUpdates: false, relationshipsUpdated: false };
|
|
2282
2358
|
}
|
|
2283
|
-
const sql = `SELECT * FROM ${this.qs(
|
|
2359
|
+
const sql = `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntities')} WHERE ExternalDataSourceID IS NOT NULL`;
|
|
2284
2360
|
const result = await this.runQuery(pool, sql);
|
|
2285
2361
|
const externalEntities = result.recordset;
|
|
2286
2362
|
if (!externalEntities || externalEntities.length === 0) {
|
|
@@ -2397,16 +2473,16 @@ export class ManageMetadataBase {
|
|
|
2397
2473
|
}
|
|
2398
2474
|
// Map each introspected column into the veField shape that manageSingleVirtualEntityField consumes.
|
|
2399
2475
|
const eeFields = obj.Columns.map(c => {
|
|
2400
|
-
const t =
|
|
2476
|
+
const t = MapExternalNativeTypeToMJ(c.NativeType);
|
|
2401
2477
|
return { FieldName: c.Name, Type: t.Type, Length: t.Length, Precision: t.Precision, Scale: t.Scale, AllowsNull: c.Nullable, IsPrimaryKey: c.IsPrimaryKey };
|
|
2402
2478
|
});
|
|
2403
2479
|
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
2404
2480
|
const entity = md.EntityByName(externalEntity.Name);
|
|
2405
2481
|
if (entity) {
|
|
2406
2482
|
// remove EntityFields no longer present in the remote object
|
|
2407
|
-
const sqlRemove = this.buildExternalFieldRemoveSQL(
|
|
2483
|
+
const sqlRemove = this.buildExternalFieldRemoveSQL(MjCoreSchema(), entity.Fields, eeFields.map(ef => ef.FieldName));
|
|
2408
2484
|
if (sqlRemove) {
|
|
2409
|
-
await this.
|
|
2485
|
+
await this.logSQLAndExecute(pool, sqlRemove, `SQL text to remove fields from external entity ${externalEntity.Name}`);
|
|
2410
2486
|
bUpdated = true;
|
|
2411
2487
|
}
|
|
2412
2488
|
// create/update each remote column. Use the real PK info from introspection; fall back
|
|
@@ -2437,8 +2513,8 @@ export class ManageMetadataBase {
|
|
|
2437
2513
|
}
|
|
2438
2514
|
}
|
|
2439
2515
|
if (bUpdated) {
|
|
2440
|
-
const sqlUpdate = `UPDATE ${this.qs(
|
|
2441
|
-
await this.
|
|
2516
|
+
const sqlUpdate = `UPDATE ${this.qs(MjCoreSchema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${externalEntity.ID}'`;
|
|
2517
|
+
await this.logSQLAndExecute(pool, sqlUpdate, `SQL text to update external entity updated date for ${externalEntity.Name}`);
|
|
2442
2518
|
}
|
|
2443
2519
|
return { success: bSuccess, updatedEntity: bUpdated, relationshipsUpdated: bRelationshipsUpdated };
|
|
2444
2520
|
}
|
|
@@ -2495,7 +2571,7 @@ export class ManageMetadataBase {
|
|
|
2495
2571
|
if (alreadySet) {
|
|
2496
2572
|
continue;
|
|
2497
2573
|
}
|
|
2498
|
-
sqlStatements.push(`UPDATE ${this.qs(
|
|
2574
|
+
sqlStatements.push(`UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
2499
2575
|
SET RelatedEntityID='${relatedEntity.ID}',
|
|
2500
2576
|
RelatedEntityFieldName='${refColumn.replace(/'/g, "''")}',
|
|
2501
2577
|
IsSoftForeignKey=1
|
|
@@ -2505,7 +2581,7 @@ export class ManageMetadataBase {
|
|
|
2505
2581
|
if (sqlStatements.length === 0) {
|
|
2506
2582
|
return false;
|
|
2507
2583
|
}
|
|
2508
|
-
await this.
|
|
2584
|
+
await this.logSQLBatchAndExecute(pool, sqlStatements, `Set external foreign keys for ${externalEntity.Name}`);
|
|
2509
2585
|
return true;
|
|
2510
2586
|
}
|
|
2511
2587
|
async manageSingleVirtualEntity(pool, virtualEntity) {
|
|
@@ -2529,9 +2605,9 @@ export class ManageMetadataBase {
|
|
|
2529
2605
|
removeList.push(f.ID);
|
|
2530
2606
|
}
|
|
2531
2607
|
if (removeList.length > 0) {
|
|
2532
|
-
const sqlRemove = `DELETE FROM ${this.qs(
|
|
2608
|
+
const sqlRemove = `DELETE FROM ${this.qs(MjCoreSchema(), 'EntityField')} WHERE ID IN (${removeList.map(removeId => `'${removeId}'`).join(',')})`;
|
|
2533
2609
|
// this removes the fields that shouldn't be there anymore
|
|
2534
|
-
await this.
|
|
2610
|
+
await this.logSQLAndExecute(pool, sqlRemove, `SQL text to remove fields from entity ${virtualEntity.Name}`);
|
|
2535
2611
|
bUpdated = true;
|
|
2536
2612
|
}
|
|
2537
2613
|
// check to see if any of the fields in the virtual entity have Pkey attribute set. If not, we will default to the first field
|
|
@@ -2551,8 +2627,8 @@ export class ManageMetadataBase {
|
|
|
2551
2627
|
}
|
|
2552
2628
|
if (bUpdated) {
|
|
2553
2629
|
// finally make sure we update the UpdatedAt field for the entity if we made changes to its fields
|
|
2554
|
-
const sqlUpdate = `UPDATE ${this.qs(
|
|
2555
|
-
await this.
|
|
2630
|
+
const sqlUpdate = `UPDATE ${this.qs(MjCoreSchema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${virtualEntity.ID}'`;
|
|
2631
|
+
await this.logSQLAndExecute(pool, sqlUpdate, `SQL text to update virtual entity updated date for ${virtualEntity.Name}`);
|
|
2556
2632
|
}
|
|
2557
2633
|
return { success: bSuccess, updatedEntity: bUpdated };
|
|
2558
2634
|
}
|
|
@@ -2620,7 +2696,7 @@ export class ManageMetadataBase {
|
|
|
2620
2696
|
field.Sequence !== fieldSequence) {
|
|
2621
2697
|
// the field needs to be updated, so update it
|
|
2622
2698
|
const sqlUpdate = `UPDATE
|
|
2623
|
-
${this.qs(
|
|
2699
|
+
${this.qs(MjCoreSchema(), 'EntityField')}
|
|
2624
2700
|
SET
|
|
2625
2701
|
Sequence=${fieldSequence},
|
|
2626
2702
|
Type='${veField.Type}',
|
|
@@ -2631,7 +2707,7 @@ export class ManageMetadataBase {
|
|
|
2631
2707
|
Scale=${veField.Scale}
|
|
2632
2708
|
WHERE
|
|
2633
2709
|
ID = '${field.ID}'`; // don't need to update the __mj_UpdatedAt field here, that happens automatically via the trigger
|
|
2634
|
-
await this.
|
|
2710
|
+
await this.logSQLAndExecute(pool, sqlUpdate, `SQL text to update virtual entity field ${veField.FieldName} for entity ${virtualEntity.Name}`);
|
|
2635
2711
|
didUpdate = true;
|
|
2636
2712
|
}
|
|
2637
2713
|
}
|
|
@@ -2641,7 +2717,7 @@ export class ManageMetadataBase {
|
|
|
2641
2717
|
// Apply-time Sequence: see applyTimeEntityFieldSequenceSQL. A literal here would be
|
|
2642
2718
|
// replayed verbatim from the CodeGen capture and collide on a fresh install.
|
|
2643
2719
|
const q = (n) => this.qi(n);
|
|
2644
|
-
const sqlAdd = `INSERT INTO ${this.qs(
|
|
2720
|
+
const sqlAdd = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntityField')} (
|
|
2645
2721
|
${q('ID')}, ${q('EntityID')}, ${q('Name')}, ${q('Type')}, ${q('AllowsNull')},
|
|
2646
2722
|
${q('Length')}, ${q('Precision')}, ${q('Scale')},
|
|
2647
2723
|
${q('Sequence')}, ${q('IsPrimaryKey')}, ${q('IsUnique')},
|
|
@@ -2651,7 +2727,7 @@ export class ManageMetadataBase {
|
|
|
2651
2727
|
${this.applyTimeEntityFieldSequenceSQL(entity.ID)}, ${this.boolLit(wantPrimaryKey)}, ${this.boolLit(wantUnique)},
|
|
2652
2728
|
${this.utcNow()}, ${this.utcNow()}
|
|
2653
2729
|
)`;
|
|
2654
|
-
await this.
|
|
2730
|
+
await this.logSQLAndExecute(pool, sqlAdd, `SQL text to add virtual entity field ${veField.FieldName} for entity ${virtualEntity.Name}`);
|
|
2655
2731
|
ManageMetadataBase.registerNewField(entity.ID, veField.FieldName);
|
|
2656
2732
|
didUpdate = true;
|
|
2657
2733
|
}
|
|
@@ -2747,7 +2823,7 @@ export class ManageMetadataBase {
|
|
|
2747
2823
|
return { decorated: false, skipped: false };
|
|
2748
2824
|
}
|
|
2749
2825
|
// Apply results to EntityField records
|
|
2750
|
-
const schema =
|
|
2826
|
+
const schema = MjCoreSchema();
|
|
2751
2827
|
let anyUpdated = false;
|
|
2752
2828
|
// Apply primary keys
|
|
2753
2829
|
anyUpdated = await this.applyLLMPrimaryKeys(pool, entity, result.primaryKeys, schema) || anyUpdated;
|
|
@@ -2759,7 +2835,7 @@ export class ManageMetadataBase {
|
|
|
2759
2835
|
anyUpdated = await this.applyVEFieldCategories(pool, entity, result) || anyUpdated;
|
|
2760
2836
|
if (anyUpdated) {
|
|
2761
2837
|
const sqlUpdate = `UPDATE ${this.qs(schema, 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${entity.ID}'`;
|
|
2762
|
-
await this.
|
|
2838
|
+
await this.logSQLAndExecute(pool, sqlUpdate, `Update entity timestamp for ${entity.Name} after LLM decoration`);
|
|
2763
2839
|
}
|
|
2764
2840
|
return { decorated: anyUpdated, skipped: false };
|
|
2765
2841
|
}
|
|
@@ -2813,7 +2889,7 @@ export class ManageMetadataBase {
|
|
|
2813
2889
|
return false;
|
|
2814
2890
|
}
|
|
2815
2891
|
// Load VE EntityField rows from DB (we need the ID and auto-update flags)
|
|
2816
|
-
const schema =
|
|
2892
|
+
const schema = MjCoreSchema();
|
|
2817
2893
|
const fieldsSQL = `
|
|
2818
2894
|
SELECT ID, Name, Category, AutoUpdateCategory, AutoUpdateDisplayName, AutoUpdateExtendedType, GeneratedFormSection, DisplayName, ExtendedType, CodeType, ValueListType
|
|
2819
2895
|
FROM ${this.qs(schema, 'EntityField')}
|
|
@@ -2877,7 +2953,7 @@ export class ManageMetadataBase {
|
|
|
2877
2953
|
WHERE EntityID='${entity.ID}' AND Name='${pk}'`);
|
|
2878
2954
|
logStatus(` ✓ Set PK for ${entity.Name}.${pk} (LLM-identified)`);
|
|
2879
2955
|
}
|
|
2880
|
-
await this.
|
|
2956
|
+
await this.logSQLBatchAndExecute(pool, sqlStatements, `Set LLM-identified PKs for ${entity.Name}: ${validPKs.join(', ')}`);
|
|
2881
2957
|
return true;
|
|
2882
2958
|
}
|
|
2883
2959
|
/**
|
|
@@ -2922,7 +2998,7 @@ export class ManageMetadataBase {
|
|
|
2922
2998
|
if (sqlStatements.length === 0) {
|
|
2923
2999
|
return false;
|
|
2924
3000
|
}
|
|
2925
|
-
await this.
|
|
3001
|
+
await this.logSQLBatchAndExecute(pool, sqlStatements, `Set LLM-identified FKs for ${entity.Name}`);
|
|
2926
3002
|
return true;
|
|
2927
3003
|
}
|
|
2928
3004
|
/**
|
|
@@ -2962,7 +3038,7 @@ export class ManageMetadataBase {
|
|
|
2962
3038
|
if (sqlStatements.length === 0) {
|
|
2963
3039
|
return false;
|
|
2964
3040
|
}
|
|
2965
|
-
await this.
|
|
3041
|
+
await this.logSQLBatchAndExecute(pool, sqlStatements, `Set LLM-generated descriptions for ${entity.Name} (${sqlStatements.length} fields)`);
|
|
2966
3042
|
return true;
|
|
2967
3043
|
}
|
|
2968
3044
|
/**
|
|
@@ -3087,7 +3163,7 @@ export class ManageMetadataBase {
|
|
|
3087
3163
|
// unquoted reference folds to lower case on PostgreSQL — `column "length" does not exist`.
|
|
3088
3164
|
// SQL Server's case-insensitive resolution hid this for as long as IS-A ran only there.
|
|
3089
3165
|
const existsResult = await this.runQueryWithParams(pool, `SELECT ${this.qi('ID')}, ${this.qi('IsVirtual')}, ${this.qi('Type')}, ${this.qi('Length')}, ${this.qi('Precision')}, ${this.qi('Scale')}, ${this.qi('AllowsNull')}, ${this.qi('AllowUpdateAPI')}
|
|
3090
|
-
FROM ${this.qs(
|
|
3166
|
+
FROM ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
3091
3167
|
WHERE ${this.qi('EntityID')} = @EntityID AND ${this.qi('Name')} = @FieldName`, { 'EntityID': childEntity.ID, 'FieldName': parentField.Name });
|
|
3092
3168
|
if (existsResult.recordset.length > 0) {
|
|
3093
3169
|
// Field already exists — update it to ensure it's marked as a virtual IS-A field
|
|
@@ -3100,7 +3176,7 @@ export class ManageMetadataBase {
|
|
|
3100
3176
|
existingRow.AllowsNull !== parentField.AllowsNull ||
|
|
3101
3177
|
!existingRow.AllowUpdateAPI;
|
|
3102
3178
|
if (needsUpdate) {
|
|
3103
|
-
const sqlUpdate = `UPDATE ${this.qs(
|
|
3179
|
+
const sqlUpdate = `UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
3104
3180
|
SET ${this.qi('IsVirtual')}=${this.boolLit(true)},
|
|
3105
3181
|
${this.qi('Type')}='${parentField.Type}',
|
|
3106
3182
|
${this.qi('Length')}=${parentField.Length},
|
|
@@ -3109,7 +3185,7 @@ export class ManageMetadataBase {
|
|
|
3109
3185
|
${this.qi('AllowsNull')}=${this.boolLit(parentField.AllowsNull)},
|
|
3110
3186
|
${this.qi('AllowUpdateAPI')}=${this.boolLit(true)}
|
|
3111
3187
|
WHERE ${this.qi('ID')}='${existingRow.ID}'`;
|
|
3112
|
-
await this.
|
|
3188
|
+
await this.logSQLAndExecute(pool, sqlUpdate, `Update IS-A parent field ${parentField.Name} on ${childEntity.Name}`);
|
|
3113
3189
|
bUpdated = true;
|
|
3114
3190
|
}
|
|
3115
3191
|
}
|
|
@@ -3119,7 +3195,7 @@ export class ManageMetadataBase {
|
|
|
3119
3195
|
// Apply-time Sequence (see applyTimeEntityFieldSequenceSQL); reordered by
|
|
3120
3196
|
// updateExistingEntityFieldsFromSchema afterwards.
|
|
3121
3197
|
const q = (n) => this.qi(n);
|
|
3122
|
-
const sqlInsert = `INSERT INTO ${this.qs(
|
|
3198
|
+
const sqlInsert = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntityField')} (
|
|
3123
3199
|
${q('ID')}, ${q('EntityID')}, ${q('Name')}, ${q('Type')}, ${q('AllowsNull')},
|
|
3124
3200
|
${q('Length')}, ${q('Precision')}, ${q('Scale')},
|
|
3125
3201
|
${q('Sequence')}, ${q('IsVirtual')}, ${q('AllowUpdateAPI')},
|
|
@@ -3131,7 +3207,7 @@ export class ManageMetadataBase {
|
|
|
3131
3207
|
${parentField.Length}, ${parentField.Precision}, ${parentField.Scale},
|
|
3132
3208
|
${this.applyTimeEntityFieldSequenceSQL(childEntity.ID)}, ${this.boolLit(true)}, ${this.boolLit(true)}, ${this.boolLit(false)}, ${this.boolLit(false)},
|
|
3133
3209
|
${this.utcNow()}, ${this.utcNow()})`;
|
|
3134
|
-
await this.
|
|
3210
|
+
await this.logSQLAndExecute(pool, sqlInsert, `Create IS-A parent field ${parentField.Name} on ${childEntity.Name}`);
|
|
3135
3211
|
ManageMetadataBase.registerNewField(childEntity.ID, parentField.Name);
|
|
3136
3212
|
bUpdated = true;
|
|
3137
3213
|
}
|
|
@@ -3143,13 +3219,13 @@ export class ManageMetadataBase {
|
|
|
3143
3219
|
!f.IsPrimaryKey && !f.Name.startsWith('__mj_') &&
|
|
3144
3220
|
!currentParentFieldNames.has(f.Name.toLowerCase()));
|
|
3145
3221
|
for (const staleField of staleFields) {
|
|
3146
|
-
const sqlDelete = `DELETE FROM ${this.qs(
|
|
3147
|
-
await this.
|
|
3222
|
+
const sqlDelete = `DELETE FROM ${this.qs(MjCoreSchema(), 'EntityField')} WHERE ID='${staleField.ID}'`;
|
|
3223
|
+
await this.logSQLAndExecute(pool, sqlDelete, `Remove stale IS-A parent field ${staleField.Name} from ${childEntity.Name}`);
|
|
3148
3224
|
bUpdated = true;
|
|
3149
3225
|
}
|
|
3150
3226
|
if (bUpdated) {
|
|
3151
|
-
const sqlUpdate = `UPDATE ${this.qs(
|
|
3152
|
-
await this.
|
|
3227
|
+
const sqlUpdate = `UPDATE ${this.qs(MjCoreSchema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${childEntity.ID}'`;
|
|
3228
|
+
await this.logSQLAndExecute(pool, sqlUpdate, `Update entity timestamp for ${childEntity.Name} after IS-A field sync`);
|
|
3153
3229
|
}
|
|
3154
3230
|
return { success: true, updated: bUpdated };
|
|
3155
3231
|
}
|
|
@@ -3200,16 +3276,16 @@ export class ManageMetadataBase {
|
|
|
3200
3276
|
try {
|
|
3201
3277
|
// STEP 1 - search for all foreign keys in the vwEntityFields view, we use the RelatedEntityID field to determine our FKs
|
|
3202
3278
|
const sSQL = `SELECT *
|
|
3203
|
-
FROM ${this.qs(
|
|
3279
|
+
FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')}
|
|
3204
3280
|
WHERE
|
|
3205
3281
|
RelatedEntityID IS NOT NULL AND
|
|
3206
3282
|
IsVirtual = ${this.boolLit(false)} AND
|
|
3207
|
-
EntityID NOT IN (SELECT ID FROM ${this.qs(
|
|
3283
|
+
EntityID NOT IN (SELECT ID FROM ${this.qs(MjCoreSchema(), 'Entity')} WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
|
|
3208
3284
|
ORDER BY RelatedEntityID, EntityID, Sequence, ID`;
|
|
3209
3285
|
const entityFieldsResult = await this.runQuery(pool, sSQL);
|
|
3210
3286
|
const entityFields = entityFieldsResult.recordset;
|
|
3211
3287
|
// Get the relationship counts for each entity
|
|
3212
|
-
const sSQLRelationshipCount = `SELECT ${this.qi('EntityID')}, COUNT(*) AS ${this.qi('Count')} FROM ${this.qs(
|
|
3288
|
+
const sSQLRelationshipCount = `SELECT ${this.qi('EntityID')}, COUNT(*) AS ${this.qi('Count')} FROM ${this.qs(MjCoreSchema(), 'EntityRelationship')} GROUP BY ${this.qi('EntityID')}`;
|
|
3213
3289
|
const relationshipCountsResult = await this.runQuery(pool, sSQLRelationshipCount);
|
|
3214
3290
|
const relationshipCounts = relationshipCountsResult.recordset;
|
|
3215
3291
|
const relationshipCountMap = new Map();
|
|
@@ -3218,7 +3294,7 @@ export class ManageMetadataBase {
|
|
|
3218
3294
|
relationshipCountMap.set(rc.EntityID, Number(rc.Count));
|
|
3219
3295
|
}
|
|
3220
3296
|
// get all relationships in one query for performance improvement
|
|
3221
|
-
const sSQLRelationship = `SELECT * FROM ${this.qs(
|
|
3297
|
+
const sSQLRelationship = `SELECT * FROM ${this.qs(MjCoreSchema(), 'EntityRelationship')}`;
|
|
3222
3298
|
const allRelationshipsResult = await this.runQuery(pool, sSQLRelationship);
|
|
3223
3299
|
const allRelationships = allRelationshipsResult.recordset;
|
|
3224
3300
|
// Group FK fields by entity pair (ParentEntityID -> ChildEntityID) so we can
|
|
@@ -3244,12 +3320,12 @@ export class ManageMetadataBase {
|
|
|
3244
3320
|
}
|
|
3245
3321
|
batchCount++;
|
|
3246
3322
|
if (batchCount % batchItems === 0 && batchSQL.length > 0) {
|
|
3247
|
-
await this.
|
|
3323
|
+
await this.logSQLAndExecute(pool, batchSQL);
|
|
3248
3324
|
batchSQL = '';
|
|
3249
3325
|
}
|
|
3250
3326
|
}
|
|
3251
3327
|
if (batchSQL.length > 0) {
|
|
3252
|
-
await this.
|
|
3328
|
+
await this.logSQLAndExecute(pool, batchSQL);
|
|
3253
3329
|
}
|
|
3254
3330
|
// NOTE: Stale relationship cleanup is intentionally NOT done here because this
|
|
3255
3331
|
// method runs BEFORE deleteUnneededEntityFields(). Stale EntityField records
|
|
@@ -3290,8 +3366,8 @@ export class ManageMetadataBase {
|
|
|
3290
3366
|
const relCount = relationshipCountMap.get(parentEntityID) || 0;
|
|
3291
3367
|
const sequence = relCount + 1;
|
|
3292
3368
|
const newEntityRelationshipUUID = this.createNewUUID();
|
|
3293
|
-
const checkQuery = `SELECT 1 FROM ${this.qs(
|
|
3294
|
-
const insertSQL = `INSERT INTO ${this.qs(
|
|
3369
|
+
const checkQuery = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'EntityRelationship')} WHERE ${this.qi('ID')} = '${newEntityRelationshipUUID}'`;
|
|
3370
|
+
const insertSQL = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntityRelationship')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('RelatedEntityID')}, ${this.qi('RelatedEntityJoinField')}, ${this.qi('Type')}, ${this.qi('BundleInAPI')}, ${this.qi('DisplayInForm')}, ${this.qi('Sequence')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')})
|
|
3295
3371
|
VALUES ('${newEntityRelationshipUUID}', '${f.RelatedEntityID}', '${f.EntityID}', '${f.Name}', 'One To Many', ${this.boolLit(true)}, ${this.boolLit(true)}, ${sequence}, ${this.utcNow()}, ${this.utcNow()})`;
|
|
3296
3372
|
relationshipCountMap.set(parentEntityID, sequence);
|
|
3297
3373
|
return `
|
|
@@ -3353,7 +3429,7 @@ export class ManageMetadataBase {
|
|
|
3353
3429
|
logStatus(` > Updating EntityRelationship join field: ${oldJoinField} -> ${newJoinField} (ID: ${r.ID})`);
|
|
3354
3430
|
sql += `
|
|
3355
3431
|
/* Update EntityRelationship join field from '${oldJoinField}' to '${newJoinField}' */
|
|
3356
|
-
UPDATE ${this.qs(
|
|
3432
|
+
UPDATE ${this.qs(MjCoreSchema(), 'EntityRelationship')}
|
|
3357
3433
|
SET ${this.qi('RelatedEntityJoinField')} = '${newJoinField}',
|
|
3358
3434
|
${this.qi('__mj_UpdatedAt')} = ${this.utcNow()}
|
|
3359
3435
|
WHERE ${this.qi('ID')} = '${r.ID}';
|
|
@@ -3375,18 +3451,18 @@ export class ManageMetadataBase {
|
|
|
3375
3451
|
* @param pool - database connection
|
|
3376
3452
|
* @param excludeSchemas - schemas to exclude from FK field lookup
|
|
3377
3453
|
*/
|
|
3378
|
-
async
|
|
3454
|
+
async CleanupStaleEntityRelationships(pool, excludeSchemas) {
|
|
3379
3455
|
try {
|
|
3380
3456
|
const sSQL = `SELECT *
|
|
3381
|
-
FROM ${this.qs(
|
|
3457
|
+
FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')}
|
|
3382
3458
|
WHERE
|
|
3383
3459
|
RelatedEntityID IS NOT NULL AND
|
|
3384
3460
|
IsVirtual = ${this.boolLit(false)} AND
|
|
3385
|
-
EntityID NOT IN (SELECT ID FROM ${this.qs(
|
|
3461
|
+
EntityID NOT IN (SELECT ID FROM ${this.qs(MjCoreSchema(), 'Entity')} WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
|
|
3386
3462
|
ORDER BY RelatedEntityID, EntityID, Sequence, ID`;
|
|
3387
3463
|
const entityFieldsResult = await this.runQuery(pool, sSQL);
|
|
3388
3464
|
const entityFields = entityFieldsResult.recordset;
|
|
3389
|
-
const sSQLRelationship = `SELECT * FROM ${this.qs(
|
|
3465
|
+
const sSQLRelationship = `SELECT * FROM ${this.qs(MjCoreSchema(), 'EntityRelationship')}`;
|
|
3390
3466
|
const allRelationshipsResult = await this.runQuery(pool, sSQLRelationship);
|
|
3391
3467
|
const allRelationships = allRelationshipsResult.recordset;
|
|
3392
3468
|
await this.removeStaleOneToManyRelationships(pool, allRelationships, entityFields);
|
|
@@ -3397,6 +3473,10 @@ export class ManageMetadataBase {
|
|
|
3397
3473
|
return false;
|
|
3398
3474
|
}
|
|
3399
3475
|
}
|
|
3476
|
+
/** @deprecated Use {@link CleanupStaleEntityRelationships}. */
|
|
3477
|
+
async cleanupStaleEntityRelationships(pool, excludeSchemas) {
|
|
3478
|
+
return this.CleanupStaleEntityRelationships(pool, excludeSchemas);
|
|
3479
|
+
}
|
|
3400
3480
|
/**
|
|
3401
3481
|
* Removes stale One-To-Many EntityRelationship records whose RelatedEntityJoinField no longer
|
|
3402
3482
|
* corresponds to a valid FK field in the database. Only removes relationships where
|
|
@@ -3419,10 +3499,10 @@ export class ManageMetadataBase {
|
|
|
3419
3499
|
logStatus(` > Removing stale EntityRelationship: ${r.Entity} -> ${r.RelatedEntity} via '${r.RelatedEntityJoinField}' (ID: ${r.ID})`);
|
|
3420
3500
|
deleteSQL += `
|
|
3421
3501
|
/* Remove stale EntityRelationship: ${r.Entity} -> ${r.RelatedEntity} (FK field '${r.RelatedEntityJoinField}' no longer exists) */
|
|
3422
|
-
DELETE FROM ${this.qs(
|
|
3502
|
+
DELETE FROM ${this.qs(MjCoreSchema(), 'EntityRelationship')} WHERE ${this.qi('ID')} = '${r.ID}';
|
|
3423
3503
|
`;
|
|
3424
3504
|
}
|
|
3425
|
-
await this.
|
|
3505
|
+
await this.logSQLAndExecute(pool, deleteSQL, 'Remove stale One-To-Many EntityRelationships');
|
|
3426
3506
|
}
|
|
3427
3507
|
}
|
|
3428
3508
|
/**
|
|
@@ -3433,7 +3513,7 @@ export class ManageMetadataBase {
|
|
|
3433
3513
|
*/
|
|
3434
3514
|
async checkAndRemoveMetadataForDeletedTables(pool, excludeSchemas) {
|
|
3435
3515
|
try {
|
|
3436
|
-
const sql = `SELECT * FROM ${this.qs(
|
|
3516
|
+
const sql = `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntitiesWithMissingBaseTables')}${this.dbProvider.getEntitiesWithMissingBaseTablesFilter()}`;
|
|
3437
3517
|
const entitiesResult = await this.runQuery(pool, sql);
|
|
3438
3518
|
const entities = entitiesResult.recordset;
|
|
3439
3519
|
if (entities && entities.length > 0) {
|
|
@@ -3456,8 +3536,8 @@ export class ManageMetadataBase {
|
|
|
3456
3536
|
// discardResult: this routine only performs deletions — nothing below reads rows
|
|
3457
3537
|
// from it — and on PostgreSQL it is declared RETURNS SETOF record, which cannot be
|
|
3458
3538
|
// invoked through the default `SELECT * FROM` form at all.
|
|
3459
|
-
const sqlDelete = this.dbProvider.callRoutineSQL(
|
|
3460
|
-
await this.
|
|
3539
|
+
const sqlDelete = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spDeleteEntityWithCoreDependencies', [`'${e.ID}'`], ['EntityID'], true);
|
|
3540
|
+
await this.logSQLAndExecute(pool, sqlDelete, `SQL text to remove entity ${e.Name}`);
|
|
3461
3541
|
logStatus(` > Removed metadata for table ${e.SchemaName}.${e.BaseTable}`);
|
|
3462
3542
|
// Recorded only after the delete succeeds, so a failed removal (logged below for
|
|
3463
3543
|
// an admin to handle) does not mark a schema dirty for an entity still present.
|
|
@@ -3494,9 +3574,9 @@ export class ManageMetadataBase {
|
|
|
3494
3574
|
// Object type codes: P = Stored Procedure, V = View, FN = Scalar Function, IF/TF = Table-Valued Function
|
|
3495
3575
|
const upperType = type.toUpperCase();
|
|
3496
3576
|
const sqlDelete = this.dbProvider.dropObjectSQL(upperType, schemaName, name);
|
|
3497
|
-
await this.
|
|
3577
|
+
await this.logSQLAndExecute(pool, sqlDelete, `SQL text to remove ${type} ${schemaName}.${name}`);
|
|
3498
3578
|
// next up, we need to clean up the cache of saved DB objects that may exist for this entity in the appropriate sub-directory.
|
|
3499
|
-
const sqlOutputDir =
|
|
3579
|
+
const sqlOutputDir = OutputDir('SQL', true);
|
|
3500
3580
|
if (sqlOutputDir) {
|
|
3501
3581
|
// now do the same thing for the /schema directory within the provided directory
|
|
3502
3582
|
const fType = type === 'procedure' ? 'sp' : type === 'view' ? 'view' : 'full_text_search_function';
|
|
@@ -3570,7 +3650,7 @@ export class ManageMetadataBase {
|
|
|
3570
3650
|
* which is the typical "no schema changes since last run" case in Pass 2.
|
|
3571
3651
|
* `undefined` (default) preserves prior full-scan behavior.
|
|
3572
3652
|
*/
|
|
3573
|
-
async
|
|
3653
|
+
async ManageEntityFields(pool, excludeSchemas, skipCreatedAtUpdatedAtDeletedAtFieldValidation, skipEntityFieldValues, currentUser, skipAdvancedGeneration, skipDeleteUnneededFields = false, entityFilter) {
|
|
3574
3654
|
let bSuccess = true;
|
|
3575
3655
|
const startTime = new Date();
|
|
3576
3656
|
// Fast-exit: an explicit empty filter means "no entities changed in Pass 1, so Pass 2 has nothing to do"
|
|
@@ -3623,9 +3703,9 @@ export class ManageMetadataBase {
|
|
|
3623
3703
|
// Fix: derive virtual field AllowsNull from the FK column that drives the JOIN.
|
|
3624
3704
|
// The virtual field name = FK field name minus trailing "ID" (e.g. ClassID → Class).
|
|
3625
3705
|
if (this.dbProvider.NeedsVirtualFieldNullabilityFix) {
|
|
3626
|
-
const fixSQL = this.dbProvider.getFixVirtualFieldNullabilitySQL(
|
|
3706
|
+
const fixSQL = this.dbProvider.getFixVirtualFieldNullabilitySQL(MjCoreSchema());
|
|
3627
3707
|
if (fixSQL) {
|
|
3628
|
-
await this.
|
|
3708
|
+
await this.logSQLAndExecute(pool, fixSQL, 'SQL to fix virtual field nullability');
|
|
3629
3709
|
}
|
|
3630
3710
|
}
|
|
3631
3711
|
// AN: 14-June-2025 - we are now running this AFTER we create new entity fields from schema
|
|
@@ -3694,13 +3774,13 @@ export class ManageMetadataBase {
|
|
|
3694
3774
|
// Advanced Generation - Smart field identification and form layout
|
|
3695
3775
|
if (!skipAdvancedGeneration) {
|
|
3696
3776
|
const step7StartTime = new Date();
|
|
3697
|
-
|
|
3777
|
+
StartSpinner('Applying AI-powered advanced generation (smart fields, form layout)...');
|
|
3698
3778
|
if (!await this.applyAdvancedGeneration(pool, excludeSchemas, currentUser)) {
|
|
3699
3779
|
logError('Error applying advanced generation features');
|
|
3700
3780
|
// Don't fail the entire process - advanced generation is optional
|
|
3701
3781
|
}
|
|
3702
3782
|
const step7Elapsed = ((new Date().getTime() - step7StartTime.getTime()) / 1000).toFixed(1);
|
|
3703
|
-
|
|
3783
|
+
SucceedSpinner(`Advanced generation completed (${step7Elapsed}s)`);
|
|
3704
3784
|
}
|
|
3705
3785
|
// Deterministic search-flag hygiene. Deliberately OUTSIDE the `skipAdvancedGeneration`
|
|
3706
3786
|
// guard above: advanced generation is off by default, and an entity created under that
|
|
@@ -3721,6 +3801,10 @@ export class ManageMetadataBase {
|
|
|
3721
3801
|
logStatus(` Total time to manage entity fields: ${(new Date().getTime() - startTime.getTime()) / 1000} seconds`);
|
|
3722
3802
|
return bSuccess;
|
|
3723
3803
|
}
|
|
3804
|
+
/** @deprecated Use {@link ManageEntityFields}. */
|
|
3805
|
+
async manageEntityFields(pool, excludeSchemas, skipCreatedAtUpdatedAtDeletedAtFieldValidation, skipEntityFieldValues, currentUser, skipAdvancedGeneration, skipDeleteUnneededFields = false, entityFilter) {
|
|
3806
|
+
return this.ManageEntityFields(pool, excludeSchemas, skipCreatedAtUpdatedAtDeletedAtFieldValidation, skipEntityFieldValues, currentUser, skipAdvancedGeneration, skipDeleteUnneededFields, entityFilter);
|
|
3807
|
+
}
|
|
3724
3808
|
/**
|
|
3725
3809
|
* This method ensures that the __mj_DeletedAt field exists in each entity that has DeleteType=Soft. If the field does not exist, it is created.
|
|
3726
3810
|
*/
|
|
@@ -3729,7 +3813,7 @@ export class ManageMetadataBase {
|
|
|
3729
3813
|
const sqlEntities = `SELECT
|
|
3730
3814
|
*
|
|
3731
3815
|
FROM
|
|
3732
|
-
${this.qs(
|
|
3816
|
+
${this.qs(MjCoreSchema(), 'vwEntities')}
|
|
3733
3817
|
WHERE
|
|
3734
3818
|
VirtualEntity=${this.boolLit(false)} AND
|
|
3735
3819
|
DeleteType='Soft' AND
|
|
@@ -3790,7 +3874,7 @@ export class ManageMetadataBase {
|
|
|
3790
3874
|
}
|
|
3791
3875
|
let totalPKs = 0;
|
|
3792
3876
|
let totalFKs = 0;
|
|
3793
|
-
const schema =
|
|
3877
|
+
const schema = MjCoreSchema();
|
|
3794
3878
|
// Config supports two formats:
|
|
3795
3879
|
// 1. Schema-as-key (template format): { "dbo": [{ "TableName": "Orders", ... }] }
|
|
3796
3880
|
// 2. Flat tables array (legacy format): { "tables": [{ "SchemaName": "dbo", "TableName": "Orders", ... }] }
|
|
@@ -3829,7 +3913,7 @@ export class ManageMetadataBase {
|
|
|
3829
3913
|
${this.qi('IsPrimaryKey')} = ${this.boolLit(true)},
|
|
3830
3914
|
${this.qi('IsSoftPrimaryKey')} = ${this.boolLit(true)}
|
|
3831
3915
|
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = '${pk.FieldName}'`;
|
|
3832
|
-
const result = await this.
|
|
3916
|
+
const result = await this.logSQLAndExecute(pool, sSQL, `Set soft PK for ${tableSchema}.${tableName}.${pk.FieldName}`);
|
|
3833
3917
|
if (result !== null) {
|
|
3834
3918
|
logStatus(` ✓ Set IsPrimaryKey=1, IsSoftPrimaryKey=1 for ${tableName}.${pk.FieldName}`);
|
|
3835
3919
|
totalPKs++;
|
|
@@ -3873,7 +3957,7 @@ export class ManageMetadataBase {
|
|
|
3873
3957
|
${this.qi('IsSoftForeignKey')} = ${this.boolLit(true)},
|
|
3874
3958
|
${this.qi('AutoUpdateRelatedEntityInfo')} = ${this.boolLit(false)}
|
|
3875
3959
|
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = '${fk.FieldName}'`;
|
|
3876
|
-
const result = await this.
|
|
3960
|
+
const result = await this.logSQLAndExecute(pool, sSQL, `Set soft FK for ${tableSchema}.${tableName}.${fk.FieldName} → ${fk.RelatedTable}.${fk.RelatedField}`);
|
|
3877
3961
|
if (result !== null) {
|
|
3878
3962
|
logStatus(` ✓ Set soft FK for ${tableName}.${fk.FieldName} → ${fk.RelatedTable}.${fk.RelatedField}`);
|
|
3879
3963
|
totalFKs++;
|
|
@@ -3904,7 +3988,7 @@ export class ManageMetadataBase {
|
|
|
3904
3988
|
return true;
|
|
3905
3989
|
try {
|
|
3906
3990
|
// Load all existing EntityFieldValue rows for dedup checking
|
|
3907
|
-
const efvSQL = `SELECT * FROM ${this.qs(
|
|
3991
|
+
const efvSQL = `SELECT * FROM ${this.qs(MjCoreSchema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence, Value, ID`;
|
|
3908
3992
|
const allEFVResult = await this.runQuery(pool, efvSQL);
|
|
3909
3993
|
const allEntityFieldValues = allEFVResult.recordset;
|
|
3910
3994
|
let totalApplied = 0;
|
|
@@ -3930,7 +4014,7 @@ export class ManageMetadataBase {
|
|
|
3930
4014
|
* EntityFieldValue rows already exist (from CHECK constraints) and skips if so.
|
|
3931
4015
|
*/
|
|
3932
4016
|
async applyValueListsForTable(pool, schemaName, tableName, fields, allEntityFieldValues) {
|
|
3933
|
-
const schema =
|
|
4017
|
+
const schema = MjCoreSchema();
|
|
3934
4018
|
let applied = 0;
|
|
3935
4019
|
let skipped = 0;
|
|
3936
4020
|
// Look up entity ID
|
|
@@ -3976,7 +4060,7 @@ export class ManageMetadataBase {
|
|
|
3976
4060
|
const newType = fieldCfg.ValueListType;
|
|
3977
4061
|
if (currentValueListType !== newType.toLowerCase()) {
|
|
3978
4062
|
const updateSQL = `UPDATE ${this.qs(schema, 'EntityField')} SET ValueListType='${newType}' WHERE ID='${entityFieldID}'`;
|
|
3979
|
-
await this.
|
|
4063
|
+
await this.logSQLAndExecute(pool, updateSQL, `Set soft ValueListType='${newType}' for ${schemaName}.${tableName}.${fieldCfg.FieldName}`);
|
|
3980
4064
|
}
|
|
3981
4065
|
logStatus(` ✓ Applied soft value-list for ${tableName}.${fieldCfg.FieldName} (${sortedValues.length} values, type=${newType})`);
|
|
3982
4066
|
return 'applied';
|
|
@@ -3998,7 +4082,7 @@ export class ManageMetadataBase {
|
|
|
3998
4082
|
const sqlEntities = `SELECT
|
|
3999
4083
|
*
|
|
4000
4084
|
FROM
|
|
4001
|
-
${this.qs(
|
|
4085
|
+
${this.qs(MjCoreSchema(), 'vwEntities')}
|
|
4002
4086
|
WHERE
|
|
4003
4087
|
VirtualEntity = ${this.boolLit(false)} AND
|
|
4004
4088
|
${externalEntityClause}
|
|
@@ -4062,7 +4146,7 @@ export class ManageMetadataBase {
|
|
|
4062
4146
|
// and the subsequent UPDATE referencing that column run in separate batches.
|
|
4063
4147
|
// SQL Server compiles an entire batch before executing, so without the separator
|
|
4064
4148
|
// the UPDATE fails with "Invalid column name" since the column doesn't exist yet at compile time.
|
|
4065
|
-
await this.
|
|
4149
|
+
await this.logSQLAndExecute(pool, stmt, `SQL text to add special date field ${fieldName} to entity ${entity.SchemaName}.${entity.BaseTable}`, false, true, this.dbProvider.BatchSeparator);
|
|
4066
4150
|
}
|
|
4067
4151
|
}
|
|
4068
4152
|
else {
|
|
@@ -4075,7 +4159,7 @@ export class ManageMetadataBase {
|
|
|
4075
4159
|
// modifying the column, and finally adding the default constraint back in.
|
|
4076
4160
|
await this.dropExistingDefaultConstraint(pool, entity, fieldName);
|
|
4077
4161
|
const sql = this.dbProvider.alterColumnTypeAndNullabilitySQL(entity.SchemaName, entity.BaseTable, fieldName, this.timestampType, allowNull);
|
|
4078
|
-
await this.
|
|
4162
|
+
await this.logSQLAndExecute(pool, sql, `SQL text to update special date field ${fieldName} in entity ${entity.SchemaName}.${entity.BaseTable}`);
|
|
4079
4163
|
if (!allowNull)
|
|
4080
4164
|
await this.createDefaultConstraintForSpecialDateField(pool, entity, fieldName);
|
|
4081
4165
|
}
|
|
@@ -4105,7 +4189,7 @@ export class ManageMetadataBase {
|
|
|
4105
4189
|
async createDefaultConstraintForSpecialDateField(pool, entity, fieldName) {
|
|
4106
4190
|
try {
|
|
4107
4191
|
const sqlAddDefaultConstraint = this.dbProvider.addDefaultConstraintSQL(entity.SchemaName, entity.BaseTable, fieldName, this.utcNow());
|
|
4108
|
-
await this.
|
|
4192
|
+
await this.logSQLAndExecute(pool, sqlAddDefaultConstraint, `SQL text to add default constraint for special date field ${fieldName} in entity ${entity.SchemaName}.${entity.BaseTable}`);
|
|
4109
4193
|
}
|
|
4110
4194
|
catch (e) {
|
|
4111
4195
|
logError(e);
|
|
@@ -4132,7 +4216,7 @@ export class ManageMetadataBase {
|
|
|
4132
4216
|
try {
|
|
4133
4217
|
const sqlDropDefaultConstraint = this.dbProvider.dropDefaultConstraintSQL(entity.SchemaName, entity.BaseTable, fieldName);
|
|
4134
4218
|
// DECLARE-bearing block: close its batch in the replayable log (see SQLLogging.appendToSQLLogFile).
|
|
4135
|
-
await this.
|
|
4219
|
+
await this.logSQLAndExecute(pool, sqlDropDefaultConstraint, `SQL text to drop default existing default constraints in entity ${entity.SchemaName}.${entity.BaseTable}`, false, true, this.dbProvider.BatchSeparator);
|
|
4136
4220
|
}
|
|
4137
4221
|
catch (e) {
|
|
4138
4222
|
logError(e);
|
|
@@ -4157,15 +4241,15 @@ export class ManageMetadataBase {
|
|
|
4157
4241
|
// "Unclosed quotation mark" (SQL Server) at the very end of an otherwise-complete
|
|
4158
4242
|
// pass. '' doubling is correct on both supported dialects.
|
|
4159
4243
|
const esc = (s) => s.replace(/'/g, "''");
|
|
4160
|
-
const dataResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(
|
|
4244
|
+
const dataResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntities')} WHERE Name = '${esc(e)}'`);
|
|
4161
4245
|
const data = dataResult.recordset;
|
|
4162
|
-
const fieldsResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(
|
|
4246
|
+
const fieldsResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')} WHERE EntityID='${data[0].ID}'`);
|
|
4163
4247
|
const fields = fieldsResult.recordset;
|
|
4164
4248
|
// Use new API to generate entity description
|
|
4165
4249
|
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);
|
|
4166
4250
|
if (result?.entityDescription && result.entityDescription.length > 0) {
|
|
4167
|
-
const sSQL = `UPDATE ${this.qs(
|
|
4168
|
-
await this.
|
|
4251
|
+
const sSQL = `UPDATE ${this.qs(MjCoreSchema(), 'Entity')} SET ${this.qi('Description')} = '${esc(result.entityDescription)}', ${this.qi('AutoUpdateDescription')} = ${this.boolLit(false)} WHERE ${this.qi('Name')} = '${esc(e)}'`;
|
|
4252
|
+
await this.logSQLAndExecute(pool, sSQL, `SQL text to update entity description for entity ${e}`);
|
|
4169
4253
|
}
|
|
4170
4254
|
else {
|
|
4171
4255
|
console.warn(' >>> Advanced Generation Error: LLM returned invalid result, skipping entity description for entity ' + e);
|
|
@@ -4186,9 +4270,9 @@ export class ManageMetadataBase {
|
|
|
4186
4270
|
const sql = `SELECT
|
|
4187
4271
|
ef.ID, ef.Name
|
|
4188
4272
|
FROM
|
|
4189
|
-
${this.qs(
|
|
4273
|
+
${this.qs(MjCoreSchema(), 'vwEntityFields')} ef
|
|
4190
4274
|
INNER JOIN
|
|
4191
|
-
${this.qs(
|
|
4275
|
+
${this.qs(MjCoreSchema(), 'vwEntities')} e
|
|
4192
4276
|
ON
|
|
4193
4277
|
ef.EntityID = e.ID
|
|
4194
4278
|
WHERE
|
|
@@ -4203,8 +4287,8 @@ export class ManageMetadataBase {
|
|
|
4203
4287
|
const namingOptions = configInfo.entityNaming?.normalizeFieldNames !== false ? this.getEntityNamingOptions() : undefined;
|
|
4204
4288
|
const sDisplayName = stripTrailingChars(createDisplayName(field.Name, namingOptions), 'ID', true).trim();
|
|
4205
4289
|
if (sDisplayName.length > 0 && sDisplayName.toLowerCase().trim() !== field.Name.toLowerCase().trim()) {
|
|
4206
|
-
const sSQL = `UPDATE ${this.qs(
|
|
4207
|
-
await this.
|
|
4290
|
+
const sSQL = `UPDATE ${this.qs(MjCoreSchema(), 'EntityField')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()}, DisplayName = '${sDisplayName}' WHERE ID = '${field.ID}'`;
|
|
4291
|
+
await this.logSQLAndExecute(pool, sSQL, `SQL text to update display name for field ${field.Name}`);
|
|
4208
4292
|
}
|
|
4209
4293
|
}
|
|
4210
4294
|
return true;
|
|
@@ -4224,12 +4308,12 @@ export class ManageMetadataBase {
|
|
|
4224
4308
|
*/
|
|
4225
4309
|
async setDefaultColumnWidthWhereNeeded(pool, excludeSchemas) {
|
|
4226
4310
|
try {
|
|
4227
|
-
const heal =
|
|
4228
|
-
authoredExclude:
|
|
4311
|
+
const heal = BuildHealSchemaRoutineParams({
|
|
4312
|
+
authoredExclude: GetAuthoredExcludeSchemas(excludeSchemas),
|
|
4229
4313
|
includeSchemas: configInfo.includeSchemas,
|
|
4230
4314
|
});
|
|
4231
|
-
const sSQL = this.dbProvider.callRoutineSQL(
|
|
4232
|
-
await this.
|
|
4315
|
+
const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spSetDefaultColumnWidthWhereNeeded', heal.Values, heal.Names);
|
|
4316
|
+
await this.logSQLAndExecute(pool, sSQL, `SQL text to set default column width where needed`, true);
|
|
4233
4317
|
return true;
|
|
4234
4318
|
}
|
|
4235
4319
|
catch (e) {
|
|
@@ -4249,7 +4333,7 @@ export class ManageMetadataBase {
|
|
|
4249
4333
|
* @returns {string} - The SQL statement to retrieve pending entity fields.
|
|
4250
4334
|
*/
|
|
4251
4335
|
getPendingEntityFieldsSELECTSQL(entityIDs, excludeSchemas) {
|
|
4252
|
-
const schema =
|
|
4336
|
+
const schema = MjCoreSchema();
|
|
4253
4337
|
return this.dbProvider.getPendingEntityFieldsSQL(schema, entityIDs, excludeSchemas);
|
|
4254
4338
|
}
|
|
4255
4339
|
/**
|
|
@@ -4292,7 +4376,7 @@ export class ManageMetadataBase {
|
|
|
4292
4376
|
const quotedDefaultValue = (parsedDefaultValue == null || parsedDefaultValue.trim().length === 0)
|
|
4293
4377
|
? 'NULL'
|
|
4294
4378
|
: (parsedDefaultValue.trim().toLowerCase() === 'null' ? 'NULL' : `'${escapedParsedDefault}'`);
|
|
4295
|
-
const conflictCheck = `SELECT 1 FROM ${this.qs(
|
|
4379
|
+
const conflictCheck = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'EntityField')} WHERE ID = '${newEntityFieldUUID}' OR (EntityID = '${n.EntityID}' AND Name = '${n.FieldName}')`;
|
|
4296
4380
|
const guard = this.dbProvider.wrapInsertWithConflictGuard(conflictCheck);
|
|
4297
4381
|
// Sequence is an apply-time expression, never a literal: see applyTimeEntityFieldSequenceSQL.
|
|
4298
4382
|
// `n.Sequence` (MAX+100000+ordinal from the pending SELECT) is only that query's ORDER BY key
|
|
@@ -4300,7 +4384,7 @@ export class ManageMetadataBase {
|
|
|
4300
4384
|
const sequenceExpr = this.applyTimeEntityFieldSequenceSQL(n.EntityID);
|
|
4301
4385
|
return `
|
|
4302
4386
|
${guard.prefix}
|
|
4303
|
-
INSERT INTO ${this.qs(
|
|
4387
|
+
INSERT INTO ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
4304
4388
|
(
|
|
4305
4389
|
${this.qi('ID')},
|
|
4306
4390
|
${this.qi('EntityID')},
|
|
@@ -4415,7 +4499,7 @@ export class ManageMetadataBase {
|
|
|
4415
4499
|
for (let i = 0; i < inserts.length; i += CHUNK_SIZE) {
|
|
4416
4500
|
const chunk = inserts.slice(i, i + CHUNK_SIZE);
|
|
4417
4501
|
try {
|
|
4418
|
-
await this.
|
|
4502
|
+
await this.logSQLBatchAndExecute(pool, chunk, `SQL text to insert ${chunk.length} new entity field(s)`);
|
|
4419
4503
|
// an error blows up the transaction (all-or-nothing), which is what we want
|
|
4420
4504
|
}
|
|
4421
4505
|
catch (e) {
|
|
@@ -4449,10 +4533,10 @@ export class ManageMetadataBase {
|
|
|
4449
4533
|
* @param relatedEntityNameFieldMap
|
|
4450
4534
|
* @returns
|
|
4451
4535
|
*/
|
|
4452
|
-
async
|
|
4536
|
+
async UpdateEntityFieldRelatedEntityNameFieldMap(pool, entityFieldID, relatedEntityNameFieldMap) {
|
|
4453
4537
|
try {
|
|
4454
|
-
const sSQL = this.dbProvider.callRoutineSQL(
|
|
4455
|
-
await this.
|
|
4538
|
+
const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spUpdateEntityFieldRelatedEntityNameFieldMap', [`'${entityFieldID}'`, `'${relatedEntityNameFieldMap}'`], ['EntityFieldID', 'RelatedEntityNameFieldMap']);
|
|
4539
|
+
await this.logSQLAndExecute(pool, sSQL, `SQL text to update entity field related entity name field map for entity field ID ${entityFieldID}`);
|
|
4456
4540
|
return true;
|
|
4457
4541
|
}
|
|
4458
4542
|
catch (e) {
|
|
@@ -4460,14 +4544,18 @@ export class ManageMetadataBase {
|
|
|
4460
4544
|
return false;
|
|
4461
4545
|
}
|
|
4462
4546
|
}
|
|
4547
|
+
/** @deprecated Use {@link UpdateEntityFieldRelatedEntityNameFieldMap}. */
|
|
4548
|
+
async updateEntityFieldRelatedEntityNameFieldMap(pool, entityFieldID, relatedEntityNameFieldMap) {
|
|
4549
|
+
return this.UpdateEntityFieldRelatedEntityNameFieldMap(pool, entityFieldID, relatedEntityNameFieldMap);
|
|
4550
|
+
}
|
|
4463
4551
|
async updateExistingEntitiesFromSchema(pool, excludeSchemas) {
|
|
4464
4552
|
try {
|
|
4465
|
-
const heal =
|
|
4466
|
-
authoredExclude:
|
|
4553
|
+
const heal = BuildHealSchemaRoutineParams({
|
|
4554
|
+
authoredExclude: GetAuthoredExcludeSchemas(excludeSchemas),
|
|
4467
4555
|
includeSchemas: configInfo.includeSchemas,
|
|
4468
4556
|
});
|
|
4469
|
-
const sSQL = this.dbProvider.callRoutineSQL(
|
|
4470
|
-
const result = await this.
|
|
4557
|
+
const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spUpdateExistingEntitiesFromSchema', heal.Values, heal.Names);
|
|
4558
|
+
const result = await this.logSQLAndExecute(pool, sSQL, `SQL text to update existing entities from schema`, true);
|
|
4471
4559
|
// result contains the updated entities, and there is a property of each row called Name which has the entity name that was modified
|
|
4472
4560
|
// add these to the modified entity list if they're not already in there
|
|
4473
4561
|
if (result && result.length > 0) {
|
|
@@ -4483,30 +4571,34 @@ export class ManageMetadataBase {
|
|
|
4483
4571
|
/**
|
|
4484
4572
|
* Adds a list of entity names to the modified entity list if they're not already in there
|
|
4485
4573
|
*/
|
|
4486
|
-
static
|
|
4574
|
+
static AddNewEntitiesToModifiedList(entityNames) {
|
|
4487
4575
|
const distinctEntityNames = [...new Set(entityNames)];
|
|
4488
4576
|
const newlyModifiedEntityNames = distinctEntityNames.filter((e) => !ManageMetadataBase._modifiedEntityList.includes(e));
|
|
4489
4577
|
// now make sure that each of these entity names is in the modified entity list
|
|
4490
4578
|
ManageMetadataBase._modifiedEntityList = ManageMetadataBase._modifiedEntityList.concat(newlyModifiedEntityNames);
|
|
4491
4579
|
}
|
|
4580
|
+
/** @deprecated Use {@link AddNewEntitiesToModifiedList}. */
|
|
4581
|
+
static addNewEntitiesToModifiedList(entityNames) {
|
|
4582
|
+
return this.AddNewEntitiesToModifiedList(entityNames);
|
|
4583
|
+
}
|
|
4492
4584
|
async updateExistingEntityFieldsFromSchema(pool, excludeSchemas, entityIDs) {
|
|
4493
4585
|
try {
|
|
4494
4586
|
// One SP call regardless of scope: pass the entire entity ID list as a comma-delimited
|
|
4495
4587
|
// string (mirrors the @ExcludedSchemaNames pattern). The SP fans the list out into
|
|
4496
4588
|
// a table variable via STRING_SPLIT and joins once. This avoids both per-entity
|
|
4497
4589
|
// round-trips (slow) and parallel calls (page-level lock contention on EntityField).
|
|
4498
|
-
const heal =
|
|
4499
|
-
authoredExclude:
|
|
4590
|
+
const heal = BuildHealSchemaRoutineParams({
|
|
4591
|
+
authoredExclude: GetAuthoredExcludeSchemas(excludeSchemas),
|
|
4500
4592
|
includeSchemas: configInfo.includeSchemas,
|
|
4501
4593
|
entityIDs,
|
|
4502
4594
|
});
|
|
4503
|
-
const sSQL = this.dbProvider.callRoutineSQL(
|
|
4595
|
+
const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spUpdateExistingEntityFieldsFromSchema', heal.Values, heal.Names);
|
|
4504
4596
|
const isScoped = entityIDs !== undefined && entityIDs.length > 0;
|
|
4505
4597
|
const label = isScoped
|
|
4506
4598
|
? `SQL text to update existing entity fields from schema (${entityIDs.length} scoped entities)`
|
|
4507
4599
|
: `SQL text to update existing entity fields from schema`;
|
|
4508
4600
|
const before = await this.snapshotEntityFields(pool, entityIDs);
|
|
4509
|
-
await this.
|
|
4601
|
+
await this.logSQLAndExecute(pool, sSQL, label, true);
|
|
4510
4602
|
const after = await this.snapshotEntityFields(pool, entityIDs);
|
|
4511
4603
|
const changes = diffEntityFieldSnapshots(before, after, (e, n) => ManageMetadataBase.isFieldNew(e, n));
|
|
4512
4604
|
if (changes.length > 0) {
|
|
@@ -4537,12 +4629,12 @@ export class ManageMetadataBase {
|
|
|
4537
4629
|
*/
|
|
4538
4630
|
async updateSchemaInfoFromDatabase(pool, excludeSchemas) {
|
|
4539
4631
|
try {
|
|
4540
|
-
const heal =
|
|
4541
|
-
authoredExclude:
|
|
4632
|
+
const heal = BuildHealSchemaRoutineParams({
|
|
4633
|
+
authoredExclude: GetAuthoredExcludeSchemas(excludeSchemas),
|
|
4542
4634
|
includeSchemas: configInfo.includeSchemas,
|
|
4543
4635
|
});
|
|
4544
|
-
const sSQL = this.dbProvider.callRoutineSQL(
|
|
4545
|
-
const result = await this.
|
|
4636
|
+
const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spUpdateSchemaInfoFromDatabase', heal.Values, heal.Names);
|
|
4637
|
+
const result = await this.logSQLAndExecute(pool, sSQL, `SQL text to sync schema info from database schemas`, true);
|
|
4546
4638
|
if (result && result.length > 0) {
|
|
4547
4639
|
logStatus(` > Updated/created ${result.length} SchemaInfo records`);
|
|
4548
4640
|
this.cacheSchemaInfoRecords(result);
|
|
@@ -4572,7 +4664,7 @@ export class ManageMetadataBase {
|
|
|
4572
4664
|
*/
|
|
4573
4665
|
async loadSchemaInfoRecords(pool) {
|
|
4574
4666
|
try {
|
|
4575
|
-
const sSQL = `SELECT * FROM ${this.qs(
|
|
4667
|
+
const sSQL = `SELECT * FROM ${this.qs(MjCoreSchema(), 'SchemaInfo')}`;
|
|
4576
4668
|
const result = await this.runQuery(pool, sSQL);
|
|
4577
4669
|
if (result?.recordset?.length > 0) {
|
|
4578
4670
|
this.cacheSchemaInfoRecords(result.recordset);
|
|
@@ -4638,17 +4730,17 @@ export class ManageMetadataBase {
|
|
|
4638
4730
|
// string (mirrors the @ExcludedSchemaNames pattern). The SP fans the list out into
|
|
4639
4731
|
// a table variable via STRING_SPLIT and filters once. Avoids both per-entity
|
|
4640
4732
|
// round-trips and parallel calls (page-level lock contention on EntityField).
|
|
4641
|
-
const heal =
|
|
4642
|
-
authoredExclude:
|
|
4733
|
+
const heal = BuildHealSchemaRoutineParams({
|
|
4734
|
+
authoredExclude: GetAuthoredExcludeSchemas(excludeSchemas),
|
|
4643
4735
|
includeSchemas: configInfo.includeSchemas,
|
|
4644
4736
|
entityIDs,
|
|
4645
4737
|
});
|
|
4646
|
-
const sSQL = this.dbProvider.callRoutineSQL(
|
|
4738
|
+
const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spDeleteUnneededEntityFields', heal.Values, heal.Names);
|
|
4647
4739
|
const isScoped = entityIDs !== undefined && entityIDs.length > 0;
|
|
4648
4740
|
const label = isScoped
|
|
4649
4741
|
? `SQL text to delete unneeded entity fields (${entityIDs.length} scoped entities)`
|
|
4650
4742
|
: `SQL text to delete unneeded entity fields`;
|
|
4651
|
-
const result = await this.
|
|
4743
|
+
const result = await this.logSQLAndExecute(pool, sSQL, label, true);
|
|
4652
4744
|
// result contains the DELETED entity fields. Get a distinct list of entity names
|
|
4653
4745
|
// and add them to the modified entity list if they're not already in there.
|
|
4654
4746
|
if (result && result.length > 0) {
|
|
@@ -4668,13 +4760,13 @@ export class ManageMetadataBase {
|
|
|
4668
4760
|
// for the field and sync that up with the EntityFieldValue table. If it is not a simple series of OR statements, we will not be able to parse it and we'll
|
|
4669
4761
|
// just ignore it.
|
|
4670
4762
|
const filter = this.dbProvider.getCheckConstraintsSchemaFilter(excludeSchemas);
|
|
4671
|
-
const sSQL = `SELECT * FROM ${this.qs(
|
|
4763
|
+
const sSQL = `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntityFieldsWithCheckConstraints')}${filter}`;
|
|
4672
4764
|
const resultResult = await this.runQuery(pool, sSQL);
|
|
4673
4765
|
const result = resultResult.recordset;
|
|
4674
|
-
const efvSQL = `SELECT * FROM ${this.qs(
|
|
4766
|
+
const efvSQL = `SELECT * FROM ${this.qs(MjCoreSchema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence, Value, ID`;
|
|
4675
4767
|
const allEntityFieldValuesResult = await this.runQuery(pool, efvSQL);
|
|
4676
4768
|
const allEntityFieldValues = allEntityFieldValuesResult.recordset;
|
|
4677
|
-
const efSQL = `SELECT * FROM ${this.qs(
|
|
4769
|
+
const efSQL = `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')} ORDER BY EntityID, Sequence, Name, ID`;
|
|
4678
4770
|
const allEntityFieldsResult = await this.runQuery(pool, efSQL);
|
|
4679
4771
|
const allEntityFields = allEntityFieldsResult.recordset;
|
|
4680
4772
|
const generationPromises = [];
|
|
@@ -4692,25 +4784,30 @@ export class ManageMetadataBase {
|
|
|
4692
4784
|
for (const r of columnLevelResults) {
|
|
4693
4785
|
// now, for each of the constraints we get back here, loop through and evaluate if they're simple and if they're simple, parse and sync with entity field values for that field
|
|
4694
4786
|
if (r.ConstraintDefinition && r.ConstraintDefinition.length > 0) {
|
|
4695
|
-
|
|
4787
|
+
// parse first, then decide whether THIS field should carry the list (bit, PK singleton guards).
|
|
4788
|
+
// UUIDsEqual, not ===: PG returns lowercase UUIDs and SQL Server uppercase, and a missed
|
|
4789
|
+
// lookup here fails OPEN (the exclusions stop applying), so a case mismatch would be silent.
|
|
4790
|
+
const field = allEntityFields.find((f) => UUIDsEqual(f.ID, r.EntityFieldID));
|
|
4791
|
+
const parsedValues = ManageMetadataBase.valueListForField(field, this.parseCheckConstraintValues(r.ConstraintDefinition, r.ColumnName, r.EntityName));
|
|
4696
4792
|
if (parsedValues) {
|
|
4697
4793
|
if (!skipDBUpdate) {
|
|
4698
4794
|
// we only do this part if we are not skiping the database update as this code will sync values from the CHECK
|
|
4699
4795
|
// with the EntityFieldValues in the database.
|
|
4700
|
-
// Sort values
|
|
4701
|
-
//
|
|
4702
|
-
//
|
|
4703
|
-
|
|
4796
|
+
// Sort the values to ensure consistent sequences across all databases. This guarantees the
|
|
4797
|
+
// same value always gets the same sequence number regardless of how SQL Server returns
|
|
4798
|
+
// CHECK constraint values (which can vary). An all-numeric list sorts numerically so the
|
|
4799
|
+
// dropdown reads 1, 2, 10 rather than the lexical 1, 10, 2 — still fully deterministic.
|
|
4800
|
+
ManageMetadataBase.sortCheckConstraintValues(parsedValues);
|
|
4704
4801
|
// we have parsed values from the check constraint, so sync them with the entity field values
|
|
4705
4802
|
await this.syncEntityFieldValues(pool, r.EntityFieldID, parsedValues, allEntityFieldValues);
|
|
4706
4803
|
// finally, make sure the ValueListType column within the EntityField table is set to "List" because for check constraints we only allow the values specified in the list.
|
|
4707
4804
|
// check to see if the ValueListType is already set to "List", if not, update it
|
|
4708
|
-
const sSQLCheck = `SELECT ValueListType FROM ${this.qs(
|
|
4805
|
+
const sSQLCheck = `SELECT ValueListType FROM ${this.qs(MjCoreSchema(), 'EntityField')} WHERE ID='${r.EntityFieldID}'`;
|
|
4709
4806
|
const checkResultResult = await this.runQuery(pool, sSQLCheck);
|
|
4710
4807
|
const checkResult = checkResultResult.recordset;
|
|
4711
4808
|
if (checkResult && checkResult.length > 0 && checkResult[0].ValueListType.trim().toLowerCase() !== 'list') {
|
|
4712
|
-
const sSQL = `UPDATE ${this.qs(
|
|
4713
|
-
await this.
|
|
4809
|
+
const sSQL = `UPDATE ${this.qs(MjCoreSchema(), 'EntityField')} SET ValueListType='List' WHERE ID='${r.EntityFieldID}'`;
|
|
4810
|
+
await this.logSQLAndExecute(pool, sSQL, `SQL text to update ValueListType for entity field ID ${r.EntityFieldID}`);
|
|
4714
4811
|
}
|
|
4715
4812
|
}
|
|
4716
4813
|
else {
|
|
@@ -4752,7 +4849,7 @@ export class ManageMetadataBase {
|
|
|
4752
4849
|
* @param pool
|
|
4753
4850
|
* @param currentUser
|
|
4754
4851
|
*/
|
|
4755
|
-
async
|
|
4852
|
+
async LoadGeneratedCode(pool, currentUser) {
|
|
4756
4853
|
try {
|
|
4757
4854
|
// right now we're just doing validator functions which are handled here
|
|
4758
4855
|
return await this.manageEntityFieldValuesAndValidatorFunctions(pool, [], currentUser, true);
|
|
@@ -4762,6 +4859,10 @@ export class ManageMetadataBase {
|
|
|
4762
4859
|
return false;
|
|
4763
4860
|
}
|
|
4764
4861
|
}
|
|
4862
|
+
/** @deprecated Use {@link LoadGeneratedCode}. */
|
|
4863
|
+
async loadGeneratedCode(pool, currentUser) {
|
|
4864
|
+
return this.LoadGeneratedCode(pool, currentUser);
|
|
4865
|
+
}
|
|
4765
4866
|
async runValidationGeneration(r, allEntityFields, generateNewCode, currentUser) {
|
|
4766
4867
|
const generatedFunction = await this.generateValidatorFunctionFromCheckConstraint(r, allEntityFields, currentUser, generateNewCode);
|
|
4767
4868
|
if (generatedFunction?.success) {
|
|
@@ -4842,8 +4943,8 @@ export class ManageMetadataBase {
|
|
|
4842
4943
|
for (const ev of existingValues) {
|
|
4843
4944
|
if (!possibleValues.find(v => v === ev.Value)) {
|
|
4844
4945
|
// delete the value from the database
|
|
4845
|
-
const sSQLDelete = `DELETE FROM ${this.qs(
|
|
4846
|
-
await this.
|
|
4946
|
+
const sSQLDelete = `DELETE FROM ${this.qs(MjCoreSchema(), 'EntityFieldValue')} WHERE ID='${ev.ID}'`;
|
|
4947
|
+
await this.logSQLAndExecute(ds, sSQLDelete, `SQL text to delete entity field value ID ${ev.ID}`);
|
|
4847
4948
|
numRemoved++;
|
|
4848
4949
|
}
|
|
4849
4950
|
}
|
|
@@ -4854,11 +4955,11 @@ export class ManageMetadataBase {
|
|
|
4854
4955
|
// Generate a UUID for this new EntityFieldValue record
|
|
4855
4956
|
const newId = uuidv4();
|
|
4856
4957
|
// add the value to the database with explicit ID
|
|
4857
|
-
const sSQLInsert = `INSERT INTO ${this.qs(
|
|
4958
|
+
const sSQLInsert = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntityFieldValue')}
|
|
4858
4959
|
(${this.qi('ID')}, ${this.qi('EntityFieldID')}, ${this.qi('Sequence')}, ${this.qi('Value')}, ${this.qi('Code')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')})
|
|
4859
4960
|
VALUES
|
|
4860
4961
|
('${newId}', '${entityFieldID}', ${1 + possibleValues.indexOf(v)}, '${v}', '${v}', ${this.utcNow()}, ${this.utcNow()})`;
|
|
4861
|
-
await this.
|
|
4962
|
+
await this.logSQLAndExecute(ds, sSQLInsert, `SQL text to insert entity field value with ID ${newId}`);
|
|
4862
4963
|
numAdded++;
|
|
4863
4964
|
}
|
|
4864
4965
|
}
|
|
@@ -4870,8 +4971,8 @@ export class ManageMetadataBase {
|
|
|
4870
4971
|
// DB returns a string for Sequence instead of a number.
|
|
4871
4972
|
if (ev && ev.Sequence != 1 + possibleValues.indexOf(v)) {
|
|
4872
4973
|
// update the sequence to match the order in the possible values list, if it doesn't already match
|
|
4873
|
-
const sSQLUpdate = `UPDATE ${this.qs(
|
|
4874
|
-
await this.
|
|
4974
|
+
const sSQLUpdate = `UPDATE ${this.qs(MjCoreSchema(), 'EntityFieldValue')} SET Sequence=${1 + possibleValues.indexOf(v)} WHERE ID='${ev.ID}'`;
|
|
4975
|
+
await this.logSQLAndExecute(ds, sSQLUpdate, `SQL text to update entity field value sequence`);
|
|
4875
4976
|
numUpdated++;
|
|
4876
4977
|
}
|
|
4877
4978
|
}
|
|
@@ -4888,10 +4989,24 @@ export class ManageMetadataBase {
|
|
|
4888
4989
|
return false;
|
|
4889
4990
|
}
|
|
4890
4991
|
}
|
|
4992
|
+
/**
|
|
4993
|
+
* Parses a column-level CHECK constraint into the list of values it permits, or returns null when the
|
|
4994
|
+
* constraint is not a simple value list (a range, a `LEN()` predicate, a mixed condition, and so on).
|
|
4995
|
+
*
|
|
4996
|
+
* Both of the ways `IN (...)` is rendered by SQL Server are handled. String, date and GUID literals come
|
|
4997
|
+
* back quoted — `([Status]='Active' OR [Status]='Inactive')` — but numeric and `bit` literals come back
|
|
4998
|
+
* **unquoted and parenthesized**: `([Level]=(3) OR [Level]=(2) OR [Level]=(1))`. Matching only the quoted
|
|
4999
|
+
* form left a numeric IN-list with no value list at all, and so no dropdown in Explorer either (#3978).
|
|
5000
|
+
* PostgreSQL's `= ANY (ARRAY[...])` rendering is handled separately, by parsePgArrayConstraint.
|
|
5001
|
+
*
|
|
5002
|
+
* A single-value list is captured too: `CHECK (Status='Active')` — which is also how SQL Server renders
|
|
5003
|
+
* `IN ('Active')` — permits exactly one value, which is a one-item list.
|
|
5004
|
+
*
|
|
5005
|
+
* This is a parser, not a policy: it reports what the constraint permits. WHICH fields get a value list
|
|
5006
|
+
* out of that is decided after the fact, by valueListForField.
|
|
5007
|
+
*/
|
|
4891
5008
|
parseCheckConstraintValues(constraintDefinition, fieldName, entityName) {
|
|
4892
|
-
//
|
|
4893
|
-
// SQL Server uses [FieldName]='Value' quoting, PostgreSQL uses "FieldName" or unquoted FieldName
|
|
4894
|
-
// We handle both: [FieldName], "FieldName", or bare FieldName
|
|
5009
|
+
// Normalize N'literal' to 'literal' so one pattern handles both.
|
|
4895
5010
|
// Note: Assuming fieldName does not contain regex special characters; otherwise, it needs to be escaped as well.
|
|
4896
5011
|
const processedConstraint = constraintDefinition.replace(/(^|[=(\s])N'([^']*)'/g, "$1'$2'");
|
|
4897
5012
|
// PostgreSQL ANY (ARRAY[...]) pattern:
|
|
@@ -4904,46 +5019,92 @@ export class ManageMetadataBase {
|
|
|
4904
5019
|
// Build a regex fragment that matches the field name in any quoting style:
|
|
4905
5020
|
// [FieldName] (SQL Server) or "FieldName" (PostgreSQL) or bare FieldName
|
|
4906
5021
|
const quotedField = `(?:\\[${fieldName}\\]|"${fieldName}"|${fieldName})`;
|
|
4907
|
-
//
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
5022
|
+
// One literal, either quoting style. The unquoted numeric forms are the ones sys.check_constraints
|
|
5023
|
+
// actually stores: (3) (-1) (1.00) (2.5) (1.0000000000000000e+030) and (12345678901234567890.) —
|
|
5024
|
+
// an integral literal with a trailing point once the precision is large enough.
|
|
5025
|
+
const literal = `(?:'[^']+'|${ManageMetadataBase.NUMERIC_CHECK_LITERAL})`;
|
|
5026
|
+
const assignment = `${quotedField}=${literal}`;
|
|
5027
|
+
// Nested pattern: (Field IS NULL OR (Field=<literal> OR ...)) — how `X IS NULL OR X IN (...)` renders
|
|
5028
|
+
const nestedNullRegex = new RegExp(`^\\(${quotedField} IS NULL OR \\(${assignment}(?: OR ${assignment})*\\)\\)$`);
|
|
5029
|
+
// Standard pattern, with an optional trailing IS NULL: (Field=<literal> OR ... [OR Field IS NULL])
|
|
5030
|
+
const structureRegex = new RegExp(`^\\(${assignment}(?: OR ${assignment})*(?: OR ${quotedField} IS NULL)?\\)$`);
|
|
5031
|
+
if (!nestedNullRegex.test(processedConstraint) && !structureRegex.test(processedConstraint)) {
|
|
5032
|
+
return null;
|
|
5033
|
+
}
|
|
5034
|
+
// Extract the values: group 1 is a quoted literal, group 2 an unquoted numeric one.
|
|
5035
|
+
const valueRegex = new RegExp(`${quotedField}=(?:'([^']+)'|(${ManageMetadataBase.NUMERIC_CHECK_LITERAL}))`, 'g');
|
|
5036
|
+
const possibleValues = [];
|
|
5037
|
+
let match;
|
|
5038
|
+
while ((match = valueRegex.exec(processedConstraint)) !== null) {
|
|
5039
|
+
// This is necessary to avoid infinite loops with zero-width matches
|
|
5040
|
+
if (match.index === valueRegex.lastIndex) {
|
|
5041
|
+
valueRegex.lastIndex++;
|
|
5042
|
+
}
|
|
5043
|
+
const value = match[1] ?? (match[2] ? ManageMetadataBase.normalizeNumericCheckLiteral(match[2]) : undefined);
|
|
5044
|
+
if (value && value.length > 0) {
|
|
5045
|
+
possibleValues.push(value);
|
|
4921
5046
|
}
|
|
4922
|
-
return possibleValues.length > 0 ? possibleValues : null;
|
|
4923
5047
|
}
|
|
4924
|
-
//
|
|
4925
|
-
|
|
4926
|
-
|
|
5048
|
+
// An empty result is not a value list — returning [] here would set ValueListType='List' on a field
|
|
5049
|
+
// with no EntityFieldValue rows behind it.
|
|
5050
|
+
return possibleValues.length > 0 ? possibleValues : null;
|
|
5051
|
+
}
|
|
5052
|
+
/**
|
|
5053
|
+
* The value list to actually store for a field, or null when a parsed list should NOT become
|
|
5054
|
+
* EntityFieldValue rows (and so should not become a dropdown in Explorer either).
|
|
5055
|
+
*
|
|
5056
|
+
* Runs AFTER parsing rather than gating it, so each exclusion is no broader than its own reason and
|
|
5057
|
+
* neither one can drop a list that was captured before #3978:
|
|
5058
|
+
*
|
|
5059
|
+
* - `bit`: `IN (0,1)` is vacuous — bit already permits exactly 0 and 1 — and `= 1` is a validator,
|
|
5060
|
+
* not a two-item dropdown over what renders as a checkbox. Every bit list is rendered unquoted, so
|
|
5061
|
+
* none of them was reachable before this change and nothing regresses.
|
|
5062
|
+
* - a PRIMARY KEY carrying a SINGLE value: `CHECK (ID=1)` is the single-row-table guard MJ's own
|
|
5063
|
+
* sequence tables use — a structural invariant, not a set of values a user picks from. A
|
|
5064
|
+
* MULTI-value list on a primary key is left alone: `CHECK (Code IN ('US','CA'))` on a natural-key
|
|
5065
|
+
* PK is a real domain list, it was captured before this change, and CodeGen also runs over customer
|
|
5066
|
+
* schemas where natural-key primary keys are ordinary.
|
|
5067
|
+
*
|
|
5068
|
+
* A field whose metadata row was not found keeps whatever parsed, which is the pre-#3978 behaviour.
|
|
5069
|
+
*/
|
|
5070
|
+
static valueListForField(field, parsedValues) {
|
|
5071
|
+
if (!parsedValues || !field) {
|
|
5072
|
+
return parsedValues;
|
|
5073
|
+
}
|
|
5074
|
+
if (field.Type?.trim().toLowerCase() === 'bit') {
|
|
4927
5075
|
return null;
|
|
4928
5076
|
}
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
const valueRegex = new RegExp(`${quotedField}='([^']+)\'`, 'g');
|
|
4932
|
-
let match;
|
|
4933
|
-
const possibleValues = [];
|
|
4934
|
-
// Use regex to find matches and extract the values
|
|
4935
|
-
while ((match = valueRegex.exec(processedConstraint)) !== null) {
|
|
4936
|
-
// This is necessary to avoid infinite loops with zero-width matches
|
|
4937
|
-
if (match.index === valueRegex.lastIndex) {
|
|
4938
|
-
valueRegex.lastIndex++;
|
|
4939
|
-
}
|
|
4940
|
-
// The first captured group contains the value
|
|
4941
|
-
if (match[1]) {
|
|
4942
|
-
possibleValues.push(match[1]);
|
|
4943
|
-
}
|
|
4944
|
-
}
|
|
4945
|
-
return possibleValues;
|
|
5077
|
+
if (field.IsPrimaryKey && parsedValues.length === 1) {
|
|
5078
|
+
return null;
|
|
4946
5079
|
}
|
|
5080
|
+
return parsedValues;
|
|
5081
|
+
}
|
|
5082
|
+
/**
|
|
5083
|
+
* Sorts a parsed value list in place, so the sequence a value gets is stable across databases and runs.
|
|
5084
|
+
* An all-numeric list is compared as numbers (1, 2, 10); anything else keeps the default lexical order.
|
|
5085
|
+
*
|
|
5086
|
+
* Keyed off the VALUES, not the column type — so a string column whose list happens to be all-numeric
|
|
5087
|
+
* (`CHECK (Code IN ('1','2','10'))` on nvarchar) would re-sequence too. Measured over every column CHECK
|
|
5088
|
+
* constraint in a live 6.x database (MJ core + BizApps accounting/orders): of the 431 lists the previous
|
|
5089
|
+
* parser already captured, ZERO re-sequence under this comparison. If one ever turns up and the churn is
|
|
5090
|
+
* unwanted, gate `allNumeric` on the field being numeric as well.
|
|
5091
|
+
*/
|
|
5092
|
+
static sortCheckConstraintValues(values) {
|
|
5093
|
+
const allNumeric = values.length > 0 && values.every(v => /^-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?$/.test(v));
|
|
5094
|
+
return allNumeric ? values.sort((a, b) => Number(a) - Number(b)) : values.sort();
|
|
5095
|
+
}
|
|
5096
|
+
/**
|
|
5097
|
+
* A numeric literal as SQL Server renders it inside a CHECK constraint: parenthesized, unquoted, and
|
|
5098
|
+
* possibly signed, fractional or exponential — `(3)`, `(-1)`, `(1.00)`, `(2.5)`, `(1.0e+030)`.
|
|
5099
|
+
*/
|
|
5100
|
+
static { this.NUMERIC_CHECK_LITERAL = `\\(-?\\d+(?:\\.\\d*)?(?:[eE][+-]?\\d+)?\\)`; }
|
|
5101
|
+
/**
|
|
5102
|
+
* Turns a matched numeric literal into the value to store: strips the wrapping parentheses, and the
|
|
5103
|
+
* trailing point SQL Server adds to a large integral literal (`(12345678901234567890.)`).
|
|
5104
|
+
*/
|
|
5105
|
+
static normalizeNumericCheckLiteral(literal) {
|
|
5106
|
+
const inner = literal.slice(1, -1);
|
|
5107
|
+
return inner.endsWith('.') ? inner.slice(0, -1) : inner;
|
|
4947
5108
|
}
|
|
4948
5109
|
/**
|
|
4949
5110
|
* Parses PostgreSQL ANY (ARRAY[...]) CHECK constraint syntax.
|
|
@@ -5006,7 +5167,7 @@ export class ManageMetadataBase {
|
|
|
5006
5167
|
// it — user-visible AND editable, letting someone mutate the snapshot the refresher overwrites. The
|
|
5007
5168
|
// wrapper view is already excluded (it carries the minted entity, so EntityID IS NOT NULL). The outer
|
|
5008
5169
|
// view is aliased `t` so the correlated MaterializedResult subquery can't resolve columns ambiguously.
|
|
5009
|
-
const coreSchema =
|
|
5170
|
+
const coreSchema = MjCoreSchema();
|
|
5010
5171
|
// Gate the MaterializedResult reference on the table actually existing. Referencing it unconditionally
|
|
5011
5172
|
// would throw — breaking new-entity creation for EVERY entity — on any DB where MaterializedResult
|
|
5012
5173
|
// isn't present yet (e.g. the PostgreSQL parallel world's object-availability lag). If it's absent we
|
|
@@ -5150,7 +5311,7 @@ export class ManageMetadataBase {
|
|
|
5150
5311
|
const result = await ag.generateEntityName(newEntity.TableName, currentUser);
|
|
5151
5312
|
// Checked here as well as inside generateEntityName: a subclass or a stub can return anything, and
|
|
5152
5313
|
// a non-name that reaches the INSERT fails it (see createNewEntity's catch for why that used to be silent).
|
|
5153
|
-
if (result &&
|
|
5314
|
+
if (result && IsPlausibleEntityName(result.entityName)) {
|
|
5154
5315
|
return this.markupEntityName(newEntity.SchemaName, result.entityName);
|
|
5155
5316
|
}
|
|
5156
5317
|
else {
|
|
@@ -5215,7 +5376,7 @@ export class ManageMetadataBase {
|
|
|
5215
5376
|
return configInfo.newEntityDefaults?.NameRulesBySchema?.find(r => {
|
|
5216
5377
|
let schemaNameToUse = r.SchemaName;
|
|
5217
5378
|
if (schemaNameToUse?.trim().toLowerCase() === '${mj_core_schema}') {
|
|
5218
|
-
schemaNameToUse =
|
|
5379
|
+
schemaNameToUse = MjCoreSchema();
|
|
5219
5380
|
}
|
|
5220
5381
|
return schemaNameToUse.trim().toLowerCase() === schemaName.trim().toLowerCase();
|
|
5221
5382
|
});
|
|
@@ -5265,7 +5426,7 @@ export class ManageMetadataBase {
|
|
|
5265
5426
|
// process a single new entity
|
|
5266
5427
|
let newEntityName = await this.createNewEntityName(newEntity, currentUser);
|
|
5267
5428
|
const newEntityDisplayName = this.createNewEntityDisplayName(newEntity, newEntityName);
|
|
5268
|
-
const { name: uniqueName, suffix: uniqueSuffix } = this.
|
|
5429
|
+
const { name: uniqueName, suffix: uniqueSuffix } = this.ResolveUniqueEntityName(newEntityName, newEntity.SchemaName, md.Entities.map(e => e.Name));
|
|
5269
5430
|
if (uniqueName !== newEntityName) {
|
|
5270
5431
|
LogError(` >>>> WARNING: Entity name already exists, so using ${uniqueName} instead. If you did not intend for this, please rename the ${newEntity.SchemaName}.${newEntity.TableName} table in the database.`);
|
|
5271
5432
|
}
|
|
@@ -5274,7 +5435,7 @@ export class ManageMetadataBase {
|
|
|
5274
5435
|
const isNewSchema = await this.isSchemaNew(pool, newEntity.SchemaName);
|
|
5275
5436
|
const newEntityID = this.createNewUUID();
|
|
5276
5437
|
const sSQLInsert = this.createNewEntityInsertSQL(newEntityID, newEntityName, newEntity, suffix, newEntityDisplayName);
|
|
5277
|
-
await this.
|
|
5438
|
+
await this.logSQLAndExecute(pool, sSQLInsert, `SQL generated to create new entity ${newEntityName}`);
|
|
5278
5439
|
// if we get here we created a new entity safely, otherwise we get exception
|
|
5279
5440
|
// add it to the new entity list
|
|
5280
5441
|
ManageMetadataBase.newEntityList.push(newEntityName);
|
|
@@ -5307,10 +5468,10 @@ export class ManageMetadataBase {
|
|
|
5307
5468
|
if (configInfo.newEntityDefaults.AddToApplicationWithSchemaName) {
|
|
5308
5469
|
// only do this if the configuration setting is set to add new entities to applications for schema names
|
|
5309
5470
|
for (const appUUID of apps) {
|
|
5310
|
-
const sSQLInsertApplicationEntity = `INSERT INTO ${this.qs(
|
|
5471
|
+
const sSQLInsertApplicationEntity = `INSERT INTO ${this.qs(MjCoreSchema(), 'ApplicationEntity')}
|
|
5311
5472
|
(${this.qi('ApplicationID')}, ${this.qi('EntityID')}, ${this.qi('Sequence')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')}) VALUES
|
|
5312
|
-
('${appUUID}', '${newEntityID}', (SELECT COALESCE(MAX(${this.qi('Sequence')}),0)+1 FROM ${this.qs(
|
|
5313
|
-
await this.
|
|
5473
|
+
('${appUUID}', '${newEntityID}', (SELECT COALESCE(MAX(${this.qi('Sequence')}),0)+1 FROM ${this.qs(MjCoreSchema(), 'ApplicationEntity')} WHERE ${this.qi('ApplicationID')} = '${appUUID}'), ${this.utcNow()}, ${this.utcNow()})`;
|
|
5474
|
+
await this.logSQLAndExecute(pool, sSQLInsertApplicationEntity, `SQL generated to add new entity ${newEntityName} to application ID: '${appUUID}'`);
|
|
5314
5475
|
}
|
|
5315
5476
|
}
|
|
5316
5477
|
else {
|
|
@@ -5329,7 +5490,7 @@ export class ManageMetadataBase {
|
|
|
5329
5490
|
const RoleID = md.Roles.find(r => r.Name.trim().toLowerCase() === p.RoleName.trim().toLowerCase())?.ID;
|
|
5330
5491
|
if (RoleID) {
|
|
5331
5492
|
const sSQLInsertPermission = this.buildEntityPermissionInsertSQL(newEntityID, RoleID, p.CanRead, p.CanCreate, p.CanUpdate, p.CanDelete);
|
|
5332
|
-
await this.
|
|
5493
|
+
await this.logSQLAndExecute(pool, sSQLInsertPermission, `SQL generated to add new permission for entity ${newEntityName} for role ${p.RoleName}`);
|
|
5333
5494
|
}
|
|
5334
5495
|
else
|
|
5335
5496
|
LogError(` >>>> ERROR: Unable to find Role ID for role ${p.RoleName} to add permissions for new entity ${newEntityName}`);
|
|
@@ -5363,7 +5524,7 @@ export class ManageMetadataBase {
|
|
|
5363
5524
|
async isSchemaNew(pool, schemaName) {
|
|
5364
5525
|
// check to see if there are any entities in the db with this schema name
|
|
5365
5526
|
// Quote the alias so PostgreSQL preserves PascalCase (unquoted aliases are lowercased in PG)
|
|
5366
|
-
const sSQL = `SELECT COUNT(*) AS ${this.qi('Count')} FROM ${this.qs(
|
|
5527
|
+
const sSQL = `SELECT COUNT(*) AS ${this.qi('Count')} FROM ${this.qs(MjCoreSchema(), 'Entity')} WHERE ${this.qi('SchemaName')} = '${schemaName}'`;
|
|
5367
5528
|
const resultResult = await this.runQuery(pool, sSQL);
|
|
5368
5529
|
const result = resultResult.recordset;
|
|
5369
5530
|
// Use Number() because PG returns COUNT(*) as bigint, which the pg driver delivers as a string
|
|
@@ -5395,7 +5556,7 @@ export class ManageMetadataBase {
|
|
|
5395
5556
|
// Guard the INSERT with an existence check so the emitted (and re-runnable) migration SQL is
|
|
5396
5557
|
// idempotent. drop-schema clears an app's ApplicationEntity/EntityPermission/Entity rows but NOT
|
|
5397
5558
|
// its Application row, so replaying this block would otherwise collide on the Application PK.
|
|
5398
|
-
const appCheckQuery = `SELECT 1 FROM ${this.qs(
|
|
5559
|
+
const appCheckQuery = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'Application')} WHERE ${this.qi('ID')} = '${appID}'`;
|
|
5399
5560
|
// Schema-named bucket apps are plumbing (entity links, role grants, SchemaAutoAddNewEntities),
|
|
5400
5561
|
// not products — hide them from new users. Application.DefaultForNewUser defaults to 1 in the
|
|
5401
5562
|
// DB, so omitting the column here is what put raw '__mj_*'-named apps in every new user's
|
|
@@ -5404,10 +5565,10 @@ export class ManageMetadataBase {
|
|
|
5404
5565
|
// statement in PG's `DO $$ ... $$` block, and the identifier auto-quoter skips dollar-quoted
|
|
5405
5566
|
// blocks wholesale (they can legally contain arbitrary text), so nothing downstream will quote
|
|
5406
5567
|
// these for us. Bare `ID` reaches PG folded to `id` and the INSERT fails on every run.
|
|
5407
|
-
const appInsert = `INSERT INTO ${this.qs(
|
|
5568
|
+
const appInsert = `INSERT INTO ${this.qs(MjCoreSchema(), 'Application')} (${this.qi('ID')}, ${this.qi('Name')}, ${this.qi('Description')}, ${this.qi('SchemaAutoAddNewEntities')}, ${this.qi('Path')}, ${this.qi('AutoUpdatePath')}, ${this.qi('DefaultForNewUser')})
|
|
5408
5569
|
VALUES ('${appID}', '${appName}', 'Generated for schema', '${schemaName}', '${path}', ${this.dialect.BooleanLiteral(true)}, ${this.dialect.BooleanLiteral(false)})`;
|
|
5409
5570
|
const sSQL = this.conditionalInsert(appCheckQuery, appInsert);
|
|
5410
|
-
await this.
|
|
5571
|
+
await this.logSQLAndExecute(pool, sSQL, `SQL generated to create new application ${appName}`);
|
|
5411
5572
|
LogStatus(`Created new application ${appName} with Path: ${path}`);
|
|
5412
5573
|
// Auto-assign default roles to the new application
|
|
5413
5574
|
await this.addDefaultRolesForApplication(pool, appID, appName);
|
|
@@ -5435,12 +5596,12 @@ export class ManageMetadataBase {
|
|
|
5435
5596
|
// This covers BOTH re-running the block after drop-schema (which leaves ApplicationRole rows
|
|
5436
5597
|
// intact) AND the collision with the Integration role that manageMetadata recreates
|
|
5437
5598
|
// out-of-band — either path would otherwise violate UQ_ApplicationRole_App_Role.
|
|
5438
|
-
const roleCheckQuery = `SELECT 1 FROM ${this.qs(
|
|
5439
|
-
const roleInsert = `INSERT INTO ${this.qs(
|
|
5599
|
+
const roleCheckQuery = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'ApplicationRole')} WHERE ${this.qi('ApplicationID')} = '${appId}' AND ${this.qi('RoleID')} = '${role.ID}'`;
|
|
5600
|
+
const roleInsert = `INSERT INTO ${this.qs(MjCoreSchema(), 'ApplicationRole')}
|
|
5440
5601
|
(${this.qi('ApplicationID')}, ${this.qi('RoleID')}, ${this.qi('CanAccess')}, ${this.qi('CanAdmin')}) VALUES
|
|
5441
5602
|
('${appId}', '${role.ID}', ${this.boolLit(roleDef.CanAccess)}, ${this.boolLit(roleDef.CanAdmin)})`;
|
|
5442
5603
|
const sSQLInsert = this.conditionalInsert(roleCheckQuery, roleInsert);
|
|
5443
|
-
await this.
|
|
5604
|
+
await this.logSQLAndExecute(pool, sSQLInsert, `Adding role ${roleDef.RoleName} to application ${appName}`);
|
|
5444
5605
|
}
|
|
5445
5606
|
else {
|
|
5446
5607
|
LogError(`Unable to find Role '${roleDef.RoleName}' for application ${appName}`);
|
|
@@ -5448,14 +5609,14 @@ export class ManageMetadataBase {
|
|
|
5448
5609
|
}
|
|
5449
5610
|
}
|
|
5450
5611
|
async applicationExists(pool, applicationName) {
|
|
5451
|
-
const sSQL = `SELECT ID FROM ${this.qs(
|
|
5612
|
+
const sSQL = `SELECT ID FROM ${this.qs(MjCoreSchema(), 'Application')} WHERE Name = '${applicationName}'`;
|
|
5452
5613
|
const resultResult = await this.runQuery(pool, sSQL);
|
|
5453
5614
|
const result = resultResult.recordset;
|
|
5454
5615
|
return result && result.length > 0 ? result[0].ID.length > 0 : false;
|
|
5455
5616
|
}
|
|
5456
5617
|
async getApplicationIDForSchema(pool, schemaName) {
|
|
5457
5618
|
// get all the apps each time from DB as we might be adding, don't use Metadata here for that reason
|
|
5458
|
-
const sSQL = `SELECT ID, Name, SchemaAutoAddNewEntities FROM ${this.qs(
|
|
5619
|
+
const sSQL = `SELECT ID, Name, SchemaAutoAddNewEntities FROM ${this.qs(MjCoreSchema(), 'vwApplications')}`;
|
|
5459
5620
|
const resultResult = await this.runQuery(pool, sSQL);
|
|
5460
5621
|
const result = resultResult.recordset;
|
|
5461
5622
|
if (!result || result.length === 0) {
|
|
@@ -5497,10 +5658,10 @@ export class ManageMetadataBase {
|
|
|
5497
5658
|
if (apps && apps.length > 0) {
|
|
5498
5659
|
if (configInfo.newEntityDefaults.AddToApplicationWithSchemaName) {
|
|
5499
5660
|
for (const appUUID of apps) {
|
|
5500
|
-
const sSQLInsert = `INSERT INTO ${this.qs(
|
|
5661
|
+
const sSQLInsert = `INSERT INTO ${this.qs(MjCoreSchema(), 'ApplicationEntity')}
|
|
5501
5662
|
(${this.qi('ApplicationID')}, ${this.qi('EntityID')}, ${this.qi('Sequence')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')}) VALUES
|
|
5502
|
-
('${appUUID}', '${entityId}', (SELECT COALESCE(MAX(${this.qi('Sequence')}),0)+1 FROM ${this.qs(
|
|
5503
|
-
await this.
|
|
5663
|
+
('${appUUID}', '${entityId}', (SELECT COALESCE(MAX(${this.qi('Sequence')}),0)+1 FROM ${this.qs(MjCoreSchema(), 'ApplicationEntity')} WHERE ${this.qi('ApplicationID')} = '${appUUID}'), ${this.utcNow()}, ${this.utcNow()})`;
|
|
5664
|
+
await this.logSQLAndExecute(pool, sSQLInsert, `SQL generated to add entity ${entityName} to application ID: '${appUUID}'`);
|
|
5504
5665
|
}
|
|
5505
5666
|
}
|
|
5506
5667
|
}
|
|
@@ -5526,7 +5687,7 @@ export class ManageMetadataBase {
|
|
|
5526
5687
|
* valid on both SQL Server and PostgreSQL, so no provider branch is needed.
|
|
5527
5688
|
*/
|
|
5528
5689
|
buildEntityPermissionInsertSQL(entityId, roleId, canRead, canCreate, canUpdate, canDelete) {
|
|
5529
|
-
const table = this.qs(
|
|
5690
|
+
const table = this.qs(MjCoreSchema(), 'EntityPermission');
|
|
5530
5691
|
const entityLit = this.uuidLit(entityId);
|
|
5531
5692
|
const roleLit = this.uuidLit(roleId);
|
|
5532
5693
|
return `INSERT INTO ${table}
|
|
@@ -5551,7 +5712,7 @@ export class ManageMetadataBase {
|
|
|
5551
5712
|
const RoleID = md.Roles.find(r => r.Name.trim().toLowerCase() === p.RoleName.trim().toLowerCase())?.ID;
|
|
5552
5713
|
if (RoleID) {
|
|
5553
5714
|
const sSQLInsert = this.buildEntityPermissionInsertSQL(entityId, RoleID, p.CanRead, p.CanCreate, p.CanUpdate, p.CanDelete);
|
|
5554
|
-
await this.
|
|
5715
|
+
await this.logSQLAndExecute(pool, sSQLInsert, `SQL generated to add permission for entity ${entityName} for role ${p.RoleName}`);
|
|
5555
5716
|
}
|
|
5556
5717
|
else {
|
|
5557
5718
|
LogError(` >>>> ERROR: Unable to find Role ID for role ${p.RoleName} to add permissions for entity ${entityName}`);
|
|
@@ -5713,7 +5874,7 @@ export class ManageMetadataBase {
|
|
|
5713
5874
|
continue;
|
|
5714
5875
|
}
|
|
5715
5876
|
const sSQLInsert = this.buildEntityPermissionInsertSQL(entityId, roleId, true, false, false, false);
|
|
5716
|
-
await this.
|
|
5877
|
+
await this.logSQLAndExecute(pool, sSQLInsert, `SQL generated to add read permission for materialized entity ${entityName} for role ${p.RoleName}`);
|
|
5717
5878
|
}
|
|
5718
5879
|
}
|
|
5719
5880
|
/**
|
|
@@ -5723,8 +5884,8 @@ export class ManageMetadataBase {
|
|
|
5723
5884
|
* human authors protection. Non-destructive (rows are kept, just flipped) so the grant can be restored.
|
|
5724
5885
|
*/
|
|
5725
5886
|
async revokeMaterializedEntityReadAccess(pool, entityId, entityLabel, reason) {
|
|
5726
|
-
const sql = `UPDATE ${this.qs(
|
|
5727
|
-
await this.
|
|
5887
|
+
const sql = `UPDATE ${this.qs(MjCoreSchema(), 'EntityPermission')} SET ${this.qi('CanRead')}=${this.boolLit(false)} WHERE ${this.qi('EntityID')}='${entityId}' AND ${this.qi('CanRead')}=${this.boolLit(true)}`;
|
|
5888
|
+
await this.logSQLAndExecute(pool, sql, `Revoke read access on materialized entity "${entityLabel}" (${reason})`);
|
|
5728
5889
|
}
|
|
5729
5890
|
/**
|
|
5730
5891
|
* ROLE-RLS LAYER ONLY — true if any of the entity's role permissions carries a non-empty `ReadRLSFilterID`.
|
|
@@ -5795,7 +5956,7 @@ export class ManageMetadataBase {
|
|
|
5795
5956
|
for (const table of ['APIKeyScope', 'APIApplicationScope']) {
|
|
5796
5957
|
if (!(await this.columnExistsInCoreSchema(pool, table, 'RowFilterID')))
|
|
5797
5958
|
continue;
|
|
5798
|
-
const res = await this.runQuery(pool, `SELECT DISTINCT ${this.qi('ResourcePattern')} AS ResourcePattern FROM ${this.qs(
|
|
5959
|
+
const res = await this.runQuery(pool, `SELECT DISTINCT ${this.qi('ResourcePattern')} AS ResourcePattern FROM ${this.qs(MjCoreSchema(), table)} WHERE ${this.qi('RowFilterID')} IS NOT NULL`);
|
|
5799
5960
|
for (const row of res.recordset ?? []) {
|
|
5800
5961
|
const r = row;
|
|
5801
5962
|
const pattern = (r.ResourcePattern ?? r.resourcepattern) ?? '';
|
|
@@ -5828,7 +5989,7 @@ export class ManageMetadataBase {
|
|
|
5828
5989
|
if (cached !== undefined)
|
|
5829
5990
|
return cached;
|
|
5830
5991
|
const sql = `SELECT COUNT(*) AS ColExists FROM INFORMATION_SCHEMA.COLUMNS ` +
|
|
5831
|
-
`WHERE TABLE_SCHEMA = '${
|
|
5992
|
+
`WHERE TABLE_SCHEMA = '${MjCoreSchema()}' AND TABLE_NAME = '${table}' AND COLUMN_NAME = '${column}'`;
|
|
5832
5993
|
const result = await this.runQuery(pool, sql);
|
|
5833
5994
|
const row = (result.recordset?.[0] ?? {});
|
|
5834
5995
|
const cnt = row.ColExists ?? row.colexists ?? 0;
|
|
@@ -5848,14 +6009,14 @@ export class ManageMetadataBase {
|
|
|
5848
6009
|
if (sourceEntities.length === 0)
|
|
5849
6010
|
return 0; // no sources to intersect against — the RLS/empty gate handles this
|
|
5850
6011
|
const roleCanReadAllSources = (roleId) => sourceEntities.every((e) => e.Permissions.some((p) => UUIDsEqual(p.RoleID, roleId) && p.CanRead));
|
|
5851
|
-
const grants = await this.runQuery(pool, `SELECT ${this.qi('RoleID')} FROM ${this.qs(
|
|
6012
|
+
const grants = await this.runQuery(pool, `SELECT ${this.qi('RoleID')} FROM ${this.qs(MjCoreSchema(), 'EntityPermission')} WHERE ${this.qi('EntityID')}='${entityId}' AND ${this.qi('CanRead')}=${this.boolLit(true)}`);
|
|
5852
6013
|
let revoked = 0;
|
|
5853
6014
|
for (const g of grants.recordset) {
|
|
5854
6015
|
const roleId = g.RoleID;
|
|
5855
6016
|
if (!roleId)
|
|
5856
6017
|
continue;
|
|
5857
6018
|
if (!roleCanReadAllSources(roleId)) {
|
|
5858
|
-
await this.
|
|
6019
|
+
await this.logSQLAndExecute(pool, `UPDATE ${this.qs(MjCoreSchema(), 'EntityPermission')} SET ${this.qi('CanRead')}=${this.boolLit(false)} WHERE ${this.qi('EntityID')}='${entityId}' AND ${this.qi('RoleID')}='${roleId}' AND ${this.qi('CanRead')}=${this.boolLit(true)}`, `Narrow read grant on materialized entity "${entityLabel}": role ${roleId} can no longer read every source (C2 intersection re-narrowed)`);
|
|
5859
6020
|
revoked++;
|
|
5860
6021
|
}
|
|
5861
6022
|
}
|
|
@@ -5885,7 +6046,7 @@ export class ManageMetadataBase {
|
|
|
5885
6046
|
* earlier in this run (which are not in metadata yet)
|
|
5886
6047
|
* @returns the free name and the suffix used to reach it ('' when the name was already free)
|
|
5887
6048
|
*/
|
|
5888
|
-
|
|
6049
|
+
ResolveUniqueEntityName(desiredName, schemaName, takenNames) {
|
|
5889
6050
|
const taken = new Set([...takenNames, ...ManageMetadataBase.newEntityList].map(n => n.toLowerCase()));
|
|
5890
6051
|
const isTaken = (candidate) => taken.has(candidate.toLowerCase());
|
|
5891
6052
|
if (!isTaken(desiredName)) {
|
|
@@ -5904,13 +6065,17 @@ export class ManageMetadataBase {
|
|
|
5904
6065
|
}
|
|
5905
6066
|
return { name, suffix };
|
|
5906
6067
|
}
|
|
6068
|
+
/** @deprecated Use {@link ResolveUniqueEntityName}. */
|
|
6069
|
+
resolveUniqueEntityName(desiredName, schemaName, takenNames) {
|
|
6070
|
+
return this.ResolveUniqueEntityName(desiredName, schemaName, takenNames);
|
|
6071
|
+
}
|
|
5907
6072
|
createNewEntityInsertSQL(newEntityUUID, newEntityName, newEntity, newEntitySuffix, newEntityDisplayName) {
|
|
5908
6073
|
const newEntityDefaults = configInfo.newEntityDefaults;
|
|
5909
6074
|
const newEntityDescriptionEscaped = newEntity.EntityDescription ? `'${newEntity.EntityDescription.replace(/'/g, "''")}'` : null;
|
|
5910
6075
|
const allowCaching = this.resolveAllowCachingForSchema(newEntity.SchemaName);
|
|
5911
6076
|
const q = (name) => this.qi(name);
|
|
5912
6077
|
const sSQLInsert = `
|
|
5913
|
-
INSERT INTO ${this.qs(
|
|
6078
|
+
INSERT INTO ${this.qs(MjCoreSchema(), 'Entity')} (
|
|
5914
6079
|
${q('ID')},
|
|
5915
6080
|
${q('Name')},
|
|
5916
6081
|
${q('DisplayName')},
|
|
@@ -5971,7 +6136,7 @@ export class ManageMetadataBase {
|
|
|
5971
6136
|
const match = overrides.find(entry => {
|
|
5972
6137
|
let candidate = entry.SchemaName;
|
|
5973
6138
|
if (candidate?.trim().toLowerCase() === '${mj_core_schema}') {
|
|
5974
|
-
candidate =
|
|
6139
|
+
candidate = MjCoreSchema();
|
|
5975
6140
|
}
|
|
5976
6141
|
return candidate.trim().toLowerCase() === schemaName.trim().toLowerCase();
|
|
5977
6142
|
});
|
|
@@ -6032,7 +6197,7 @@ export class ManageMetadataBase {
|
|
|
6032
6197
|
e.FullTextSearchEnabled,
|
|
6033
6198
|
e.AutoUpdateFullTextSearch
|
|
6034
6199
|
FROM
|
|
6035
|
-
${this.qs(
|
|
6200
|
+
${this.qs(MjCoreSchema(), 'vwEntities')} e
|
|
6036
6201
|
WHERE
|
|
6037
6202
|
${whereClause}
|
|
6038
6203
|
ORDER BY
|
|
@@ -6081,7 +6246,7 @@ export class ManageMetadataBase {
|
|
|
6081
6246
|
ef.AutoUpdateFullTextSearch,
|
|
6082
6247
|
ef.${this.qi('Length')} as MaxLength
|
|
6083
6248
|
FROM
|
|
6084
|
-
${this.qs(
|
|
6249
|
+
${this.qs(MjCoreSchema(), 'vwEntityFields')} ef
|
|
6085
6250
|
WHERE
|
|
6086
6251
|
ef.EntityID IN (${entityIds})
|
|
6087
6252
|
ORDER BY
|
|
@@ -6099,7 +6264,7 @@ export class ManageMetadataBase {
|
|
|
6099
6264
|
es.Name,
|
|
6100
6265
|
es.Value
|
|
6101
6266
|
FROM
|
|
6102
|
-
${this.qs(
|
|
6267
|
+
${this.qs(MjCoreSchema(), 'EntitySetting')} es
|
|
6103
6268
|
WHERE
|
|
6104
6269
|
es.EntityID IN (${entityIds})
|
|
6105
6270
|
AND es.Name = 'FieldCategoryInfo'
|
|
@@ -6174,13 +6339,13 @@ export class ManageMetadataBase {
|
|
|
6174
6339
|
}
|
|
6175
6340
|
}
|
|
6176
6341
|
const pct = Math.round((processedCount / total) * 100);
|
|
6177
|
-
|
|
6342
|
+
UpdateSpinner(`Advanced generation: ${processedCount}/${total} entities (${pct}%)${errorCount > 0 ? ` — ${errorCount} error(s)` : ''}`);
|
|
6178
6343
|
// Credential circuit tripped (e.g. keyless / mis-credentialed env) — stop issuing further
|
|
6179
6344
|
// AI calls; the remaining entities would only produce doomed round-trips. One clear message
|
|
6180
6345
|
// was already logged when the circuit opened.
|
|
6181
6346
|
if (ag.AICircuitOpen) {
|
|
6182
6347
|
const skipped = total - processedCount;
|
|
6183
|
-
|
|
6348
|
+
UpdateSpinner(`Advanced generation: AI credential circuit open — skipping remaining ${skipped} entit${skipped === 1 ? 'y' : 'ies'} (check AI credentials).`);
|
|
6184
6349
|
break;
|
|
6185
6350
|
}
|
|
6186
6351
|
}
|
|
@@ -6351,7 +6516,7 @@ export class ManageMetadataBase {
|
|
|
6351
6516
|
// Execute all updates in one batch
|
|
6352
6517
|
if (sqlStatements.length > 0) {
|
|
6353
6518
|
try {
|
|
6354
|
-
await this.
|
|
6519
|
+
await this.logSQLBatchAndExecute(pool, sqlStatements, `Set field properties for entity`, false);
|
|
6355
6520
|
}
|
|
6356
6521
|
catch (ex) {
|
|
6357
6522
|
logError('Error executing combined smart field SQL: ', ex);
|
|
@@ -6379,7 +6544,7 @@ export class ManageMetadataBase {
|
|
|
6379
6544
|
const proposedSearchableCount = (result.searchableFields ?? []).length;
|
|
6380
6545
|
const proposedAllowUserSearch = result.allowUserSearch === true;
|
|
6381
6546
|
// 1. Shape cleanup (defensive — identifyFields() also runs this).
|
|
6382
|
-
const cleaned =
|
|
6547
|
+
const cleaned = NormalizeSmartFieldResultShape(result);
|
|
6383
6548
|
result.searchableFields = cleaned.searchableFields ?? [];
|
|
6384
6549
|
result.searchPredicates = cleaned.searchPredicates ?? [];
|
|
6385
6550
|
result.allowUserSearch = cleaned.allowUserSearch;
|
|
@@ -6400,14 +6565,14 @@ export class ManageMetadataBase {
|
|
|
6400
6565
|
droppedIneligibleCount += 1;
|
|
6401
6566
|
continue;
|
|
6402
6567
|
}
|
|
6403
|
-
if (
|
|
6568
|
+
if (IsNarrativeFieldName(name) && !ftxEnabled) {
|
|
6404
6569
|
droppedNarrativeCount += 1;
|
|
6405
6570
|
continue;
|
|
6406
6571
|
}
|
|
6407
6572
|
eligible.push(name);
|
|
6408
6573
|
}
|
|
6409
6574
|
// 3. Per-entity cap.
|
|
6410
|
-
const { accepted, dropped: droppedByCap } =
|
|
6575
|
+
const { accepted, dropped: droppedByCap } = ApplySearchableFieldsCap(eligible);
|
|
6411
6576
|
result.searchableFields = accepted;
|
|
6412
6577
|
// 4. Predicate normalization. Build a fresh searchPredicates list keyed
|
|
6413
6578
|
// on the accepted fields. The LLM's proposed predicates are the input
|
|
@@ -6422,7 +6587,7 @@ export class ManageMetadataBase {
|
|
|
6422
6587
|
const normalizedPredicates = [];
|
|
6423
6588
|
let predicatesRewrittenCount = 0;
|
|
6424
6589
|
for (const fieldName of accepted) {
|
|
6425
|
-
const { predicate, rewritten } =
|
|
6590
|
+
const { predicate, rewritten } = NormalizePredicate({
|
|
6426
6591
|
fieldName,
|
|
6427
6592
|
proposed: proposedByName.get(fieldName),
|
|
6428
6593
|
isInFullTextSearchFields: ftsFieldSet.has(fieldName),
|
|
@@ -6497,7 +6662,7 @@ export class ManageMetadataBase {
|
|
|
6497
6662
|
// Set the winner when it isn't flagged yet (and auto-update is allowed).
|
|
6498
6663
|
if (winner && winner.AutoUpdateIsNameField && winner.ID && !winner.IsNameField) {
|
|
6499
6664
|
sqlStatements.push(`
|
|
6500
|
-
UPDATE ${this.qs(
|
|
6665
|
+
UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
6501
6666
|
SET IsNameField = ${this.boolLit(true)}
|
|
6502
6667
|
WHERE ID = '${winner.ID}'
|
|
6503
6668
|
AND AutoUpdateIsNameField = ${this.boolLit(true)}
|
|
@@ -6509,7 +6674,7 @@ export class ManageMetadataBase {
|
|
|
6509
6674
|
continue;
|
|
6510
6675
|
}
|
|
6511
6676
|
sqlStatements.push(`
|
|
6512
|
-
UPDATE ${this.qs(
|
|
6677
|
+
UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
6513
6678
|
SET IsNameField = ${this.boolLit(false)}
|
|
6514
6679
|
WHERE ID = '${f.ID}'
|
|
6515
6680
|
AND AutoUpdateIsNameField = ${this.boolLit(true)}
|
|
@@ -6658,7 +6823,7 @@ export class ManageMetadataBase {
|
|
|
6658
6823
|
for (const field of defaultInViewFields) {
|
|
6659
6824
|
if (!field.DefaultInView) {
|
|
6660
6825
|
sqlStatements.push(`
|
|
6661
|
-
UPDATE ${this.qs(
|
|
6826
|
+
UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
6662
6827
|
SET DefaultInView = ${this.boolLit(true)}
|
|
6663
6828
|
WHERE ID = '${field.ID}'
|
|
6664
6829
|
AND AutoUpdateDefaultInView = ${this.boolLit(true)}
|
|
@@ -6696,7 +6861,7 @@ export class ManageMetadataBase {
|
|
|
6696
6861
|
}
|
|
6697
6862
|
if (!field.IncludeInUserSearchAPI) {
|
|
6698
6863
|
sqlStatements.push(`
|
|
6699
|
-
UPDATE ${this.qs(
|
|
6864
|
+
UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
6700
6865
|
SET IncludeInUserSearchAPI = ${this.boolLit(true)}
|
|
6701
6866
|
WHERE ID = '${field.ID}'
|
|
6702
6867
|
AND AutoUpdateIncludeInUserSearchAPI = ${this.boolLit(true)}
|
|
@@ -6751,7 +6916,7 @@ export class ManageMetadataBase {
|
|
|
6751
6916
|
* which is T-SQL-only. The subquery form is ANSI and runs unchanged on both platforms.
|
|
6752
6917
|
*/
|
|
6753
6918
|
buildSearchFlagHygieneSQL(excludeSchemas) {
|
|
6754
|
-
const coreSchema =
|
|
6919
|
+
const coreSchema = MjCoreSchema();
|
|
6755
6920
|
const entity = this.qs(coreSchema, 'Entity');
|
|
6756
6921
|
const entityField = this.qs(coreSchema, 'EntityField');
|
|
6757
6922
|
const yes = this.boolLit(true);
|
|
@@ -6764,7 +6929,7 @@ export class ManageMetadataBase {
|
|
|
6764
6929
|
// to 'Contains' in the database, which is `LIKE '%term%'` — the unindexable scan the
|
|
6765
6930
|
// guardrails exist to prevent. Seeding the flag without the predicate would have made every
|
|
6766
6931
|
// seeded entity a full scan on every keystroke.
|
|
6767
|
-
const seedPredicate =
|
|
6932
|
+
const seedPredicate = DefaultPredicateFor(NAME_LIKE_FIELD_NAMES[0]);
|
|
6768
6933
|
// The entity-shape guardrails the LLM path applies (`entityLevelEnableBlockedReason`).
|
|
6769
6934
|
// A log / audit / run-history table grows without bound and a detail / line-item child is
|
|
6770
6935
|
// reached through its parent; neither is a global-search target, whichever columns it has.
|
|
@@ -6917,7 +7082,7 @@ export class ManageMetadataBase {
|
|
|
6917
7082
|
const seedCount = await this.searchFlagHygieneCandidateCount(pool, seedProbeSQL);
|
|
6918
7083
|
if (seedCount > 0) {
|
|
6919
7084
|
logStatus(` Search-flag hygiene: seeding ${seedCount} name field(s)`);
|
|
6920
|
-
await this.
|
|
7085
|
+
await this.logSQLBatchAndExecute(pool, [seedSQL], 'Deterministic search-flag hygiene — seed name fields', false);
|
|
6921
7086
|
}
|
|
6922
7087
|
// Probed AFTER the seed has run, not alongside it. Seeding changes which entities still
|
|
6923
7088
|
// have nothing searchable, so a clear probe taken before it would count entities the seed
|
|
@@ -6930,7 +7095,7 @@ export class ManageMetadataBase {
|
|
|
6930
7095
|
const shown = clearNames.slice(0, 25).join(', ');
|
|
6931
7096
|
const more = clearNames.length > 25 ? `, ... and ${clearNames.length - 25} more` : '';
|
|
6932
7097
|
logStatus(` Search-flag hygiene: turning AllowUserSearchAPI OFF on ${clearNames.length} entity(ies): ${shown}${more}`);
|
|
6933
|
-
await this.
|
|
7098
|
+
await this.logSQLBatchAndExecute(pool, [clearSQL], 'Deterministic search-flag hygiene — clear AllowUserSearchAPI', false);
|
|
6934
7099
|
}
|
|
6935
7100
|
return true;
|
|
6936
7101
|
}
|
|
@@ -7018,7 +7183,7 @@ export class ManageMetadataBase {
|
|
|
7018
7183
|
// Only update if the current value differs from the recommended predicate
|
|
7019
7184
|
if (field.UserSearchPredicateAPI !== sp.predicate) {
|
|
7020
7185
|
sqlStatements.push(`
|
|
7021
|
-
UPDATE ${this.qs(
|
|
7186
|
+
UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
7022
7187
|
SET UserSearchPredicateAPI = '${sp.predicate}'
|
|
7023
7188
|
WHERE ID = '${field.ID}'
|
|
7024
7189
|
AND AutoUpdateUserSearchPredicate = ${this.boolLit(true)}
|
|
@@ -7049,7 +7214,7 @@ export class ManageMetadataBase {
|
|
|
7049
7214
|
// the field-level guardrails, and refuses log/audit and detail/line-
|
|
7050
7215
|
// item-shaped entities. Anything blocked here gets a one-line note in
|
|
7051
7216
|
// the CodeGen run report so the team can audit proposals.
|
|
7052
|
-
const blocked =
|
|
7217
|
+
const blocked = EntityLevelEnableBlockedReason({
|
|
7053
7218
|
entityName: entity.Name,
|
|
7054
7219
|
confidence: result.confidence,
|
|
7055
7220
|
acceptedSearchableFieldsCount: (result.searchableFields ?? []).length,
|
|
@@ -7065,7 +7230,7 @@ export class ManageMetadataBase {
|
|
|
7065
7230
|
}
|
|
7066
7231
|
if (newValue !== currentValue) {
|
|
7067
7232
|
sqlStatements.push(`
|
|
7068
|
-
UPDATE ${this.qs(
|
|
7233
|
+
UPDATE ${this.qs(MjCoreSchema(), 'Entity')}
|
|
7069
7234
|
SET AllowUserSearchAPI = ${this.boolLit(newValue)}
|
|
7070
7235
|
WHERE ID = '${entity.ID}'
|
|
7071
7236
|
AND AutoUpdateAllowUserSearchAPI = ${this.boolLit(true)}
|
|
@@ -7108,7 +7273,7 @@ export class ManageMetadataBase {
|
|
|
7108
7273
|
const currentValue = !!entity.FullTextSearchEnabled;
|
|
7109
7274
|
if (newValue !== currentValue) {
|
|
7110
7275
|
sqlStatements.push(`
|
|
7111
|
-
UPDATE ${this.qs(
|
|
7276
|
+
UPDATE ${this.qs(MjCoreSchema(), 'Entity')}
|
|
7112
7277
|
SET FullTextSearchEnabled = ${this.boolLit(newValue)}
|
|
7113
7278
|
WHERE ID = '${entity.ID}'
|
|
7114
7279
|
AND AutoUpdateFullTextSearch = ${this.boolLit(true)}
|
|
@@ -7130,7 +7295,7 @@ export class ManageMetadataBase {
|
|
|
7130
7295
|
}
|
|
7131
7296
|
if (!field.FullTextSearchEnabled) {
|
|
7132
7297
|
sqlStatements.push(`
|
|
7133
|
-
UPDATE ${this.qs(
|
|
7298
|
+
UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
7134
7299
|
SET FullTextSearchEnabled = ${this.boolLit(true)}
|
|
7135
7300
|
WHERE ID = '${field.ID}'
|
|
7136
7301
|
AND AutoUpdateFullTextSearch = ${this.boolLit(true)}
|
|
@@ -7175,7 +7340,7 @@ export class ManageMetadataBase {
|
|
|
7175
7340
|
* Respects the AutoUpdateSupportsGeoCoding flag — if 0, the value is locked.
|
|
7176
7341
|
*/
|
|
7177
7342
|
async detectAndSetGeoCodingSupport(pool, entity) {
|
|
7178
|
-
const schema =
|
|
7343
|
+
const schema = MjCoreSchema();
|
|
7179
7344
|
// Check if the entity's AutoUpdateSupportsGeoCoding flag allows us to modify it
|
|
7180
7345
|
const autoUpdateResult = await pool.query(`
|
|
7181
7346
|
SELECT ${this.qi('AutoUpdateSupportsGeoCoding')}, ${this.qi('SupportsGeoCoding')}
|
|
@@ -7198,7 +7363,7 @@ export class ManageMetadataBase {
|
|
|
7198
7363
|
const shouldSupportGeo = existingGeoCount > 0;
|
|
7199
7364
|
const currentValue = row.SupportsGeoCoding ? true : false;
|
|
7200
7365
|
if (shouldSupportGeo !== currentValue) {
|
|
7201
|
-
await this.
|
|
7366
|
+
await this.logSQLAndExecute(pool, `
|
|
7202
7367
|
UPDATE ${this.qs(schema, 'Entity')}
|
|
7203
7368
|
SET ${this.qi('SupportsGeoCoding')} = ${this.boolLit(shouldSupportGeo)}
|
|
7204
7369
|
WHERE ${this.qi('ID')} = '${entity.ID}' AND ${this.qi('AutoUpdateSupportsGeoCoding')} = ${this.boolLit(true)}
|
|
@@ -7272,14 +7437,14 @@ export class ManageMetadataBase {
|
|
|
7272
7437
|
continue;
|
|
7273
7438
|
}
|
|
7274
7439
|
const lockCtx = {
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
existingCategories,
|
|
7440
|
+
IsNewEntity: ctx.isNewEntity,
|
|
7441
|
+
IsNewField: ManageMetadataBase.isFieldNew(entity.ID, field.Name),
|
|
7442
|
+
DescriptionReopened: ManageMetadataBase.isDisplayNameReopened(entity.ID, field.Name),
|
|
7443
|
+
TypeReopened: ManageMetadataBase.isTypeReopened(entity.ID, field.Name),
|
|
7444
|
+
ExistingCategories: existingCategories,
|
|
7280
7445
|
};
|
|
7281
|
-
const update =
|
|
7282
|
-
for (const skip of update.
|
|
7446
|
+
const update = ComputeFieldMetadataUpdate(field, fieldCategory, lockCtx, this.validateExtendedType.bind(this), (val, fn) => this.sanitizeCodeType(val, fn ?? field.Name, entity.Name));
|
|
7447
|
+
for (const skip of update.Skipped) {
|
|
7283
7448
|
if (skip.reason === 'locked') {
|
|
7284
7449
|
reporter.counter('ai.fieldsLocked');
|
|
7285
7450
|
}
|
|
@@ -7306,7 +7471,7 @@ export class ManageMetadataBase {
|
|
|
7306
7471
|
setClauses.push(`CodeType = ${codeVal}`);
|
|
7307
7472
|
}
|
|
7308
7473
|
if (setClauses.length > 0) {
|
|
7309
|
-
sqlStatements.push(`\n-- UPDATE Entity Field Category Info ${entity.Name}.${field.Name} \nUPDATE ${this.qs(
|
|
7474
|
+
sqlStatements.push(`\n-- UPDATE Entity Field Category Info ${entity.Name}.${field.Name} \nUPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
|
|
7310
7475
|
SET
|
|
7311
7476
|
${setClauses.join(',\n ')}
|
|
7312
7477
|
WHERE
|
|
@@ -7315,7 +7480,7 @@ WHERE
|
|
|
7315
7480
|
}
|
|
7316
7481
|
if (sqlStatements.length > 0) {
|
|
7317
7482
|
try {
|
|
7318
|
-
await this.
|
|
7483
|
+
await this.logSQLBatchAndExecute(pool, sqlStatements, `Set categories for ${sqlStatements.length} fields`, false);
|
|
7319
7484
|
}
|
|
7320
7485
|
catch (ex) {
|
|
7321
7486
|
logError('Error Applying Field Categories', ex);
|
|
@@ -7328,19 +7493,19 @@ WHERE
|
|
|
7328
7493
|
async applyEntityIcon(pool, entityId, entityIcon, entityName) {
|
|
7329
7494
|
if (!entityIcon || entityIcon.trim().length === 0)
|
|
7330
7495
|
return;
|
|
7331
|
-
const checkSQL = `SELECT Icon FROM ${this.qs(
|
|
7496
|
+
const checkSQL = `SELECT Icon FROM ${this.qs(MjCoreSchema(), 'Entity')} WHERE ID = '${entityId}'`;
|
|
7332
7497
|
const entityCheck = await this.runQuery(pool, checkSQL);
|
|
7333
7498
|
if (entityCheck.recordset.length > 0) {
|
|
7334
7499
|
const currentIcon = entityCheck.recordset[0].Icon;
|
|
7335
7500
|
if (!currentIcon || currentIcon.trim().length === 0) {
|
|
7336
7501
|
const escapedIcon = entityIcon.replace(/'/g, "''");
|
|
7337
7502
|
const updateSQL = `
|
|
7338
|
-
UPDATE ${this.qs(
|
|
7503
|
+
UPDATE ${this.qs(MjCoreSchema(), 'Entity')}
|
|
7339
7504
|
SET ${this.qi('Icon')} = '${escapedIcon}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
7340
7505
|
WHERE ${this.qi('ID')} = '${entityId}'
|
|
7341
7506
|
`;
|
|
7342
7507
|
try {
|
|
7343
|
-
await this.
|
|
7508
|
+
await this.logSQLAndExecute(pool, updateSQL, `Set entity icon to ${entityIcon}`, false);
|
|
7344
7509
|
logStatus(` Set entity icon: ${entityIcon}`);
|
|
7345
7510
|
}
|
|
7346
7511
|
catch (ex) {
|
|
@@ -7355,26 +7520,26 @@ WHERE
|
|
|
7355
7520
|
async applyCategoryInfoSettings(pool, entityId, categoryInfo, _entityName) {
|
|
7356
7521
|
if (!categoryInfo || Object.keys(categoryInfo).length === 0)
|
|
7357
7522
|
return;
|
|
7358
|
-
const canonicalInfo =
|
|
7523
|
+
const canonicalInfo = CanonicalJSONStringify(categoryInfo, 2);
|
|
7359
7524
|
const infoJSON = canonicalInfo.replace(/'/g, "''");
|
|
7360
7525
|
const ENTITY_SETTING_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
7361
7526
|
// Upsert FieldCategoryInfo (new format)
|
|
7362
|
-
const checkNewSQL = `SELECT ${this.qi('ID')}, ${this.qi('Value')} FROM ${this.qs(
|
|
7527
|
+
const checkNewSQL = `SELECT ${this.qi('ID')}, ${this.qi('Value')} FROM ${this.qs(MjCoreSchema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'`;
|
|
7363
7528
|
const existingNew = await this.runQuery(pool, checkNewSQL);
|
|
7364
7529
|
if (existingNew.recordset.length > 0) {
|
|
7365
7530
|
let isMatch = false;
|
|
7366
7531
|
try {
|
|
7367
7532
|
const rawVal = existingNew.recordset[0].Value;
|
|
7368
7533
|
const parsed = typeof rawVal === 'string' ? JSON.parse(rawVal) : rawVal;
|
|
7369
|
-
isMatch =
|
|
7534
|
+
isMatch = DeepEqualJSON(parsed, categoryInfo);
|
|
7370
7535
|
}
|
|
7371
7536
|
catch {
|
|
7372
7537
|
isMatch = false;
|
|
7373
7538
|
}
|
|
7374
7539
|
if (!isMatch) {
|
|
7375
7540
|
try {
|
|
7376
|
-
await this.
|
|
7377
|
-
UPDATE ${this.qs(
|
|
7541
|
+
await this.logSQLAndExecute(pool, `
|
|
7542
|
+
UPDATE ${this.qs(MjCoreSchema(), 'EntitySetting')}
|
|
7378
7543
|
SET ${this.qi('Value')} = '${infoJSON}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
7379
7544
|
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'
|
|
7380
7545
|
`, `Update FieldCategoryInfo setting for entity`, false);
|
|
@@ -7387,11 +7552,11 @@ WHERE
|
|
|
7387
7552
|
else {
|
|
7388
7553
|
const newId = uuidv5(`${entityId}|FieldCategoryInfo`, ENTITY_SETTING_NAMESPACE);
|
|
7389
7554
|
try {
|
|
7390
|
-
const checkQuery = `SELECT 1 FROM ${this.qs(
|
|
7391
|
-
const insertSQL = `INSERT INTO ${this.qs(
|
|
7555
|
+
const checkQuery = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'`;
|
|
7556
|
+
const insertSQL = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntitySetting')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Name')}, ${this.qi('Value')}, ${this.qi(EntityInfo.CreatedAtFieldName)}, ${this.qi(EntityInfo.UpdatedAtFieldName)})
|
|
7392
7557
|
VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}', ${this.utcNow()}, ${this.utcNow()})`;
|
|
7393
7558
|
const sSQL = this.conditionalInsert(checkQuery, insertSQL);
|
|
7394
|
-
await this.
|
|
7559
|
+
await this.logSQLAndExecute(pool, sSQL, `Insert FieldCategoryInfo setting for entity`, false);
|
|
7395
7560
|
}
|
|
7396
7561
|
catch (ex) {
|
|
7397
7562
|
logError('Error Applying Category Info Settings: Part 2', ex);
|
|
@@ -7404,24 +7569,24 @@ WHERE
|
|
|
7404
7569
|
iconsOnly[category] = info.icon;
|
|
7405
7570
|
}
|
|
7406
7571
|
}
|
|
7407
|
-
const canonicalIcons =
|
|
7572
|
+
const canonicalIcons = CanonicalJSONStringify(iconsOnly, 2);
|
|
7408
7573
|
const iconsJSON = canonicalIcons.replace(/'/g, "''");
|
|
7409
|
-
const checkLegacySQL = `SELECT ${this.qi('ID')}, ${this.qi('Value')} FROM ${this.qs(
|
|
7574
|
+
const checkLegacySQL = `SELECT ${this.qi('ID')}, ${this.qi('Value')} FROM ${this.qs(MjCoreSchema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'`;
|
|
7410
7575
|
const existingLegacy = await this.runQuery(pool, checkLegacySQL);
|
|
7411
7576
|
if (existingLegacy.recordset.length > 0) {
|
|
7412
7577
|
let isMatch = false;
|
|
7413
7578
|
try {
|
|
7414
7579
|
const rawVal = existingLegacy.recordset[0].Value;
|
|
7415
7580
|
const parsed = typeof rawVal === 'string' ? JSON.parse(rawVal) : rawVal;
|
|
7416
|
-
isMatch =
|
|
7581
|
+
isMatch = DeepEqualJSON(parsed, iconsOnly);
|
|
7417
7582
|
}
|
|
7418
7583
|
catch {
|
|
7419
7584
|
isMatch = false;
|
|
7420
7585
|
}
|
|
7421
7586
|
if (!isMatch) {
|
|
7422
7587
|
try {
|
|
7423
|
-
await this.
|
|
7424
|
-
UPDATE ${this.qs(
|
|
7588
|
+
await this.logSQLAndExecute(pool, `
|
|
7589
|
+
UPDATE ${this.qs(MjCoreSchema(), 'EntitySetting')}
|
|
7425
7590
|
SET ${this.qi('Value')} = '${iconsJSON}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
7426
7591
|
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'
|
|
7427
7592
|
`, `Update FieldCategoryIcons setting (legacy)`, false);
|
|
@@ -7434,11 +7599,11 @@ WHERE
|
|
|
7434
7599
|
else {
|
|
7435
7600
|
const newId = uuidv5(`${entityId}|FieldCategoryIcons`, ENTITY_SETTING_NAMESPACE);
|
|
7436
7601
|
try {
|
|
7437
|
-
const checkQuery = `SELECT 1 FROM ${this.qs(
|
|
7438
|
-
const insertSQL = `INSERT INTO ${this.qs(
|
|
7602
|
+
const checkQuery = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'`;
|
|
7603
|
+
const insertSQL = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntitySetting')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Name')}, ${this.qi('Value')}, ${this.qi(EntityInfo.CreatedAtFieldName)}, ${this.qi(EntityInfo.UpdatedAtFieldName)})
|
|
7439
7604
|
VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}', ${this.utcNow()}, ${this.utcNow()})`;
|
|
7440
7605
|
const sSQL = this.conditionalInsert(checkQuery, insertSQL);
|
|
7441
|
-
await this.
|
|
7606
|
+
await this.logSQLAndExecute(pool, sSQL, `Insert FieldCategoryIcons setting (legacy)`, false);
|
|
7442
7607
|
}
|
|
7443
7608
|
catch (ex) {
|
|
7444
7609
|
logError('Error Applying Category Info Settings: Part 4', ex);
|
|
@@ -7451,12 +7616,12 @@ WHERE
|
|
|
7451
7616
|
*/
|
|
7452
7617
|
async applyEntityImportance(pool, entityId, importance, entityName) {
|
|
7453
7618
|
const updateSQL = `
|
|
7454
|
-
UPDATE ${this.qs(
|
|
7619
|
+
UPDATE ${this.qs(MjCoreSchema(), 'ApplicationEntity')}
|
|
7455
7620
|
SET ${this.qi('DefaultForNewUser')} = ${this.boolLit(importance.defaultForNewUser)}, ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
|
|
7456
7621
|
WHERE ${this.qi('EntityID')} = '${entityId}'
|
|
7457
7622
|
`;
|
|
7458
7623
|
try {
|
|
7459
|
-
await this.
|
|
7624
|
+
await this.logSQLAndExecute(pool, updateSQL, `Set DefaultForNewUser=${importance.defaultForNewUser} for NEW entity (category: ${importance.entityCategory}, confidence: ${importance.confidence})`, false);
|
|
7460
7625
|
logStatus(` Entity importance (NEW Entity): ${importance.entityCategory} (defaultForNewUser: ${importance.defaultForNewUser}, confidence: ${importance.confidence})`);
|
|
7461
7626
|
logStatus(` Reasoning: ${importance.reasoning}`);
|
|
7462
7627
|
}
|
|
@@ -7474,7 +7639,7 @@ WHERE
|
|
|
7474
7639
|
* @param isRecurringScript - if set to true tells the logger that the provided SQL represents a recurring script meaning it is something that is executed, generally, for all CodeGen runs. In these cases, the Config settings can result in omitting these recurring scripts from being logged because the configuration environment may have those recurring scripts already set to run after all run-specific migrations get run.
|
|
7475
7640
|
* @returns - The result of the query execution.
|
|
7476
7641
|
*/
|
|
7477
|
-
async
|
|
7642
|
+
async logSQLAndExecute(pool, query, description, isRecurringScript = false, includeBatchSeparator = false, batchSeparator = 'GO', requiresOwnBatch = false) {
|
|
7478
7643
|
return await SQLLogging.LogSQLAndExecute(pool, this.qsql(query), description, isRecurringScript, includeBatchSeparator, batchSeparator, requiresOwnBatch);
|
|
7479
7644
|
}
|
|
7480
7645
|
/**
|
|
@@ -7493,17 +7658,17 @@ WHERE
|
|
|
7493
7658
|
* Empty / whitespace-only statements are filtered out. Returns
|
|
7494
7659
|
* `undefined` (no execution) when nothing remains.
|
|
7495
7660
|
*/
|
|
7496
|
-
async
|
|
7661
|
+
async logSQLBatchAndExecute(pool, statements, description, isRecurringScript = false, includeBatchSeparator = false, batchSeparator = 'GO') {
|
|
7497
7662
|
const terminated = [];
|
|
7498
7663
|
for (const s of statements) {
|
|
7499
|
-
const trimmed =
|
|
7664
|
+
const trimmed = TrimTrailingStatementTerminators(s ?? '');
|
|
7500
7665
|
if (trimmed.length === 0)
|
|
7501
7666
|
continue;
|
|
7502
7667
|
terminated.push(`${trimmed};`);
|
|
7503
7668
|
}
|
|
7504
7669
|
if (terminated.length === 0)
|
|
7505
7670
|
return undefined;
|
|
7506
|
-
return await this.
|
|
7671
|
+
return await this.logSQLAndExecute(pool, terminated.join('\n'), description, isRecurringScript, includeBatchSeparator, batchSeparator);
|
|
7507
7672
|
}
|
|
7508
7673
|
}
|
|
7509
7674
|
//# sourceMappingURL=manage-metadata.js.map
|