@memberjunction/codegen-lib 3.4.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +828 -630
- package/dist/Angular/angular-codegen.d.ts +10 -3
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +164 -199
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.d.ts +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.d.ts.map +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.js +8 -10
- package/dist/Angular/entity-data-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/join-grid-related-entity-component.d.ts +1 -1
- package/dist/Angular/join-grid-related-entity-component.js +8 -36
- package/dist/Angular/join-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/related-entity-components.js +13 -79
- package/dist/Angular/related-entity-components.js.map +1 -1
- package/dist/Angular/timeline-related-entity-component.d.ts +1 -1
- package/dist/Angular/timeline-related-entity-component.js +14 -38
- package/dist/Angular/timeline-related-entity-component.js.map +1 -1
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +171 -177
- package/dist/Config/config.js.map +1 -1
- package/dist/Config/db-connection.d.ts +1 -1
- package/dist/Config/db-connection.d.ts.map +1 -1
- package/dist/Config/db-connection.js +6 -33
- package/dist/Config/db-connection.js.map +1 -1
- package/dist/Database/dbSchema.js +28 -35
- package/dist/Database/dbSchema.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +302 -6
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +1294 -445
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/reorder-columns.d.ts +1 -1
- package/dist/Database/reorder-columns.d.ts.map +1 -1
- package/dist/Database/reorder-columns.js +1 -5
- package/dist/Database/reorder-columns.js.map +1 -1
- package/dist/Database/sql.d.ts +1 -1
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +67 -98
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +15 -2
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +282 -253
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +11 -0
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.js +43 -41
- package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +15 -26
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.d.ts +69 -7
- package/dist/Misc/advanced_generation.d.ts.map +1 -1
- package/dist/Misc/advanced_generation.js +114 -75
- package/dist/Misc/advanced_generation.js.map +1 -1
- package/dist/Misc/createNewUser.d.ts +1 -1
- package/dist/Misc/createNewUser.js +22 -26
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts +7 -2
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +56 -45
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +39 -42
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/runCommand.d.ts +1 -1
- package/dist/Misc/runCommand.js +13 -20
- package/dist/Misc/runCommand.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +1 -1
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +21 -51
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/Misc/status_logging.js +45 -60
- package/dist/Misc/status_logging.js.map +1 -1
- package/dist/Misc/system_integrity.d.ts +1 -1
- package/dist/Misc/system_integrity.d.ts.map +1 -1
- package/dist/Misc/system_integrity.js +12 -16
- package/dist/Misc/system_integrity.js.map +1 -1
- package/dist/Misc/temp_batch_file.js +15 -22
- package/dist/Misc/temp_batch_file.js.map +1 -1
- package/dist/Misc/util.d.ts.map +1 -1
- package/dist/Misc/util.js +17 -28
- package/dist/Misc/util.js.map +1 -1
- package/dist/__tests__/metadataConfig.test.d.ts +12 -0
- package/dist/__tests__/metadataConfig.test.d.ts.map +1 -0
- package/dist/__tests__/metadataConfig.test.js +604 -0
- package/dist/__tests__/metadataConfig.test.js.map +1 -0
- package/dist/index.d.ts +21 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -41
- package/dist/index.js.map +1 -1
- package/dist/runCodeGen.d.ts +1 -0
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +150 -178
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +29 -23
|
@@ -1,102 +1,76 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const config_1 = require("../Config/config");
|
|
32
|
-
const core_1 = require("@memberjunction/core");
|
|
33
|
-
const status_logging_1 = require("../Misc/status_logging");
|
|
34
|
-
const sql_1 = require("./sql");
|
|
35
|
-
const advanced_generation_1 = require("../Misc/advanced_generation");
|
|
36
|
-
const global_1 = require("@memberjunction/global");
|
|
37
|
-
const uuid_1 = require("uuid");
|
|
38
|
-
const fs = __importStar(require("fs"));
|
|
39
|
-
const path_1 = __importDefault(require("path"));
|
|
40
|
-
const sql_logging_1 = require("../Misc/sql_logging");
|
|
41
|
-
class ValidatorResult {
|
|
42
|
-
entityName = "";
|
|
43
|
-
fieldName;
|
|
44
|
-
sourceCheckConstraint = "";
|
|
45
|
-
functionText = "";
|
|
46
|
-
functionName = "";
|
|
47
|
-
functionDescription = "";
|
|
48
|
-
/**
|
|
49
|
-
* The ID value in the Generated Codes entity that was created for this validator.
|
|
50
|
-
*/
|
|
51
|
-
generatedCodeId = "";
|
|
52
|
-
/**
|
|
53
|
-
* The ID for the AI Model that was used to generate the code
|
|
54
|
-
*/
|
|
55
|
-
aiModelID = "";
|
|
56
|
-
wasGenerated = true;
|
|
57
|
-
success = false;
|
|
1
|
+
import sql from 'mssql';
|
|
2
|
+
import { configInfo, currentWorkingDirectory, getSettingValue, mj_core_schema, outputDir } from '../Config/config.js';
|
|
3
|
+
import { CodeNameFromString, EntityInfo, ExtractActualDefaultValue, LogError, LogStatus, Metadata, SeverityType } from "@memberjunction/core";
|
|
4
|
+
import { logError, logMessage, logStatus } from "../Misc/status_logging.js";
|
|
5
|
+
import { SQLUtilityBase } from "./sql.js";
|
|
6
|
+
import { AdvancedGeneration } from "../Misc/advanced_generation.js";
|
|
7
|
+
import { SQLParser } from "@memberjunction/core-entities-server";
|
|
8
|
+
import { convertCamelCaseToHaveSpaces, generatePluralName, MJGlobal, stripTrailingChars } from "@memberjunction/global";
|
|
9
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { SQLLogging } from "../Misc/sql_logging.js";
|
|
13
|
+
export class ValidatorResult {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.entityName = "";
|
|
16
|
+
this.sourceCheckConstraint = "";
|
|
17
|
+
this.functionText = "";
|
|
18
|
+
this.functionName = "";
|
|
19
|
+
this.functionDescription = "";
|
|
20
|
+
/**
|
|
21
|
+
* The ID value in the Generated Codes entity that was created for this validator.
|
|
22
|
+
*/
|
|
23
|
+
this.generatedCodeId = "";
|
|
24
|
+
/**
|
|
25
|
+
* The ID for the AI Model that was used to generate the code
|
|
26
|
+
*/
|
|
27
|
+
this.aiModelID = "";
|
|
28
|
+
this.wasGenerated = true;
|
|
29
|
+
this.success = false;
|
|
30
|
+
}
|
|
58
31
|
}
|
|
59
|
-
exports.ValidatorResult = ValidatorResult;
|
|
60
32
|
/**
|
|
61
33
|
* Base class for managing metadata within the CodeGen system. This class can be sub-classed to extend/override base class functionality. Make sure to use the RegisterClass decorator from the @memberjunction/global package
|
|
62
34
|
* to properly register your subclass with a priority of 1+ to ensure it gets instantiated.
|
|
63
35
|
*/
|
|
64
|
-
class ManageMetadataBase {
|
|
65
|
-
|
|
36
|
+
export class ManageMetadataBase {
|
|
37
|
+
constructor() {
|
|
38
|
+
this._sqlUtilityObject = MJGlobal.Instance.ClassFactory.CreateInstance(SQLUtilityBase);
|
|
39
|
+
}
|
|
66
40
|
get SQLUtilityObject() {
|
|
67
41
|
return this._sqlUtilityObject;
|
|
68
42
|
}
|
|
69
|
-
static _newEntityList = [];
|
|
43
|
+
static { this._newEntityList = []; }
|
|
70
44
|
/**
|
|
71
45
|
* Globally scoped list of entities that have been created during the metadata management process.
|
|
72
46
|
*/
|
|
73
47
|
static get newEntityList() {
|
|
74
48
|
return this._newEntityList;
|
|
75
49
|
}
|
|
76
|
-
static _modifiedEntityList = [];
|
|
50
|
+
static { this._modifiedEntityList = []; }
|
|
77
51
|
/**
|
|
78
52
|
* Globally scoped list of entities that have been modified during the metadata management process.
|
|
79
53
|
*/
|
|
80
54
|
static get modifiedEntityList() {
|
|
81
55
|
return this._modifiedEntityList;
|
|
82
56
|
}
|
|
83
|
-
static _generatedValidators = [];
|
|
57
|
+
static { this._generatedValidators = []; }
|
|
84
58
|
/**
|
|
85
59
|
* Globally scoped list of validators that have been generated during the metadata management process.
|
|
86
60
|
*/
|
|
87
61
|
static get generatedValidators() {
|
|
88
62
|
return this._generatedValidators;
|
|
89
63
|
}
|
|
90
|
-
static _softPKFKConfigCache = null;
|
|
91
|
-
static _softPKFKConfigPath = '';
|
|
64
|
+
static { this._softPKFKConfigCache = null; }
|
|
65
|
+
static { this._softPKFKConfigPath = ''; }
|
|
92
66
|
/**
|
|
93
67
|
* Loads and caches the soft PK/FK configuration from the additionalSchemaInfo file.
|
|
94
68
|
* The file is only loaded once per session to avoid repeated I/O.
|
|
95
69
|
*/
|
|
96
70
|
static getSoftPKFKConfig() {
|
|
97
71
|
// Return cached config if path hasn't changed
|
|
98
|
-
const configPath =
|
|
99
|
-
?
|
|
72
|
+
const configPath = configInfo.additionalSchemaInfo
|
|
73
|
+
? path.join(currentWorkingDirectory, configInfo.additionalSchemaInfo)
|
|
100
74
|
: '';
|
|
101
75
|
if (this._softPKFKConfigCache !== null && this._softPKFKConfigPath === configPath) {
|
|
102
76
|
return this._softPKFKConfigCache;
|
|
@@ -119,79 +93,330 @@ class ManageMetadataBase {
|
|
|
119
93
|
return null;
|
|
120
94
|
}
|
|
121
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Extracts a flat array of table configs from the config file, handling both formats:
|
|
98
|
+
* 1. Schema-as-key (template format): { "dbo": [{ "TableName": "Orders", ... }] }
|
|
99
|
+
* 2. Flat tables array (legacy format): { "Tables": [{ "SchemaName": "dbo", "TableName": "Orders", ... }] }
|
|
100
|
+
* Returns a normalized array where each entry has SchemaName, TableName, PrimaryKey[], and ForeignKeys[].
|
|
101
|
+
*/
|
|
102
|
+
extractTablesFromConfig(config) {
|
|
103
|
+
const results = [];
|
|
104
|
+
// Check for flat "Tables" array format first
|
|
105
|
+
if (Array.isArray(config.Tables)) {
|
|
106
|
+
for (const table of config.Tables) {
|
|
107
|
+
const t = table;
|
|
108
|
+
results.push({
|
|
109
|
+
SchemaName: t.SchemaName || 'dbo',
|
|
110
|
+
TableName: t.TableName,
|
|
111
|
+
PrimaryKey: t.PrimaryKey || [],
|
|
112
|
+
ForeignKeys: t.ForeignKeys || [],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
// Schema-as-key format: iterate over keys, skip metadata and special section keys
|
|
118
|
+
const metadataKeys = new Set(['$schema', 'description', 'version', 'VirtualEntities', 'ISARelationships', 'Tables']);
|
|
119
|
+
for (const key of Object.keys(config)) {
|
|
120
|
+
if (metadataKeys.has(key))
|
|
121
|
+
continue;
|
|
122
|
+
const schemaName = key;
|
|
123
|
+
const tables = config[key];
|
|
124
|
+
if (!Array.isArray(tables))
|
|
125
|
+
continue;
|
|
126
|
+
for (const table of tables) {
|
|
127
|
+
const t = table;
|
|
128
|
+
results.push({
|
|
129
|
+
SchemaName: schemaName,
|
|
130
|
+
TableName: t.TableName,
|
|
131
|
+
PrimaryKey: t.PrimaryKey || [],
|
|
132
|
+
ForeignKeys: t.ForeignKeys || [],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Extracts VirtualEntities array from the additionalSchemaInfo config file.
|
|
140
|
+
* The config may contain a top-level "VirtualEntities" key with an array of
|
|
141
|
+
* virtual entity definitions.
|
|
142
|
+
*/
|
|
143
|
+
extractVirtualEntitiesFromConfig(config) {
|
|
144
|
+
const virtualEntities = config.VirtualEntities;
|
|
145
|
+
if (!Array.isArray(virtualEntities))
|
|
146
|
+
return [];
|
|
147
|
+
return virtualEntities.map((ve) => ({
|
|
148
|
+
ViewName: ve.ViewName,
|
|
149
|
+
SchemaName: ve.SchemaName || undefined,
|
|
150
|
+
EntityName: ve.EntityName || undefined,
|
|
151
|
+
Description: ve.Description || undefined,
|
|
152
|
+
PrimaryKey: Array.isArray(ve.PrimaryKey) ? ve.PrimaryKey : undefined,
|
|
153
|
+
ForeignKeys: Array.isArray(ve.ForeignKeys) ? ve.ForeignKeys : undefined,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Extracts ISARelationships array from the additionalSchemaInfo config file.
|
|
158
|
+
* The config may contain a top-level "ISARelationships" key with an array of
|
|
159
|
+
* parent-child relationship definitions.
|
|
160
|
+
*/
|
|
161
|
+
extractISARelationshipsFromConfig(config) {
|
|
162
|
+
const relationships = config.ISARelationships;
|
|
163
|
+
if (!Array.isArray(relationships))
|
|
164
|
+
return [];
|
|
165
|
+
return relationships.map((rel) => ({
|
|
166
|
+
ChildEntity: rel.ChildEntity,
|
|
167
|
+
ParentEntity: rel.ParentEntity,
|
|
168
|
+
SchemaName: rel.SchemaName || undefined,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Processes IS-A relationship configurations from the additionalSchemaInfo config.
|
|
173
|
+
* For each configured relationship, looks up both entities by name (or by table name
|
|
174
|
+
* within the given schema) and sets Entity.ParentID on the child entity.
|
|
175
|
+
* Must run AFTER entities are created but BEFORE manageParentEntityFields().
|
|
176
|
+
*/
|
|
177
|
+
async processISARelationshipConfig(pool) {
|
|
178
|
+
const config = ManageMetadataBase.getSoftPKFKConfig();
|
|
179
|
+
if (!config)
|
|
180
|
+
return { success: true, updatedCount: 0 };
|
|
181
|
+
const relationships = this.extractISARelationshipsFromConfig(config);
|
|
182
|
+
if (relationships.length === 0)
|
|
183
|
+
return { success: true, updatedCount: 0 };
|
|
184
|
+
let updatedCount = 0;
|
|
185
|
+
const schema = mj_core_schema();
|
|
186
|
+
for (const rel of relationships) {
|
|
187
|
+
try {
|
|
188
|
+
// Look up the parent entity — try by Name first, then by BaseTable within the given schema
|
|
189
|
+
const parentResult = await pool.request()
|
|
190
|
+
.input('ParentName', rel.ParentEntity)
|
|
191
|
+
.input('SchemaName', rel.SchemaName || null)
|
|
192
|
+
.query(`
|
|
193
|
+
SELECT TOP 1 ID, Name
|
|
194
|
+
FROM [${schema}].vwEntities
|
|
195
|
+
WHERE Name = @ParentName
|
|
196
|
+
OR (BaseTable = @ParentName AND (@SchemaName IS NULL OR SchemaName = @SchemaName))
|
|
197
|
+
ORDER BY CASE WHEN Name = @ParentName THEN 0 ELSE 1 END
|
|
198
|
+
`);
|
|
199
|
+
if (parentResult.recordset.length === 0) {
|
|
200
|
+
logError(` > IS-A config: parent entity "${rel.ParentEntity}" not found — skipping`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const parentId = parentResult.recordset[0].ID;
|
|
204
|
+
const parentName = parentResult.recordset[0].Name;
|
|
205
|
+
// Look up the child entity — same strategy
|
|
206
|
+
const childResult = await pool.request()
|
|
207
|
+
.input('ChildName', rel.ChildEntity)
|
|
208
|
+
.input('SchemaName', rel.SchemaName || null)
|
|
209
|
+
.query(`
|
|
210
|
+
SELECT TOP 1 ID, Name, ParentID
|
|
211
|
+
FROM [${schema}].vwEntities
|
|
212
|
+
WHERE Name = @ChildName
|
|
213
|
+
OR (BaseTable = @ChildName AND (@SchemaName IS NULL OR SchemaName = @SchemaName))
|
|
214
|
+
ORDER BY CASE WHEN Name = @ChildName THEN 0 ELSE 1 END
|
|
215
|
+
`);
|
|
216
|
+
if (childResult.recordset.length === 0) {
|
|
217
|
+
logError(` > IS-A config: child entity "${rel.ChildEntity}" not found — skipping`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const childId = childResult.recordset[0].ID;
|
|
221
|
+
const childName = childResult.recordset[0].Name;
|
|
222
|
+
const existingParentId = childResult.recordset[0].ParentID;
|
|
223
|
+
// Skip if already set correctly
|
|
224
|
+
if (existingParentId === parentId) {
|
|
225
|
+
logStatus(` > IS-A: "${childName}" already has ParentID set to "${parentName}", skipping`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// Set ParentID on the child entity
|
|
229
|
+
await pool.request()
|
|
230
|
+
.input('ParentID', parentId)
|
|
231
|
+
.input('ChildID', childId)
|
|
232
|
+
.query(`UPDATE [${schema}].Entity SET ParentID = @ParentID WHERE ID = @ChildID`);
|
|
233
|
+
if (existingParentId) {
|
|
234
|
+
logStatus(` > IS-A: Updated "${childName}" ParentID from previous value to "${parentName}"`);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
logStatus(` > IS-A: Set "${childName}" ParentID to "${parentName}"`);
|
|
238
|
+
}
|
|
239
|
+
updatedCount++;
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
243
|
+
logError(` > IS-A config: Failed to set ParentID for "${rel.ChildEntity}": ${errMessage}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { success: true, updatedCount };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Processes virtual entity configurations from the additionalSchemaInfo config.
|
|
250
|
+
* For each configured virtual entity, checks if it already exists and creates
|
|
251
|
+
* it if not. Uses the spCreateVirtualEntity stored procedure.
|
|
252
|
+
* Must run BEFORE manageVirtualEntities() so newly created entities get field-synced.
|
|
253
|
+
*/
|
|
254
|
+
async processVirtualEntityConfig(pool, currentUser) {
|
|
255
|
+
const config = ManageMetadataBase.getSoftPKFKConfig();
|
|
256
|
+
if (!config)
|
|
257
|
+
return { success: true, createdCount: 0 };
|
|
258
|
+
const virtualEntities = this.extractVirtualEntitiesFromConfig(config);
|
|
259
|
+
if (virtualEntities.length === 0)
|
|
260
|
+
return { success: true, createdCount: 0 };
|
|
261
|
+
let createdCount = 0;
|
|
262
|
+
const schema = mj_core_schema();
|
|
263
|
+
for (const ve of virtualEntities) {
|
|
264
|
+
const viewSchema = ve.SchemaName || schema;
|
|
265
|
+
const viewName = ve.ViewName;
|
|
266
|
+
const entityName = ve.EntityName || this.deriveEntityNameFromView(viewName);
|
|
267
|
+
const pkField = ve.PrimaryKey?.[0] || 'ID';
|
|
268
|
+
// Check if entity already exists for this view
|
|
269
|
+
const existsResult = await pool.request()
|
|
270
|
+
.input('ViewName', viewName)
|
|
271
|
+
.input('SchemaName', viewSchema)
|
|
272
|
+
.query(`SELECT ID FROM [${schema}].vwEntities WHERE BaseView = @ViewName AND SchemaName = @SchemaName`);
|
|
273
|
+
if (existsResult.recordset.length > 0) {
|
|
274
|
+
logStatus(` > Virtual entity "${entityName}" already exists for view [${viewSchema}].[${viewName}], skipping creation`);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
// Verify the view actually exists in the database
|
|
278
|
+
const viewExistsResult = await pool.request()
|
|
279
|
+
.input('ViewName', viewName)
|
|
280
|
+
.input('SchemaName', viewSchema)
|
|
281
|
+
.query(`SELECT 1 FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = @ViewName AND TABLE_SCHEMA = @SchemaName`);
|
|
282
|
+
if (viewExistsResult.recordset.length === 0) {
|
|
283
|
+
logError(` > View [${viewSchema}].[${viewName}] does not exist — skipping virtual entity creation for "${entityName}"`);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
// Create the virtual entity via the stored procedure
|
|
287
|
+
try {
|
|
288
|
+
const createResult = await pool.request()
|
|
289
|
+
.input('Name', entityName)
|
|
290
|
+
.input('BaseView', viewName)
|
|
291
|
+
.input('SchemaName', viewSchema)
|
|
292
|
+
.input('PrimaryKeyFieldName', pkField)
|
|
293
|
+
.input('Description', ve.Description || null)
|
|
294
|
+
.execute(`[${schema}].spCreateVirtualEntity`);
|
|
295
|
+
const newEntityId = createResult.recordset?.[0]?.['']
|
|
296
|
+
|| createResult.recordset?.[0]?.ID
|
|
297
|
+
|| createResult.recordset?.[0]?.Column0;
|
|
298
|
+
logStatus(` > Created virtual entity "${entityName}" (ID: ${newEntityId}) for view [${viewSchema}].[${viewName}]`);
|
|
299
|
+
createdCount++;
|
|
300
|
+
// Add virtual entity to the application for its schema and set default permissions
|
|
301
|
+
// (same logic as table-backed entities)
|
|
302
|
+
if (newEntityId) {
|
|
303
|
+
await this.addEntityToApplicationForSchema(pool, newEntityId, entityName, viewSchema, currentUser);
|
|
304
|
+
await this.addDefaultPermissionsForEntity(pool, newEntityId, entityName);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
309
|
+
logError(` > Failed to create virtual entity "${entityName}": ${errMessage}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { success: true, createdCount };
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Derives an entity name from a view name by removing common prefixes (vw, v_)
|
|
316
|
+
* and converting to a human-friendly format.
|
|
317
|
+
*/
|
|
318
|
+
deriveEntityNameFromView(viewName) {
|
|
319
|
+
let name = viewName;
|
|
320
|
+
// Remove common view prefixes
|
|
321
|
+
if (name.startsWith('vw'))
|
|
322
|
+
name = name.substring(2);
|
|
323
|
+
else if (name.startsWith('v_'))
|
|
324
|
+
name = name.substring(2);
|
|
325
|
+
// Add spaces before capital letters (PascalCase → "Pascal Case")
|
|
326
|
+
name = name.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
327
|
+
return name.trim();
|
|
328
|
+
}
|
|
122
329
|
/**
|
|
123
330
|
* Primary function to manage metadata within the CodeGen system. This function will call a series of sub-functions to manage the metadata.
|
|
124
331
|
* @param pool - the ConnectionPool object to use for querying and updating the database
|
|
125
332
|
* @returns
|
|
126
333
|
*/
|
|
127
334
|
async manageMetadata(pool, currentUser) {
|
|
128
|
-
const md = new
|
|
129
|
-
const excludeSchemas =
|
|
335
|
+
const md = new Metadata();
|
|
336
|
+
const excludeSchemas = configInfo.excludeSchemas ? configInfo.excludeSchemas : [];
|
|
130
337
|
let bSuccess = true;
|
|
131
338
|
let start = new Date();
|
|
132
|
-
|
|
339
|
+
logStatus(' Creating new entities...');
|
|
133
340
|
if (!await this.createNewEntities(pool, currentUser)) {
|
|
134
|
-
|
|
341
|
+
logError(' Error creating new entities');
|
|
135
342
|
bSuccess = false;
|
|
136
343
|
}
|
|
137
|
-
|
|
344
|
+
logStatus(` > Created new entities in ${(new Date().getTime() - start.getTime()) / 1000} seconds`);
|
|
138
345
|
start = new Date();
|
|
139
|
-
|
|
346
|
+
logStatus(' Updating existing entities...');
|
|
140
347
|
if (!await this.updateExistingEntitiesFromSchema(pool, excludeSchemas)) {
|
|
141
|
-
|
|
348
|
+
logError(' Error updating existing entities');
|
|
142
349
|
bSuccess = false;
|
|
143
350
|
}
|
|
144
|
-
|
|
351
|
+
logStatus(` > Updated existing entities in ${(new Date().getTime() - start.getTime()) / 1000} seconds`);
|
|
145
352
|
start = new Date();
|
|
146
|
-
|
|
353
|
+
logStatus(' Scanning for tables that were deleted where entity metadata still exists...');
|
|
147
354
|
if (!await this.checkAndRemoveMetadataForDeletedTables(pool, excludeSchemas)) {
|
|
148
|
-
|
|
355
|
+
logError(' Error removing metadata for tables that were removed');
|
|
149
356
|
bSuccess = false;
|
|
150
357
|
}
|
|
151
|
-
|
|
358
|
+
logStatus(` > Removed metadata for deleted tables in ${(new Date().getTime() - start.getTime()) / 1000} seconds`);
|
|
152
359
|
start = new Date();
|
|
153
|
-
|
|
154
|
-
const sqlUtility =
|
|
155
|
-
const adminSchema =
|
|
156
|
-
const schemasToExclude =
|
|
360
|
+
logStatus(' Recompiling base views...');
|
|
361
|
+
const sqlUtility = MJGlobal.Instance.ClassFactory.CreateInstance(SQLUtilityBase);
|
|
362
|
+
const adminSchema = getSettingValue('mj_core_schema', '__mj');
|
|
363
|
+
const schemasToExclude = getSettingValue('recompile_mj_views', true)
|
|
157
364
|
? excludeSchemas.filter((s) => s !== adminSchema)
|
|
158
365
|
: excludeSchemas;
|
|
159
366
|
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*/)) {
|
|
160
|
-
|
|
367
|
+
logMessage(' Warning: Non-Fatal error recompiling base views', SeverityType.Warning, false);
|
|
161
368
|
// many times the former versions of base views will NOT succesfully recompile, so don't consider that scenario to be a
|
|
162
369
|
// failure for this entire function
|
|
163
370
|
}
|
|
164
|
-
|
|
371
|
+
logStatus(` > Recompiled base views in ${(new Date().getTime() - start.getTime()) / 1000} seconds`);
|
|
165
372
|
start = new Date();
|
|
166
|
-
|
|
373
|
+
logStatus(' Managing entity fields...');
|
|
167
374
|
// note that we skip Advanced Generation here because we do it again later when the manageSQLScriptsAndExecution occurs in SQLCodeGen class
|
|
168
375
|
if (!await this.manageEntityFields(pool, excludeSchemas, false, false, currentUser, true)) {
|
|
169
|
-
|
|
376
|
+
logError(' Error managing entity fields');
|
|
170
377
|
bSuccess = false;
|
|
171
378
|
}
|
|
172
|
-
|
|
379
|
+
logStatus(` > Managed entity fields in ${(new Date().getTime() - start.getTime()) / 1000} seconds`);
|
|
173
380
|
start = new Date();
|
|
174
|
-
|
|
381
|
+
logStatus(' Managing entity relationships...');
|
|
175
382
|
if (!await this.manageEntityRelationships(pool, excludeSchemas, md)) {
|
|
176
|
-
|
|
383
|
+
logError(' Error managing entity relationships');
|
|
177
384
|
bSuccess = false;
|
|
178
385
|
}
|
|
179
|
-
|
|
386
|
+
logStatus(` > Managed entity relationships in ${(new Date().getTime() - start.getTime()) / 1000} seconds`);
|
|
180
387
|
if (ManageMetadataBase.newEntityList.length > 0) {
|
|
181
388
|
await this.generateNewEntityDescriptions(pool, md, currentUser); // don't pass excludeSchemas becuase by definition this is the NEW entities we created
|
|
182
389
|
}
|
|
390
|
+
// Config-driven virtual entity creation — run BEFORE manageVirtualEntities
|
|
391
|
+
// so newly created entities get their fields synced in the next step
|
|
392
|
+
const vecResult = await this.processVirtualEntityConfig(pool, currentUser);
|
|
393
|
+
if (vecResult.createdCount > 0) {
|
|
394
|
+
logStatus(` > Created ${vecResult.createdCount} virtual entit${vecResult.createdCount === 1 ? 'y' : 'ies'} from config`);
|
|
395
|
+
// Refresh metadata so manageVirtualEntities can find the newly-created entities
|
|
396
|
+
// in the cache — otherwise EntityByName() returns null and field sync is silently skipped
|
|
397
|
+
const md = new Metadata();
|
|
398
|
+
await md.Refresh();
|
|
399
|
+
}
|
|
183
400
|
const veResult = await this.manageVirtualEntities(pool);
|
|
184
401
|
if (!veResult.success) {
|
|
185
|
-
|
|
402
|
+
logError(' Error managing virtual entities');
|
|
186
403
|
bSuccess = false;
|
|
187
404
|
}
|
|
405
|
+
// LLM-assisted virtual entity field decoration — identify PKs, FKs, and descriptions
|
|
406
|
+
await this.decorateVirtualEntitiesWithLLM(pool, currentUser);
|
|
407
|
+
// Config-driven IS-A relationship setup — set ParentID on child entities
|
|
408
|
+
// Must run AFTER entities exist but BEFORE manageEntityFields() which calls manageParentEntityFields()
|
|
409
|
+
const isaConfigResult = await this.processISARelationshipConfig(pool);
|
|
410
|
+
if (isaConfigResult.updatedCount > 0) {
|
|
411
|
+
logStatus(` > Set ParentID on ${isaConfigResult.updatedCount} IS-A child entit${isaConfigResult.updatedCount === 1 ? 'y' : 'ies'} from config`);
|
|
412
|
+
}
|
|
188
413
|
start = new Date();
|
|
189
|
-
|
|
414
|
+
logStatus(' Syncing schema info from database...');
|
|
190
415
|
if (!await this.updateSchemaInfoFromDatabase(pool, excludeSchemas)) {
|
|
191
|
-
|
|
416
|
+
logError(' Error syncing schema info');
|
|
192
417
|
bSuccess = false;
|
|
193
418
|
}
|
|
194
|
-
|
|
419
|
+
logStatus(` > Synced schema info in ${(new Date().getTime() - start.getTime()) / 1000} seconds`);
|
|
195
420
|
return bSuccess;
|
|
196
421
|
}
|
|
197
422
|
async manageVirtualEntities(pool) {
|
|
@@ -199,7 +424,7 @@ class ManageMetadataBase {
|
|
|
199
424
|
// virtual entities are records defined in the entity metadata and do NOT define a distinct base table
|
|
200
425
|
// but they do specify a base view. We DO NOT generate a base view for a virtual entity, we simply use it to figure
|
|
201
426
|
// 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
|
|
202
|
-
const sql = `SELECT * FROM [${
|
|
427
|
+
const sql = `SELECT * FROM [${mj_core_schema()}].vwEntities WHERE VirtualEntity = 1`;
|
|
203
428
|
const virtualEntitiesResult = await pool.request().query(sql);
|
|
204
429
|
const virtualEntities = virtualEntitiesResult.recordset;
|
|
205
430
|
let anyUpdates = false;
|
|
@@ -209,7 +434,7 @@ class ManageMetadataBase {
|
|
|
209
434
|
const { success, updatedEntity } = await this.manageSingleVirtualEntity(pool, ve);
|
|
210
435
|
anyUpdates = anyUpdates || updatedEntity;
|
|
211
436
|
if (!success) {
|
|
212
|
-
|
|
437
|
+
logError(` Error managing virtual entity ${ve.Name}`);
|
|
213
438
|
bSuccess = false;
|
|
214
439
|
}
|
|
215
440
|
}
|
|
@@ -240,7 +465,7 @@ class ManageMetadataBase {
|
|
|
240
465
|
if (veFields && veFields.length > 0) {
|
|
241
466
|
// we have 1+ fields, now loop through them and process each one
|
|
242
467
|
// first though, remove any fields that are no longer in the view
|
|
243
|
-
const md = new
|
|
468
|
+
const md = new Metadata();
|
|
244
469
|
const entity = md.EntityByName(virtualEntity.Name);
|
|
245
470
|
if (entity) {
|
|
246
471
|
const removeList = [];
|
|
@@ -249,7 +474,7 @@ class ManageMetadataBase {
|
|
|
249
474
|
removeList.push(f.ID);
|
|
250
475
|
}
|
|
251
476
|
if (removeList.length > 0) {
|
|
252
|
-
const sqlRemove = `DELETE FROM [${
|
|
477
|
+
const sqlRemove = `DELETE FROM [${mj_core_schema()}].EntityField WHERE ID IN (${removeList.map(removeId => `'${removeId}'`).join(',')})`;
|
|
253
478
|
// this removes the fields that shouldn't be there anymore
|
|
254
479
|
await this.LogSQLAndExecute(pool, sqlRemove, `SQL text to remove fields from entity ${virtualEntity.Name}`);
|
|
255
480
|
bUpdated = true;
|
|
@@ -263,7 +488,7 @@ class ManageMetadataBase {
|
|
|
263
488
|
const { success, updatedField } = await this.manageSingleVirtualEntityField(pool, virtualEntity, vef, i + 1, !hasPkey && i === 0);
|
|
264
489
|
bUpdated = bUpdated || updatedField;
|
|
265
490
|
if (!success) {
|
|
266
|
-
|
|
491
|
+
logError(`Error managing virtual entity field ${vef.FieldName} for virtual entity ${virtualEntity.Name}`);
|
|
267
492
|
bSuccess = false;
|
|
268
493
|
}
|
|
269
494
|
}
|
|
@@ -271,13 +496,13 @@ class ManageMetadataBase {
|
|
|
271
496
|
}
|
|
272
497
|
if (bUpdated) {
|
|
273
498
|
// finally make sure we update the UpdatedAt field for the entity if we made changes to its fields
|
|
274
|
-
const sqlUpdate = `UPDATE [${
|
|
499
|
+
const sqlUpdate = `UPDATE [${mj_core_schema()}].Entity SET [${EntityInfo.UpdatedAtFieldName}]=GETUTCDATE() WHERE ID='${virtualEntity.ID}'`;
|
|
275
500
|
await this.LogSQLAndExecute(pool, sqlUpdate, `SQL text to update virtual entity updated date for ${virtualEntity.Name}`);
|
|
276
501
|
}
|
|
277
502
|
return { success: bSuccess, updatedEntity: bUpdated };
|
|
278
503
|
}
|
|
279
504
|
catch (e) {
|
|
280
|
-
|
|
505
|
+
logError(e);
|
|
281
506
|
return { success: false, updatedEntity: bUpdated };
|
|
282
507
|
}
|
|
283
508
|
}
|
|
@@ -285,7 +510,7 @@ class ManageMetadataBase {
|
|
|
285
510
|
// this protected checks to see if the field exists in the entity definition, and if not, adds it
|
|
286
511
|
// if it exist it updates the entity field to match the view's data type and nullability attributes
|
|
287
512
|
// first, get the entity definition
|
|
288
|
-
const md = new
|
|
513
|
+
const md = new Metadata();
|
|
289
514
|
const entity = md.EntityByName(virtualEntity.Name);
|
|
290
515
|
let newEntityFieldUUID = null;
|
|
291
516
|
let didUpdate = false;
|
|
@@ -302,7 +527,7 @@ class ManageMetadataBase {
|
|
|
302
527
|
field.Sequence !== fieldSequence) {
|
|
303
528
|
// the field needs to be updated, so update it
|
|
304
529
|
const sqlUpdate = `UPDATE
|
|
305
|
-
[${
|
|
530
|
+
[${mj_core_schema()}].EntityField
|
|
306
531
|
SET
|
|
307
532
|
Sequence=${fieldSequence},
|
|
308
533
|
Type='${veField.Type}',
|
|
@@ -320,7 +545,7 @@ class ManageMetadataBase {
|
|
|
320
545
|
else {
|
|
321
546
|
// this means that we do NOT have a match so the field does not exist in the entity definition, so we need to add it
|
|
322
547
|
newEntityFieldUUID = this.createNewUUID();
|
|
323
|
-
const sqlAdd = `INSERT INTO [${
|
|
548
|
+
const sqlAdd = `INSERT INTO [${mj_core_schema()}].EntityField (
|
|
324
549
|
ID, EntityID, Name, Type, AllowsNull,
|
|
325
550
|
Length, Precision, Scale,
|
|
326
551
|
Sequence, IsPrimaryKey, IsUnique )
|
|
@@ -334,6 +559,480 @@ class ManageMetadataBase {
|
|
|
334
559
|
}
|
|
335
560
|
return { success: true, updatedField: didUpdate, newFieldID: newEntityFieldUUID };
|
|
336
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* Iterates over all virtual entities and applies LLM-assisted field decoration
|
|
564
|
+
* to identify primary keys, foreign keys, and field descriptions.
|
|
565
|
+
* Only runs if the VirtualEntityFieldDecoration advanced generation feature is enabled.
|
|
566
|
+
* Idempotent: skips entities that already have soft PK/FK annotations.
|
|
567
|
+
*/
|
|
568
|
+
async decorateVirtualEntitiesWithLLM(pool, currentUser) {
|
|
569
|
+
const ag = new AdvancedGeneration();
|
|
570
|
+
if (!ag.featureEnabled('VirtualEntityFieldDecoration')) {
|
|
571
|
+
return; // Feature not enabled, nothing to do
|
|
572
|
+
}
|
|
573
|
+
const md = new Metadata();
|
|
574
|
+
const virtualEntities = md.Entities.filter(e => e.VirtualEntity);
|
|
575
|
+
if (virtualEntities.length === 0) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// Pre-build available entities list once (shared across all virtual entity decorations)
|
|
579
|
+
const availableEntities = md.Entities
|
|
580
|
+
.filter(e => !e.VirtualEntity && e.PrimaryKeys.length > 0)
|
|
581
|
+
.map(e => ({
|
|
582
|
+
Name: e.Name,
|
|
583
|
+
SchemaName: e.SchemaName,
|
|
584
|
+
BaseTable: e.BaseTable,
|
|
585
|
+
PrimaryKeyField: e.PrimaryKeys[0]?.Name || 'ID'
|
|
586
|
+
}));
|
|
587
|
+
logStatus(` Decorating virtual entity fields with LLM (${virtualEntities.length} entities)...`);
|
|
588
|
+
let decoratedCount = 0;
|
|
589
|
+
let skippedCount = 0;
|
|
590
|
+
// Process in batches of up to 5 in parallel for better throughput
|
|
591
|
+
const batchSize = 5;
|
|
592
|
+
for (let i = 0; i < virtualEntities.length; i += batchSize) {
|
|
593
|
+
const batch = virtualEntities.slice(i, i + batchSize);
|
|
594
|
+
const results = await Promise.all(batch.map(entity => this.decorateSingleVirtualEntityWithLLM(pool, entity, ag, currentUser, availableEntities)));
|
|
595
|
+
for (const result of results) {
|
|
596
|
+
if (result.decorated) {
|
|
597
|
+
decoratedCount++;
|
|
598
|
+
}
|
|
599
|
+
else if (result.skipped) {
|
|
600
|
+
skippedCount++;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (decoratedCount > 0 || skippedCount > 0) {
|
|
605
|
+
logStatus(` > LLM field decoration: ${decoratedCount} decorated, ${skippedCount} skipped (already annotated)`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Applies LLM-assisted field decoration to a single virtual entity.
|
|
610
|
+
* Parses the view SQL to identify source entities, enriches the LLM prompt with their
|
|
611
|
+
* field metadata (descriptions, categories), then applies PKs, FKs, descriptions, and categories.
|
|
612
|
+
* @returns Whether the entity was decorated, skipped, or encountered an error.
|
|
613
|
+
*/
|
|
614
|
+
async decorateSingleVirtualEntityWithLLM(pool, entity, ag, currentUser, availableEntities) {
|
|
615
|
+
try {
|
|
616
|
+
// Idempotency check: if entity already has soft PK or soft FK annotations, skip
|
|
617
|
+
// unless forceRegenerate option is enabled on this feature
|
|
618
|
+
const feature = ag.getFeature('VirtualEntityFieldDecoration');
|
|
619
|
+
const forceRegenerate = feature?.options?.find(o => o.name === 'forceRegenerate')?.value === true;
|
|
620
|
+
const hasSoftAnnotations = entity.Fields.some(f => f.IsSoftPrimaryKey || f.IsSoftForeignKey);
|
|
621
|
+
if (hasSoftAnnotations && !forceRegenerate) {
|
|
622
|
+
return { decorated: false, skipped: true };
|
|
623
|
+
}
|
|
624
|
+
// Get view definition from SQL Server
|
|
625
|
+
const viewDefSQL = `SELECT OBJECT_DEFINITION(OBJECT_ID('[${entity.SchemaName}].[${entity.BaseView}]')) AS ViewDef`;
|
|
626
|
+
const viewDefResult = await pool.request().query(viewDefSQL);
|
|
627
|
+
const viewDefinition = viewDefResult.recordset[0]?.ViewDef;
|
|
628
|
+
if (!viewDefinition) {
|
|
629
|
+
logStatus(` Could not get view definition for ${entity.SchemaName}.${entity.BaseView} — skipping LLM decoration`);
|
|
630
|
+
return { decorated: false, skipped: false };
|
|
631
|
+
}
|
|
632
|
+
// Parse the view SQL to identify referenced tables, then resolve to entities
|
|
633
|
+
const sourceEntities = this.buildSourceEntityContext(viewDefinition);
|
|
634
|
+
// Build field info for the prompt
|
|
635
|
+
const fields = entity.Fields.map(f => ({
|
|
636
|
+
Name: f.Name,
|
|
637
|
+
Type: f.Type,
|
|
638
|
+
Length: f.Length,
|
|
639
|
+
AllowsNull: f.AllowsNull,
|
|
640
|
+
IsPrimaryKey: f.IsPrimaryKey,
|
|
641
|
+
RelatedEntityName: f.RelatedEntity || null
|
|
642
|
+
}));
|
|
643
|
+
// Call the LLM with enriched source entity context
|
|
644
|
+
const result = await ag.decorateVirtualEntityFields(entity.Name, entity.SchemaName, entity.BaseView, viewDefinition, entity.Description || '', fields, availableEntities, sourceEntities, currentUser);
|
|
645
|
+
if (!result) {
|
|
646
|
+
return { decorated: false, skipped: false };
|
|
647
|
+
}
|
|
648
|
+
// Apply results to EntityField records
|
|
649
|
+
const schema = mj_core_schema();
|
|
650
|
+
let anyUpdated = false;
|
|
651
|
+
// Apply primary keys
|
|
652
|
+
anyUpdated = await this.applyLLMPrimaryKeys(pool, entity, result.primaryKeys, schema) || anyUpdated;
|
|
653
|
+
// Apply foreign keys
|
|
654
|
+
anyUpdated = await this.applyLLMForeignKeys(pool, entity, result.foreignKeys, schema) || anyUpdated;
|
|
655
|
+
// Apply field descriptions
|
|
656
|
+
anyUpdated = await this.applyLLMFieldDescriptions(pool, entity, result.fieldDescriptions, schema) || anyUpdated;
|
|
657
|
+
// Apply categories using the shared methods (same stability rules as regular entities)
|
|
658
|
+
anyUpdated = await this.applyVEFieldCategories(pool, entity, result) || anyUpdated;
|
|
659
|
+
if (anyUpdated) {
|
|
660
|
+
const sqlUpdate = `UPDATE [${schema}].Entity SET [${EntityInfo.UpdatedAtFieldName}]=GETUTCDATE() WHERE ID='${entity.ID}'`;
|
|
661
|
+
await this.LogSQLAndExecute(pool, sqlUpdate, `Update entity timestamp for ${entity.Name} after LLM decoration`);
|
|
662
|
+
}
|
|
663
|
+
return { decorated: anyUpdated, skipped: false };
|
|
664
|
+
}
|
|
665
|
+
catch (e) {
|
|
666
|
+
logError(` Error decorating virtual entity ${entity.Name} with LLM: ${e}`);
|
|
667
|
+
return { decorated: false, skipped: false };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Parses a view definition SQL and resolves referenced tables to MJ entities.
|
|
672
|
+
* Returns enriched source entity context (all fields with descriptions and categories)
|
|
673
|
+
* for the LLM to use when decorating virtual entity fields.
|
|
674
|
+
*/
|
|
675
|
+
buildSourceEntityContext(viewDefinition) {
|
|
676
|
+
const parseResult = SQLParser.Parse(viewDefinition);
|
|
677
|
+
const md = new Metadata();
|
|
678
|
+
const sourceEntities = [];
|
|
679
|
+
const seen = new Set();
|
|
680
|
+
for (const tableRef of parseResult.Tables) {
|
|
681
|
+
// Match against MJ entities by BaseTable/BaseView + SchemaName
|
|
682
|
+
const matchingEntity = md.Entities.find(e => (e.BaseTable.toLowerCase() === tableRef.TableName.toLowerCase() ||
|
|
683
|
+
e.BaseView.toLowerCase() === tableRef.TableName.toLowerCase()) &&
|
|
684
|
+
e.SchemaName.toLowerCase() === tableRef.SchemaName.toLowerCase());
|
|
685
|
+
if (matchingEntity && !seen.has(matchingEntity.ID)) {
|
|
686
|
+
seen.add(matchingEntity.ID);
|
|
687
|
+
sourceEntities.push({
|
|
688
|
+
Name: matchingEntity.Name,
|
|
689
|
+
Description: matchingEntity.Description || '',
|
|
690
|
+
Fields: matchingEntity.Fields.map(f => ({
|
|
691
|
+
Name: f.Name,
|
|
692
|
+
Type: f.Type,
|
|
693
|
+
Description: f.Description || '',
|
|
694
|
+
Category: f.Category || null,
|
|
695
|
+
IsPrimaryKey: f.IsPrimaryKey,
|
|
696
|
+
IsForeignKey: !!(f.RelatedEntityID)
|
|
697
|
+
}))
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return sourceEntities;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Applies category assignments from VE decoration results using the shared category methods.
|
|
705
|
+
* Loads field records from DB (needs ID, Name, Category, AutoUpdateCategory, AutoUpdateDisplayName)
|
|
706
|
+
* then delegates to the shared methods.
|
|
707
|
+
*/
|
|
708
|
+
async applyVEFieldCategories(pool, entity, result) {
|
|
709
|
+
// Check if the LLM returned any category data
|
|
710
|
+
const hasCategories = result.fieldDescriptions?.some(fd => fd.category);
|
|
711
|
+
if (!hasCategories) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
// Load VE EntityField rows from DB (we need the ID and auto-update flags)
|
|
715
|
+
const schema = mj_core_schema();
|
|
716
|
+
const fieldsSQL = `
|
|
717
|
+
SELECT ID, Name, Category, AutoUpdateCategory, AutoUpdateDisplayName
|
|
718
|
+
FROM [${schema}].EntityField
|
|
719
|
+
WHERE EntityID = '${entity.ID}'
|
|
720
|
+
`;
|
|
721
|
+
const fieldsResult = await pool.request().query(fieldsSQL);
|
|
722
|
+
const dbFields = fieldsResult.recordset;
|
|
723
|
+
if (dbFields.length === 0)
|
|
724
|
+
return false;
|
|
725
|
+
// Convert VE decoration field descriptions into the format expected by applyFieldCategories
|
|
726
|
+
const fieldCategories = result.fieldDescriptions
|
|
727
|
+
.filter(fd => fd.category)
|
|
728
|
+
.map(fd => ({
|
|
729
|
+
fieldName: fd.fieldName,
|
|
730
|
+
category: fd.category,
|
|
731
|
+
displayName: fd.displayName || undefined,
|
|
732
|
+
extendedType: fd.extendedType,
|
|
733
|
+
codeType: fd.codeType
|
|
734
|
+
}));
|
|
735
|
+
if (fieldCategories.length === 0)
|
|
736
|
+
return false;
|
|
737
|
+
const existingCategories = this.buildExistingCategorySet(dbFields);
|
|
738
|
+
await this.applyFieldCategories(pool, entity.ID, dbFields, fieldCategories, existingCategories);
|
|
739
|
+
// Apply entity icon if provided
|
|
740
|
+
if (result.entityIcon) {
|
|
741
|
+
await this.applyEntityIcon(pool, entity.ID, result.entityIcon);
|
|
742
|
+
}
|
|
743
|
+
// Apply category info settings if provided
|
|
744
|
+
if (result.categoryInfo && Object.keys(result.categoryInfo).length > 0) {
|
|
745
|
+
await this.applyCategoryInfoSettings(pool, entity.ID, result.categoryInfo);
|
|
746
|
+
}
|
|
747
|
+
logStatus(` Applied categories for VE ${entity.Name} (${fieldCategories.length} fields)`);
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Applies LLM-identified primary keys to entity fields.
|
|
752
|
+
* Sets IsPrimaryKey=1 and IsSoftPrimaryKey=1 for identified fields.
|
|
753
|
+
* First clears any default PK that was set by field-sync (field #1 fallback).
|
|
754
|
+
* All SQL updates are batched into a single execution for performance.
|
|
755
|
+
*/
|
|
756
|
+
async applyLLMPrimaryKeys(pool, entity, primaryKeys, schema) {
|
|
757
|
+
if (!primaryKeys || primaryKeys.length === 0) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
// Validate that all identified PK fields exist on the entity
|
|
761
|
+
const validPKs = primaryKeys.filter(pk => entity.Fields.some(f => f.Name.toLowerCase() === pk.toLowerCase()));
|
|
762
|
+
if (validPKs.length === 0) {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
// Build batched SQL: clear default PK + set all LLM-identified PKs
|
|
766
|
+
const sqlStatements = [];
|
|
767
|
+
// Clear existing default PK (field #1 fallback) before applying LLM-identified PKs
|
|
768
|
+
sqlStatements.push(`UPDATE [${schema}].[EntityField]
|
|
769
|
+
SET IsPrimaryKey=0, IsUnique=0
|
|
770
|
+
WHERE EntityID='${entity.ID}' AND IsPrimaryKey=1 AND IsSoftPrimaryKey=0`);
|
|
771
|
+
// Set LLM-identified PKs
|
|
772
|
+
for (const pk of validPKs) {
|
|
773
|
+
sqlStatements.push(`UPDATE [${schema}].[EntityField]
|
|
774
|
+
SET IsPrimaryKey=1, IsUnique=1, IsSoftPrimaryKey=1
|
|
775
|
+
WHERE EntityID='${entity.ID}' AND Name='${pk}'`);
|
|
776
|
+
logStatus(` ✓ Set PK for ${entity.Name}.${pk} (LLM-identified)`);
|
|
777
|
+
}
|
|
778
|
+
await this.LogSQLAndExecute(pool, sqlStatements.join('\n'), `Set LLM-identified PKs for ${entity.Name}: ${validPKs.join(', ')}`);
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Applies LLM-identified foreign keys to entity fields.
|
|
783
|
+
* Sets RelatedEntityID, RelatedEntityFieldName, and IsSoftForeignKey=1.
|
|
784
|
+
* Only applies high and medium confidence FKs.
|
|
785
|
+
* All SQL updates are batched into a single execution for performance.
|
|
786
|
+
*/
|
|
787
|
+
async applyLLMForeignKeys(pool, entity, foreignKeys, schema) {
|
|
788
|
+
if (!foreignKeys || foreignKeys.length === 0) {
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
const md = new Metadata();
|
|
792
|
+
const sqlStatements = [];
|
|
793
|
+
for (const fk of foreignKeys) {
|
|
794
|
+
// Only apply high/medium confidence
|
|
795
|
+
if (fk.confidence !== 'high' && fk.confidence !== 'medium') {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
// Validate that the field exists on this entity
|
|
799
|
+
const field = entity.Fields.find(f => f.Name.toLowerCase() === fk.fieldName.toLowerCase());
|
|
800
|
+
if (!field) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
// Skip if field already has a FK set (config-defined takes precedence)
|
|
804
|
+
if (field.RelatedEntityID) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
// Look up the related entity by name
|
|
808
|
+
const relatedEntity = md.EntityByName(fk.relatedEntityName);
|
|
809
|
+
if (!relatedEntity) {
|
|
810
|
+
logStatus(` ⚠️ LLM FK: related entity '${fk.relatedEntityName}' not found for ${entity.Name}.${fk.fieldName}`);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
sqlStatements.push(`UPDATE [${schema}].[EntityField]
|
|
814
|
+
SET RelatedEntityID='${relatedEntity.ID}',
|
|
815
|
+
RelatedEntityFieldName='${fk.relatedFieldName}',
|
|
816
|
+
IsSoftForeignKey=1
|
|
817
|
+
WHERE EntityID='${entity.ID}' AND Name='${field.Name}'`);
|
|
818
|
+
logStatus(` ✓ Set FK for ${entity.Name}.${field.Name} → ${fk.relatedEntityName}.${fk.relatedFieldName} (${fk.confidence}, LLM)`);
|
|
819
|
+
}
|
|
820
|
+
if (sqlStatements.length === 0) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
await this.LogSQLAndExecute(pool, sqlStatements.join('\n'), `Set LLM-identified FKs for ${entity.Name}`);
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Applies LLM-generated field descriptions to entity fields that lack descriptions.
|
|
828
|
+
* All SQL updates are batched into a single execution for performance.
|
|
829
|
+
*/
|
|
830
|
+
async applyLLMFieldDescriptions(pool, entity, fieldDescriptions, schema) {
|
|
831
|
+
if (!fieldDescriptions || fieldDescriptions.length === 0) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
const sqlStatements = [];
|
|
835
|
+
for (const fd of fieldDescriptions) {
|
|
836
|
+
const field = entity.Fields.find(f => f.Name.toLowerCase() === fd.fieldName.toLowerCase());
|
|
837
|
+
if (!field) {
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
// Only apply if field doesn't already have a description
|
|
841
|
+
if (field.Description && field.Description.trim().length > 0) {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const escapedDescription = fd.description.replace(/'/g, "''");
|
|
845
|
+
let setClauses = `Description='${escapedDescription}'`;
|
|
846
|
+
// Apply extended type if provided and valid
|
|
847
|
+
if (fd.extendedType) {
|
|
848
|
+
const validExtendedType = this.validateExtendedType(fd.extendedType);
|
|
849
|
+
if (validExtendedType) {
|
|
850
|
+
setClauses += `, ExtendedType='${validExtendedType}'`;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
sqlStatements.push(`UPDATE [${schema}].[EntityField]
|
|
854
|
+
SET ${setClauses}
|
|
855
|
+
WHERE EntityID='${entity.ID}' AND Name='${field.Name}'`);
|
|
856
|
+
}
|
|
857
|
+
if (sqlStatements.length === 0) {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
await this.LogSQLAndExecute(pool, sqlStatements.join('\n'), `Set LLM-generated descriptions for ${entity.Name} (${sqlStatements.length} fields)`);
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Valid values for EntityField.ExtendedType, plus common LLM aliases mapped to valid values.
|
|
865
|
+
*/
|
|
866
|
+
static { this.VALID_EXTENDED_TYPES = new Set([
|
|
867
|
+
'Code', 'Email', 'FaceTime', 'Geo', 'MSTeams', 'Other', 'SIP', 'SMS', 'Skype', 'Tel', 'URL', 'WhatsApp', 'ZoomMtg'
|
|
868
|
+
]); }
|
|
869
|
+
static { this.EXTENDED_TYPE_ALIASES = {
|
|
870
|
+
'phone': 'Tel',
|
|
871
|
+
'telephone': 'Tel',
|
|
872
|
+
'website': 'URL',
|
|
873
|
+
'link': 'URL',
|
|
874
|
+
'hyperlink': 'URL',
|
|
875
|
+
'mail': 'Email',
|
|
876
|
+
'e-mail': 'Email',
|
|
877
|
+
'text': 'SMS',
|
|
878
|
+
'location': 'Geo',
|
|
879
|
+
'address': 'Geo',
|
|
880
|
+
'teams': 'MSTeams',
|
|
881
|
+
'facetime': 'FaceTime',
|
|
882
|
+
'zoom': 'ZoomMtg',
|
|
883
|
+
'whatsapp': 'WhatsApp',
|
|
884
|
+
'skype': 'Skype',
|
|
885
|
+
}; }
|
|
886
|
+
/**
|
|
887
|
+
* Validates an LLM-suggested ExtendedType against the allowed values in EntityField.
|
|
888
|
+
* Returns the valid value (case-corrected) or null if invalid.
|
|
889
|
+
*/
|
|
890
|
+
validateExtendedType(suggested) {
|
|
891
|
+
// Direct match (case-insensitive)
|
|
892
|
+
for (const valid of ManageMetadataBase.VALID_EXTENDED_TYPES) {
|
|
893
|
+
if (valid.toLowerCase() === suggested.toLowerCase()) {
|
|
894
|
+
return valid;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// Check aliases
|
|
898
|
+
const alias = ManageMetadataBase.EXTENDED_TYPE_ALIASES[suggested.toLowerCase()];
|
|
899
|
+
if (alias) {
|
|
900
|
+
return alias;
|
|
901
|
+
}
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Manages virtual EntityField records for IS-A parent entity fields.
|
|
906
|
+
* For each entity with ParentID set (IS-A child), creates/updates virtual field records
|
|
907
|
+
* that mirror the parent entity's base table fields (excluding PKs, timestamps, and virtual fields).
|
|
908
|
+
* Runs collision detection to prevent child table columns from shadowing parent fields.
|
|
909
|
+
*/
|
|
910
|
+
async manageParentEntityFields(pool) {
|
|
911
|
+
let bSuccess = true;
|
|
912
|
+
let anyUpdates = false;
|
|
913
|
+
const md = new Metadata();
|
|
914
|
+
const childEntities = md.Entities.filter(e => e.IsChildType);
|
|
915
|
+
if (childEntities.length === 0) {
|
|
916
|
+
return { success: true, anyUpdates: false };
|
|
917
|
+
}
|
|
918
|
+
logStatus(` Processing IS-A parent fields for ${childEntities.length} child entit${childEntities.length === 1 ? 'y' : 'ies'}...`);
|
|
919
|
+
for (const childEntity of childEntities) {
|
|
920
|
+
try {
|
|
921
|
+
const { success, updated } = await this.manageSingleEntityParentFields(pool, childEntity);
|
|
922
|
+
anyUpdates = anyUpdates || updated;
|
|
923
|
+
if (!success) {
|
|
924
|
+
logError(` Error managing IS-A parent fields for ${childEntity.Name}`);
|
|
925
|
+
bSuccess = false;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
catch (e) {
|
|
929
|
+
logError(` Exception managing IS-A parent fields for ${childEntity.Name}: ${e}`);
|
|
930
|
+
bSuccess = false;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return { success: bSuccess, anyUpdates };
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Creates/updates virtual EntityField records for a single child entity's parent fields.
|
|
937
|
+
* Detects field name collisions between child's own base table columns and parent fields.
|
|
938
|
+
*/
|
|
939
|
+
async manageSingleEntityParentFields(pool, childEntity) {
|
|
940
|
+
let bUpdated = false;
|
|
941
|
+
// Get all parent fields: non-PK, non-__mj_, non-virtual from each parent in chain
|
|
942
|
+
const parentFields = childEntity.AllParentFields;
|
|
943
|
+
if (parentFields.length === 0) {
|
|
944
|
+
return { success: true, updated: false };
|
|
945
|
+
}
|
|
946
|
+
// Get child's own (non-virtual) field names for collision detection
|
|
947
|
+
const childOwnFieldNames = new Set(childEntity.Fields.filter(f => !f.IsVirtual).map(f => f.Name.toLowerCase()));
|
|
948
|
+
for (const parentField of parentFields) {
|
|
949
|
+
// Collision detection: child's own base table column has same name as parent field.
|
|
950
|
+
// This uses in-memory metadata which filters to non-virtual (base table) fields only.
|
|
951
|
+
if (childOwnFieldNames.has(parentField.Name.toLowerCase())) {
|
|
952
|
+
logError(` FIELD COLLISION: Entity '${childEntity.Name}' has its own column '${parentField.Name}' ` +
|
|
953
|
+
`that conflicts with IS-A parent field '${parentField.Name}' from '${parentField.Entity}'. ` +
|
|
954
|
+
`Rename the child column to resolve this collision. Skipping IS-A field sync for this entity.`);
|
|
955
|
+
return { success: false, updated: false };
|
|
956
|
+
}
|
|
957
|
+
// Check the DATABASE for existing field record — in-memory metadata may be stale
|
|
958
|
+
// (e.g. createNewEntityFieldsFromSchema may have already added this field from the view)
|
|
959
|
+
const existsResult = await pool.request()
|
|
960
|
+
.input('EntityID', childEntity.ID)
|
|
961
|
+
.input('FieldName', parentField.Name)
|
|
962
|
+
.query(`SELECT ID, IsVirtual, Type, Length, Precision, Scale, AllowsNull, AllowUpdateAPI
|
|
963
|
+
FROM [${mj_core_schema()}].EntityField
|
|
964
|
+
WHERE EntityID = @EntityID AND Name = @FieldName`);
|
|
965
|
+
if (existsResult.recordset.length > 0) {
|
|
966
|
+
// Field already exists — update it to ensure it's marked as a virtual IS-A field
|
|
967
|
+
const existingRow = existsResult.recordset[0];
|
|
968
|
+
const needsUpdate = !existingRow.IsVirtual ||
|
|
969
|
+
existingRow.Type?.trim().toLowerCase() !== parentField.Type.trim().toLowerCase() ||
|
|
970
|
+
existingRow.Length !== parentField.Length ||
|
|
971
|
+
existingRow.Precision !== parentField.Precision ||
|
|
972
|
+
existingRow.Scale !== parentField.Scale ||
|
|
973
|
+
existingRow.AllowsNull !== parentField.AllowsNull ||
|
|
974
|
+
!existingRow.AllowUpdateAPI;
|
|
975
|
+
if (needsUpdate) {
|
|
976
|
+
const sqlUpdate = `UPDATE [${mj_core_schema()}].EntityField
|
|
977
|
+
SET IsVirtual=1,
|
|
978
|
+
Type='${parentField.Type}',
|
|
979
|
+
Length=${parentField.Length},
|
|
980
|
+
Precision=${parentField.Precision},
|
|
981
|
+
Scale=${parentField.Scale},
|
|
982
|
+
AllowsNull=${parentField.AllowsNull ? 1 : 0},
|
|
983
|
+
AllowUpdateAPI=1
|
|
984
|
+
WHERE ID='${existingRow.ID}'`;
|
|
985
|
+
await this.LogSQLAndExecute(pool, sqlUpdate, `Update IS-A parent field ${parentField.Name} on ${childEntity.Name}`);
|
|
986
|
+
bUpdated = true;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
// Create new virtual field record for this parent field
|
|
991
|
+
const newFieldID = this.createNewUUID();
|
|
992
|
+
// Use high sequence — will be reordered by updateExistingEntityFieldsFromSchema
|
|
993
|
+
const sequence = 100000 + parentFields.indexOf(parentField);
|
|
994
|
+
const sqlInsert = `INSERT INTO [${mj_core_schema()}].EntityField (
|
|
995
|
+
ID, EntityID, Name, Type, AllowsNull,
|
|
996
|
+
Length, Precision, Scale,
|
|
997
|
+
Sequence, IsVirtual, AllowUpdateAPI,
|
|
998
|
+
IsPrimaryKey, IsUnique)
|
|
999
|
+
VALUES (
|
|
1000
|
+
'${newFieldID}', '${childEntity.ID}', '${parentField.Name}',
|
|
1001
|
+
'${parentField.Type}', ${parentField.AllowsNull ? 1 : 0},
|
|
1002
|
+
${parentField.Length}, ${parentField.Precision}, ${parentField.Scale},
|
|
1003
|
+
${sequence}, 1, 1, 0, 0)`;
|
|
1004
|
+
await this.LogSQLAndExecute(pool, sqlInsert, `Create IS-A parent field ${parentField.Name} on ${childEntity.Name}`);
|
|
1005
|
+
bUpdated = true;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
// Remove stale IS-A parent virtual fields no longer in the parent chain.
|
|
1009
|
+
// IS-A parent fields are identified by IsVirtual=true AND AllowUpdateAPI=true.
|
|
1010
|
+
const currentParentFieldNames = new Set(parentFields.map(f => f.Name.toLowerCase()));
|
|
1011
|
+
const staleFields = childEntity.Fields.filter(f => f.IsVirtual && f.AllowUpdateAPI &&
|
|
1012
|
+
!f.IsPrimaryKey && !f.Name.startsWith('__mj_') &&
|
|
1013
|
+
!currentParentFieldNames.has(f.Name.toLowerCase()));
|
|
1014
|
+
for (const staleField of staleFields) {
|
|
1015
|
+
const sqlDelete = `DELETE FROM [${mj_core_schema()}].EntityField WHERE ID='${staleField.ID}'`;
|
|
1016
|
+
await this.LogSQLAndExecute(pool, sqlDelete, `Remove stale IS-A parent field ${staleField.Name} from ${childEntity.Name}`);
|
|
1017
|
+
bUpdated = true;
|
|
1018
|
+
}
|
|
1019
|
+
if (bUpdated) {
|
|
1020
|
+
const sqlUpdate = `UPDATE [${mj_core_schema()}].Entity SET [${EntityInfo.UpdatedAtFieldName}]=GETUTCDATE() WHERE ID='${childEntity.ID}'`;
|
|
1021
|
+
await this.LogSQLAndExecute(pool, sqlUpdate, `Update entity timestamp for ${childEntity.Name} after IS-A field sync`);
|
|
1022
|
+
}
|
|
1023
|
+
return { success: true, updated: bUpdated };
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Checks if an existing virtual parent field record needs to be updated to match the parent field.
|
|
1027
|
+
*/
|
|
1028
|
+
parentFieldNeedsUpdate(existing, parentField) {
|
|
1029
|
+
return existing.Type.trim().toLowerCase() !== parentField.Type.trim().toLowerCase() ||
|
|
1030
|
+
existing.Length !== parentField.Length ||
|
|
1031
|
+
existing.Precision !== parentField.Precision ||
|
|
1032
|
+
existing.Scale !== parentField.Scale ||
|
|
1033
|
+
existing.AllowsNull !== parentField.AllowsNull ||
|
|
1034
|
+
!existing.AllowUpdateAPI;
|
|
1035
|
+
}
|
|
337
1036
|
/**
|
|
338
1037
|
* This method creates and updates relationships in the metadata based on foreign key relationships in the database.
|
|
339
1038
|
* @param pool
|
|
@@ -370,16 +1069,16 @@ class ManageMetadataBase {
|
|
|
370
1069
|
try {
|
|
371
1070
|
// STEP 1 - search for all foreign keys in the vwEntityFields view, we use the RelatedEntityID field to determine our FKs
|
|
372
1071
|
const sSQL = `SELECT *
|
|
373
|
-
FROM ${
|
|
1072
|
+
FROM ${mj_core_schema()}.vwEntityFields
|
|
374
1073
|
WHERE
|
|
375
1074
|
RelatedEntityID IS NOT NULL AND
|
|
376
1075
|
IsVirtual = 0 AND
|
|
377
|
-
EntityID NOT IN (SELECT ID FROM ${
|
|
1076
|
+
EntityID NOT IN (SELECT ID FROM ${mj_core_schema()}.Entity WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
|
|
378
1077
|
ORDER BY RelatedEntityID`;
|
|
379
1078
|
const entityFieldsResult = await pool.request().query(sSQL);
|
|
380
1079
|
const entityFields = entityFieldsResult.recordset;
|
|
381
1080
|
// Get the relationship counts for each entity
|
|
382
|
-
const sSQLRelationshipCount = `SELECT EntityID, COUNT(*) AS Count FROM ${
|
|
1081
|
+
const sSQLRelationshipCount = `SELECT EntityID, COUNT(*) AS Count FROM ${mj_core_schema()}.EntityRelationship GROUP BY EntityID`;
|
|
383
1082
|
const relationshipCountsResult = await pool.request().query(sSQLRelationshipCount);
|
|
384
1083
|
const relationshipCounts = relationshipCountsResult.recordset;
|
|
385
1084
|
const relationshipCountMap = new Map();
|
|
@@ -387,7 +1086,7 @@ class ManageMetadataBase {
|
|
|
387
1086
|
relationshipCountMap.set(rc.EntityID, rc.Count);
|
|
388
1087
|
}
|
|
389
1088
|
// get all relationships in one query for performance improvement
|
|
390
|
-
const sSQLRelationship = `SELECT * FROM ${
|
|
1089
|
+
const sSQLRelationship = `SELECT * FROM ${mj_core_schema()}.EntityRelationship`;
|
|
391
1090
|
const allRelationshipsResult = await pool.request().query(sSQLRelationship);
|
|
392
1091
|
const allRelationships = allRelationshipsResult.recordset;
|
|
393
1092
|
// Function to process a batch of entity fields
|
|
@@ -406,11 +1105,11 @@ class ManageMetadataBase {
|
|
|
406
1105
|
batchSQL += `
|
|
407
1106
|
IF NOT EXISTS (
|
|
408
1107
|
SELECT 1
|
|
409
|
-
FROM [${
|
|
1108
|
+
FROM [${mj_core_schema()}].EntityRelationship
|
|
410
1109
|
WHERE ID = '${newEntityRelationshipUUID}'
|
|
411
1110
|
)
|
|
412
1111
|
BEGIN
|
|
413
|
-
INSERT INTO ${
|
|
1112
|
+
INSERT INTO ${mj_core_schema()}.EntityRelationship (ID, EntityID, RelatedEntityID, RelatedEntityJoinField, Type, BundleInAPI, DisplayInForm, DisplayName, Sequence)
|
|
414
1113
|
VALUES ('${newEntityRelationshipUUID}', '${f.RelatedEntityID}', '${f.EntityID}', '${f.Name}', 'One To Many', 1, 1, '${e.Name}', ${sequence});
|
|
415
1114
|
END
|
|
416
1115
|
`;
|
|
@@ -430,7 +1129,7 @@ class ManageMetadataBase {
|
|
|
430
1129
|
return true;
|
|
431
1130
|
}
|
|
432
1131
|
catch (e) {
|
|
433
|
-
|
|
1132
|
+
logError(e);
|
|
434
1133
|
return false;
|
|
435
1134
|
}
|
|
436
1135
|
}
|
|
@@ -442,7 +1141,7 @@ class ManageMetadataBase {
|
|
|
442
1141
|
*/
|
|
443
1142
|
async checkAndRemoveMetadataForDeletedTables(pool, excludeSchemas) {
|
|
444
1143
|
try {
|
|
445
|
-
const sql = `SELECT * FROM ${
|
|
1144
|
+
const sql = `SELECT * FROM ${mj_core_schema()}.vwEntitiesWithMissingBaseTables WHERE VirtualEntity=0`;
|
|
446
1145
|
const entitiesResult = await pool.request().query(sql);
|
|
447
1146
|
const entities = entitiesResult.recordset;
|
|
448
1147
|
if (entities && entities.length > 0) {
|
|
@@ -453,7 +1152,7 @@ class ManageMetadataBase {
|
|
|
453
1152
|
try {
|
|
454
1153
|
const sqlDelete = `__mj.spDeleteEntityWithCoreDependencies @EntityID='${e.ID}'`;
|
|
455
1154
|
await this.LogSQLAndExecute(pool, sqlDelete, `SQL text to remove entity ${e.Name}`);
|
|
456
|
-
|
|
1155
|
+
logStatus(` > Removed metadata for table ${e.SchemaName}.${e.BaseTable}`);
|
|
457
1156
|
// next up we need to remove the spCreate, spDelete, spUpdate, BaseView, and FullTextSearchFunction, if provided.
|
|
458
1157
|
// We only remoe these artifcacts when they are generated which is info we have in the BaseViewGenerated, spCreateGenerated, etc. fields
|
|
459
1158
|
await this.checkDropSQLObject(pool, e.BaseViewGenerated, 'view', e.SchemaName, e.BaseView);
|
|
@@ -463,17 +1162,17 @@ class ManageMetadataBase {
|
|
|
463
1162
|
await this.checkDropSQLObject(pool, e.FullTextSearchFunctionGenerated, 'function', e.SchemaName, e.FullTextSearchFunction);
|
|
464
1163
|
}
|
|
465
1164
|
catch (ex) {
|
|
466
|
-
|
|
1165
|
+
logError(`Error removing metadata for entity ${ex.Name}, error: ${ex}`);
|
|
467
1166
|
}
|
|
468
1167
|
}
|
|
469
1168
|
// if we get here we now need to refresh our metadata object
|
|
470
|
-
const md = new
|
|
1169
|
+
const md = new Metadata();
|
|
471
1170
|
await md.Refresh();
|
|
472
1171
|
}
|
|
473
1172
|
return true;
|
|
474
1173
|
}
|
|
475
1174
|
catch (e) {
|
|
476
|
-
|
|
1175
|
+
logError(e);
|
|
477
1176
|
return false;
|
|
478
1177
|
}
|
|
479
1178
|
}
|
|
@@ -487,23 +1186,23 @@ class ManageMetadataBase {
|
|
|
487
1186
|
const sqlDelete = `IF OBJECT_ID('[${schemaName}].[${name}]', '${objectTypeCode}') IS NOT NULL\n DROP ${upperType} [${schemaName}].[${name}]`;
|
|
488
1187
|
await this.LogSQLAndExecute(pool, sqlDelete, `SQL text to remove ${type} ${schemaName}.${name}`);
|
|
489
1188
|
// next up, we need to clean up the cache of saved DB objects that may exist for this entity in the appropriate sub-directory.
|
|
490
|
-
const sqlOutputDir =
|
|
1189
|
+
const sqlOutputDir = outputDir('SQL', true);
|
|
491
1190
|
if (sqlOutputDir) {
|
|
492
1191
|
// now do the same thing for the /schema directory within the provided directory
|
|
493
1192
|
const fType = type === 'procedure' ? 'sp' : type === 'view' ? 'view' : 'full_text_search_function';
|
|
494
|
-
const filePath =
|
|
495
|
-
const filePathPermissions =
|
|
1193
|
+
const filePath = path.join(sqlOutputDir, this.SQLUtilityObject.getDBObjectFileName(fType, schemaName, name, false, true));
|
|
1194
|
+
const filePathPermissions = path.join(sqlOutputDir, this.SQLUtilityObject.getDBObjectFileName(fType, schemaName, name, true, true));
|
|
496
1195
|
// if the files exist, delete them
|
|
497
1196
|
if (fs.existsSync(filePath))
|
|
498
1197
|
fs.unlinkSync(filePath);
|
|
499
1198
|
if (fs.existsSync(filePathPermissions))
|
|
500
1199
|
fs.unlinkSync(filePathPermissions);
|
|
501
1200
|
}
|
|
502
|
-
|
|
1201
|
+
logStatus(` > Removed ${type} ${schemaName}.${name}`);
|
|
503
1202
|
}
|
|
504
1203
|
}
|
|
505
1204
|
catch (e) {
|
|
506
|
-
|
|
1205
|
+
logError(` > Error removing ${type} ${schemaName}.${name}, error: ${e}`);
|
|
507
1206
|
}
|
|
508
1207
|
}
|
|
509
1208
|
/**
|
|
@@ -529,72 +1228,90 @@ class ManageMetadataBase {
|
|
|
529
1228
|
if (!skipCreatedAtUpdatedAtDeletedAtFieldValidation) {
|
|
530
1229
|
if (!await this.ensureCreatedAtUpdatedAtFieldsExist(pool, excludeSchemas) ||
|
|
531
1230
|
!await this.ensureDeletedAtFieldsExist(pool, excludeSchemas)) {
|
|
532
|
-
|
|
1231
|
+
logError(`Error ensuring ${EntityInfo.CreatedAtFieldName}, ${EntityInfo.UpdatedAtFieldName} and ${EntityInfo.DeletedAtFieldName} fields exist`);
|
|
533
1232
|
bSuccess = false;
|
|
534
1233
|
}
|
|
535
|
-
|
|
1234
|
+
logStatus(` Ensured ${EntityInfo.CreatedAtFieldName}/${EntityInfo.UpdatedAtFieldName}/${EntityInfo.DeletedAtFieldName} fields exist in ${(new Date().getTime() - startTime.getTime()) / 1000} seconds`);
|
|
536
1235
|
}
|
|
537
1236
|
const step1StartTime = new Date();
|
|
538
1237
|
if (!await this.deleteUnneededEntityFields(pool, excludeSchemas)) {
|
|
539
|
-
|
|
1238
|
+
logError('Error deleting unneeded entity fields');
|
|
540
1239
|
bSuccess = false;
|
|
541
1240
|
}
|
|
542
|
-
|
|
1241
|
+
logStatus(` Deleted unneeded entity fields in ${(new Date().getTime() - step1StartTime.getTime()) / 1000} seconds`);
|
|
543
1242
|
// AN: 14-June-2025 - See note below about the new order of these steps, this must
|
|
544
1243
|
// happen before we update existing entity fields from schema.
|
|
545
1244
|
const step2StartTime = new Date();
|
|
546
1245
|
if (!await this.createNewEntityFieldsFromSchema(pool)) { // has its own internal filtering for exclude schema/table so don't pass in
|
|
547
|
-
|
|
1246
|
+
logError('Error creating new entity fields from schema');
|
|
548
1247
|
bSuccess = false;
|
|
549
1248
|
}
|
|
550
|
-
|
|
1249
|
+
logStatus(` Created new entity fields from schema in ${(new Date().getTime() - step2StartTime.getTime()) / 1000} seconds`);
|
|
551
1250
|
// AN: 14-June-2025 - we are now running this AFTER we create new entity fields from schema
|
|
552
1251
|
// which results in the same pattern of behavior as migrations where we first create new fields
|
|
553
1252
|
// with VERY HIGH sequence numbers (e.g. 100,000 above what they will be approx) and then
|
|
554
1253
|
// we align them properly in sequential order from 1+ via this method below.
|
|
555
1254
|
const step3StartTime = new Date();
|
|
556
1255
|
if (!await this.updateExistingEntityFieldsFromSchema(pool, excludeSchemas)) {
|
|
557
|
-
|
|
1256
|
+
logError('Error updating existing entity fields from schema');
|
|
558
1257
|
bSuccess = false;
|
|
559
1258
|
}
|
|
560
|
-
|
|
1259
|
+
logStatus(` Updated existing entity fields from schema in ${(new Date().getTime() - step3StartTime.getTime()) / 1000} seconds`);
|
|
561
1260
|
// Apply soft PK/FK configuration if config file exists
|
|
562
1261
|
const stepConfigStartTime = new Date();
|
|
563
1262
|
if (!await this.applySoftPKFKConfig(pool)) {
|
|
564
|
-
|
|
1263
|
+
logError('Error applying soft PK/FK configuration');
|
|
1264
|
+
}
|
|
1265
|
+
logStatus(` Applied soft PK/FK configuration in ${(new Date().getTime() - stepConfigStartTime.getTime()) / 1000} seconds`);
|
|
1266
|
+
// CRITICAL: Refresh metadata to pick up soft PK/FK flags
|
|
1267
|
+
// Without this, downstream SQL and TypeScript generation will fail
|
|
1268
|
+
// because entity.Fields and entity.PrimaryKeys won't reflect the updated flags
|
|
1269
|
+
if (configInfo.additionalSchemaInfo) {
|
|
1270
|
+
logStatus(' Refreshing metadata after applying soft PK/FK configuration...');
|
|
1271
|
+
const md = new Metadata();
|
|
1272
|
+
await md.Refresh();
|
|
1273
|
+
logStatus(' Metadata refresh complete');
|
|
1274
|
+
}
|
|
1275
|
+
// IS-A parent field sync: create/update virtual EntityField records for parent chain fields
|
|
1276
|
+
// Must run AFTER metadata refresh so it sees current soft PK/FK flags
|
|
1277
|
+
const stepISAStartTime = new Date();
|
|
1278
|
+
const isaResult = await this.manageParentEntityFields(pool);
|
|
1279
|
+
if (!isaResult.success) {
|
|
1280
|
+
logError('Error managing IS-A parent entity fields');
|
|
1281
|
+
bSuccess = false;
|
|
565
1282
|
}
|
|
566
|
-
|
|
1283
|
+
logStatus(` Managed IS-A parent entity fields in ${(new Date().getTime() - stepISAStartTime.getTime()) / 1000} seconds`);
|
|
567
1284
|
const step4StartTime = new Date();
|
|
568
1285
|
if (!await this.setDefaultColumnWidthWhereNeeded(pool, excludeSchemas)) {
|
|
569
|
-
|
|
1286
|
+
logError('Error setting default column width where needed');
|
|
570
1287
|
bSuccess = false;
|
|
571
1288
|
}
|
|
572
|
-
|
|
1289
|
+
logStatus(` Set default column width where needed in ${(new Date().getTime() - step4StartTime.getTime()) / 1000} seconds`);
|
|
573
1290
|
const step5StartTime = new Date();
|
|
574
1291
|
if (!await this.updateEntityFieldDisplayNameWhereNull(pool, excludeSchemas)) {
|
|
575
|
-
|
|
1292
|
+
logError('Error updating entity field display name where null');
|
|
576
1293
|
bSuccess = false;
|
|
577
1294
|
}
|
|
578
|
-
|
|
1295
|
+
logStatus(` Updated entity field display name where null in ${(new Date().getTime() - step5StartTime.getTime()) / 1000} seconds`);
|
|
579
1296
|
if (!skipEntityFieldValues) {
|
|
580
1297
|
const step6StartTime = new Date();
|
|
581
|
-
|
|
1298
|
+
logStatus(` Starting to manage entity field values...`);
|
|
582
1299
|
if (!await this.manageEntityFieldValuesAndValidatorFunctions(pool, excludeSchemas, currentUser, false)) {
|
|
583
|
-
|
|
1300
|
+
logError('Error managing entity field values');
|
|
584
1301
|
bSuccess = false;
|
|
585
1302
|
}
|
|
586
|
-
|
|
1303
|
+
logStatus(` Managed entity field values in ${(new Date().getTime() - step6StartTime.getTime()) / 1000} seconds`);
|
|
587
1304
|
}
|
|
588
1305
|
// Advanced Generation - Smart field identification and form layout
|
|
589
1306
|
if (!skipAdvancedGeneration) {
|
|
590
1307
|
const step7StartTime = new Date();
|
|
591
1308
|
if (!await this.applyAdvancedGeneration(pool, excludeSchemas, currentUser)) {
|
|
592
|
-
|
|
1309
|
+
logError('Error applying advanced generation features');
|
|
593
1310
|
// Don't fail the entire process - advanced generation is optional
|
|
594
1311
|
}
|
|
595
|
-
|
|
1312
|
+
logStatus(` Applied advanced generation features in ${(new Date().getTime() - step7StartTime.getTime()) / 1000} seconds`);
|
|
596
1313
|
}
|
|
597
|
-
|
|
1314
|
+
logStatus(` Total time to manage entity fields: ${(new Date().getTime() - startTime.getTime()) / 1000} seconds`);
|
|
598
1315
|
return bSuccess;
|
|
599
1316
|
}
|
|
600
1317
|
/**
|
|
@@ -605,7 +1322,7 @@ class ManageMetadataBase {
|
|
|
605
1322
|
const sqlEntities = `SELECT
|
|
606
1323
|
*
|
|
607
1324
|
FROM
|
|
608
|
-
[${
|
|
1325
|
+
[${mj_core_schema()}].vwEntities
|
|
609
1326
|
WHERE
|
|
610
1327
|
VirtualEntity=0 AND
|
|
611
1328
|
DeleteType='Soft' AND
|
|
@@ -620,21 +1337,21 @@ class ManageMetadataBase {
|
|
|
620
1337
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
621
1338
|
WHERE
|
|
622
1339
|
${entities.map((e) => `(TABLE_SCHEMA='${e.SchemaName}' AND TABLE_NAME='${e.BaseTable}')`).join(' OR ')}
|
|
623
|
-
AND COLUMN_NAME='${
|
|
1340
|
+
AND COLUMN_NAME='${EntityInfo.DeletedAtFieldName}'`;
|
|
624
1341
|
const resultResult = await pool.request().query(sql);
|
|
625
1342
|
const result = resultResult.recordset;
|
|
626
1343
|
for (const e of entities) {
|
|
627
1344
|
const eResult = result.filter((r) => r.TABLE_NAME === e.BaseTable && r.TABLE_SCHEMA === e.SchemaName); // get just the fields for this entity
|
|
628
|
-
const deletedAt = eResult.find((r) => r.COLUMN_NAME.trim().toLowerCase() ===
|
|
1345
|
+
const deletedAt = eResult.find((r) => r.COLUMN_NAME.trim().toLowerCase() === EntityInfo.DeletedAtFieldName.trim().toLowerCase());
|
|
629
1346
|
// now, if we have the fields, we need to check the default value and update if necessary
|
|
630
|
-
const fieldResult = await this.ensureSpecialDateFieldExistsAndHasCorrectDefaultValue(pool, e,
|
|
1347
|
+
const fieldResult = await this.ensureSpecialDateFieldExistsAndHasCorrectDefaultValue(pool, e, EntityInfo.DeletedAtFieldName, deletedAt, true);
|
|
631
1348
|
overallResult = overallResult && fieldResult;
|
|
632
1349
|
}
|
|
633
1350
|
}
|
|
634
1351
|
return overallResult;
|
|
635
1352
|
}
|
|
636
1353
|
catch (e) {
|
|
637
|
-
|
|
1354
|
+
logError(e);
|
|
638
1355
|
return false;
|
|
639
1356
|
}
|
|
640
1357
|
}
|
|
@@ -646,76 +1363,86 @@ class ManageMetadataBase {
|
|
|
646
1363
|
*/
|
|
647
1364
|
async applySoftPKFKConfig(pool) {
|
|
648
1365
|
// Check if additionalSchemaInfo is configured in mj.config.cjs
|
|
649
|
-
if (!
|
|
1366
|
+
if (!configInfo.additionalSchemaInfo) {
|
|
650
1367
|
// No additional schema info configured - this is fine, it's optional
|
|
651
1368
|
return true;
|
|
652
1369
|
}
|
|
653
|
-
const configPath =
|
|
1370
|
+
const configPath = path.join(currentWorkingDirectory, configInfo.additionalSchemaInfo);
|
|
654
1371
|
if (!fs.existsSync(configPath)) {
|
|
655
|
-
|
|
1372
|
+
logStatus(` ⚠️ additionalSchemaInfo configured but file not found: ${configPath}`);
|
|
656
1373
|
return true;
|
|
657
1374
|
}
|
|
658
1375
|
try {
|
|
659
|
-
|
|
1376
|
+
logStatus(` Found ${configInfo.additionalSchemaInfo}, applying soft PK/FK configuration...`);
|
|
660
1377
|
const config = ManageMetadataBase.getSoftPKFKConfig();
|
|
661
1378
|
let totalPKs = 0;
|
|
662
1379
|
let totalFKs = 0;
|
|
663
|
-
const schema =
|
|
664
|
-
|
|
1380
|
+
const schema = mj_core_schema();
|
|
1381
|
+
// Config supports two formats:
|
|
1382
|
+
// 1. Schema-as-key (template format): { "dbo": [{ "TableName": "Orders", ... }] }
|
|
1383
|
+
// 2. Flat tables array (legacy format): { "tables": [{ "SchemaName": "dbo", "TableName": "Orders", ... }] }
|
|
1384
|
+
// Both use PascalCase property names.
|
|
1385
|
+
const tables = this.extractTablesFromConfig(config);
|
|
1386
|
+
for (const table of tables) {
|
|
1387
|
+
const tableSchema = table.SchemaName;
|
|
1388
|
+
const tableName = table.TableName;
|
|
665
1389
|
// Look up entity ID (SELECT query - no need to log to migration file)
|
|
666
|
-
const entityLookupSQL = `SELECT ID FROM [${schema}].[Entity] WHERE SchemaName = '${
|
|
1390
|
+
const entityLookupSQL = `SELECT ID FROM [${schema}].[Entity] WHERE SchemaName = '${tableSchema}' AND BaseTable = '${tableName}'`;
|
|
667
1391
|
const entityResult = await pool.request().query(entityLookupSQL);
|
|
668
1392
|
if (entityResult.recordset.length === 0) {
|
|
669
|
-
|
|
1393
|
+
logStatus(` ⚠️ Entity not found for ${tableSchema}.${tableName} - skipping`);
|
|
670
1394
|
continue;
|
|
671
1395
|
}
|
|
672
1396
|
const entityId = entityResult.recordset[0].ID;
|
|
673
1397
|
// Process primary keys - set BOTH IsPrimaryKey = 1 AND IsSoftPrimaryKey = 1
|
|
674
1398
|
// IsPrimaryKey is the source of truth, IsSoftPrimaryKey protects it from schema sync
|
|
675
|
-
|
|
676
|
-
|
|
1399
|
+
const primaryKeys = table.PrimaryKey || [];
|
|
1400
|
+
if (primaryKeys.length > 0) {
|
|
1401
|
+
for (const pk of primaryKeys) {
|
|
677
1402
|
const sSQL = `UPDATE [${schema}].[EntityField]
|
|
678
|
-
SET ${
|
|
1403
|
+
SET ${EntityInfo.UpdatedAtFieldName}=GETUTCDATE(),
|
|
679
1404
|
[IsPrimaryKey] = 1,
|
|
680
1405
|
[IsSoftPrimaryKey] = 1
|
|
681
|
-
WHERE [EntityID] = '${entityId}' AND [Name] = '${pk.
|
|
682
|
-
const result = await this.LogSQLAndExecute(pool, sSQL, `Set soft PK for ${
|
|
1406
|
+
WHERE [EntityID] = '${entityId}' AND [Name] = '${pk.FieldName}'`;
|
|
1407
|
+
const result = await this.LogSQLAndExecute(pool, sSQL, `Set soft PK for ${tableSchema}.${tableName}.${pk.FieldName}`);
|
|
683
1408
|
if (result !== null) {
|
|
684
|
-
|
|
1409
|
+
logStatus(` ✓ Set IsPrimaryKey=1, IsSoftPrimaryKey=1 for ${tableName}.${pk.FieldName}`);
|
|
685
1410
|
totalPKs++;
|
|
686
1411
|
}
|
|
687
1412
|
}
|
|
688
1413
|
}
|
|
689
1414
|
// Process foreign keys - set RelatedEntityID, RelatedEntityFieldName, and IsSoftForeignKey = 1
|
|
690
|
-
|
|
691
|
-
|
|
1415
|
+
const foreignKeys = table.ForeignKeys || [];
|
|
1416
|
+
if (foreignKeys.length > 0) {
|
|
1417
|
+
for (const fk of foreignKeys) {
|
|
1418
|
+
const fkSchema = fk.SchemaName || tableSchema;
|
|
692
1419
|
// Look up related entity ID (SELECT query - no need to log to migration file)
|
|
693
|
-
const relatedLookupSQL = `SELECT ID FROM [${schema}].[Entity] WHERE SchemaName = '${
|
|
1420
|
+
const relatedLookupSQL = `SELECT ID FROM [${schema}].[Entity] WHERE SchemaName = '${fkSchema}' AND BaseTable = '${fk.RelatedTable}'`;
|
|
694
1421
|
const relatedEntityResult = await pool.request().query(relatedLookupSQL);
|
|
695
1422
|
if (relatedEntityResult.recordset.length === 0) {
|
|
696
|
-
|
|
1423
|
+
logStatus(` ⚠️ Related entity not found for ${fkSchema}.${fk.RelatedTable} - skipping FK ${fk.FieldName}`);
|
|
697
1424
|
continue;
|
|
698
1425
|
}
|
|
699
1426
|
const relatedEntityId = relatedEntityResult.recordset[0].ID;
|
|
700
1427
|
const sSQL = `UPDATE [${schema}].[EntityField]
|
|
701
|
-
SET ${
|
|
1428
|
+
SET ${EntityInfo.UpdatedAtFieldName}=GETUTCDATE(),
|
|
702
1429
|
[RelatedEntityID] = '${relatedEntityId}',
|
|
703
|
-
[RelatedEntityFieldName] = '${fk.
|
|
1430
|
+
[RelatedEntityFieldName] = '${fk.RelatedField}',
|
|
704
1431
|
[IsSoftForeignKey] = 1
|
|
705
|
-
WHERE [EntityID] = '${entityId}' AND [Name] = '${fk.
|
|
706
|
-
const result = await this.LogSQLAndExecute(pool, sSQL, `Set soft FK for ${
|
|
1432
|
+
WHERE [EntityID] = '${entityId}' AND [Name] = '${fk.FieldName}'`;
|
|
1433
|
+
const result = await this.LogSQLAndExecute(pool, sSQL, `Set soft FK for ${tableSchema}.${tableName}.${fk.FieldName} → ${fk.RelatedTable}.${fk.RelatedField}`);
|
|
707
1434
|
if (result !== null) {
|
|
708
|
-
|
|
1435
|
+
logStatus(` ✓ Set soft FK for ${tableName}.${fk.FieldName} → ${fk.RelatedTable}.${fk.RelatedField}`);
|
|
709
1436
|
totalFKs++;
|
|
710
1437
|
}
|
|
711
1438
|
}
|
|
712
1439
|
}
|
|
713
1440
|
}
|
|
714
|
-
|
|
1441
|
+
logStatus(` Applied ${totalPKs} soft PK(s) and ${totalFKs} soft FK(s) from configuration`);
|
|
715
1442
|
return true;
|
|
716
1443
|
}
|
|
717
1444
|
catch (e) {
|
|
718
|
-
|
|
1445
|
+
logError(`Error applying soft PK/FK configuration: ${e}`);
|
|
719
1446
|
return false;
|
|
720
1447
|
}
|
|
721
1448
|
}
|
|
@@ -730,7 +1457,7 @@ class ManageMetadataBase {
|
|
|
730
1457
|
const sqlEntities = `SELECT
|
|
731
1458
|
*
|
|
732
1459
|
FROM
|
|
733
|
-
[${
|
|
1460
|
+
[${mj_core_schema()}].vwEntities
|
|
734
1461
|
WHERE
|
|
735
1462
|
VirtualEntity = 0 AND
|
|
736
1463
|
TrackRecordChanges = 1 AND
|
|
@@ -746,24 +1473,24 @@ class ManageMetadataBase {
|
|
|
746
1473
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
747
1474
|
WHERE
|
|
748
1475
|
${entities.map((e) => `(TABLE_SCHEMA='${e.SchemaName}' AND TABLE_NAME='${e.BaseTable}')`).join(' OR ')}
|
|
749
|
-
AND COLUMN_NAME IN ('${
|
|
1476
|
+
AND COLUMN_NAME IN ('${EntityInfo.CreatedAtFieldName}','${EntityInfo.UpdatedAtFieldName}')`;
|
|
750
1477
|
const resultResult = await pool.request().query(sqlCreatedUpdated);
|
|
751
1478
|
const result = resultResult.recordset;
|
|
752
1479
|
for (const e of entities) {
|
|
753
1480
|
// result has both created at and updated at fields, so filter on the result for each and do what we need to based on that
|
|
754
1481
|
const eResult = result.filter((r) => r.TABLE_NAME === e.BaseTable && r.TABLE_SCHEMA === e.SchemaName); // get just the fields for this entity
|
|
755
|
-
const createdAt = eResult.find((r) => r.COLUMN_NAME.trim().toLowerCase() ===
|
|
756
|
-
const updatedAt = eResult.find((r) => r.COLUMN_NAME.trim().toLowerCase() ===
|
|
1482
|
+
const createdAt = eResult.find((r) => r.COLUMN_NAME.trim().toLowerCase() === EntityInfo.CreatedAtFieldName.trim().toLowerCase());
|
|
1483
|
+
const updatedAt = eResult.find((r) => r.COLUMN_NAME.trim().toLowerCase() === EntityInfo.UpdatedAtFieldName.trim().toLowerCase());
|
|
757
1484
|
// now, if we have the fields, we need to check the default value and update if necessary
|
|
758
|
-
const fieldResult = await this.ensureSpecialDateFieldExistsAndHasCorrectDefaultValue(pool, e,
|
|
759
|
-
await this.ensureSpecialDateFieldExistsAndHasCorrectDefaultValue(pool, e,
|
|
1485
|
+
const fieldResult = await this.ensureSpecialDateFieldExistsAndHasCorrectDefaultValue(pool, e, EntityInfo.CreatedAtFieldName, createdAt, false) &&
|
|
1486
|
+
await this.ensureSpecialDateFieldExistsAndHasCorrectDefaultValue(pool, e, EntityInfo.UpdatedAtFieldName, updatedAt, false);
|
|
760
1487
|
overallResult = overallResult && fieldResult;
|
|
761
1488
|
}
|
|
762
1489
|
}
|
|
763
1490
|
return overallResult;
|
|
764
1491
|
}
|
|
765
1492
|
catch (e) {
|
|
766
|
-
|
|
1493
|
+
logError(e);
|
|
767
1494
|
return false;
|
|
768
1495
|
}
|
|
769
1496
|
}
|
|
@@ -799,7 +1526,7 @@ class ManageMetadataBase {
|
|
|
799
1526
|
// field that is NOT NULL
|
|
800
1527
|
if (!allowNull) {
|
|
801
1528
|
const defaultValue = currentFieldData.COLUMN_DEFAULT;
|
|
802
|
-
const realDefaultValue =
|
|
1529
|
+
const realDefaultValue = ExtractActualDefaultValue(defaultValue);
|
|
803
1530
|
if (!realDefaultValue || realDefaultValue.trim().toLowerCase() !== 'getutcdate()') {
|
|
804
1531
|
await this.dropAndCreateDefaultConstraintForSpecialDateField(pool, entity, fieldName);
|
|
805
1532
|
}
|
|
@@ -810,7 +1537,7 @@ class ManageMetadataBase {
|
|
|
810
1537
|
return true;
|
|
811
1538
|
}
|
|
812
1539
|
catch (e) {
|
|
813
|
-
|
|
1540
|
+
logError(e);
|
|
814
1541
|
return false;
|
|
815
1542
|
}
|
|
816
1543
|
}
|
|
@@ -819,11 +1546,11 @@ class ManageMetadataBase {
|
|
|
819
1546
|
*/
|
|
820
1547
|
async createDefaultConstraintForSpecialDateField(pool, entity, fieldName) {
|
|
821
1548
|
try {
|
|
822
|
-
const sqlAddDefaultConstraint = `ALTER TABLE [${entity.SchemaName}].[${entity.BaseTable}] ADD CONSTRAINT DF_${entity.SchemaName}_${
|
|
1549
|
+
const sqlAddDefaultConstraint = `ALTER TABLE [${entity.SchemaName}].[${entity.BaseTable}] ADD CONSTRAINT DF_${entity.SchemaName}_${CodeNameFromString(entity.BaseTable)}_${fieldName} DEFAULT GETUTCDATE() FOR [${fieldName}]`;
|
|
823
1550
|
await this.LogSQLAndExecute(pool, sqlAddDefaultConstraint, `SQL text to add default constraint for special date field ${fieldName} in entity ${entity.SchemaName}.${entity.BaseTable}`);
|
|
824
1551
|
}
|
|
825
1552
|
catch (e) {
|
|
826
|
-
|
|
1553
|
+
logError(e);
|
|
827
1554
|
}
|
|
828
1555
|
}
|
|
829
1556
|
/**
|
|
@@ -867,7 +1594,7 @@ class ManageMetadataBase {
|
|
|
867
1594
|
await this.LogSQLAndExecute(pool, sqlDropDefaultConstraint, `SQL text to drop default existing default constraints in entity ${entity.SchemaName}.${entity.BaseTable}`);
|
|
868
1595
|
}
|
|
869
1596
|
catch (e) {
|
|
870
|
-
|
|
1597
|
+
logError(e);
|
|
871
1598
|
}
|
|
872
1599
|
}
|
|
873
1600
|
/**
|
|
@@ -879,18 +1606,18 @@ class ManageMetadataBase {
|
|
|
879
1606
|
*/
|
|
880
1607
|
async generateNewEntityDescriptions(pool, md, currentUser) {
|
|
881
1608
|
// for the list of new entities, go through and attempt to generate new entity descriptions
|
|
882
|
-
const ag = new
|
|
1609
|
+
const ag = new AdvancedGeneration();
|
|
883
1610
|
if (ag.featureEnabled('EntityDescriptions')) {
|
|
884
1611
|
// we have the feature enabled, so let's loop through the new entities and generate descriptions for them
|
|
885
1612
|
for (let e of ManageMetadataBase.newEntityList) {
|
|
886
|
-
const dataResult = await pool.request().query(`SELECT * FROM [${
|
|
1613
|
+
const dataResult = await pool.request().query(`SELECT * FROM [${mj_core_schema()}].vwEntities WHERE Name = '${e}'`);
|
|
887
1614
|
const data = dataResult.recordset;
|
|
888
|
-
const fieldsResult = await pool.request().query(`SELECT * FROM [${
|
|
1615
|
+
const fieldsResult = await pool.request().query(`SELECT * FROM [${mj_core_schema()}].vwEntityFields WHERE EntityID='${data[0].ID}'`);
|
|
889
1616
|
const fields = fieldsResult.recordset;
|
|
890
1617
|
// Use new API to generate entity description
|
|
891
1618
|
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);
|
|
892
1619
|
if (result?.entityDescription && result.entityDescription.length > 0) {
|
|
893
|
-
const sSQL = `UPDATE [${
|
|
1620
|
+
const sSQL = `UPDATE [${mj_core_schema()}].Entity SET Description = '${result.entityDescription}' WHERE Name = '${e}'`;
|
|
894
1621
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to update entity description for entity ${e}`);
|
|
895
1622
|
}
|
|
896
1623
|
else {
|
|
@@ -912,9 +1639,9 @@ class ManageMetadataBase {
|
|
|
912
1639
|
const sql = `SELECT
|
|
913
1640
|
ef.ID, ef.Name
|
|
914
1641
|
FROM
|
|
915
|
-
[${
|
|
1642
|
+
[${mj_core_schema()}].vwEntityFields ef
|
|
916
1643
|
INNER JOIN
|
|
917
|
-
[${
|
|
1644
|
+
[${mj_core_schema()}].vwEntities e
|
|
918
1645
|
ON
|
|
919
1646
|
ef.EntityID = e.ID
|
|
920
1647
|
WHERE
|
|
@@ -927,16 +1654,16 @@ class ManageMetadataBase {
|
|
|
927
1654
|
const fields = fieldsResult.recordset;
|
|
928
1655
|
if (fields && fields.length > 0)
|
|
929
1656
|
for (const field of fields) {
|
|
930
|
-
const sDisplayName =
|
|
1657
|
+
const sDisplayName = stripTrailingChars(convertCamelCaseToHaveSpaces(field.Name), 'ID', true).trim();
|
|
931
1658
|
if (sDisplayName.length > 0 && sDisplayName.toLowerCase().trim() !== field.Name.toLowerCase().trim()) {
|
|
932
|
-
const sSQL = `UPDATE [${
|
|
1659
|
+
const sSQL = `UPDATE [${mj_core_schema()}].EntityField SET ${EntityInfo.UpdatedAtFieldName}=GETUTCDATE(), DisplayName = '${sDisplayName}' WHERE ID = '${field.ID}'`;
|
|
933
1660
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to update display name for field ${field.Name}`);
|
|
934
1661
|
}
|
|
935
1662
|
}
|
|
936
1663
|
return true;
|
|
937
1664
|
}
|
|
938
1665
|
catch (e) {
|
|
939
|
-
|
|
1666
|
+
logError(e);
|
|
940
1667
|
return false;
|
|
941
1668
|
}
|
|
942
1669
|
}
|
|
@@ -950,12 +1677,12 @@ class ManageMetadataBase {
|
|
|
950
1677
|
*/
|
|
951
1678
|
async setDefaultColumnWidthWhereNeeded(pool, excludeSchemas) {
|
|
952
1679
|
try {
|
|
953
|
-
const sSQL = `EXEC ${
|
|
1680
|
+
const sSQL = `EXEC ${mj_core_schema()}.spSetDefaultColumnWidthWhereNeeded @ExcludedSchemaNames='${excludeSchemas.join(',')}'`;
|
|
954
1681
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to set default column width where needed`, true);
|
|
955
1682
|
return true;
|
|
956
1683
|
}
|
|
957
1684
|
catch (e) {
|
|
958
|
-
|
|
1685
|
+
logError(e);
|
|
959
1686
|
return false;
|
|
960
1687
|
}
|
|
961
1688
|
}
|
|
@@ -977,7 +1704,7 @@ class ManageMetadataBase {
|
|
|
977
1704
|
EntityID,
|
|
978
1705
|
ISNULL(MAX(Sequence), 0) AS MaxSequence
|
|
979
1706
|
FROM
|
|
980
|
-
[${
|
|
1707
|
+
[${mj_core_schema()}].EntityField
|
|
981
1708
|
GROUP BY
|
|
982
1709
|
EntityID
|
|
983
1710
|
),
|
|
@@ -996,9 +1723,9 @@ NumberedRows AS (
|
|
|
996
1723
|
sf.AllowsNull,
|
|
997
1724
|
sf.DefaultValue,
|
|
998
1725
|
sf.AutoIncrement,
|
|
999
|
-
IIF(sf.IsVirtual = 1, 0, IIF(sf.FieldName = '${
|
|
1000
|
-
sf.FieldName = '${
|
|
1001
|
-
sf.FieldName = '${
|
|
1726
|
+
IIF(sf.IsVirtual = 1, 0, IIF(sf.FieldName = '${EntityInfo.CreatedAtFieldName}' OR
|
|
1727
|
+
sf.FieldName = '${EntityInfo.UpdatedAtFieldName}' OR
|
|
1728
|
+
sf.FieldName = '${EntityInfo.DeletedAtFieldName}' OR
|
|
1002
1729
|
pk.ColumnName IS NOT NULL, 0, 1)) AllowUpdateAPI,
|
|
1003
1730
|
sf.IsVirtual,
|
|
1004
1731
|
e.RelationshipDefaultDisplayType,
|
|
@@ -1020,34 +1747,34 @@ NumberedRows AS (
|
|
|
1020
1747
|
END,
|
|
1021
1748
|
ROW_NUMBER() OVER (PARTITION BY sf.EntityID, sf.FieldName ORDER BY (SELECT NULL)) AS rn
|
|
1022
1749
|
FROM
|
|
1023
|
-
[${
|
|
1750
|
+
[${mj_core_schema()}].vwSQLColumnsAndEntityFields sf
|
|
1024
1751
|
LEFT OUTER JOIN
|
|
1025
1752
|
MaxSequences ms
|
|
1026
1753
|
ON
|
|
1027
1754
|
sf.EntityID = ms.EntityID
|
|
1028
1755
|
LEFT OUTER JOIN
|
|
1029
|
-
[${
|
|
1756
|
+
[${mj_core_schema()}].Entity e
|
|
1030
1757
|
ON
|
|
1031
1758
|
sf.EntityID = e.ID
|
|
1032
1759
|
LEFT OUTER JOIN
|
|
1033
|
-
[${
|
|
1760
|
+
[${mj_core_schema()}].vwForeignKeys fk
|
|
1034
1761
|
ON
|
|
1035
1762
|
sf.FieldName = fk.[column] AND
|
|
1036
1763
|
e.BaseTable = fk.[table] AND
|
|
1037
1764
|
e.SchemaName = fk.[schema_name]
|
|
1038
1765
|
LEFT OUTER JOIN
|
|
1039
|
-
[${
|
|
1766
|
+
[${mj_core_schema()}].Entity re -- Related Entity
|
|
1040
1767
|
ON
|
|
1041
1768
|
re.BaseTable = fk.referenced_table AND
|
|
1042
1769
|
re.SchemaName = fk.[referenced_schema]
|
|
1043
1770
|
LEFT OUTER JOIN
|
|
1044
|
-
[${
|
|
1771
|
+
[${mj_core_schema()}].vwTablePrimaryKeys pk
|
|
1045
1772
|
ON
|
|
1046
1773
|
e.BaseTable = pk.TableName AND
|
|
1047
1774
|
sf.FieldName = pk.ColumnName AND
|
|
1048
1775
|
e.SchemaName = pk.SchemaName
|
|
1049
1776
|
LEFT OUTER JOIN
|
|
1050
|
-
[${
|
|
1777
|
+
[${mj_core_schema()}].vwTableUniqueKeys uk
|
|
1051
1778
|
ON
|
|
1052
1779
|
e.BaseTable = uk.TableName AND
|
|
1053
1780
|
sf.FieldName = uk.ColumnName AND
|
|
@@ -1088,22 +1815,22 @@ NumberedRows AS (
|
|
|
1088
1815
|
const isPrimaryKey = n.FieldName?.trim().toLowerCase() === 'id';
|
|
1089
1816
|
const isForeignKey = n.RelatedEntityID && n.RelatedEntityID.length > 0; // Foreign keys have RelatedEntityID set
|
|
1090
1817
|
const isNameField = n.FieldName?.trim().toLowerCase() === 'name' || n.IsNameField;
|
|
1091
|
-
const isEarlySequence = n.Sequence <=
|
|
1818
|
+
const isEarlySequence = n.Sequence <= configInfo.newEntityDefaults?.IncludeFirstNFieldsAsDefaultInView;
|
|
1092
1819
|
const bDefaultInView = (isNameField || isEarlySequence) && !isPrimaryKey && !isForeignKey;
|
|
1093
1820
|
const escapedDescription = n.Description ? `'${n.Description.replace(/'/g, "''")}'` : 'NULL';
|
|
1094
1821
|
let fieldDisplayName = '';
|
|
1095
1822
|
switch (n.FieldName.trim().toLowerCase()) {
|
|
1096
|
-
case
|
|
1823
|
+
case EntityInfo.CreatedAtFieldName.trim().toLowerCase():
|
|
1097
1824
|
fieldDisplayName = "Created At";
|
|
1098
1825
|
break;
|
|
1099
|
-
case
|
|
1826
|
+
case EntityInfo.UpdatedAtFieldName.trim().toLowerCase():
|
|
1100
1827
|
fieldDisplayName = "Updated At";
|
|
1101
1828
|
break;
|
|
1102
|
-
case
|
|
1829
|
+
case EntityInfo.DeletedAtFieldName.trim().toLowerCase():
|
|
1103
1830
|
fieldDisplayName = "Deleted At";
|
|
1104
1831
|
break;
|
|
1105
1832
|
default:
|
|
1106
|
-
fieldDisplayName =
|
|
1833
|
+
fieldDisplayName = convertCamelCaseToHaveSpaces(n.FieldName).trim();
|
|
1107
1834
|
break;
|
|
1108
1835
|
}
|
|
1109
1836
|
const parsedDefaultValue = this.parseDefaultValue(n.DefaultValue);
|
|
@@ -1112,13 +1839,13 @@ NumberedRows AS (
|
|
|
1112
1839
|
// in the above we are setting quotedDefaultValue to NULL if the parsed default value is an empty string or the string 'NULL' (case insensitive)
|
|
1113
1840
|
return `
|
|
1114
1841
|
IF NOT EXISTS (
|
|
1115
|
-
SELECT 1 FROM [${
|
|
1842
|
+
SELECT 1 FROM [${mj_core_schema()}].EntityField
|
|
1116
1843
|
WHERE ID = '${newEntityFieldUUID}' OR
|
|
1117
1844
|
(EntityID = '${n.EntityID}' AND Name = '${n.FieldName}')
|
|
1118
1845
|
-- check to make sure we're not inserting a duplicate entity field metadata record
|
|
1119
1846
|
)
|
|
1120
1847
|
BEGIN
|
|
1121
|
-
INSERT INTO [${
|
|
1848
|
+
INSERT INTO [${mj_core_schema()}].EntityField
|
|
1122
1849
|
(
|
|
1123
1850
|
ID,
|
|
1124
1851
|
EntityID,
|
|
@@ -1219,7 +1946,7 @@ NumberedRows AS (
|
|
|
1219
1946
|
}
|
|
1220
1947
|
catch (e) {
|
|
1221
1948
|
// this is here so we can catch the error for debug. We want the transaction to die
|
|
1222
|
-
|
|
1949
|
+
logError(`Error inserting new entity field. SQL: \n${sSQLInsert}`);
|
|
1223
1950
|
throw e;
|
|
1224
1951
|
}
|
|
1225
1952
|
}
|
|
@@ -1237,7 +1964,7 @@ NumberedRows AS (
|
|
|
1237
1964
|
return true;
|
|
1238
1965
|
}
|
|
1239
1966
|
catch (e) {
|
|
1240
|
-
|
|
1967
|
+
logError(e);
|
|
1241
1968
|
return false;
|
|
1242
1969
|
}
|
|
1243
1970
|
}
|
|
@@ -1250,20 +1977,20 @@ NumberedRows AS (
|
|
|
1250
1977
|
*/
|
|
1251
1978
|
async updateEntityFieldRelatedEntityNameFieldMap(pool, entityFieldID, relatedEntityNameFieldMap) {
|
|
1252
1979
|
try {
|
|
1253
|
-
const sSQL = `EXEC [${
|
|
1980
|
+
const sSQL = `EXEC [${mj_core_schema()}].spUpdateEntityFieldRelatedEntityNameFieldMap
|
|
1254
1981
|
@EntityFieldID='${entityFieldID}',
|
|
1255
1982
|
@RelatedEntityNameFieldMap='${relatedEntityNameFieldMap}'`;
|
|
1256
1983
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to update entity field related entity name field map for entity field ID ${entityFieldID}`);
|
|
1257
1984
|
return true;
|
|
1258
1985
|
}
|
|
1259
1986
|
catch (e) {
|
|
1260
|
-
|
|
1987
|
+
logError(e);
|
|
1261
1988
|
return false;
|
|
1262
1989
|
}
|
|
1263
1990
|
}
|
|
1264
1991
|
async updateExistingEntitiesFromSchema(pool, excludeSchemas) {
|
|
1265
1992
|
try {
|
|
1266
|
-
const sSQL = `EXEC [${
|
|
1993
|
+
const sSQL = `EXEC [${mj_core_schema()}].spUpdateExistingEntitiesFromSchema @ExcludedSchemaNames='${excludeSchemas.join(',')}'`;
|
|
1267
1994
|
const result = await this.LogSQLAndExecute(pool, sSQL, `SQL text to update existing entities from schema`, true);
|
|
1268
1995
|
// result contains the updated entities, and there is a property of each row called Name which has the entity name that was modified
|
|
1269
1996
|
// add these to the modified entity list if they're not already in there
|
|
@@ -1273,7 +2000,7 @@ NumberedRows AS (
|
|
|
1273
2000
|
return true;
|
|
1274
2001
|
}
|
|
1275
2002
|
catch (e) {
|
|
1276
|
-
|
|
2003
|
+
logError(e);
|
|
1277
2004
|
return false;
|
|
1278
2005
|
}
|
|
1279
2006
|
}
|
|
@@ -1288,7 +2015,7 @@ NumberedRows AS (
|
|
|
1288
2015
|
}
|
|
1289
2016
|
async updateExistingEntityFieldsFromSchema(pool, excludeSchemas) {
|
|
1290
2017
|
try {
|
|
1291
|
-
const sSQL = `EXEC [${
|
|
2018
|
+
const sSQL = `EXEC [${mj_core_schema()}].spUpdateExistingEntityFieldsFromSchema @ExcludedSchemaNames='${excludeSchemas.join(',')}'`;
|
|
1292
2019
|
const result = await this.LogSQLAndExecute(pool, sSQL, `SQL text to update existing entity fields from schema`, true);
|
|
1293
2020
|
// result contains the updated entity fields
|
|
1294
2021
|
// there is a field in there called EntityName. Get a distinct list of entity names from this and add them
|
|
@@ -1299,7 +2026,7 @@ NumberedRows AS (
|
|
|
1299
2026
|
return true;
|
|
1300
2027
|
}
|
|
1301
2028
|
catch (e) {
|
|
1302
|
-
|
|
2029
|
+
logError(e);
|
|
1303
2030
|
return false;
|
|
1304
2031
|
}
|
|
1305
2032
|
}
|
|
@@ -1313,21 +2040,21 @@ NumberedRows AS (
|
|
|
1313
2040
|
*/
|
|
1314
2041
|
async updateSchemaInfoFromDatabase(pool, excludeSchemas) {
|
|
1315
2042
|
try {
|
|
1316
|
-
const sSQL = `EXEC [${
|
|
2043
|
+
const sSQL = `EXEC [${mj_core_schema()}].spUpdateSchemaInfoFromDatabase @ExcludedSchemaNames='${excludeSchemas.join(',')}'`;
|
|
1317
2044
|
const result = await this.LogSQLAndExecute(pool, sSQL, `SQL text to sync schema info from database schemas`, true);
|
|
1318
2045
|
if (result && result.length > 0) {
|
|
1319
|
-
|
|
2046
|
+
logStatus(` > Updated/created ${result.length} SchemaInfo records`);
|
|
1320
2047
|
}
|
|
1321
2048
|
return true;
|
|
1322
2049
|
}
|
|
1323
2050
|
catch (e) {
|
|
1324
|
-
|
|
2051
|
+
logError(e);
|
|
1325
2052
|
return false;
|
|
1326
2053
|
}
|
|
1327
2054
|
}
|
|
1328
2055
|
async deleteUnneededEntityFields(pool, excludeSchemas) {
|
|
1329
2056
|
try {
|
|
1330
|
-
const sSQL = `EXEC [${
|
|
2057
|
+
const sSQL = `EXEC [${mj_core_schema()}].spDeleteUnneededEntityFields @ExcludedSchemaNames='${excludeSchemas.join(',')}'`;
|
|
1331
2058
|
const result = await this.LogSQLAndExecute(pool, sSQL, `SQL text to delete unneeded entity fields`, true);
|
|
1332
2059
|
// result contains the DELETED entity fields
|
|
1333
2060
|
// there is a field in there called Entity. Get a distinct list of entity names from this and add them
|
|
@@ -1338,7 +2065,7 @@ NumberedRows AS (
|
|
|
1338
2065
|
return true;
|
|
1339
2066
|
}
|
|
1340
2067
|
catch (e) {
|
|
1341
|
-
|
|
2068
|
+
logError(e);
|
|
1342
2069
|
return false;
|
|
1343
2070
|
}
|
|
1344
2071
|
}
|
|
@@ -1349,13 +2076,13 @@ NumberedRows AS (
|
|
|
1349
2076
|
// 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
|
|
1350
2077
|
// just ignore it.
|
|
1351
2078
|
const filter = excludeSchemas && excludeSchemas.length > 0 ? ` WHERE SchemaName NOT IN (${excludeSchemas.map(s => `'${s}'`).join(',')})` : '';
|
|
1352
|
-
const sSQL = `SELECT * FROM [${
|
|
2079
|
+
const sSQL = `SELECT * FROM [${mj_core_schema()}].vwEntityFieldsWithCheckConstraints${filter}`;
|
|
1353
2080
|
const resultResult = await pool.request().query(sSQL);
|
|
1354
2081
|
const result = resultResult.recordset;
|
|
1355
|
-
const efvSQL = `SELECT * FROM [${
|
|
2082
|
+
const efvSQL = `SELECT * FROM [${mj_core_schema()}].EntityFieldValue`;
|
|
1356
2083
|
const allEntityFieldValuesResult = await pool.request().query(efvSQL);
|
|
1357
2084
|
const allEntityFieldValues = allEntityFieldValuesResult.recordset;
|
|
1358
|
-
const efSQL = `SELECT * FROM [${
|
|
2085
|
+
const efSQL = `SELECT * FROM [${mj_core_schema()}].vwEntityFields ORDER BY EntityID, Sequence`;
|
|
1359
2086
|
const allEntityFieldsResult = await pool.request().query(efSQL);
|
|
1360
2087
|
const allEntityFields = allEntityFieldsResult.recordset;
|
|
1361
2088
|
const generationPromises = [];
|
|
@@ -1377,11 +2104,11 @@ NumberedRows AS (
|
|
|
1377
2104
|
await this.syncEntityFieldValues(pool, r.EntityFieldID, parsedValues, allEntityFieldValues);
|
|
1378
2105
|
// 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.
|
|
1379
2106
|
// check to see if the ValueListType is already set to "List", if not, update it
|
|
1380
|
-
const sSQLCheck = `SELECT ValueListType FROM [${
|
|
2107
|
+
const sSQLCheck = `SELECT ValueListType FROM [${mj_core_schema()}].EntityField WHERE ID='${r.EntityFieldID}'`;
|
|
1381
2108
|
const checkResultResult = await pool.request().query(sSQLCheck);
|
|
1382
2109
|
const checkResult = checkResultResult.recordset;
|
|
1383
2110
|
if (checkResult && checkResult.length > 0 && checkResult[0].ValueListType.trim().toLowerCase() !== 'list') {
|
|
1384
|
-
const sSQL = `UPDATE [${
|
|
2111
|
+
const sSQL = `UPDATE [${mj_core_schema()}].EntityField SET ValueListType='List' WHERE ID='${r.EntityFieldID}'`;
|
|
1385
2112
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to update ValueListType for entity field ID ${r.EntityFieldID}`);
|
|
1386
2113
|
}
|
|
1387
2114
|
}
|
|
@@ -1392,8 +2119,8 @@ NumberedRows AS (
|
|
|
1392
2119
|
else {
|
|
1393
2120
|
// if we get here that means we don't have a simple condition in the check constraint that the RegEx could parse. If Advanced Generation is enabled, we will
|
|
1394
2121
|
// attempt to use an LLM to do things fancier now
|
|
1395
|
-
if (
|
|
1396
|
-
|
|
2122
|
+
if (configInfo.advancedGeneration?.enableAdvancedGeneration &&
|
|
2123
|
+
configInfo.advancedGeneration?.features.find(f => f.name === 'ParseCheckConstraints' && f.enabled)) {
|
|
1397
2124
|
// the user has the feature turned on, let's generate a description of the constraint and then build a Validate function for the constraint
|
|
1398
2125
|
// run this in parallel
|
|
1399
2126
|
generationPromises.push(this.runValidationGeneration(r, allEntityFields, !skipDBUpdate, currentUser));
|
|
@@ -1403,8 +2130,8 @@ NumberedRows AS (
|
|
|
1403
2130
|
}
|
|
1404
2131
|
// now for the table level constraints run the process for advanced generation
|
|
1405
2132
|
for (const r of tableLevelResults) {
|
|
1406
|
-
if (
|
|
1407
|
-
|
|
2133
|
+
if (configInfo.advancedGeneration?.enableAdvancedGeneration &&
|
|
2134
|
+
configInfo.advancedGeneration?.features.find(f => f.name === 'ParseCheckConstraints' && f.enabled)) {
|
|
1408
2135
|
// the user has the feature turned on, let's generate a description of the constraint and then build a Validate function for the constraint
|
|
1409
2136
|
// run this in parallel
|
|
1410
2137
|
generationPromises.push(this.runValidationGeneration(r, allEntityFields, !skipDBUpdate, currentUser));
|
|
@@ -1415,7 +2142,7 @@ NumberedRows AS (
|
|
|
1415
2142
|
return true;
|
|
1416
2143
|
}
|
|
1417
2144
|
catch (e) {
|
|
1418
|
-
|
|
2145
|
+
logError(e);
|
|
1419
2146
|
return false;
|
|
1420
2147
|
}
|
|
1421
2148
|
}
|
|
@@ -1430,7 +2157,7 @@ NumberedRows AS (
|
|
|
1430
2157
|
return await this.manageEntityFieldValuesAndValidatorFunctions(pool, [], currentUser, true);
|
|
1431
2158
|
}
|
|
1432
2159
|
catch (e) {
|
|
1433
|
-
|
|
2160
|
+
logError(e);
|
|
1434
2161
|
return false;
|
|
1435
2162
|
}
|
|
1436
2163
|
}
|
|
@@ -1476,9 +2203,9 @@ NumberedRows AS (
|
|
|
1476
2203
|
}
|
|
1477
2204
|
}
|
|
1478
2205
|
try {
|
|
1479
|
-
if (generateNewCode &&
|
|
2206
|
+
if (generateNewCode && configInfo.advancedGeneration?.enableAdvancedGeneration && configInfo.advancedGeneration?.features.find(f => f.name === 'ParseCheckConstraints' && f.enabled)) {
|
|
1480
2207
|
// feature is enabled, so let's call the AI to generate a function for us
|
|
1481
|
-
const ag = new
|
|
2208
|
+
const ag = new AdvancedGeneration();
|
|
1482
2209
|
const entityFieldListInfo = allEntityFields.filter(item => item.Entity.trim().toLowerCase() === data.EntityName.trim().toLowerCase()).map(item => ` * ${item.Name} - ${item.Type}${item.AllowsNull ? ' (nullable)' : ' (not null)'}`).join('\n');
|
|
1483
2210
|
// Use new API to parse check constraint
|
|
1484
2211
|
const result = await ag.parseCheckConstraint(constraintDefinition, entityFieldListInfo, generatedValidationFunctionName, currentUser);
|
|
@@ -1491,12 +2218,12 @@ NumberedRows AS (
|
|
|
1491
2218
|
returnResult.success = true;
|
|
1492
2219
|
}
|
|
1493
2220
|
else {
|
|
1494
|
-
|
|
2221
|
+
logError(`Error generating field validator function from check constraint for entity ${entityName} and field ${fieldName}. LLM returned invalid result.`);
|
|
1495
2222
|
}
|
|
1496
2223
|
}
|
|
1497
2224
|
}
|
|
1498
2225
|
catch (e) {
|
|
1499
|
-
|
|
2226
|
+
logError(e);
|
|
1500
2227
|
}
|
|
1501
2228
|
finally {
|
|
1502
2229
|
return returnResult;
|
|
@@ -1515,7 +2242,7 @@ NumberedRows AS (
|
|
|
1515
2242
|
for (const ev of existingValues) {
|
|
1516
2243
|
if (!possibleValues.find(v => v === ev.Value)) {
|
|
1517
2244
|
// delete the value from the database
|
|
1518
|
-
const sSQLDelete = `DELETE FROM [${
|
|
2245
|
+
const sSQLDelete = `DELETE FROM [${mj_core_schema()}].EntityFieldValue WHERE ID='${ev.ID}'`;
|
|
1519
2246
|
await this.LogSQLAndExecute(ds, sSQLDelete, `SQL text to delete entity field value ID ${ev.ID}`);
|
|
1520
2247
|
numRemoved++;
|
|
1521
2248
|
}
|
|
@@ -1525,9 +2252,9 @@ NumberedRows AS (
|
|
|
1525
2252
|
for (const v of possibleValues) {
|
|
1526
2253
|
if (!existingValues.find((ev) => ev.Value === v)) {
|
|
1527
2254
|
// Generate a UUID for this new EntityFieldValue record
|
|
1528
|
-
const newId = (
|
|
2255
|
+
const newId = uuidv4();
|
|
1529
2256
|
// add the value to the database with explicit ID
|
|
1530
|
-
const sSQLInsert = `INSERT INTO [${
|
|
2257
|
+
const sSQLInsert = `INSERT INTO [${mj_core_schema()}].EntityFieldValue
|
|
1531
2258
|
(ID, EntityFieldID, Sequence, Value, Code)
|
|
1532
2259
|
VALUES
|
|
1533
2260
|
('${newId}', '${entityFieldID}', ${1 + possibleValues.indexOf(v)}, '${v}', '${v}')`;
|
|
@@ -1541,7 +2268,7 @@ NumberedRows AS (
|
|
|
1541
2268
|
const ev = existingValues.find((ev) => ev.Value === v);
|
|
1542
2269
|
if (ev && ev.Sequence !== 1 + possibleValues.indexOf(v)) {
|
|
1543
2270
|
// update the sequence to match the order in the possible values list, if it doesn't already match
|
|
1544
|
-
const sSQLUpdate = `UPDATE [${
|
|
2271
|
+
const sSQLUpdate = `UPDATE [${mj_core_schema()}].EntityFieldValue SET Sequence=${1 + possibleValues.indexOf(v)} WHERE ID='${ev.ID}'`;
|
|
1545
2272
|
await this.LogSQLAndExecute(ds, sSQLUpdate, `SQL text to update entity field value sequence`);
|
|
1546
2273
|
numUpdated++;
|
|
1547
2274
|
}
|
|
@@ -1555,7 +2282,7 @@ NumberedRows AS (
|
|
|
1555
2282
|
return true;
|
|
1556
2283
|
}
|
|
1557
2284
|
catch (e) {
|
|
1558
|
-
|
|
2285
|
+
logError(e);
|
|
1559
2286
|
return false;
|
|
1560
2287
|
}
|
|
1561
2288
|
}
|
|
@@ -1614,9 +2341,9 @@ NumberedRows AS (
|
|
|
1614
2341
|
createExcludeTablesAndSchemasFilter(fieldPrefix) {
|
|
1615
2342
|
let sExcludeTables = '';
|
|
1616
2343
|
let sExcludeSchemas = '';
|
|
1617
|
-
if (
|
|
1618
|
-
for (let i = 0; i <
|
|
1619
|
-
const t =
|
|
2344
|
+
if (configInfo.excludeTables) {
|
|
2345
|
+
for (let i = 0; i < configInfo.excludeTables.length; ++i) {
|
|
2346
|
+
const t = configInfo.excludeTables[i];
|
|
1620
2347
|
sExcludeTables += (sExcludeTables.length > 0 ? ' AND ' : '') +
|
|
1621
2348
|
(t.schema.indexOf('%') > -1 ? ` NOT ( ${fieldPrefix}SchemaName LIKE '${t.schema}'` :
|
|
1622
2349
|
` NOT ( ${fieldPrefix}SchemaName = '${t.schema}'`);
|
|
@@ -1624,9 +2351,9 @@ NumberedRows AS (
|
|
|
1624
2351
|
` AND ${fieldPrefix}TableName = '${t.table}') `);
|
|
1625
2352
|
}
|
|
1626
2353
|
}
|
|
1627
|
-
if (
|
|
1628
|
-
for (let i = 0; i <
|
|
1629
|
-
const s =
|
|
2354
|
+
if (configInfo.excludeSchemas) {
|
|
2355
|
+
for (let i = 0; i < configInfo.excludeSchemas.length; ++i) {
|
|
2356
|
+
const s = configInfo.excludeSchemas[i];
|
|
1630
2357
|
sExcludeSchemas += (sExcludeSchemas.length > 0 ? ' AND ' : '') +
|
|
1631
2358
|
(s.indexOf('%') > -1 ? `${fieldPrefix}SchemaName NOT LIKE '${s}'` : `${fieldPrefix}SchemaName <> '${s}'`);
|
|
1632
2359
|
}
|
|
@@ -1638,11 +2365,11 @@ NumberedRows AS (
|
|
|
1638
2365
|
}
|
|
1639
2366
|
async createNewEntities(pool, currentUser) {
|
|
1640
2367
|
try {
|
|
1641
|
-
const sSQL = `SELECT * FROM [${
|
|
2368
|
+
const sSQL = `SELECT * FROM [${mj_core_schema()}].vwSQLTablesAndEntities WHERE EntityID IS NULL ` + this.createExcludeTablesAndSchemasFilter('');
|
|
1642
2369
|
const newEntitiesResult = await pool.request().query(sSQL);
|
|
1643
2370
|
const newEntities = newEntitiesResult.recordset;
|
|
1644
2371
|
if (newEntities && newEntities.length > 0) {
|
|
1645
|
-
const md = new
|
|
2372
|
+
const md = new Metadata();
|
|
1646
2373
|
const transaction = new sql.Transaction(pool);
|
|
1647
2374
|
await transaction.begin();
|
|
1648
2375
|
try {
|
|
@@ -1659,14 +2386,14 @@ NumberedRows AS (
|
|
|
1659
2386
|
}
|
|
1660
2387
|
if (ManageMetadataBase.newEntityList.length > 0) {
|
|
1661
2388
|
// only do this if we actually created new entities
|
|
1662
|
-
|
|
2389
|
+
LogStatus(` Done creating entities, refreshing metadata to reflect new entities...`);
|
|
1663
2390
|
await md.Refresh(); // refresh now since we've added some new entities
|
|
1664
2391
|
}
|
|
1665
2392
|
}
|
|
1666
2393
|
return true; // if we get here, we succeeded
|
|
1667
2394
|
}
|
|
1668
2395
|
catch (e) {
|
|
1669
|
-
|
|
2396
|
+
LogError(e);
|
|
1670
2397
|
return false;
|
|
1671
2398
|
}
|
|
1672
2399
|
}
|
|
@@ -1675,14 +2402,14 @@ NumberedRows AS (
|
|
|
1675
2402
|
// criteria:
|
|
1676
2403
|
// 1) entity has a field that is a primary key
|
|
1677
2404
|
// validate all of these factors by getting the sql from SQL Server and check the result, if failure, shouldCreate=false and generate validation message, otherwise return empty validation message and true for shouldCreate.
|
|
1678
|
-
const query = `EXEC ${
|
|
2405
|
+
const query = `EXEC ${Metadata.Provider.ConfigData.MJCoreSchemaName}.spGetPrimaryKeyForTable @TableName='${newEntity.TableName}', @SchemaName='${newEntity.SchemaName}'`;
|
|
1679
2406
|
try {
|
|
1680
2407
|
const resultResult = await ds.request().query(query);
|
|
1681
2408
|
const result = resultResult.recordset;
|
|
1682
2409
|
if (result.length === 0) {
|
|
1683
2410
|
// No database PK constraint found - check if there's a soft PK defined in config
|
|
1684
2411
|
if (this.hasSoftPrimaryKeyInConfig(newEntity.SchemaName, newEntity.TableName)) {
|
|
1685
|
-
|
|
2412
|
+
logStatus(` ✓ No database PK for ${newEntity.SchemaName}.${newEntity.TableName}, but soft PK found in config - allowing entity creation`);
|
|
1686
2413
|
return { shouldCreate: true, validationMessage: '' };
|
|
1687
2414
|
}
|
|
1688
2415
|
return { shouldCreate: false, validationMessage: "No primary key found" };
|
|
@@ -1700,35 +2427,35 @@ NumberedRows AS (
|
|
|
1700
2427
|
*/
|
|
1701
2428
|
hasSoftPrimaryKeyInConfig(schemaName, tableName) {
|
|
1702
2429
|
// Check if additionalSchemaInfo is configured
|
|
1703
|
-
if (!
|
|
2430
|
+
if (!configInfo.additionalSchemaInfo) {
|
|
1704
2431
|
return false;
|
|
1705
2432
|
}
|
|
1706
|
-
const configPath =
|
|
2433
|
+
const configPath = path.join(currentWorkingDirectory, configInfo.additionalSchemaInfo);
|
|
1707
2434
|
if (!fs.existsSync(configPath)) {
|
|
1708
|
-
|
|
2435
|
+
logStatus(` [Soft PK Check] Config file not found at: ${configPath}`);
|
|
1709
2436
|
return false;
|
|
1710
2437
|
}
|
|
1711
2438
|
try {
|
|
1712
2439
|
const config = ManageMetadataBase.getSoftPKFKConfig();
|
|
1713
2440
|
if (!config || !config.tables) {
|
|
1714
|
-
|
|
2441
|
+
logStatus(` [Soft PK Check] Config file found but no tables array`);
|
|
1715
2442
|
return false;
|
|
1716
2443
|
}
|
|
1717
2444
|
const tableConfig = config.tables.find((t) => t.schemaName?.toLowerCase() === schemaName?.toLowerCase() &&
|
|
1718
2445
|
t.tableName?.toLowerCase() === tableName?.toLowerCase());
|
|
1719
2446
|
const found = Boolean(tableConfig?.primaryKeys && tableConfig.primaryKeys.length > 0);
|
|
1720
2447
|
if (!found) {
|
|
1721
|
-
|
|
2448
|
+
logStatus(` [Soft PK Check] No config found for ${schemaName}.${tableName} (config has ${config.tables.length} tables)`);
|
|
1722
2449
|
}
|
|
1723
2450
|
return found;
|
|
1724
2451
|
}
|
|
1725
2452
|
catch (e) {
|
|
1726
|
-
|
|
2453
|
+
logStatus(` [Soft PK Check] Error reading config: ${e}`);
|
|
1727
2454
|
return false;
|
|
1728
2455
|
}
|
|
1729
2456
|
}
|
|
1730
2457
|
async createNewEntityName(newEntity, currentUser) {
|
|
1731
|
-
const ag = new
|
|
2458
|
+
const ag = new AdvancedGeneration();
|
|
1732
2459
|
if (ag.featureEnabled('EntityNames')) {
|
|
1733
2460
|
return this.newEntityNameWithAdvancedGeneration(ag, newEntity, currentUser);
|
|
1734
2461
|
}
|
|
@@ -1768,8 +2495,8 @@ NumberedRows AS (
|
|
|
1768
2495
|
}
|
|
1769
2496
|
}
|
|
1770
2497
|
simpleNewEntityName(schemaName, tableName) {
|
|
1771
|
-
const convertedTableName =
|
|
1772
|
-
const pluralName =
|
|
2498
|
+
const convertedTableName = convertCamelCaseToHaveSpaces(tableName);
|
|
2499
|
+
const pluralName = generatePluralName(convertedTableName, { capitalizeFirstLetterOnly: true });
|
|
1773
2500
|
return this.markupEntityName(schemaName, pluralName);
|
|
1774
2501
|
}
|
|
1775
2502
|
/**
|
|
@@ -1789,18 +2516,18 @@ NumberedRows AS (
|
|
|
1789
2516
|
}
|
|
1790
2517
|
}
|
|
1791
2518
|
getNewEntityNameRule(schemaName) {
|
|
1792
|
-
const rule =
|
|
2519
|
+
const rule = configInfo.newEntityDefaults?.NameRulesBySchema?.find(r => {
|
|
1793
2520
|
let schemaNameToUse = r.SchemaName;
|
|
1794
2521
|
if (schemaNameToUse?.trim().toLowerCase() === '${mj_core_schema}') {
|
|
1795
2522
|
// markup for this is to be replaced with the mj_core_schema() config
|
|
1796
|
-
schemaNameToUse =
|
|
2523
|
+
schemaNameToUse = mj_core_schema();
|
|
1797
2524
|
}
|
|
1798
2525
|
return schemaNameToUse.trim().toLowerCase() === schemaName.trim().toLowerCase();
|
|
1799
2526
|
});
|
|
1800
2527
|
return rule;
|
|
1801
2528
|
}
|
|
1802
2529
|
createNewUUID() {
|
|
1803
|
-
return (
|
|
2530
|
+
return uuidv4();
|
|
1804
2531
|
}
|
|
1805
2532
|
async createNewEntity(pool, newEntity, md, currentUser) {
|
|
1806
2533
|
try {
|
|
@@ -1816,7 +2543,7 @@ NumberedRows AS (
|
|
|
1816
2543
|
// the generated name is already in place, so we need another name
|
|
1817
2544
|
suffix = '__' + newEntity.SchemaName;
|
|
1818
2545
|
newEntityName = newEntityName + suffix;
|
|
1819
|
-
|
|
2546
|
+
LogError(` >>>> WARNING: Entity name already exists, so using ${newEntityName} instead. If you did not intend for this, please rename the ${newEntity.SchemaName}.${newEntity.TableName} table in the database.`);
|
|
1820
2547
|
}
|
|
1821
2548
|
const isNewSchema = await this.isSchemaNew(pool, newEntity.SchemaName);
|
|
1822
2549
|
const newEntityID = this.createNewUUID();
|
|
@@ -1828,7 +2555,7 @@ NumberedRows AS (
|
|
|
1828
2555
|
// next, check if this entity is in a schema that is new (e.g. no other entities have been added to this schema yet), if so and if
|
|
1829
2556
|
// our config option is set to create new applications from new schemas, then create a new application for this schema
|
|
1830
2557
|
let apps;
|
|
1831
|
-
if (isNewSchema &&
|
|
2558
|
+
if (isNewSchema && configInfo.newSchemaDefaults.CreateNewApplicationWithSchemaName) {
|
|
1832
2559
|
// new schema and config option is to create a new application from the schema name so do that
|
|
1833
2560
|
// check to see if the app already exists
|
|
1834
2561
|
apps = await this.getApplicationIDForSchema(pool, newEntity.SchemaName);
|
|
@@ -1840,7 +2567,7 @@ NumberedRows AS (
|
|
|
1840
2567
|
apps = [newAppID];
|
|
1841
2568
|
}
|
|
1842
2569
|
else {
|
|
1843
|
-
|
|
2570
|
+
LogError(` >>>> ERROR: Unable to create new application for schema ${newEntity.SchemaName}`);
|
|
1844
2571
|
}
|
|
1845
2572
|
await md.Refresh(); // refresh now since we've added a new application, not super efficient to do this for each new application but that won't happen super
|
|
1846
2573
|
// often so not a huge deal, would be more efficient do this in batch after all new apps are created but that would be an over optimization IMO
|
|
@@ -1851,12 +2578,12 @@ NumberedRows AS (
|
|
|
1851
2578
|
apps = await this.getApplicationIDForSchema(pool, newEntity.SchemaName);
|
|
1852
2579
|
}
|
|
1853
2580
|
if (apps && apps.length > 0) {
|
|
1854
|
-
if (
|
|
2581
|
+
if (configInfo.newEntityDefaults.AddToApplicationWithSchemaName) {
|
|
1855
2582
|
// only do this if the configuration setting is set to add new entities to applications for schema names
|
|
1856
2583
|
for (const appUUID of apps) {
|
|
1857
|
-
const sSQLInsertApplicationEntity = `INSERT INTO ${
|
|
2584
|
+
const sSQLInsertApplicationEntity = `INSERT INTO ${mj_core_schema()}.ApplicationEntity
|
|
1858
2585
|
(ApplicationID, EntityID, Sequence) VALUES
|
|
1859
|
-
('${appUUID}', '${newEntityID}', (SELECT ISNULL(MAX(Sequence),0)+1 FROM ${
|
|
2586
|
+
('${appUUID}', '${newEntityID}', (SELECT ISNULL(MAX(Sequence),0)+1 FROM ${mj_core_schema()}.ApplicationEntity WHERE ApplicationID = '${appUUID}'))`;
|
|
1860
2587
|
await this.LogSQLAndExecute(pool, sSQLInsertApplicationEntity, `SQL generated to add new entity ${newEntityName} to application ID: '${appUUID}'`);
|
|
1861
2588
|
}
|
|
1862
2589
|
}
|
|
@@ -1866,38 +2593,38 @@ NumberedRows AS (
|
|
|
1866
2593
|
}
|
|
1867
2594
|
else {
|
|
1868
2595
|
// this is an error condition, we should have an application for this schema, if we don't, log an error, non fatal, but should be logged
|
|
1869
|
-
|
|
2596
|
+
LogError(` >>>> ERROR: Unable to add new entity ${newEntityName} to an application because no Application has SchemaAutoAddNewEntities='${newEntity.SchemaName}'. To fix this, update an existing Application record or create a new one with SchemaAutoAddNewEntities='${newEntity.SchemaName}'.`);
|
|
1870
2597
|
}
|
|
1871
2598
|
// next up, we need to check if we're configured to add permissions for new entities, and if so, add them
|
|
1872
|
-
if (
|
|
2599
|
+
if (configInfo.newEntityDefaults.PermissionDefaults && configInfo.newEntityDefaults.PermissionDefaults.AutoAddPermissionsForNewEntities) {
|
|
1873
2600
|
// we are asked to add permissions for new entities, so do that by looping through the permissions and adding them
|
|
1874
|
-
const permissions =
|
|
2601
|
+
const permissions = configInfo.newEntityDefaults.PermissionDefaults.Permissions;
|
|
1875
2602
|
for (const p of permissions) {
|
|
1876
2603
|
const RoleID = md.Roles.find(r => r.Name.trim().toLowerCase() === p.RoleName.trim().toLowerCase())?.ID;
|
|
1877
2604
|
if (RoleID) {
|
|
1878
|
-
const sSQLInsertPermission = `INSERT INTO ${
|
|
2605
|
+
const sSQLInsertPermission = `INSERT INTO ${mj_core_schema()}.EntityPermission
|
|
1879
2606
|
(EntityID, RoleID, CanRead, CanCreate, CanUpdate, CanDelete) VALUES
|
|
1880
2607
|
('${newEntityID}', '${RoleID}', ${p.CanRead ? 1 : 0}, ${p.CanCreate ? 1 : 0}, ${p.CanUpdate ? 1 : 0}, ${p.CanDelete ? 1 : 0})`;
|
|
1881
2608
|
await this.LogSQLAndExecute(pool, sSQLInsertPermission, `SQL generated to add new permission for entity ${newEntityName} for role ${p.RoleName}`);
|
|
1882
2609
|
}
|
|
1883
2610
|
else
|
|
1884
|
-
|
|
2611
|
+
LogError(` >>>> ERROR: Unable to find Role ID for role ${p.RoleName} to add permissions for new entity ${newEntityName}`);
|
|
1885
2612
|
}
|
|
1886
2613
|
}
|
|
1887
|
-
|
|
2614
|
+
LogStatus(` Created new entity ${newEntityName} for table ${newEntity.SchemaName}.${newEntity.TableName}`);
|
|
1888
2615
|
}
|
|
1889
2616
|
else {
|
|
1890
|
-
|
|
2617
|
+
LogStatus(` Skipping new entity ${newEntity.TableName} because it doesn't qualify to be created. Reason: ${validationMessage}`);
|
|
1891
2618
|
return;
|
|
1892
2619
|
}
|
|
1893
2620
|
}
|
|
1894
2621
|
catch (e) {
|
|
1895
|
-
|
|
2622
|
+
LogError(`Failed to create new entity ${newEntity?.TableName}`);
|
|
1896
2623
|
}
|
|
1897
2624
|
}
|
|
1898
2625
|
async isSchemaNew(pool, schemaName) {
|
|
1899
2626
|
// check to see if there are any entities in the db with this schema name
|
|
1900
|
-
const sSQL = `SELECT COUNT(*) AS Count FROM [${
|
|
2627
|
+
const sSQL = `SELECT COUNT(*) AS Count FROM [${mj_core_schema()}].Entity WHERE SchemaName = '${schemaName}'`;
|
|
1901
2628
|
const resultResult = await pool.request().query(sSQL);
|
|
1902
2629
|
const result = resultResult.recordset;
|
|
1903
2630
|
return result && result.length > 0 ? result[0].Count === 0 : true;
|
|
@@ -1925,26 +2652,26 @@ NumberedRows AS (
|
|
|
1925
2652
|
.replace(/[^a-z0-9-]/g, '') // remove special chars
|
|
1926
2653
|
.replace(/-+/g, '-') // collapse multiple hyphens
|
|
1927
2654
|
.replace(/^-|-$/g, ''); // trim hyphens from start/end
|
|
1928
|
-
const sSQL = `INSERT INTO [${
|
|
2655
|
+
const sSQL = `INSERT INTO [${mj_core_schema()}].Application (ID, Name, Description, SchemaAutoAddNewEntities, Path, AutoUpdatePath)
|
|
1929
2656
|
VALUES ('${appID}', '${appName}', 'Generated for schema', '${schemaName}', '${path}', 1)`;
|
|
1930
2657
|
await this.LogSQLAndExecute(pool, sSQL, `SQL generated to create new application ${appName}`);
|
|
1931
|
-
|
|
2658
|
+
LogStatus(`Created new application ${appName} with Path: ${path}`);
|
|
1932
2659
|
return appID;
|
|
1933
2660
|
}
|
|
1934
2661
|
catch (e) {
|
|
1935
|
-
|
|
2662
|
+
LogError(`Failed to create new application ${appName} for schema ${schemaName}`, null, e);
|
|
1936
2663
|
return null;
|
|
1937
2664
|
}
|
|
1938
2665
|
}
|
|
1939
2666
|
async applicationExists(pool, applicationName) {
|
|
1940
|
-
const sSQL = `SELECT ID FROM [${
|
|
2667
|
+
const sSQL = `SELECT ID FROM [${mj_core_schema()}].Application WHERE Name = '${applicationName}'`;
|
|
1941
2668
|
const resultResult = await pool.request().query(sSQL);
|
|
1942
2669
|
const result = resultResult.recordset;
|
|
1943
2670
|
return result && result.length > 0 ? result[0].ID.length > 0 : false;
|
|
1944
2671
|
}
|
|
1945
2672
|
async getApplicationIDForSchema(pool, schemaName) {
|
|
1946
2673
|
// get all the apps each time from DB as we might be adding, don't use Metadata here for that reason
|
|
1947
|
-
const sSQL = `SELECT ID, Name, SchemaAutoAddNewEntities FROM [${
|
|
2674
|
+
const sSQL = `SELECT ID, Name, SchemaAutoAddNewEntities FROM [${mj_core_schema()}].vwApplications`;
|
|
1948
2675
|
const resultResult = await pool.request().query(sSQL);
|
|
1949
2676
|
const result = resultResult.recordset;
|
|
1950
2677
|
if (!result || result.length === 0) {
|
|
@@ -1963,11 +2690,68 @@ NumberedRows AS (
|
|
|
1963
2690
|
return apps.map((a) => a.ID);
|
|
1964
2691
|
}
|
|
1965
2692
|
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Adds a newly created entity to the application(s) that match its schema name.
|
|
2695
|
+
* If no application exists for the schema and config allows it, creates one.
|
|
2696
|
+
* Shared by both table-backed entity creation and virtual entity creation.
|
|
2697
|
+
*/
|
|
2698
|
+
async addEntityToApplicationForSchema(pool, entityId, entityName, schemaName, currentUser) {
|
|
2699
|
+
let apps = await this.getApplicationIDForSchema(pool, schemaName);
|
|
2700
|
+
// If no app exists and config says to create one for new schemas, create it
|
|
2701
|
+
if ((!apps || apps.length === 0) && configInfo.newSchemaDefaults.CreateNewApplicationWithSchemaName) {
|
|
2702
|
+
const appUUID = this.createNewUUID();
|
|
2703
|
+
const newAppID = await this.createNewApplication(pool, appUUID, schemaName, schemaName, currentUser);
|
|
2704
|
+
if (newAppID) {
|
|
2705
|
+
apps = [newAppID];
|
|
2706
|
+
const md = new Metadata();
|
|
2707
|
+
await md.Refresh();
|
|
2708
|
+
}
|
|
2709
|
+
else {
|
|
2710
|
+
LogError(` >>>> ERROR: Unable to create new application for schema ${schemaName}`);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
if (apps && apps.length > 0) {
|
|
2714
|
+
if (configInfo.newEntityDefaults.AddToApplicationWithSchemaName) {
|
|
2715
|
+
for (const appUUID of apps) {
|
|
2716
|
+
const sSQLInsert = `INSERT INTO ${mj_core_schema()}.ApplicationEntity
|
|
2717
|
+
(ApplicationID, EntityID, Sequence) VALUES
|
|
2718
|
+
('${appUUID}', '${entityId}', (SELECT ISNULL(MAX(Sequence),0)+1 FROM ${mj_core_schema()}.ApplicationEntity WHERE ApplicationID = '${appUUID}'))`;
|
|
2719
|
+
await this.LogSQLAndExecute(pool, sSQLInsert, `SQL generated to add entity ${entityName} to application ID: '${appUUID}'`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
else {
|
|
2724
|
+
LogError(` >>>> WARNING: No application found for schema ${schemaName} to add entity ${entityName}`);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
/**
|
|
2728
|
+
* Adds default permissions for a newly created entity based on config settings.
|
|
2729
|
+
* Shared by both table-backed entity creation and virtual entity creation.
|
|
2730
|
+
*/
|
|
2731
|
+
async addDefaultPermissionsForEntity(pool, entityId, entityName) {
|
|
2732
|
+
if (!configInfo.newEntityDefaults.PermissionDefaults?.AutoAddPermissionsForNewEntities) {
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
const md = new Metadata();
|
|
2736
|
+
const permissions = configInfo.newEntityDefaults.PermissionDefaults.Permissions;
|
|
2737
|
+
for (const p of permissions) {
|
|
2738
|
+
const RoleID = md.Roles.find(r => r.Name.trim().toLowerCase() === p.RoleName.trim().toLowerCase())?.ID;
|
|
2739
|
+
if (RoleID) {
|
|
2740
|
+
const sSQLInsert = `INSERT INTO ${mj_core_schema()}.EntityPermission
|
|
2741
|
+
(EntityID, RoleID, CanRead, CanCreate, CanUpdate, CanDelete) VALUES
|
|
2742
|
+
('${entityId}', '${RoleID}', ${p.CanRead ? 1 : 0}, ${p.CanCreate ? 1 : 0}, ${p.CanUpdate ? 1 : 0}, ${p.CanDelete ? 1 : 0})`;
|
|
2743
|
+
await this.LogSQLAndExecute(pool, sSQLInsert, `SQL generated to add permission for entity ${entityName} for role ${p.RoleName}`);
|
|
2744
|
+
}
|
|
2745
|
+
else {
|
|
2746
|
+
LogError(` >>>> ERROR: Unable to find Role ID for role ${p.RoleName} to add permissions for entity ${entityName}`);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
1966
2750
|
createNewEntityInsertSQL(newEntityUUID, newEntityName, newEntity, newEntitySuffix, newEntityDisplayName) {
|
|
1967
|
-
const newEntityDefaults =
|
|
2751
|
+
const newEntityDefaults = configInfo.newEntityDefaults;
|
|
1968
2752
|
const newEntityDescriptionEscaped = newEntity.Description ? `'${newEntity.Description.replace(/'/g, "''")}` : null;
|
|
1969
2753
|
const sSQLInsert = `
|
|
1970
|
-
INSERT INTO [${
|
|
2754
|
+
INSERT INTO [${mj_core_schema()}].Entity (
|
|
1971
2755
|
ID,
|
|
1972
2756
|
Name,
|
|
1973
2757
|
DisplayName,
|
|
@@ -1994,7 +2778,7 @@ NumberedRows AS (
|
|
|
1994
2778
|
${newEntityDescriptionEscaped ? newEntityDescriptionEscaped : 'NULL' /*if no description, then null*/},
|
|
1995
2779
|
${newEntitySuffix && newEntitySuffix.length > 0 ? `'${newEntitySuffix}'` : 'NULL'},
|
|
1996
2780
|
'${newEntity.TableName}',
|
|
1997
|
-
'vw${
|
|
2781
|
+
'vw${generatePluralName(newEntity.TableName, { capitalizeFirstLetterOnly: true }) + (newEntitySuffix && newEntitySuffix.length > 0 ? newEntitySuffix : '')}',
|
|
1998
2782
|
'${newEntity.SchemaName}',
|
|
1999
2783
|
1,
|
|
2000
2784
|
${newEntityDefaults.AllowUserSearchAPI === undefined ? 1 : newEntityDefaults.AllowUserSearchAPI ? 1 : 0}
|
|
@@ -2015,7 +2799,7 @@ NumberedRows AS (
|
|
|
2015
2799
|
*/
|
|
2016
2800
|
async applyAdvancedGeneration(pool, excludeSchemas, currentUser) {
|
|
2017
2801
|
try {
|
|
2018
|
-
const ag = new
|
|
2802
|
+
const ag = new AdvancedGeneration();
|
|
2019
2803
|
if (!ag.enabled) {
|
|
2020
2804
|
return true;
|
|
2021
2805
|
}
|
|
@@ -2024,13 +2808,13 @@ NumberedRows AS (
|
|
|
2024
2808
|
// Otherwise, only process new or modified entities
|
|
2025
2809
|
let entitiesToProcess = [];
|
|
2026
2810
|
let whereClause = '';
|
|
2027
|
-
if (
|
|
2811
|
+
if (configInfo.forceRegeneration?.enabled) {
|
|
2028
2812
|
// Force regeneration mode - process all entities (or filtered by entityWhereClause)
|
|
2029
|
-
|
|
2813
|
+
logStatus(` Force regeneration enabled - processing all entities...`);
|
|
2030
2814
|
whereClause = 'e.VirtualEntity = 0';
|
|
2031
|
-
if (
|
|
2032
|
-
whereClause += ` AND (${
|
|
2033
|
-
|
|
2815
|
+
if (configInfo.forceRegeneration.entityWhereClause && configInfo.forceRegeneration.entityWhereClause.trim().length > 0) {
|
|
2816
|
+
whereClause += ` AND (${configInfo.forceRegeneration.entityWhereClause})`;
|
|
2817
|
+
logStatus(` Filtered by: ${configInfo.forceRegeneration.entityWhereClause}`);
|
|
2034
2818
|
}
|
|
2035
2819
|
whereClause += ` AND e.SchemaName NOT IN (${excludeSchemas.map(s => `'${s}'`).join(',')})`;
|
|
2036
2820
|
}
|
|
@@ -2046,7 +2830,7 @@ NumberedRows AS (
|
|
|
2046
2830
|
if (entitiesToProcess.length === 0) {
|
|
2047
2831
|
return true;
|
|
2048
2832
|
}
|
|
2049
|
-
|
|
2833
|
+
logStatus(` Advanced Generation enabled, processing ${entitiesToProcess.length} entities...`);
|
|
2050
2834
|
whereClause = `e.VirtualEntity = 0 AND e.Name IN (${entitiesToProcess.map(name => `'${name}'`).join(',')}) AND e.SchemaName NOT IN (${excludeSchemas.map(s => `'${s}'`).join(',')})`;
|
|
2051
2835
|
}
|
|
2052
2836
|
// Get entity details for entities that need processing
|
|
@@ -2056,9 +2840,10 @@ NumberedRows AS (
|
|
|
2056
2840
|
e.Name,
|
|
2057
2841
|
e.Description,
|
|
2058
2842
|
e.SchemaName,
|
|
2059
|
-
e.BaseTable
|
|
2843
|
+
e.BaseTable,
|
|
2844
|
+
e.ParentID
|
|
2060
2845
|
FROM
|
|
2061
|
-
[${
|
|
2846
|
+
[${mj_core_schema()}].vwEntities e
|
|
2062
2847
|
WHERE
|
|
2063
2848
|
${whereClause}
|
|
2064
2849
|
ORDER BY
|
|
@@ -2089,9 +2874,11 @@ NumberedRows AS (
|
|
|
2089
2874
|
ef.AutoUpdateCategory,
|
|
2090
2875
|
ef.AutoUpdateDisplayName,
|
|
2091
2876
|
ef.EntityIDFieldName,
|
|
2092
|
-
ef.RelatedEntity
|
|
2877
|
+
ef.RelatedEntity,
|
|
2878
|
+
ef.IsVirtual,
|
|
2879
|
+
ef.AllowUpdateAPI
|
|
2093
2880
|
FROM
|
|
2094
|
-
[${
|
|
2881
|
+
[${mj_core_schema()}].vwEntityFields ef
|
|
2095
2882
|
WHERE
|
|
2096
2883
|
ef.EntityID IN (${entityIds})
|
|
2097
2884
|
ORDER BY
|
|
@@ -2107,7 +2894,7 @@ NumberedRows AS (
|
|
|
2107
2894
|
es.Name,
|
|
2108
2895
|
es.Value
|
|
2109
2896
|
FROM
|
|
2110
|
-
[${
|
|
2897
|
+
[${mj_core_schema()}].EntitySetting es
|
|
2111
2898
|
WHERE
|
|
2112
2899
|
es.EntityID IN (${entityIds})
|
|
2113
2900
|
AND es.Name IN ('FieldCategoryIcons', 'FieldCategoryInfo')
|
|
@@ -2130,7 +2917,7 @@ NumberedRows AS (
|
|
|
2130
2917
|
return await this.processEntitiesBatched(pool, entities, allFields, ag, currentUser);
|
|
2131
2918
|
}
|
|
2132
2919
|
catch (error) {
|
|
2133
|
-
|
|
2920
|
+
logError(`Advanced Generation failed: ${error}`);
|
|
2134
2921
|
return false;
|
|
2135
2922
|
}
|
|
2136
2923
|
}
|
|
@@ -2158,12 +2945,12 @@ NumberedRows AS (
|
|
|
2158
2945
|
}
|
|
2159
2946
|
else {
|
|
2160
2947
|
errorCount++;
|
|
2161
|
-
|
|
2948
|
+
logError(` Error processing entity: ${result.reason}`);
|
|
2162
2949
|
}
|
|
2163
2950
|
}
|
|
2164
|
-
|
|
2951
|
+
logStatus(` Progress: ${processedCount}/${entities.length} entities processed`);
|
|
2165
2952
|
}
|
|
2166
|
-
|
|
2953
|
+
logStatus(` Advanced Generation complete: ${processedCount} entities processed, ${errorCount} errors`);
|
|
2167
2954
|
return errorCount === 0;
|
|
2168
2955
|
}
|
|
2169
2956
|
/**
|
|
@@ -2194,18 +2981,82 @@ NumberedRows AS (
|
|
|
2194
2981
|
// Form Layout Generation
|
|
2195
2982
|
// Only run if at least one field allows auto-update
|
|
2196
2983
|
if (fields.some((f) => f.AutoUpdateCategory)) {
|
|
2984
|
+
// Build IS-A parent chain context if this entity has a parent
|
|
2985
|
+
const parentChainContext = this.buildParentChainContext(entity, fields);
|
|
2197
2986
|
const layoutAnalysis = await ag.generateFormLayout({
|
|
2198
2987
|
Name: entity.Name,
|
|
2199
2988
|
Description: entity.Description,
|
|
2200
2989
|
SchemaName: entity.SchemaName,
|
|
2201
2990
|
Settings: entity.Settings,
|
|
2202
|
-
Fields: fields
|
|
2991
|
+
Fields: fields,
|
|
2992
|
+
...parentChainContext
|
|
2203
2993
|
}, currentUser, isNewEntity);
|
|
2204
2994
|
if (layoutAnalysis) {
|
|
2205
2995
|
await this.applyFormLayout(pool, entity.ID, fields, layoutAnalysis, isNewEntity);
|
|
2206
|
-
|
|
2996
|
+
logStatus(` Applied form layout for ${entity.Name}`);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
/**
|
|
3001
|
+
* Builds IS-A parent chain context for an entity, computing which parent each
|
|
3002
|
+
* inherited field originates from. Used to provide the LLM with inheritance
|
|
3003
|
+
* awareness during form layout generation.
|
|
3004
|
+
*
|
|
3005
|
+
* Returns an empty object for entities without parents, so it can be safely spread
|
|
3006
|
+
* into the entity object passed to generateFormLayout().
|
|
3007
|
+
*/
|
|
3008
|
+
buildParentChainContext(entity, fields) {
|
|
3009
|
+
if (!entity.ParentID) {
|
|
3010
|
+
return {};
|
|
3011
|
+
}
|
|
3012
|
+
// Walk the IS-A chain using in-memory metadata
|
|
3013
|
+
const md = new Metadata();
|
|
3014
|
+
const allEntities = md.Entities;
|
|
3015
|
+
const parentChain = [];
|
|
3016
|
+
const visited = new Set();
|
|
3017
|
+
let currentParentID = entity.ParentID;
|
|
3018
|
+
while (currentParentID) {
|
|
3019
|
+
if (visited.has(currentParentID))
|
|
3020
|
+
break; // circular reference guard
|
|
3021
|
+
visited.add(currentParentID);
|
|
3022
|
+
const parentEntity = allEntities.find(e => e.ID === currentParentID);
|
|
3023
|
+
if (!parentEntity)
|
|
3024
|
+
break;
|
|
3025
|
+
parentChain.push({ entityID: parentEntity.ID, entityName: parentEntity.Name });
|
|
3026
|
+
currentParentID = parentEntity.ParentID ?? null;
|
|
3027
|
+
}
|
|
3028
|
+
if (parentChain.length === 0) {
|
|
3029
|
+
return {};
|
|
3030
|
+
}
|
|
3031
|
+
// Annotate each field with its source parent (if inherited)
|
|
3032
|
+
// An IS-A inherited field is: IsVirtual=true, AllowUpdateAPI=true, not PK, not __mj_
|
|
3033
|
+
for (const field of fields) {
|
|
3034
|
+
if (field.IsVirtual && field.AllowUpdateAPI && !field.IsPrimaryKey && !field.Name.startsWith('__mj_')) {
|
|
3035
|
+
const sourceParent = this.findFieldSourceParent(field.Name, parentChain, allEntities);
|
|
3036
|
+
if (sourceParent) {
|
|
3037
|
+
field.InheritedFromEntityID = sourceParent.entityID;
|
|
3038
|
+
field.InheritedFromEntityName = sourceParent.entityName;
|
|
3039
|
+
}
|
|
2207
3040
|
}
|
|
2208
3041
|
}
|
|
3042
|
+
return { ParentChain: parentChain, IsChildEntity: true };
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* For an inherited field, walks the parent chain to find which specific parent entity
|
|
3046
|
+
* originally defines this field (by matching non-virtual fields on each parent).
|
|
3047
|
+
*/
|
|
3048
|
+
findFieldSourceParent(fieldName, parentChain, allEntities) {
|
|
3049
|
+
for (const parent of parentChain) {
|
|
3050
|
+
const parentEntity = allEntities.find(e => e.ID === parent.entityID);
|
|
3051
|
+
if (!parentEntity)
|
|
3052
|
+
continue;
|
|
3053
|
+
// Check if this parent has a non-virtual field with this name
|
|
3054
|
+
const hasField = parentEntity.Fields.some(f => f.Name === fieldName && !f.IsVirtual);
|
|
3055
|
+
if (hasField) {
|
|
3056
|
+
return parent;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
return null;
|
|
2209
3060
|
}
|
|
2210
3061
|
/**
|
|
2211
3062
|
* Apply smart field identification results to entity fields
|
|
@@ -2216,26 +3067,26 @@ NumberedRows AS (
|
|
|
2216
3067
|
const nameField = fields.find(f => f.Name === result.nameField);
|
|
2217
3068
|
if (nameField && nameField.AutoUpdateIsNameField && nameField.ID) {
|
|
2218
3069
|
sqlStatements.push(`
|
|
2219
|
-
UPDATE [${
|
|
3070
|
+
UPDATE [${mj_core_schema()}].EntityField
|
|
2220
3071
|
SET IsNameField = 1
|
|
2221
3072
|
WHERE ID = '${nameField.ID}'
|
|
2222
3073
|
AND AutoUpdateIsNameField = 1
|
|
2223
3074
|
`);
|
|
2224
3075
|
}
|
|
2225
3076
|
else if (!nameField) {
|
|
2226
|
-
|
|
3077
|
+
logError(`Smart field identification returned invalid nameField: '${result.nameField}' not found in entity fields`);
|
|
2227
3078
|
}
|
|
2228
3079
|
// Find all default in view fields (one or more)
|
|
2229
3080
|
const defaultInViewFields = fields.filter(f => result.defaultInView.includes(f.Name) && f.AutoUpdateDefaultInView && f.ID);
|
|
2230
3081
|
// Warn about any fields that weren't found
|
|
2231
3082
|
const missingFields = result.defaultInView.filter(name => !fields.some(f => f.Name === name));
|
|
2232
3083
|
if (missingFields.length > 0) {
|
|
2233
|
-
|
|
3084
|
+
logError(`Smart field identification returned invalid defaultInView fields: ${missingFields.join(', ')} not found in entity`);
|
|
2234
3085
|
}
|
|
2235
3086
|
// Build update statements for all default in view fields
|
|
2236
3087
|
for (const field of defaultInViewFields) {
|
|
2237
3088
|
sqlStatements.push(`
|
|
2238
|
-
UPDATE [${
|
|
3089
|
+
UPDATE [${mj_core_schema()}].EntityField
|
|
2239
3090
|
SET DefaultInView = 1
|
|
2240
3091
|
WHERE ID = '${field.ID}'
|
|
2241
3092
|
AND AutoUpdateDefaultInView = 1
|
|
@@ -2247,12 +3098,12 @@ NumberedRows AS (
|
|
|
2247
3098
|
// Warn about any fields that weren't found
|
|
2248
3099
|
const missingSearchableFields = result.searchableFields.filter(name => !fields.some(f => f.Name === name));
|
|
2249
3100
|
if (missingSearchableFields.length > 0) {
|
|
2250
|
-
|
|
3101
|
+
logError(`Smart field identification returned invalid searchableFields: ${missingSearchableFields.join(', ')} not found in entity`);
|
|
2251
3102
|
}
|
|
2252
3103
|
// Build update statements for all searchable fields
|
|
2253
3104
|
for (const field of searchableFields) {
|
|
2254
3105
|
sqlStatements.push(`
|
|
2255
|
-
UPDATE [${
|
|
3106
|
+
UPDATE [${mj_core_schema()}].EntityField
|
|
2256
3107
|
SET IncludeInUserSearchAPI = 1
|
|
2257
3108
|
WHERE ID = '${field.ID}'
|
|
2258
3109
|
AND AutoUpdateIncludeInUserSearchAPI = 1
|
|
@@ -2266,7 +3117,8 @@ NumberedRows AS (
|
|
|
2266
3117
|
}
|
|
2267
3118
|
}
|
|
2268
3119
|
/**
|
|
2269
|
-
* Apply form layout generation results to set category on entity fields
|
|
3120
|
+
* Apply form layout generation results to set category on entity fields.
|
|
3121
|
+
* Delegates to shared methods for category assignment, icon, and category info persistence.
|
|
2270
3122
|
* @param pool Database connection pool
|
|
2271
3123
|
* @param entityId Entity ID to update
|
|
2272
3124
|
* @param fields Entity fields
|
|
@@ -2274,17 +3126,47 @@ NumberedRows AS (
|
|
|
2274
3126
|
* @param isNewEntity If true, apply entityImportance; if false, skip it
|
|
2275
3127
|
*/
|
|
2276
3128
|
async applyFormLayout(pool, entityId, fields, result, isNewEntity = false) {
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
3129
|
+
const existingCategories = this.buildExistingCategorySet(fields);
|
|
3130
|
+
await this.applyFieldCategories(pool, entityId, fields, result.fieldCategories, existingCategories);
|
|
3131
|
+
if (result.entityIcon) {
|
|
3132
|
+
await this.applyEntityIcon(pool, entityId, result.entityIcon);
|
|
3133
|
+
}
|
|
3134
|
+
// Resolve categoryInfo from new or legacy format
|
|
3135
|
+
const categoryInfoToStore = result.categoryInfo ||
|
|
3136
|
+
(result.categoryIcons ?
|
|
3137
|
+
Object.fromEntries(Object.entries(result.categoryIcons).map(([cat, icon]) => [cat, { icon, description: '' }])) : null);
|
|
3138
|
+
if (categoryInfoToStore) {
|
|
3139
|
+
await this.applyCategoryInfoSettings(pool, entityId, categoryInfoToStore);
|
|
3140
|
+
}
|
|
3141
|
+
if (isNewEntity && result.entityImportance) {
|
|
3142
|
+
await this.applyEntityImportance(pool, entityId, result.entityImportance);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
// ─────────────────────────────────────────────────────────────────
|
|
3146
|
+
// Shared category / icon / settings persistence methods
|
|
3147
|
+
// Used by both the regular entity pipeline and VE decoration pipeline
|
|
3148
|
+
// ─────────────────────────────────────────────────────────────────
|
|
3149
|
+
/**
|
|
3150
|
+
* Builds a set of existing category names from entity fields.
|
|
3151
|
+
* Used to enforce category stability (prevent renaming).
|
|
3152
|
+
*/
|
|
3153
|
+
buildExistingCategorySet(fields) {
|
|
2280
3154
|
const existingCategories = new Set();
|
|
2281
3155
|
for (const field of fields) {
|
|
2282
3156
|
if (field.Category && field.Category.trim() !== '') {
|
|
2283
3157
|
existingCategories.add(field.Category);
|
|
2284
3158
|
}
|
|
2285
3159
|
}
|
|
2286
|
-
|
|
2287
|
-
|
|
3160
|
+
return existingCategories;
|
|
3161
|
+
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Applies category, display name, extended type, and code type to entity fields.
|
|
3164
|
+
* Enforces stability rules: fields with existing categories cannot move to NEW categories.
|
|
3165
|
+
* All SQL updates are batched into a single execution for performance.
|
|
3166
|
+
*/
|
|
3167
|
+
async applyFieldCategories(pool, entityId, fields, fieldCategories, existingCategories) {
|
|
3168
|
+
const sqlStatements = [];
|
|
3169
|
+
for (const fieldCategory of fieldCategories) {
|
|
2288
3170
|
const field = fields.find(f => f.Name === fieldCategory.fieldName);
|
|
2289
3171
|
if (field && field.AutoUpdateCategory && field.ID) {
|
|
2290
3172
|
// Override category to "System Metadata" for __mj_ fields (system audit fields)
|
|
@@ -2293,158 +3175,126 @@ NumberedRows AS (
|
|
|
2293
3175
|
category = 'System Metadata';
|
|
2294
3176
|
}
|
|
2295
3177
|
// ENFORCEMENT: Prevent category renaming
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
// 2. Moving to another EXISTING category
|
|
2299
|
-
// New categories are only allowed for fields that don't already have a category
|
|
2300
|
-
const fieldHasExistingCategory = field.Category && field.Category.trim() !== '';
|
|
2301
|
-
const categoryIsExisting = existingCategories.has(category);
|
|
2302
|
-
const categoryIsNew = !categoryIsExisting;
|
|
3178
|
+
const fieldHasExistingCategory = field.Category != null && field.Category.trim() !== '';
|
|
3179
|
+
const categoryIsNew = !existingCategories.has(category);
|
|
2303
3180
|
if (fieldHasExistingCategory && categoryIsNew) {
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
(0, status_logging_1.logStatus)(` Rejected category change for field '${field.Name}': cannot move from existing category '${field.Category}' to new category '${category}'. Keeping original category.`);
|
|
2307
|
-
category = field.Category; // Keep the original category
|
|
3181
|
+
logStatus(` Rejected category change for field '${field.Name}': cannot move from existing category '${field.Category}' to new category '${category}'. Keeping original category.`);
|
|
3182
|
+
category = field.Category;
|
|
2308
3183
|
}
|
|
2309
|
-
// Build SET clause with all available metadata
|
|
2310
3184
|
const setClauses = [
|
|
2311
3185
|
`Category = '${category.replace(/'/g, "''")}'`,
|
|
2312
3186
|
`GeneratedFormSection = 'Category'`
|
|
2313
3187
|
];
|
|
2314
|
-
// Add DisplayName if provided and field allows auto-update
|
|
2315
3188
|
if (fieldCategory.displayName && field.AutoUpdateDisplayName) {
|
|
2316
3189
|
setClauses.push(`DisplayName = '${fieldCategory.displayName.replace(/'/g, "''")}'`);
|
|
2317
3190
|
}
|
|
2318
|
-
// Add ExtendedType if provided
|
|
2319
3191
|
if (fieldCategory.extendedType !== undefined) {
|
|
2320
|
-
const extendedType = fieldCategory.extendedType === null ? 'NULL' : `'${fieldCategory.extendedType.replace(/'/g, "''")}'`;
|
|
3192
|
+
const extendedType = fieldCategory.extendedType === null ? 'NULL' : `'${String(fieldCategory.extendedType).replace(/'/g, "''")}'`;
|
|
2321
3193
|
setClauses.push(`ExtendedType = ${extendedType}`);
|
|
2322
3194
|
}
|
|
2323
|
-
// Add CodeType if provided
|
|
2324
3195
|
if (fieldCategory.codeType !== undefined) {
|
|
2325
|
-
const codeType = fieldCategory.codeType === null ? 'NULL' : `'${fieldCategory.codeType.replace(/'/g, "''")}'`;
|
|
3196
|
+
const codeType = fieldCategory.codeType === null ? 'NULL' : `'${String(fieldCategory.codeType).replace(/'/g, "''")}'`;
|
|
2326
3197
|
setClauses.push(`CodeType = ${codeType}`);
|
|
2327
3198
|
}
|
|
2328
|
-
|
|
3199
|
+
sqlStatements.push(`UPDATE [${mj_core_schema()}].EntityField
|
|
2329
3200
|
SET ${setClauses.join(',\n ')}
|
|
2330
3201
|
WHERE ID = '${field.ID}'
|
|
2331
|
-
AND AutoUpdateCategory = 1
|
|
2332
|
-
sqlStatements.push(updateSQL);
|
|
3202
|
+
AND AutoUpdateCategory = 1`);
|
|
2333
3203
|
}
|
|
2334
3204
|
else if (!field) {
|
|
2335
|
-
|
|
3205
|
+
logError(`Form layout returned invalid fieldName: '${fieldCategory.fieldName}' not found in entity`);
|
|
2336
3206
|
}
|
|
2337
3207
|
}
|
|
2338
|
-
// Execute all field updates in one batch
|
|
2339
3208
|
if (sqlStatements.length > 0) {
|
|
2340
|
-
|
|
2341
|
-
await this.LogSQLAndExecute(pool, combinedSQL, `Set categories for ${sqlStatements.length} fields`, false);
|
|
2342
|
-
}
|
|
2343
|
-
// Store entity icon if provided and entity doesn't already have one
|
|
2344
|
-
if (result.entityIcon && result.entityIcon.trim().length > 0) {
|
|
2345
|
-
// Check if entity already has an icon
|
|
2346
|
-
const checkEntitySQL = `
|
|
2347
|
-
SELECT Icon FROM [${(0, config_1.mj_core_schema)()}].Entity
|
|
2348
|
-
WHERE ID = '${entityId}'
|
|
2349
|
-
`;
|
|
2350
|
-
const entityCheck = await pool.request().query(checkEntitySQL);
|
|
2351
|
-
if (entityCheck.recordset.length > 0) {
|
|
2352
|
-
const currentIcon = entityCheck.recordset[0].Icon;
|
|
2353
|
-
// Only update if entity doesn't have an icon set
|
|
2354
|
-
if (!currentIcon || currentIcon.trim().length === 0) {
|
|
2355
|
-
const escapedIcon = result.entityIcon.replace(/'/g, "''");
|
|
2356
|
-
const updateEntitySQL = `
|
|
2357
|
-
UPDATE [${(0, config_1.mj_core_schema)()}].Entity
|
|
2358
|
-
SET Icon = '${escapedIcon}',
|
|
2359
|
-
__mj_UpdatedAt = GETUTCDATE()
|
|
2360
|
-
WHERE ID = '${entityId}'
|
|
2361
|
-
`;
|
|
2362
|
-
await this.LogSQLAndExecute(pool, updateEntitySQL, `Set entity icon to ${result.entityIcon}`, false);
|
|
2363
|
-
(0, status_logging_1.logStatus)(` ✓ Set entity icon: ${result.entityIcon}`);
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
3209
|
+
await this.LogSQLAndExecute(pool, sqlStatements.join('\n'), `Set categories for ${sqlStatements.length} fields`, false);
|
|
2366
3210
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
const existingNew = await pool.request().query(checkNewSQL);
|
|
2381
|
-
if (existingNew.recordset.length > 0) {
|
|
2382
|
-
// Update existing setting
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Sets the entity icon if the entity doesn't already have one.
|
|
3214
|
+
*/
|
|
3215
|
+
async applyEntityIcon(pool, entityId, entityIcon) {
|
|
3216
|
+
if (!entityIcon || entityIcon.trim().length === 0)
|
|
3217
|
+
return;
|
|
3218
|
+
const checkSQL = `SELECT Icon FROM [${mj_core_schema()}].Entity WHERE ID = '${entityId}'`;
|
|
3219
|
+
const entityCheck = await pool.request().query(checkSQL);
|
|
3220
|
+
if (entityCheck.recordset.length > 0) {
|
|
3221
|
+
const currentIcon = entityCheck.recordset[0].Icon;
|
|
3222
|
+
if (!currentIcon || currentIcon.trim().length === 0) {
|
|
3223
|
+
const escapedIcon = entityIcon.replace(/'/g, "''");
|
|
2383
3224
|
const updateSQL = `
|
|
2384
|
-
UPDATE [${
|
|
2385
|
-
SET
|
|
2386
|
-
|
|
2387
|
-
WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryInfo'
|
|
2388
|
-
`;
|
|
2389
|
-
await this.LogSQLAndExecute(pool, updateSQL, `Update FieldCategoryInfo setting for entity`, false);
|
|
2390
|
-
}
|
|
2391
|
-
else {
|
|
2392
|
-
// Insert new setting
|
|
2393
|
-
const newId = (0, uuid_1.v4)();
|
|
2394
|
-
const insertSQL = `
|
|
2395
|
-
INSERT INTO [${(0, config_1.mj_core_schema)()}].EntitySetting (ID, EntityID, Name, Value, __mj_CreatedAt, __mj_UpdatedAt)
|
|
2396
|
-
VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}', GETUTCDATE(), GETUTCDATE())
|
|
3225
|
+
UPDATE [${mj_core_schema()}].Entity
|
|
3226
|
+
SET Icon = '${escapedIcon}', __mj_UpdatedAt = GETUTCDATE()
|
|
3227
|
+
WHERE ID = '${entityId}'
|
|
2397
3228
|
`;
|
|
2398
|
-
await this.LogSQLAndExecute(pool,
|
|
2399
|
-
|
|
2400
|
-
// Also update legacy FieldCategoryIcons for backwards compatibility
|
|
2401
|
-
// Extract just icons from categoryInfo
|
|
2402
|
-
const iconsOnly = {};
|
|
2403
|
-
for (const [category, info] of Object.entries(categoryInfoToStore)) {
|
|
2404
|
-
if (info && typeof info === 'object' && 'icon' in info) {
|
|
2405
|
-
iconsOnly[category] = info.icon;
|
|
2406
|
-
}
|
|
3229
|
+
await this.LogSQLAndExecute(pool, updateSQL, `Set entity icon to ${entityIcon}`, false);
|
|
3230
|
+
logStatus(` Set entity icon: ${entityIcon}`);
|
|
2407
3231
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
/**
|
|
3235
|
+
* Upserts FieldCategoryInfo (new format) and FieldCategoryIcons (legacy format) in EntitySetting.
|
|
3236
|
+
*/
|
|
3237
|
+
async applyCategoryInfoSettings(pool, entityId, categoryInfo) {
|
|
3238
|
+
if (!categoryInfo || Object.keys(categoryInfo).length === 0)
|
|
3239
|
+
return;
|
|
3240
|
+
const infoJSON = JSON.stringify(categoryInfo).replace(/'/g, "''");
|
|
3241
|
+
// Upsert FieldCategoryInfo (new format)
|
|
3242
|
+
const checkNewSQL = `SELECT ID FROM [${mj_core_schema()}].EntitySetting WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryInfo'`;
|
|
3243
|
+
const existingNew = await pool.request().query(checkNewSQL);
|
|
3244
|
+
if (existingNew.recordset.length > 0) {
|
|
3245
|
+
await this.LogSQLAndExecute(pool, `
|
|
3246
|
+
UPDATE [${mj_core_schema()}].EntitySetting
|
|
3247
|
+
SET Value = '${infoJSON}', __mj_UpdatedAt = GETUTCDATE()
|
|
3248
|
+
WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryInfo'
|
|
3249
|
+
`, `Update FieldCategoryInfo setting for entity`, false);
|
|
3250
|
+
}
|
|
3251
|
+
else {
|
|
3252
|
+
const newId = uuidv4();
|
|
3253
|
+
await this.LogSQLAndExecute(pool, `
|
|
3254
|
+
INSERT INTO [${mj_core_schema()}].EntitySetting (ID, EntityID, Name, Value, __mj_CreatedAt, __mj_UpdatedAt)
|
|
3255
|
+
VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}', GETUTCDATE(), GETUTCDATE())
|
|
3256
|
+
`, `Insert FieldCategoryInfo setting for entity`, false);
|
|
3257
|
+
}
|
|
3258
|
+
// Also upsert legacy FieldCategoryIcons for backwards compatibility
|
|
3259
|
+
const iconsOnly = {};
|
|
3260
|
+
for (const [category, info] of Object.entries(categoryInfo)) {
|
|
3261
|
+
if (info && typeof info === 'object' && 'icon' in info) {
|
|
3262
|
+
iconsOnly[category] = info.icon;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
const iconsJSON = JSON.stringify(iconsOnly).replace(/'/g, "''");
|
|
3266
|
+
const checkLegacySQL = `SELECT ID FROM [${mj_core_schema()}].EntitySetting WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryIcons'`;
|
|
3267
|
+
const existingLegacy = await pool.request().query(checkLegacySQL);
|
|
3268
|
+
if (existingLegacy.recordset.length > 0) {
|
|
3269
|
+
await this.LogSQLAndExecute(pool, `
|
|
3270
|
+
UPDATE [${mj_core_schema()}].EntitySetting
|
|
3271
|
+
SET Value = '${iconsJSON}', __mj_UpdatedAt = GETUTCDATE()
|
|
2411
3272
|
WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryIcons'
|
|
2412
|
-
|
|
2413
|
-
const existingLegacy = await pool.request().query(checkLegacySQL);
|
|
2414
|
-
if (existingLegacy.recordset.length > 0) {
|
|
2415
|
-
const updateSQL = `
|
|
2416
|
-
UPDATE [${(0, config_1.mj_core_schema)()}].EntitySetting
|
|
2417
|
-
SET Value = '${iconsJSON}',
|
|
2418
|
-
__mj_UpdatedAt = GETUTCDATE()
|
|
2419
|
-
WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryIcons'
|
|
2420
|
-
`;
|
|
2421
|
-
await this.LogSQLAndExecute(pool, updateSQL, `Update FieldCategoryIcons setting for entity (legacy format)`, false);
|
|
2422
|
-
}
|
|
2423
|
-
else {
|
|
2424
|
-
const newId = (0, uuid_1.v4)();
|
|
2425
|
-
const insertSQL = `
|
|
2426
|
-
INSERT INTO [${(0, config_1.mj_core_schema)()}].EntitySetting (ID, EntityID, Name, Value, __mj_CreatedAt, __mj_UpdatedAt)
|
|
2427
|
-
VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}', GETUTCDATE(), GETUTCDATE())
|
|
2428
|
-
`;
|
|
2429
|
-
await this.LogSQLAndExecute(pool, insertSQL, `Insert FieldCategoryIcons setting for entity (legacy format)`, false);
|
|
2430
|
-
}
|
|
3273
|
+
`, `Update FieldCategoryIcons setting (legacy)`, false);
|
|
2431
3274
|
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
UPDATE [${(0, config_1.mj_core_schema)()}].ApplicationEntity
|
|
2439
|
-
SET DefaultForNewUser = ${defaultForNewUser},
|
|
2440
|
-
__mj_UpdatedAt = GETUTCDATE()
|
|
2441
|
-
WHERE EntityID = '${entityId}'
|
|
2442
|
-
`;
|
|
2443
|
-
await this.LogSQLAndExecute(pool, updateAppEntitySQL, `Set DefaultForNewUser=${defaultForNewUser} for NEW entity based on AI analysis (category: ${result.entityImportance.entityCategory}, confidence: ${result.entityImportance.confidence})`, false);
|
|
2444
|
-
(0, status_logging_1.logStatus)(` ✓ Entity importance (NEW Entity): ${result.entityImportance.entityCategory} (defaultForNewUser: ${result.entityImportance.defaultForNewUser}, confidence: ${result.entityImportance.confidence})`);
|
|
2445
|
-
(0, status_logging_1.logStatus)(` Reasoning: ${result.entityImportance.reasoning}`);
|
|
3275
|
+
else {
|
|
3276
|
+
const newId = uuidv4();
|
|
3277
|
+
await this.LogSQLAndExecute(pool, `
|
|
3278
|
+
INSERT INTO [${mj_core_schema()}].EntitySetting (ID, EntityID, Name, Value, __mj_CreatedAt, __mj_UpdatedAt)
|
|
3279
|
+
VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}', GETUTCDATE(), GETUTCDATE())
|
|
3280
|
+
`, `Insert FieldCategoryIcons setting (legacy)`, false);
|
|
2446
3281
|
}
|
|
2447
3282
|
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Applies entity importance analysis to ApplicationEntity records.
|
|
3285
|
+
* Only called for NEW entities to set DefaultForNewUser.
|
|
3286
|
+
*/
|
|
3287
|
+
async applyEntityImportance(pool, entityId, importance) {
|
|
3288
|
+
const defaultForNewUser = importance.defaultForNewUser ? 1 : 0;
|
|
3289
|
+
const updateSQL = `
|
|
3290
|
+
UPDATE [${mj_core_schema()}].ApplicationEntity
|
|
3291
|
+
SET DefaultForNewUser = ${defaultForNewUser}, __mj_UpdatedAt = GETUTCDATE()
|
|
3292
|
+
WHERE EntityID = '${entityId}'
|
|
3293
|
+
`;
|
|
3294
|
+
await this.LogSQLAndExecute(pool, updateSQL, `Set DefaultForNewUser=${defaultForNewUser} for NEW entity (category: ${importance.entityCategory}, confidence: ${importance.confidence})`, false);
|
|
3295
|
+
logStatus(` Entity importance (NEW Entity): ${importance.entityCategory} (defaultForNewUser: ${importance.defaultForNewUser}, confidence: ${importance.confidence})`);
|
|
3296
|
+
logStatus(` Reasoning: ${importance.reasoning}`);
|
|
3297
|
+
}
|
|
2448
3298
|
/**
|
|
2449
3299
|
* Executes the given SQL query using the given ConnectionPool object.
|
|
2450
3300
|
* If the appendToLogFile parameter is true, the query will also be appended to the log file.
|
|
@@ -2456,8 +3306,7 @@ NumberedRows AS (
|
|
|
2456
3306
|
* @returns - The result of the query execution.
|
|
2457
3307
|
*/
|
|
2458
3308
|
async LogSQLAndExecute(pool, query, description, isRecurringScript = false) {
|
|
2459
|
-
return await
|
|
3309
|
+
return await SQLLogging.LogSQLAndExecute(pool, query, description, isRecurringScript);
|
|
2460
3310
|
}
|
|
2461
3311
|
}
|
|
2462
|
-
exports.ManageMetadataBase = ManageMetadataBase;
|
|
2463
3312
|
//# sourceMappingURL=manage-metadata.js.map
|