@memberjunction/codegen-lib 5.4.1 → 5.5.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 +65 -2
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +26 -12
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/related-entity-components.js +2 -2
- package/dist/Angular/related-entity-components.js.map +1 -1
- package/dist/Config/config.d.ts +10 -0
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +10 -0
- package/dist/Config/config.js.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts +544 -0
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -0
- package/dist/Database/codeGenDatabaseProvider.js +29 -0
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -0
- package/dist/Database/manage-metadata.d.ts +165 -60
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +592 -483
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.d.ts +53 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js +112 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js.map +1 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +344 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +1567 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.d.ts +42 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js +84 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +372 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +1483 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -0
- package/dist/Database/reorder-columns.d.ts +2 -2
- package/dist/Database/reorder-columns.d.ts.map +1 -1
- package/dist/Database/reorder-columns.js +9 -9
- package/dist/Database/reorder-columns.js.map +1 -1
- package/dist/Database/sql.d.ts +10 -5
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +44 -228
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +31 -29
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +209 -842
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +3 -2
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts +4 -4
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts +6 -1
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +33 -35
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +2 -2
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +1 -1
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/Misc/system_integrity.d.ts +6 -6
- package/dist/Misc/system_integrity.d.ts.map +1 -1
- package/dist/Misc/system_integrity.js +33 -8
- package/dist/Misc/system_integrity.js.map +1 -1
- package/dist/Misc/temp_batch_file.d.ts.map +1 -1
- package/dist/Misc/temp_batch_file.js +4 -1
- package/dist/Misc/temp_batch_file.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/runCodeGen.d.ts +30 -75
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +123 -215
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +18 -15
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { CodeGenDatabaseProvider } from './codeGenDatabaseProvider.js';
|
|
2
|
+
import { SQLServerCodeGenProvider } from './providers/sqlserver/SQLServerCodeGenProvider.js';
|
|
3
|
+
import { configInfo, currentWorkingDirectory, dbType, getSettingValue, mj_core_schema, outputDir } from '../Config/config.js';
|
|
4
|
+
import { EntityInfo, ExtractActualDefaultValue, LogError, LogStatus, Metadata, SeverityType } from "@memberjunction/core";
|
|
4
5
|
import { logError, logMessage, logStatus } from "../Misc/status_logging.js";
|
|
5
6
|
import { SQLUtilityBase } from "./sql.js";
|
|
6
7
|
import { AdvancedGeneration } from "../Misc/advanced_generation.js";
|
|
7
8
|
import { SQLParser } from "@memberjunction/core-entities-server";
|
|
8
|
-
import { convertCamelCaseToHaveSpaces, generatePluralName, MJGlobal, stripTrailingChars } from "@memberjunction/global";
|
|
9
|
+
import { convertCamelCaseToHaveSpaces, generatePluralName, MJGlobal, stripTrailingChars, UUIDsEqual } from "@memberjunction/global";
|
|
9
10
|
import { v4 as uuidv4 } from 'uuid';
|
|
10
11
|
import * as fs from 'fs';
|
|
11
12
|
import path from 'path';
|
|
@@ -31,8 +32,138 @@ export class ValidatorResult {
|
|
|
31
32
|
}
|
|
32
33
|
export class ManageMetadataBase {
|
|
33
34
|
constructor() {
|
|
35
|
+
// ─── Database Provider Infrastructure ─────────────────────────────
|
|
36
|
+
// All platform-specific SQL generation is delegated to the CodeGenDatabaseProvider.
|
|
37
|
+
// The provider is lazily initialized from the dbType() config on first access.
|
|
38
|
+
this._dbProvider = null;
|
|
39
|
+
// ─── End Dialect Infrastructure ───────────────────────────────────
|
|
34
40
|
this._sqlUtilityObject = MJGlobal.Instance.ClassFactory.CreateInstance(SQLUtilityBase);
|
|
35
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns the CodeGenDatabaseProvider for the current database platform.
|
|
44
|
+
* Lazily initialized from dbType() configuration.
|
|
45
|
+
*/
|
|
46
|
+
get dbProvider() {
|
|
47
|
+
if (!this._dbProvider) {
|
|
48
|
+
const platform = dbType();
|
|
49
|
+
if (platform === 'postgresql') {
|
|
50
|
+
const pgProvider = MJGlobal.Instance.ClassFactory.CreateInstance(CodeGenDatabaseProvider, 'PostgreSQLCodeGenProvider');
|
|
51
|
+
if (pgProvider) {
|
|
52
|
+
this._dbProvider = pgProvider;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
throw new Error('PostgreSQL CodeGen provider not found. Ensure @memberjunction/postgresql-dataprovider ' +
|
|
56
|
+
'is installed and its CodeGen provider is registered before running CodeGen.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this._dbProvider = new SQLServerCodeGenProvider();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return this._dbProvider;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns the SQLDialect for the current database platform, derived from the provider.
|
|
67
|
+
*/
|
|
68
|
+
get dialect() {
|
|
69
|
+
return this.dbProvider.Dialect;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Quotes a database identifier (column, table, etc.).
|
|
73
|
+
* SQL Server: [name], PostgreSQL: "name"
|
|
74
|
+
*/
|
|
75
|
+
qi(name) {
|
|
76
|
+
return this.dialect.QuoteIdentifier(name);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Produces a schema-qualified object reference.
|
|
80
|
+
* SQL Server: [schema].[object], PostgreSQL: schema."object"
|
|
81
|
+
*/
|
|
82
|
+
qs(schema, object) {
|
|
83
|
+
return this.dialect.QuoteSchema(schema, object);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns the current UTC timestamp expression.
|
|
87
|
+
* SQL Server: GETUTCDATE(), PostgreSQL: NOW() AT TIME ZONE 'UTC'
|
|
88
|
+
*/
|
|
89
|
+
utcNow() {
|
|
90
|
+
return this.dialect.CurrentTimestampUTC();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Returns a boolean literal for the platform.
|
|
94
|
+
* SQL Server: 1/0, PostgreSQL: true/false
|
|
95
|
+
*/
|
|
96
|
+
boolLit(value) {
|
|
97
|
+
return this.dialect.BooleanLiteral(value);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Quotes mixed-case identifiers in a SQL string for the current platform.
|
|
101
|
+
* Delegates to the database provider's quoteSQLForExecution method.
|
|
102
|
+
*/
|
|
103
|
+
qsql(sql) {
|
|
104
|
+
return this.dbProvider.quoteSQLForExecution(sql);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Executes a SQL query with automatic identifier quoting for the current platform.
|
|
108
|
+
*/
|
|
109
|
+
async runQuery(pool, sql) {
|
|
110
|
+
return pool.query(this.qsql(sql));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Executes a parameterized SQL query with automatic identifier quoting for the current platform.
|
|
114
|
+
*/
|
|
115
|
+
async runQueryWithParams(pool, sql, params) {
|
|
116
|
+
return pool.queryWithParams(this.qsql(sql), params);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Wraps a SELECT query with a row limit.
|
|
120
|
+
* SQL Server: SELECT TOP N ... , PostgreSQL: SELECT ... LIMIT N
|
|
121
|
+
*/
|
|
122
|
+
selectTop(n, selectBody, fromAndWhere, orderBy) {
|
|
123
|
+
const limit = this.dialect.LimitClause(n);
|
|
124
|
+
const orderClause = orderBy ? ` ORDER BY ${orderBy}` : '';
|
|
125
|
+
if (limit.prefix) {
|
|
126
|
+
// SQL Server: SELECT TOP N columns FROM ...
|
|
127
|
+
return `SELECT ${limit.prefix} ${selectBody} ${fromAndWhere}${orderClause}`;
|
|
128
|
+
}
|
|
129
|
+
// PostgreSQL: SELECT columns FROM ... LIMIT N
|
|
130
|
+
return `SELECT ${selectBody} ${fromAndWhere}${orderClause} ${limit.suffix}`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Returns ISNULL/COALESCE expression.
|
|
134
|
+
* Both platforms support COALESCE, but SQL Server also has ISNULL.
|
|
135
|
+
*/
|
|
136
|
+
coalesce(expr, fallback) {
|
|
137
|
+
return this.dialect.IsNull(expr, fallback);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns an IIF/CASE expression.
|
|
141
|
+
* SQL Server: IIF(cond, t, f), PostgreSQL: CASE WHEN cond THEN t ELSE f END
|
|
142
|
+
*/
|
|
143
|
+
iif(condition, trueVal, falseVal) {
|
|
144
|
+
return this.dialect.IIF(condition, trueVal, falseVal);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Returns the timestamp column type name for this platform.
|
|
148
|
+
* Delegates to the database provider.
|
|
149
|
+
*/
|
|
150
|
+
get timestampType() {
|
|
151
|
+
return this.dbProvider.TimestampType;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Generates a conditional existence check + DROP statement.
|
|
155
|
+
* Delegates to the database provider.
|
|
156
|
+
*/
|
|
157
|
+
dropIfExists(objectType, schema, name) {
|
|
158
|
+
return this.dbProvider.dropObjectSQL(objectType, schema, name);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Generates SQL for conditional INSERT (IF NOT EXISTS pattern).
|
|
162
|
+
* Delegates to the database provider.
|
|
163
|
+
*/
|
|
164
|
+
conditionalInsert(checkQuery, insertSQL) {
|
|
165
|
+
return this.dbProvider.conditionalInsertSQL(checkQuery, insertSQL);
|
|
166
|
+
}
|
|
36
167
|
get SQLUtilityObject() {
|
|
37
168
|
return this._sqlUtilityObject;
|
|
38
169
|
}
|
|
@@ -200,16 +331,11 @@ export class ManageMetadataBase {
|
|
|
200
331
|
for (const rel of relationships) {
|
|
201
332
|
try {
|
|
202
333
|
// Look up the parent entity — try by Name first, then by BaseTable within the given schema
|
|
203
|
-
const parentResult = await
|
|
204
|
-
|
|
205
|
-
.input('SchemaName', rel.SchemaName || null)
|
|
206
|
-
.query(`
|
|
207
|
-
SELECT TOP 1 ID, Name
|
|
208
|
-
FROM [${schema}].vwEntities
|
|
334
|
+
const parentResult = await this.runQueryWithParams(pool, `
|
|
335
|
+
${this.selectTop(1, 'ID, Name', `FROM ${this.qs(schema, 'vwEntities')}
|
|
209
336
|
WHERE Name = @ParentName
|
|
210
|
-
OR (BaseTable = @ParentName AND (@SchemaName IS NULL OR SchemaName = @SchemaName))
|
|
211
|
-
|
|
212
|
-
`);
|
|
337
|
+
OR (BaseTable = @ParentName AND (@SchemaName IS NULL OR SchemaName = @SchemaName))`, 'CASE WHEN Name = @ParentName THEN 0 ELSE 1 END')}
|
|
338
|
+
`, { 'ParentName': rel.ParentEntity, 'SchemaName': rel.SchemaName || null });
|
|
213
339
|
if (parentResult.recordset.length === 0) {
|
|
214
340
|
logError(` > IS-A config: parent entity "${rel.ParentEntity}" not found — skipping`);
|
|
215
341
|
continue;
|
|
@@ -217,16 +343,11 @@ export class ManageMetadataBase {
|
|
|
217
343
|
const parentId = parentResult.recordset[0].ID;
|
|
218
344
|
const parentName = parentResult.recordset[0].Name;
|
|
219
345
|
// Look up the child entity — same strategy
|
|
220
|
-
const childResult = await
|
|
221
|
-
|
|
222
|
-
.input('SchemaName', rel.SchemaName || null)
|
|
223
|
-
.query(`
|
|
224
|
-
SELECT TOP 1 ID, Name, ParentID
|
|
225
|
-
FROM [${schema}].vwEntities
|
|
346
|
+
const childResult = await this.runQueryWithParams(pool, `
|
|
347
|
+
${this.selectTop(1, 'ID, Name, ParentID', `FROM ${this.qs(schema, 'vwEntities')}
|
|
226
348
|
WHERE Name = @ChildName
|
|
227
|
-
OR (BaseTable = @ChildName AND (@SchemaName IS NULL OR SchemaName = @SchemaName))
|
|
228
|
-
|
|
229
|
-
`);
|
|
349
|
+
OR (BaseTable = @ChildName AND (@SchemaName IS NULL OR SchemaName = @SchemaName))`, 'CASE WHEN Name = @ChildName THEN 0 ELSE 1 END')}
|
|
350
|
+
`, { 'ChildName': rel.ChildEntity, 'SchemaName': rel.SchemaName || null });
|
|
230
351
|
if (childResult.recordset.length === 0) {
|
|
231
352
|
logError(` > IS-A config: child entity "${rel.ChildEntity}" not found — skipping`);
|
|
232
353
|
continue;
|
|
@@ -235,15 +356,12 @@ export class ManageMetadataBase {
|
|
|
235
356
|
const childName = childResult.recordset[0].Name;
|
|
236
357
|
const existingParentId = childResult.recordset[0].ParentID;
|
|
237
358
|
// Skip if already set correctly
|
|
238
|
-
if (existingParentId
|
|
359
|
+
if (UUIDsEqual(existingParentId, parentId)) {
|
|
239
360
|
logStatus(` > IS-A: "${childName}" already has ParentID set to "${parentName}", skipping`);
|
|
240
361
|
}
|
|
241
362
|
else {
|
|
242
363
|
// Set ParentID on the child entity
|
|
243
|
-
await pool.
|
|
244
|
-
.input('ParentID', parentId)
|
|
245
|
-
.input('ChildID', childId)
|
|
246
|
-
.query(`UPDATE [${schema}].Entity SET ParentID = @ParentID WHERE ID = @ChildID`);
|
|
364
|
+
await this.runQueryWithParams(pool, `UPDATE ${this.qs(schema, 'Entity')} SET ParentID = @ParentID WHERE ID = @ChildID`, { 'ParentID': parentId, 'ChildID': childId });
|
|
247
365
|
if (existingParentId) {
|
|
248
366
|
logStatus(` > IS-A: Updated "${childName}" ParentID from previous value to "${parentName}"`);
|
|
249
367
|
}
|
|
@@ -286,14 +404,10 @@ export class ManageMetadataBase {
|
|
|
286
404
|
continue;
|
|
287
405
|
}
|
|
288
406
|
// Look up the entity by BaseTable + SchemaName
|
|
289
|
-
const entityResult = await
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
SELECT TOP 1 ID, Name
|
|
294
|
-
FROM [${schema}].vwEntities
|
|
295
|
-
WHERE BaseTable = @BaseTable AND SchemaName = @SchemaName
|
|
296
|
-
`);
|
|
407
|
+
const entityResult = await this.runQueryWithParams(pool, `
|
|
408
|
+
${this.selectTop(1, 'ID, Name', `FROM ${this.qs(schema, 'vwEntities')}
|
|
409
|
+
WHERE BaseTable = @BaseTable AND SchemaName = @SchemaName`)}
|
|
410
|
+
`, { 'BaseTable': ec.BaseTable, 'SchemaName': ec.SchemaName });
|
|
297
411
|
if (entityResult.recordset.length === 0) {
|
|
298
412
|
logError(` > Entities config: entity for "${ec.SchemaName}.${ec.BaseTable}" not found — skipping`);
|
|
299
413
|
continue;
|
|
@@ -301,16 +415,16 @@ export class ManageMetadataBase {
|
|
|
301
415
|
const entityId = entityResult.recordset[0].ID;
|
|
302
416
|
const entityName = entityResult.recordset[0].Name;
|
|
303
417
|
// Build a parameterized UPDATE with one SET clause per attribute
|
|
304
|
-
const
|
|
418
|
+
const queryParams = { 'EntityID': entityId };
|
|
305
419
|
const setClauses = [];
|
|
306
420
|
for (const [key, value] of attrs) {
|
|
307
421
|
const paramName = `attr_${key}`;
|
|
308
422
|
// Convert boolean values to SQL BIT (1/0)
|
|
309
423
|
const sqlValue = typeof value === 'boolean' ? (value ? 1 : 0) : value;
|
|
310
|
-
|
|
311
|
-
setClauses.push(
|
|
424
|
+
queryParams[paramName] = sqlValue;
|
|
425
|
+
setClauses.push(`${this.qi(key)} = @${paramName}`);
|
|
312
426
|
}
|
|
313
|
-
await
|
|
427
|
+
await this.runQueryWithParams(pool, `UPDATE ${this.qs(schema, 'Entity')} SET ${setClauses.join(', ')} WHERE ID = @EntityID`, queryParams);
|
|
314
428
|
const attrSummary = attrs.map(([k, v]) => `${k}=${v}`).join(', ');
|
|
315
429
|
logStatus(` > Entities config: Set ${attrSummary} on "${entityName}"`);
|
|
316
430
|
updatedCount++;
|
|
@@ -343,32 +457,20 @@ export class ManageMetadataBase {
|
|
|
343
457
|
const entityName = ve.EntityName || this.deriveEntityNameFromView(viewName);
|
|
344
458
|
const pkField = ve.PrimaryKey?.[0] || 'ID';
|
|
345
459
|
// Check if entity already exists for this view
|
|
346
|
-
const existsResult = await pool.
|
|
347
|
-
.input('ViewName', viewName)
|
|
348
|
-
.input('SchemaName', viewSchema)
|
|
349
|
-
.query(`SELECT ID FROM [${schema}].vwEntities WHERE BaseView = @ViewName AND SchemaName = @SchemaName`);
|
|
460
|
+
const existsResult = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(schema, 'vwEntities')} WHERE BaseView = @ViewName AND SchemaName = @SchemaName`, { 'ViewName': viewName, 'SchemaName': viewSchema });
|
|
350
461
|
if (existsResult.recordset.length > 0) {
|
|
351
462
|
logStatus(` > Virtual entity "${entityName}" already exists for view [${viewSchema}].[${viewName}], skipping creation`);
|
|
352
463
|
continue;
|
|
353
464
|
}
|
|
354
465
|
// Verify the view actually exists in the database
|
|
355
|
-
const viewExistsResult = await pool.
|
|
356
|
-
.input('ViewName', viewName)
|
|
357
|
-
.input('SchemaName', viewSchema)
|
|
358
|
-
.query(`SELECT 1 FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = @ViewName AND TABLE_SCHEMA = @SchemaName`);
|
|
466
|
+
const viewExistsResult = await this.runQueryWithParams(pool, this.dbProvider.getViewExistsSQL(), { 'ViewName': viewName, 'SchemaName': viewSchema });
|
|
359
467
|
if (viewExistsResult.recordset.length === 0) {
|
|
360
468
|
logError(` > View [${viewSchema}].[${viewName}] does not exist — skipping virtual entity creation for "${entityName}"`);
|
|
361
469
|
continue;
|
|
362
470
|
}
|
|
363
471
|
// Create the virtual entity via the stored procedure
|
|
364
472
|
try {
|
|
365
|
-
const createResult = await pool.
|
|
366
|
-
.input('Name', entityName)
|
|
367
|
-
.input('BaseView', viewName)
|
|
368
|
-
.input('SchemaName', viewSchema)
|
|
369
|
-
.input('PrimaryKeyFieldName', pkField)
|
|
370
|
-
.input('Description', ve.Description || null)
|
|
371
|
-
.execute(`[${schema}].spCreateVirtualEntity`);
|
|
473
|
+
const createResult = await pool.executeStoredProcedure(`${this.qs(schema, 'spCreateVirtualEntity')}`, { 'Name': entityName, 'BaseView': viewName, 'SchemaName': viewSchema, 'PrimaryKeyFieldName': pkField, 'Description': ve.Description || null });
|
|
372
474
|
const newEntityId = createResult.recordset?.[0]?.['']
|
|
373
475
|
|| createResult.recordset?.[0]?.ID
|
|
374
476
|
|| createResult.recordset?.[0]?.Column0;
|
|
@@ -410,9 +512,24 @@ export class ManageMetadataBase {
|
|
|
410
512
|
*/
|
|
411
513
|
async manageMetadata(pool, currentUser) {
|
|
412
514
|
const md = new Metadata();
|
|
413
|
-
|
|
515
|
+
// Auto-exclude platform-specific system schemas.
|
|
516
|
+
// We mutate configInfo.excludeSchemas directly so that all downstream code
|
|
517
|
+
// (including createExcludeTablesAndSchemasFilter) picks up the exclusions.
|
|
518
|
+
const systemSchemas = this.dbProvider.getSystemSchemasToExclude();
|
|
519
|
+
if (systemSchemas.length > 0) {
|
|
520
|
+
if (!configInfo.excludeSchemas) {
|
|
521
|
+
configInfo.excludeSchemas = [];
|
|
522
|
+
}
|
|
523
|
+
for (const s of systemSchemas) {
|
|
524
|
+
if (!configInfo.excludeSchemas.includes(s)) {
|
|
525
|
+
configInfo.excludeSchemas.push(s);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const excludeSchemas = configInfo.excludeSchemas ? [...configInfo.excludeSchemas] : [];
|
|
414
530
|
let bSuccess = true;
|
|
415
531
|
let start = new Date();
|
|
532
|
+
// Very early FK check - before any operations
|
|
416
533
|
// Load SchemaInfo records early so that EntityNamePrefix/Suffix rules from the
|
|
417
534
|
// database are available when createNewEntities() names new entities.
|
|
418
535
|
logStatus(' Loading SchemaInfo records for entity name rules...');
|
|
@@ -519,8 +636,8 @@ export class ManageMetadataBase {
|
|
|
519
636
|
// virtual entities are records defined in the entity metadata and do NOT define a distinct base table
|
|
520
637
|
// but they do specify a base view. We DO NOT generate a base view for a virtual entity, we simply use it to figure
|
|
521
638
|
// 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
|
|
522
|
-
const sql = `SELECT * FROM
|
|
523
|
-
const virtualEntitiesResult = await
|
|
639
|
+
const sql = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntities')} WHERE VirtualEntity = 1`;
|
|
640
|
+
const virtualEntitiesResult = await this.runQuery(pool, sql);
|
|
524
641
|
const virtualEntities = virtualEntitiesResult.recordset;
|
|
525
642
|
let anyUpdates = false;
|
|
526
643
|
if (virtualEntities && virtualEntities.length > 0) {
|
|
@@ -542,20 +659,8 @@ export class ManageMetadataBase {
|
|
|
542
659
|
try {
|
|
543
660
|
// for a given virtual entity, we need to loop through the fields that exist in the current SQL definition for the view
|
|
544
661
|
// and add/update/delete the entity fields to match what's in the view
|
|
545
|
-
const sql =
|
|
546
|
-
|
|
547
|
-
FROM
|
|
548
|
-
sys.columns c
|
|
549
|
-
INNER JOIN
|
|
550
|
-
sys.types t ON c.user_type_id = t.user_type_id
|
|
551
|
-
INNER JOIN
|
|
552
|
-
sys.views v ON c.object_id = v.object_id
|
|
553
|
-
WHERE
|
|
554
|
-
v.name = '${virtualEntity.BaseView}' AND
|
|
555
|
-
SCHEMA_NAME(v.schema_id) = '${virtualEntity.SchemaName}'
|
|
556
|
-
ORDER BY
|
|
557
|
-
c.column_id`;
|
|
558
|
-
const veFieldsResult = await pool.request().query(sql);
|
|
662
|
+
const sql = this.dbProvider.getViewColumnsSQL(virtualEntity.SchemaName, virtualEntity.BaseView);
|
|
663
|
+
const veFieldsResult = await this.runQuery(pool, sql);
|
|
559
664
|
const veFields = veFieldsResult.recordset;
|
|
560
665
|
if (veFields && veFields.length > 0) {
|
|
561
666
|
// we have 1+ fields, now loop through them and process each one
|
|
@@ -569,7 +674,7 @@ export class ManageMetadataBase {
|
|
|
569
674
|
removeList.push(f.ID);
|
|
570
675
|
}
|
|
571
676
|
if (removeList.length > 0) {
|
|
572
|
-
const sqlRemove = `DELETE FROM
|
|
677
|
+
const sqlRemove = `DELETE FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ID IN (${removeList.map(removeId => `'${removeId}'`).join(',')})`;
|
|
573
678
|
// this removes the fields that shouldn't be there anymore
|
|
574
679
|
await this.LogSQLAndExecute(pool, sqlRemove, `SQL text to remove fields from entity ${virtualEntity.Name}`);
|
|
575
680
|
bUpdated = true;
|
|
@@ -591,7 +696,7 @@ export class ManageMetadataBase {
|
|
|
591
696
|
}
|
|
592
697
|
if (bUpdated) {
|
|
593
698
|
// finally make sure we update the UpdatedAt field for the entity if we made changes to its fields
|
|
594
|
-
const sqlUpdate = `UPDATE
|
|
699
|
+
const sqlUpdate = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${virtualEntity.ID}'`;
|
|
595
700
|
await this.LogSQLAndExecute(pool, sqlUpdate, `SQL text to update virtual entity updated date for ${virtualEntity.Name}`);
|
|
596
701
|
}
|
|
597
702
|
return { success: bSuccess, updatedEntity: bUpdated };
|
|
@@ -622,7 +727,7 @@ export class ManageMetadataBase {
|
|
|
622
727
|
field.Sequence !== fieldSequence) {
|
|
623
728
|
// the field needs to be updated, so update it
|
|
624
729
|
const sqlUpdate = `UPDATE
|
|
625
|
-
|
|
730
|
+
${this.qs(mj_core_schema(), 'EntityField')}
|
|
626
731
|
SET
|
|
627
732
|
Sequence=${fieldSequence},
|
|
628
733
|
Type='${veField.Type}',
|
|
@@ -640,13 +745,16 @@ export class ManageMetadataBase {
|
|
|
640
745
|
else {
|
|
641
746
|
// 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
|
|
642
747
|
newEntityFieldUUID = this.createNewUUID();
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
748
|
+
const q = (n) => this.qi(n);
|
|
749
|
+
const sqlAdd = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')} (
|
|
750
|
+
${q('ID')}, ${q('EntityID')}, ${q('Name')}, ${q('Type')}, ${q('AllowsNull')},
|
|
751
|
+
${q('Length')}, ${q('Precision')}, ${q('Scale')},
|
|
752
|
+
${q('Sequence')}, ${q('IsPrimaryKey')}, ${q('IsUnique')},
|
|
753
|
+
${q('__mj_CreatedAt')}, ${q('__mj_UpdatedAt')} )
|
|
647
754
|
VALUES ( '${newEntityFieldUUID}', '${entity.ID}', '${veField.FieldName}', '${veField.Type}', ${veField.AllowsNull ? 1 : 0},
|
|
648
755
|
${veField.Length}, ${veField.Precision}, ${veField.Scale},
|
|
649
|
-
${fieldSequence}, ${makePrimaryKey ? 1 : 0}, ${makePrimaryKey ? 1 : 0}
|
|
756
|
+
${fieldSequence}, ${makePrimaryKey ? 1 : 0}, ${makePrimaryKey ? 1 : 0},
|
|
757
|
+
${this.utcNow()}, ${this.utcNow()}
|
|
650
758
|
)`;
|
|
651
759
|
await this.LogSQLAndExecute(pool, sqlAdd, `SQL text to add virtual entity field ${veField.FieldName} for entity ${virtualEntity.Name}`);
|
|
652
760
|
didUpdate = true;
|
|
@@ -717,9 +825,9 @@ export class ManageMetadataBase {
|
|
|
717
825
|
return { decorated: false, skipped: true };
|
|
718
826
|
}
|
|
719
827
|
// Get view definition from SQL Server
|
|
720
|
-
const viewDefSQL =
|
|
721
|
-
const viewDefResult = await
|
|
722
|
-
const viewDefinition = viewDefResult.recordset[0]?.
|
|
828
|
+
const viewDefSQL = this.dbProvider.getViewDefinitionSQL(entity.SchemaName, entity.BaseView);
|
|
829
|
+
const viewDefResult = await this.runQuery(pool, viewDefSQL);
|
|
830
|
+
const viewDefinition = viewDefResult.recordset[0]?.ViewDefinition;
|
|
723
831
|
if (!viewDefinition) {
|
|
724
832
|
logStatus(` Could not get view definition for ${entity.SchemaName}.${entity.BaseView} — skipping LLM decoration`);
|
|
725
833
|
return { decorated: false, skipped: false };
|
|
@@ -752,7 +860,7 @@ export class ManageMetadataBase {
|
|
|
752
860
|
// Apply categories using the shared methods (same stability rules as regular entities)
|
|
753
861
|
anyUpdated = await this.applyVEFieldCategories(pool, entity, result) || anyUpdated;
|
|
754
862
|
if (anyUpdated) {
|
|
755
|
-
const sqlUpdate = `UPDATE
|
|
863
|
+
const sqlUpdate = `UPDATE ${this.qs(schema, 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${entity.ID}'`;
|
|
756
864
|
await this.LogSQLAndExecute(pool, sqlUpdate, `Update entity timestamp for ${entity.Name} after LLM decoration`);
|
|
757
865
|
}
|
|
758
866
|
return { decorated: anyUpdated, skipped: false };
|
|
@@ -810,10 +918,10 @@ export class ManageMetadataBase {
|
|
|
810
918
|
const schema = mj_core_schema();
|
|
811
919
|
const fieldsSQL = `
|
|
812
920
|
SELECT ID, Name, Category, AutoUpdateCategory, AutoUpdateDisplayName, GeneratedFormSection, DisplayName, ExtendedType, CodeType
|
|
813
|
-
FROM
|
|
921
|
+
FROM ${this.qs(schema, 'EntityField')}
|
|
814
922
|
WHERE EntityID = '${entity.ID}'
|
|
815
923
|
`;
|
|
816
|
-
const fieldsResult = await
|
|
924
|
+
const fieldsResult = await this.runQuery(pool, fieldsSQL);
|
|
817
925
|
const dbFields = fieldsResult.recordset;
|
|
818
926
|
if (dbFields.length === 0)
|
|
819
927
|
return false;
|
|
@@ -860,12 +968,12 @@ export class ManageMetadataBase {
|
|
|
860
968
|
// Build batched SQL: clear default PK + set all LLM-identified PKs
|
|
861
969
|
const sqlStatements = [];
|
|
862
970
|
// Clear existing default PK (field #1 fallback) before applying LLM-identified PKs
|
|
863
|
-
sqlStatements.push(`UPDATE
|
|
971
|
+
sqlStatements.push(`UPDATE ${this.qs(schema, 'EntityField')}
|
|
864
972
|
SET IsPrimaryKey=0, IsUnique=0
|
|
865
973
|
WHERE EntityID='${entity.ID}' AND IsPrimaryKey=1 AND IsSoftPrimaryKey=0`);
|
|
866
974
|
// Set LLM-identified PKs
|
|
867
975
|
for (const pk of validPKs) {
|
|
868
|
-
sqlStatements.push(`UPDATE
|
|
976
|
+
sqlStatements.push(`UPDATE ${this.qs(schema, 'EntityField')}
|
|
869
977
|
SET IsPrimaryKey=1, IsUnique=1, IsSoftPrimaryKey=1
|
|
870
978
|
WHERE EntityID='${entity.ID}' AND Name='${pk}'`);
|
|
871
979
|
logStatus(` ✓ Set PK for ${entity.Name}.${pk} (LLM-identified)`);
|
|
@@ -905,7 +1013,7 @@ export class ManageMetadataBase {
|
|
|
905
1013
|
logStatus(` ⚠️ LLM FK: related entity '${fk.relatedEntityName}' not found for ${entity.Name}.${fk.fieldName}`);
|
|
906
1014
|
continue;
|
|
907
1015
|
}
|
|
908
|
-
sqlStatements.push(`UPDATE
|
|
1016
|
+
sqlStatements.push(`UPDATE ${this.qs(schema, 'EntityField')}
|
|
909
1017
|
SET RelatedEntityID='${relatedEntity.ID}',
|
|
910
1018
|
RelatedEntityFieldName='${fk.relatedFieldName}',
|
|
911
1019
|
IsSoftForeignKey=1
|
|
@@ -945,7 +1053,7 @@ export class ManageMetadataBase {
|
|
|
945
1053
|
setClauses += `, ExtendedType='${validExtendedType}'`;
|
|
946
1054
|
}
|
|
947
1055
|
}
|
|
948
|
-
sqlStatements.push(`UPDATE
|
|
1056
|
+
sqlStatements.push(`UPDATE ${this.qs(schema, 'EntityField')}
|
|
949
1057
|
SET ${setClauses}
|
|
950
1058
|
WHERE EntityID='${entity.ID}' AND Name='${field.Name}'`);
|
|
951
1059
|
}
|
|
@@ -1051,12 +1159,9 @@ export class ManageMetadataBase {
|
|
|
1051
1159
|
}
|
|
1052
1160
|
// Check the DATABASE for existing field record — in-memory metadata may be stale
|
|
1053
1161
|
// (e.g. createNewEntityFieldsFromSchema may have already added this field from the view)
|
|
1054
|
-
const existsResult = await
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
.query(`SELECT ID, IsVirtual, Type, Length, Precision, Scale, AllowsNull, AllowUpdateAPI
|
|
1058
|
-
FROM [${mj_core_schema()}].EntityField
|
|
1059
|
-
WHERE EntityID = @EntityID AND Name = @FieldName`);
|
|
1162
|
+
const existsResult = await this.runQueryWithParams(pool, `SELECT ID, IsVirtual, Type, Length, Precision, Scale, AllowsNull, AllowUpdateAPI
|
|
1163
|
+
FROM ${this.qs(mj_core_schema(), 'EntityField')}
|
|
1164
|
+
WHERE EntityID = @EntityID AND Name = @FieldName`, { 'EntityID': childEntity.ID, 'FieldName': parentField.Name });
|
|
1060
1165
|
if (existsResult.recordset.length > 0) {
|
|
1061
1166
|
// Field already exists — update it to ensure it's marked as a virtual IS-A field
|
|
1062
1167
|
const existingRow = existsResult.recordset[0];
|
|
@@ -1068,7 +1173,7 @@ export class ManageMetadataBase {
|
|
|
1068
1173
|
existingRow.AllowsNull !== parentField.AllowsNull ||
|
|
1069
1174
|
!existingRow.AllowUpdateAPI;
|
|
1070
1175
|
if (needsUpdate) {
|
|
1071
|
-
const sqlUpdate = `UPDATE
|
|
1176
|
+
const sqlUpdate = `UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
1072
1177
|
SET IsVirtual=1,
|
|
1073
1178
|
Type='${parentField.Type}',
|
|
1074
1179
|
Length=${parentField.Length},
|
|
@@ -1086,16 +1191,19 @@ export class ManageMetadataBase {
|
|
|
1086
1191
|
const newFieldID = this.createNewUUID();
|
|
1087
1192
|
// Use high sequence — will be reordered by updateExistingEntityFieldsFromSchema
|
|
1088
1193
|
const sequence = 100000 + parentFields.indexOf(parentField);
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1194
|
+
const q = (n) => this.qi(n);
|
|
1195
|
+
const sqlInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')} (
|
|
1196
|
+
${q('ID')}, ${q('EntityID')}, ${q('Name')}, ${q('Type')}, ${q('AllowsNull')},
|
|
1197
|
+
${q('Length')}, ${q('Precision')}, ${q('Scale')},
|
|
1198
|
+
${q('Sequence')}, ${q('IsVirtual')}, ${q('AllowUpdateAPI')},
|
|
1199
|
+
${q('IsPrimaryKey')}, ${q('IsUnique')},
|
|
1200
|
+
${q('__mj_CreatedAt')}, ${q('__mj_UpdatedAt')})
|
|
1094
1201
|
VALUES (
|
|
1095
1202
|
'${newFieldID}', '${childEntity.ID}', '${parentField.Name}',
|
|
1096
1203
|
'${parentField.Type}', ${parentField.AllowsNull ? 1 : 0},
|
|
1097
1204
|
${parentField.Length}, ${parentField.Precision}, ${parentField.Scale},
|
|
1098
|
-
${sequence}, 1, 1, 0, 0
|
|
1205
|
+
${sequence}, 1, 1, 0, 0,
|
|
1206
|
+
${this.utcNow()}, ${this.utcNow()})`;
|
|
1099
1207
|
await this.LogSQLAndExecute(pool, sqlInsert, `Create IS-A parent field ${parentField.Name} on ${childEntity.Name}`);
|
|
1100
1208
|
bUpdated = true;
|
|
1101
1209
|
}
|
|
@@ -1107,12 +1215,12 @@ export class ManageMetadataBase {
|
|
|
1107
1215
|
!f.IsPrimaryKey && !f.Name.startsWith('__mj_') &&
|
|
1108
1216
|
!currentParentFieldNames.has(f.Name.toLowerCase()));
|
|
1109
1217
|
for (const staleField of staleFields) {
|
|
1110
|
-
const sqlDelete = `DELETE FROM
|
|
1218
|
+
const sqlDelete = `DELETE FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ID='${staleField.ID}'`;
|
|
1111
1219
|
await this.LogSQLAndExecute(pool, sqlDelete, `Remove stale IS-A parent field ${staleField.Name} from ${childEntity.Name}`);
|
|
1112
1220
|
bUpdated = true;
|
|
1113
1221
|
}
|
|
1114
1222
|
if (bUpdated) {
|
|
1115
|
-
const sqlUpdate = `UPDATE
|
|
1223
|
+
const sqlUpdate = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${childEntity.ID}'`;
|
|
1116
1224
|
await this.LogSQLAndExecute(pool, sqlUpdate, `Update entity timestamp for ${childEntity.Name} after IS-A field sync`);
|
|
1117
1225
|
}
|
|
1118
1226
|
return { success: true, updated: bUpdated };
|
|
@@ -1164,55 +1272,40 @@ export class ManageMetadataBase {
|
|
|
1164
1272
|
try {
|
|
1165
1273
|
// STEP 1 - search for all foreign keys in the vwEntityFields view, we use the RelatedEntityID field to determine our FKs
|
|
1166
1274
|
const sSQL = `SELECT *
|
|
1167
|
-
FROM ${mj_core_schema()}
|
|
1275
|
+
FROM ${this.qs(mj_core_schema(), 'vwEntityFields')}
|
|
1168
1276
|
WHERE
|
|
1169
1277
|
RelatedEntityID IS NOT NULL AND
|
|
1170
1278
|
IsVirtual = 0 AND
|
|
1171
|
-
EntityID NOT IN (SELECT ID FROM ${mj_core_schema()}
|
|
1279
|
+
EntityID NOT IN (SELECT ID FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
|
|
1172
1280
|
ORDER BY RelatedEntityID`;
|
|
1173
|
-
const entityFieldsResult = await
|
|
1281
|
+
const entityFieldsResult = await this.runQuery(pool, sSQL);
|
|
1174
1282
|
const entityFields = entityFieldsResult.recordset;
|
|
1175
1283
|
// Get the relationship counts for each entity
|
|
1176
|
-
const sSQLRelationshipCount = `SELECT EntityID, COUNT(*) AS Count FROM ${mj_core_schema()}
|
|
1177
|
-
const relationshipCountsResult = await
|
|
1284
|
+
const sSQLRelationshipCount = `SELECT ${this.qi('EntityID')}, COUNT(*) AS ${this.qi('Count')} FROM ${this.qs(mj_core_schema(), 'EntityRelationship')} GROUP BY ${this.qi('EntityID')}`;
|
|
1285
|
+
const relationshipCountsResult = await this.runQuery(pool, sSQLRelationshipCount);
|
|
1178
1286
|
const relationshipCounts = relationshipCountsResult.recordset;
|
|
1179
1287
|
const relationshipCountMap = new Map();
|
|
1180
1288
|
for (const rc of relationshipCounts) {
|
|
1181
|
-
|
|
1289
|
+
// Use Number() because PG returns COUNT(*) as bigint, which the pg driver delivers as a string
|
|
1290
|
+
relationshipCountMap.set(rc.EntityID, Number(rc.Count));
|
|
1182
1291
|
}
|
|
1183
1292
|
// get all relationships in one query for performance improvement
|
|
1184
|
-
const sSQLRelationship = `SELECT * FROM ${mj_core_schema()}
|
|
1185
|
-
const allRelationshipsResult = await
|
|
1293
|
+
const sSQLRelationship = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityRelationship')}`;
|
|
1294
|
+
const allRelationshipsResult = await this.runQuery(pool, sSQLRelationship);
|
|
1186
1295
|
const allRelationships = allRelationshipsResult.recordset;
|
|
1187
1296
|
// Function to process a batch of entity fields
|
|
1188
1297
|
const processBatch = async (batch) => {
|
|
1189
1298
|
let batchSQL = '';
|
|
1190
1299
|
batch.forEach((f) => {
|
|
1191
1300
|
// for each field determine if an existing relationship exists, if not, create it
|
|
1192
|
-
const relationships = allRelationships.filter((r) => r.EntityID
|
|
1301
|
+
const relationships = allRelationships.filter((r) => UUIDsEqual(r.EntityID, f.RelatedEntityID) && UUIDsEqual(r.RelatedEntityID, f.EntityID));
|
|
1193
1302
|
if (relationships && relationships.length === 0) {
|
|
1194
1303
|
// no relationship exists, so create it
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
//
|
|
1199
|
-
|
|
1200
|
-
const sequence = relCount + 1;
|
|
1201
|
-
const newEntityRelationshipUUID = this.createNewUUID();
|
|
1202
|
-
batchSQL += `
|
|
1203
|
-
/* Create Entity Relationship: ${parentEntityName} -> ${e.Name} (One To Many via ${f.Name}) */
|
|
1204
|
-
IF NOT EXISTS (
|
|
1205
|
-
SELECT 1
|
|
1206
|
-
FROM [${mj_core_schema()}].EntityRelationship
|
|
1207
|
-
WHERE ID = '${newEntityRelationshipUUID}'
|
|
1208
|
-
)
|
|
1209
|
-
BEGIN
|
|
1210
|
-
INSERT INTO ${mj_core_schema()}.EntityRelationship (ID, EntityID, RelatedEntityID, RelatedEntityJoinField, Type, BundleInAPI, DisplayInForm, DisplayName, Sequence)
|
|
1211
|
-
VALUES ('${newEntityRelationshipUUID}', '${f.RelatedEntityID}', '${f.EntityID}', '${f.Name}', 'One To Many', 1, 1, '${e.Name}', ${sequence});
|
|
1212
|
-
END
|
|
1213
|
-
`;
|
|
1214
|
-
// now update the map for the relationship count
|
|
1215
|
-
relationshipCountMap.set(f.EntityID, sequence);
|
|
1304
|
+
batchSQL += this.buildInsertRelationshipSQL(f, md, relationshipCountMap);
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
// relationship(s) exist - check if any need their join field updated
|
|
1308
|
+
batchSQL += this.buildUpdateRelationshipJoinFieldSQL(relationships, f);
|
|
1216
1309
|
}
|
|
1217
1310
|
});
|
|
1218
1311
|
if (batchSQL.length > 0) {
|
|
@@ -1224,6 +1317,11 @@ export class ManageMetadataBase {
|
|
|
1224
1317
|
const batch = entityFields.slice(i, i + batchItems);
|
|
1225
1318
|
await processBatch(batch);
|
|
1226
1319
|
}
|
|
1320
|
+
// NOTE: Stale relationship cleanup is intentionally NOT done here because this
|
|
1321
|
+
// method runs BEFORE deleteUnneededEntityFields(). Stale EntityField records
|
|
1322
|
+
// (for dropped columns) still exist at this point and would prevent detection
|
|
1323
|
+
// of stale relationships. The cleanup is handled by cleanupStaleEntityRelationships()
|
|
1324
|
+
// which is called from sql_codegen.ts AFTER entity fields are fully synced.
|
|
1227
1325
|
return true;
|
|
1228
1326
|
}
|
|
1229
1327
|
catch (e) {
|
|
@@ -1231,6 +1329,103 @@ export class ManageMetadataBase {
|
|
|
1231
1329
|
return false;
|
|
1232
1330
|
}
|
|
1233
1331
|
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Builds SQL to INSERT a new EntityRelationship record for a discovered FK field.
|
|
1334
|
+
*/
|
|
1335
|
+
buildInsertRelationshipSQL(f, md, relationshipCountMap) {
|
|
1336
|
+
const e = md.Entities.find(e => UUIDsEqual(e.ID, f.EntityID));
|
|
1337
|
+
const parentEntity = md.Entities.find(e => UUIDsEqual(e.ID, f.RelatedEntityID));
|
|
1338
|
+
const parentEntityName = parentEntity ? parentEntity.Name : String(f.RelatedEntityID);
|
|
1339
|
+
const relCount = relationshipCountMap.get(f.EntityID) || 0;
|
|
1340
|
+
const sequence = relCount + 1;
|
|
1341
|
+
const newEntityRelationshipUUID = this.createNewUUID();
|
|
1342
|
+
const checkQuery = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'EntityRelationship')} WHERE ${this.qi('ID')} = '${newEntityRelationshipUUID}'`;
|
|
1343
|
+
const insertSQL = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityRelationship')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('RelatedEntityID')}, ${this.qi('RelatedEntityJoinField')}, ${this.qi('Type')}, ${this.qi('BundleInAPI')}, ${this.qi('DisplayInForm')}, ${this.qi('Sequence')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')})
|
|
1344
|
+
VALUES ('${newEntityRelationshipUUID}', '${f.RelatedEntityID}', '${f.EntityID}', '${f.Name}', 'One To Many', ${this.boolLit(true)}, ${this.boolLit(true)}, ${sequence}, ${this.utcNow()}, ${this.utcNow()})`;
|
|
1345
|
+
relationshipCountMap.set(f.EntityID, sequence);
|
|
1346
|
+
return `
|
|
1347
|
+
/* Create Entity Relationship: ${parentEntityName} -> ${e.Name} (One To Many via ${f.Name}) */
|
|
1348
|
+
${this.dbProvider.conditionalInsertSQL(checkQuery, insertSQL)};
|
|
1349
|
+
`;
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Builds SQL to UPDATE an existing EntityRelationship's RelatedEntityJoinField if it no longer
|
|
1353
|
+
* matches the current FK field name. Only updates relationships where AutoUpdateFromSchema = true.
|
|
1354
|
+
*/
|
|
1355
|
+
buildUpdateRelationshipJoinFieldSQL(relationships, f) {
|
|
1356
|
+
let sql = '';
|
|
1357
|
+
for (const r of relationships) {
|
|
1358
|
+
if (r.AutoUpdateFromSchema && String(r.RelatedEntityJoinField).trim() !== String(f.Name).trim()) {
|
|
1359
|
+
logStatus(` > Updating EntityRelationship join field: ${r.RelatedEntityJoinField} -> ${f.Name} (ID: ${r.ID})`);
|
|
1360
|
+
sql += `
|
|
1361
|
+
/* Update EntityRelationship join field from '${r.RelatedEntityJoinField}' to '${f.Name}' */
|
|
1362
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntityRelationship')}
|
|
1363
|
+
SET ${this.qi('RelatedEntityJoinField')} = '${f.Name}',
|
|
1364
|
+
${this.qi('__mj_UpdatedAt')} = ${this.utcNow()}
|
|
1365
|
+
WHERE ${this.qi('ID')} = '${r.ID}';
|
|
1366
|
+
`;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return sql;
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Public entry point for cleaning up stale EntityRelationship records. This must be called
|
|
1373
|
+
* AFTER deleteUnneededEntityFields() has run, so that stale EntityField records (for dropped
|
|
1374
|
+
* columns) are removed before we check which relationships are still valid.
|
|
1375
|
+
* Called from sql_codegen.ts after the second manageEntityFields() pass.
|
|
1376
|
+
* @param pool - database connection
|
|
1377
|
+
* @param excludeSchemas - schemas to exclude from FK field lookup
|
|
1378
|
+
*/
|
|
1379
|
+
async cleanupStaleEntityRelationships(pool, excludeSchemas) {
|
|
1380
|
+
try {
|
|
1381
|
+
const sSQL = `SELECT *
|
|
1382
|
+
FROM ${this.qs(mj_core_schema(), 'vwEntityFields')}
|
|
1383
|
+
WHERE
|
|
1384
|
+
RelatedEntityID IS NOT NULL AND
|
|
1385
|
+
IsVirtual = 0 AND
|
|
1386
|
+
EntityID NOT IN (SELECT ID FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
|
|
1387
|
+
ORDER BY RelatedEntityID`;
|
|
1388
|
+
const entityFieldsResult = await this.runQuery(pool, sSQL);
|
|
1389
|
+
const entityFields = entityFieldsResult.recordset;
|
|
1390
|
+
const sSQLRelationship = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityRelationship')}`;
|
|
1391
|
+
const allRelationshipsResult = await this.runQuery(pool, sSQLRelationship);
|
|
1392
|
+
const allRelationships = allRelationshipsResult.recordset;
|
|
1393
|
+
await this.removeStaleOneToManyRelationships(pool, allRelationships, entityFields);
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1396
|
+
catch (e) {
|
|
1397
|
+
logError(e);
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Removes stale One-To-Many EntityRelationship records whose RelatedEntityJoinField no longer
|
|
1403
|
+
* corresponds to a valid FK field in the database. Only removes relationships where
|
|
1404
|
+
* AutoUpdateFromSchema = true and Type = 'One To Many'.
|
|
1405
|
+
*/
|
|
1406
|
+
async removeStaleOneToManyRelationships(pool, allRelationships, entityFields) {
|
|
1407
|
+
// Build a set of valid (ParentEntityID, ChildEntityID, JoinFieldName) tuples from current FK fields.
|
|
1408
|
+
// In EntityRelationship terms: EntityID = ParentEntityID (the "one" side), RelatedEntityID = ChildEntityID (the "many" side)
|
|
1409
|
+
// In entityFields: f.RelatedEntityID = ParentEntityID, f.EntityID = ChildEntityID, f.Name = FK field name
|
|
1410
|
+
const validRelationshipKeys = new Set();
|
|
1411
|
+
for (const f of entityFields) {
|
|
1412
|
+
validRelationshipKeys.add(`${f.RelatedEntityID}|${f.EntityID}|${f.Name}`);
|
|
1413
|
+
}
|
|
1414
|
+
const staleRelationships = allRelationships.filter(r => String(r.Type).trim() === 'One To Many' &&
|
|
1415
|
+
r.AutoUpdateFromSchema === true &&
|
|
1416
|
+
!validRelationshipKeys.has(`${r.EntityID}|${r.RelatedEntityID}|${r.RelatedEntityJoinField}`));
|
|
1417
|
+
if (staleRelationships.length > 0) {
|
|
1418
|
+
let deleteSQL = '';
|
|
1419
|
+
for (const r of staleRelationships) {
|
|
1420
|
+
logStatus(` > Removing stale EntityRelationship: ${r.Entity} -> ${r.RelatedEntity} via '${r.RelatedEntityJoinField}' (ID: ${r.ID})`);
|
|
1421
|
+
deleteSQL += `
|
|
1422
|
+
/* Remove stale EntityRelationship: ${r.Entity} -> ${r.RelatedEntity} (FK field '${r.RelatedEntityJoinField}' no longer exists) */
|
|
1423
|
+
DELETE FROM ${this.qs(mj_core_schema(), 'EntityRelationship')} WHERE ${this.qi('ID')} = '${r.ID}';
|
|
1424
|
+
`;
|
|
1425
|
+
}
|
|
1426
|
+
await this.LogSQLAndExecute(pool, deleteSQL, 'Remove stale One-To-Many EntityRelationships');
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1234
1429
|
/**
|
|
1235
1430
|
* This method will look for situations where entity metadata exist in the entities metadata table but the underlying table has been deleted. In this case, the metadata for the entity
|
|
1236
1431
|
* should be removed. This method is called as part of the manageMetadata method and is not intended to be called directly.
|
|
@@ -1239,8 +1434,8 @@ export class ManageMetadataBase {
|
|
|
1239
1434
|
*/
|
|
1240
1435
|
async checkAndRemoveMetadataForDeletedTables(pool, excludeSchemas) {
|
|
1241
1436
|
try {
|
|
1242
|
-
const sql = `SELECT * FROM ${mj_core_schema()}.
|
|
1243
|
-
const entitiesResult = await
|
|
1437
|
+
const sql = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntitiesWithMissingBaseTables')}${this.dbProvider.getEntitiesWithMissingBaseTablesFilter()}`;
|
|
1438
|
+
const entitiesResult = await this.runQuery(pool, sql);
|
|
1244
1439
|
const entities = entitiesResult.recordset;
|
|
1245
1440
|
if (entities && entities.length > 0) {
|
|
1246
1441
|
for (const e of entities) {
|
|
@@ -1248,7 +1443,7 @@ export class ManageMetadataBase {
|
|
|
1248
1443
|
// the below could fail if there are non-core dependencies on the entity, but that's ok, we will flag that in the console
|
|
1249
1444
|
// for the admin to handle manually
|
|
1250
1445
|
try {
|
|
1251
|
-
const sqlDelete =
|
|
1446
|
+
const sqlDelete = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spDeleteEntityWithCoreDependencies', [`'${e.ID}'`], ['EntityID']);
|
|
1252
1447
|
await this.LogSQLAndExecute(pool, sqlDelete, `SQL text to remove entity ${e.Name}`);
|
|
1253
1448
|
logStatus(` > Removed metadata for table ${e.SchemaName}.${e.BaseTable}`);
|
|
1254
1449
|
// next up we need to remove the spCreate, spDelete, spUpdate, BaseView, and FullTextSearchFunction, if provided.
|
|
@@ -1279,9 +1474,8 @@ export class ManageMetadataBase {
|
|
|
1279
1474
|
if (proceed && schemaName && name && schemaName.trim().length > 0 && name.trim().length > 0) {
|
|
1280
1475
|
// Use IF OBJECT_ID pattern for Flyway compatibility
|
|
1281
1476
|
// Object type codes: P = Stored Procedure, V = View, FN = Scalar Function, IF/TF = Table-Valued Function
|
|
1282
|
-
const objectTypeCode = type === 'procedure' ? 'P' : type === 'view' ? 'V' : 'FN';
|
|
1283
1477
|
const upperType = type.toUpperCase();
|
|
1284
|
-
const sqlDelete =
|
|
1478
|
+
const sqlDelete = this.dbProvider.dropObjectSQL(upperType, schemaName, name);
|
|
1285
1479
|
await this.LogSQLAndExecute(pool, sqlDelete, `SQL text to remove ${type} ${schemaName}.${name}`);
|
|
1286
1480
|
// next up, we need to clean up the cache of saved DB objects that may exist for this entity in the appropriate sub-directory.
|
|
1287
1481
|
const sqlOutputDir = outputDir('SQL', true);
|
|
@@ -1350,6 +1544,16 @@ export class ManageMetadataBase {
|
|
|
1350
1544
|
bSuccess = false;
|
|
1351
1545
|
}
|
|
1352
1546
|
logStatus(` Created new entity fields from schema in ${(new Date().getTime() - step2StartTime.getTime()) / 1000} seconds`);
|
|
1547
|
+
// PostgreSQL view columns always report attnotnull=false, so virtual fields
|
|
1548
|
+
// (FK join columns like "Location", "Class") incorrectly get AllowsNull=true.
|
|
1549
|
+
// Fix: derive virtual field AllowsNull from the FK column that drives the JOIN.
|
|
1550
|
+
// The virtual field name = FK field name minus trailing "ID" (e.g. ClassID → Class).
|
|
1551
|
+
if (this.dbProvider.NeedsVirtualFieldNullabilityFix) {
|
|
1552
|
+
const fixSQL = this.dbProvider.getFixVirtualFieldNullabilitySQL(mj_core_schema());
|
|
1553
|
+
if (fixSQL) {
|
|
1554
|
+
await this.LogSQLAndExecute(pool, fixSQL, 'SQL to fix virtual field nullability');
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1353
1557
|
// AN: 14-June-2025 - we are now running this AFTER we create new entity fields from schema
|
|
1354
1558
|
// which results in the same pattern of behavior as migrations where we first create new fields
|
|
1355
1559
|
// with VERY HIGH sequence numbers (e.g. 100,000 above what they will be approx) and then
|
|
@@ -1425,23 +1629,26 @@ export class ManageMetadataBase {
|
|
|
1425
1629
|
const sqlEntities = `SELECT
|
|
1426
1630
|
*
|
|
1427
1631
|
FROM
|
|
1428
|
-
|
|
1632
|
+
${this.qs(mj_core_schema(), 'vwEntities')}
|
|
1429
1633
|
WHERE
|
|
1430
1634
|
VirtualEntity=0 AND
|
|
1431
1635
|
DeleteType='Soft' AND
|
|
1432
1636
|
SchemaName NOT IN (${excludeSchemas.map(s => `'${s}'`).join(',')})`;
|
|
1433
|
-
const entitiesResult = await
|
|
1637
|
+
const entitiesResult = await this.runQuery(pool, sqlEntities);
|
|
1434
1638
|
const entities = entitiesResult.recordset;
|
|
1435
1639
|
let overallResult = true;
|
|
1436
1640
|
if (entities.length > 0) {
|
|
1437
1641
|
// we have 1+ entities that need the special fields, so loop through them and ensure the fields exist
|
|
1438
1642
|
// validate that each entity has the __mj_DeletedAt field, and it is a DATETIMEOFFSET fields, NOT NULL and both are fields that have a DEFAULT value of GETUTCDATE().
|
|
1439
|
-
const sql = `SELECT
|
|
1643
|
+
const sql = `SELECT
|
|
1644
|
+
TABLE_SCHEMA AS "TABLE_SCHEMA", TABLE_NAME AS "TABLE_NAME",
|
|
1645
|
+
COLUMN_NAME AS "COLUMN_NAME", DATA_TYPE AS "DATA_TYPE",
|
|
1646
|
+
IS_NULLABLE AS "IS_NULLABLE", COLUMN_DEFAULT AS "COLUMN_DEFAULT"
|
|
1440
1647
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
1441
1648
|
WHERE
|
|
1442
|
-
${entities.map((e) => `(TABLE_SCHEMA='${e.SchemaName}' AND TABLE_NAME='${e.BaseTable}')`).join(' OR ')}
|
|
1649
|
+
(${entities.map((e) => `(TABLE_SCHEMA='${e.SchemaName}' AND TABLE_NAME='${e.BaseTable}')`).join(' OR ')})
|
|
1443
1650
|
AND COLUMN_NAME='${EntityInfo.DeletedAtFieldName}'`;
|
|
1444
|
-
const resultResult = await
|
|
1651
|
+
const resultResult = await this.runQuery(pool, sql);
|
|
1445
1652
|
const result = resultResult.recordset;
|
|
1446
1653
|
for (const e of entities) {
|
|
1447
1654
|
const eResult = result.filter((r) => r.TABLE_NAME === e.BaseTable && r.TABLE_SCHEMA === e.SchemaName); // get just the fields for this entity
|
|
@@ -1490,8 +1697,8 @@ export class ManageMetadataBase {
|
|
|
1490
1697
|
const tableSchema = table.SchemaName;
|
|
1491
1698
|
const tableName = table.TableName;
|
|
1492
1699
|
// Look up entity ID (SELECT query - no need to log to migration file)
|
|
1493
|
-
const entityLookupSQL = `SELECT ID FROM
|
|
1494
|
-
const entityResult = await
|
|
1700
|
+
const entityLookupSQL = `SELECT ID FROM ${this.qs(schema, 'Entity')} WHERE SchemaName = '${tableSchema}' AND BaseTable = '${tableName}'`;
|
|
1701
|
+
const entityResult = await this.runQuery(pool, entityLookupSQL);
|
|
1495
1702
|
if (entityResult.recordset.length === 0) {
|
|
1496
1703
|
logStatus(` ⚠️ Entity not found for ${tableSchema}.${tableName} - skipping`);
|
|
1497
1704
|
continue;
|
|
@@ -1502,11 +1709,11 @@ export class ManageMetadataBase {
|
|
|
1502
1709
|
const primaryKeys = table.PrimaryKey || [];
|
|
1503
1710
|
if (primaryKeys.length > 0) {
|
|
1504
1711
|
for (const pk of primaryKeys) {
|
|
1505
|
-
const sSQL = `UPDATE
|
|
1506
|
-
SET ${EntityInfo.UpdatedAtFieldName}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
WHERE
|
|
1712
|
+
const sSQL = `UPDATE ${this.qs(schema, 'EntityField')}
|
|
1713
|
+
SET ${EntityInfo.UpdatedAtFieldName}=${this.utcNow()},
|
|
1714
|
+
${this.qi('IsPrimaryKey')} = 1,
|
|
1715
|
+
${this.qi('IsSoftPrimaryKey')} = 1
|
|
1716
|
+
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = '${pk.FieldName}'`;
|
|
1510
1717
|
const result = await this.LogSQLAndExecute(pool, sSQL, `Set soft PK for ${tableSchema}.${tableName}.${pk.FieldName}`);
|
|
1511
1718
|
if (result !== null) {
|
|
1512
1719
|
logStatus(` ✓ Set IsPrimaryKey=1, IsSoftPrimaryKey=1 for ${tableName}.${pk.FieldName}`);
|
|
@@ -1520,19 +1727,19 @@ export class ManageMetadataBase {
|
|
|
1520
1727
|
for (const fk of foreignKeys) {
|
|
1521
1728
|
const fkSchema = fk.SchemaName || tableSchema;
|
|
1522
1729
|
// Look up related entity ID (SELECT query - no need to log to migration file)
|
|
1523
|
-
const relatedLookupSQL = `SELECT ID FROM
|
|
1524
|
-
const relatedEntityResult = await
|
|
1730
|
+
const relatedLookupSQL = `SELECT ID FROM ${this.qs(schema, 'Entity')} WHERE SchemaName = '${fkSchema}' AND BaseTable = '${fk.RelatedTable}'`;
|
|
1731
|
+
const relatedEntityResult = await this.runQuery(pool, relatedLookupSQL);
|
|
1525
1732
|
if (relatedEntityResult.recordset.length === 0) {
|
|
1526
1733
|
logStatus(` ⚠️ Related entity not found for ${fkSchema}.${fk.RelatedTable} - skipping FK ${fk.FieldName}`);
|
|
1527
1734
|
continue;
|
|
1528
1735
|
}
|
|
1529
1736
|
const relatedEntityId = relatedEntityResult.recordset[0].ID;
|
|
1530
|
-
const sSQL = `UPDATE
|
|
1531
|
-
SET ${EntityInfo.UpdatedAtFieldName}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
WHERE
|
|
1737
|
+
const sSQL = `UPDATE ${this.qs(schema, 'EntityField')}
|
|
1738
|
+
SET ${EntityInfo.UpdatedAtFieldName}=${this.utcNow()},
|
|
1739
|
+
${this.qi('RelatedEntityID')} = '${relatedEntityId}',
|
|
1740
|
+
${this.qi('RelatedEntityFieldName')} = '${fk.RelatedField}',
|
|
1741
|
+
${this.qi('IsSoftForeignKey')} = 1
|
|
1742
|
+
WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = '${fk.FieldName}'`;
|
|
1536
1743
|
const result = await this.LogSQLAndExecute(pool, sSQL, `Set soft FK for ${tableSchema}.${tableName}.${fk.FieldName} → ${fk.RelatedTable}.${fk.RelatedField}`);
|
|
1537
1744
|
if (result !== null) {
|
|
1538
1745
|
logStatus(` ✓ Set soft FK for ${tableName}.${fk.FieldName} → ${fk.RelatedTable}.${fk.RelatedField}`);
|
|
@@ -1560,24 +1767,27 @@ export class ManageMetadataBase {
|
|
|
1560
1767
|
const sqlEntities = `SELECT
|
|
1561
1768
|
*
|
|
1562
1769
|
FROM
|
|
1563
|
-
|
|
1770
|
+
${this.qs(mj_core_schema(), 'vwEntities')}
|
|
1564
1771
|
WHERE
|
|
1565
1772
|
VirtualEntity = 0 AND
|
|
1566
1773
|
TrackRecordChanges = 1 AND
|
|
1567
1774
|
SchemaName NOT IN (${excludeSchemas.map(s => `'${s}'`).join(',')})`;
|
|
1568
|
-
const entitiesResult = await
|
|
1775
|
+
const entitiesResult = await this.runQuery(pool, sqlEntities);
|
|
1569
1776
|
const entities = entitiesResult.recordset;
|
|
1570
1777
|
let overallResult = true;
|
|
1571
1778
|
if (entities.length > 0) {
|
|
1572
1779
|
// we have 1+ entities that need the special fields, so loop through them and ensure the fields exist
|
|
1573
1780
|
// validate that each entity has two specific fields, the first one is __mj_CreatedAt and the second one is __mj_UpdatedAt
|
|
1574
1781
|
// both are DATETIME fields, NOT NULL and both are fields that have a DEFAULT value of GETUTCDATE().
|
|
1575
|
-
const sqlCreatedUpdated = `SELECT
|
|
1782
|
+
const sqlCreatedUpdated = `SELECT
|
|
1783
|
+
TABLE_SCHEMA AS "TABLE_SCHEMA", TABLE_NAME AS "TABLE_NAME",
|
|
1784
|
+
COLUMN_NAME AS "COLUMN_NAME", DATA_TYPE AS "DATA_TYPE",
|
|
1785
|
+
IS_NULLABLE AS "IS_NULLABLE", COLUMN_DEFAULT AS "COLUMN_DEFAULT"
|
|
1576
1786
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
1577
1787
|
WHERE
|
|
1578
|
-
${entities.map((e) => `(TABLE_SCHEMA='${e.SchemaName}' AND TABLE_NAME='${e.BaseTable}')`).join(' OR ')}
|
|
1788
|
+
(${entities.map((e) => `(TABLE_SCHEMA='${e.SchemaName}' AND TABLE_NAME='${e.BaseTable}')`).join(' OR ')})
|
|
1579
1789
|
AND COLUMN_NAME IN ('${EntityInfo.CreatedAtFieldName}','${EntityInfo.UpdatedAtFieldName}')`;
|
|
1580
|
-
const resultResult = await
|
|
1790
|
+
const resultResult = await this.runQuery(pool, sqlCreatedUpdated);
|
|
1581
1791
|
const result = resultResult.recordset;
|
|
1582
1792
|
for (const e of entities) {
|
|
1583
1793
|
// 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
|
|
@@ -1608,18 +1818,19 @@ export class ManageMetadataBase {
|
|
|
1608
1818
|
try {
|
|
1609
1819
|
if (!currentFieldData) {
|
|
1610
1820
|
// field doesn't exist, let's create it
|
|
1611
|
-
const sql =
|
|
1821
|
+
const sql = this.dbProvider.addColumnSQL(entity.SchemaName, entity.BaseTable, fieldName, this.timestampType, allowNull, allowNull ? undefined : this.utcNow());
|
|
1612
1822
|
await this.LogSQLAndExecute(pool, sql, `SQL text to add special date field ${fieldName} to entity ${entity.SchemaName}.${entity.BaseTable}`);
|
|
1613
1823
|
}
|
|
1614
1824
|
else {
|
|
1615
1825
|
// field does exist, let's first check the data type/nullability
|
|
1616
|
-
|
|
1826
|
+
const dataTypeMatches = this.dbProvider.compareDataTypes(currentFieldData.DATA_TYPE.trim().toLowerCase(), this.timestampType.toLowerCase());
|
|
1827
|
+
if (!dataTypeMatches ||
|
|
1617
1828
|
(currentFieldData.IS_NULLABLE.trim().toLowerCase() !== 'no' && !allowNull) ||
|
|
1618
1829
|
(currentFieldData.IS_NULLABLE.trim().toLowerCase() === 'no' && allowNull)) {
|
|
1619
1830
|
// the column is the wrong type, or has wrong nullability attribute, so let's update it, first removing the default constraint, then
|
|
1620
1831
|
// modifying the column, and finally adding the default constraint back in.
|
|
1621
1832
|
await this.dropExistingDefaultConstraint(pool, entity, fieldName);
|
|
1622
|
-
const sql =
|
|
1833
|
+
const sql = this.dbProvider.alterColumnTypeAndNullabilitySQL(entity.SchemaName, entity.BaseTable, fieldName, this.timestampType, allowNull);
|
|
1623
1834
|
await this.LogSQLAndExecute(pool, sql, `SQL text to update special date field ${fieldName} in entity ${entity.SchemaName}.${entity.BaseTable}`);
|
|
1624
1835
|
if (!allowNull)
|
|
1625
1836
|
await this.createDefaultConstraintForSpecialDateField(pool, entity, fieldName);
|
|
@@ -1630,7 +1841,7 @@ export class ManageMetadataBase {
|
|
|
1630
1841
|
if (!allowNull) {
|
|
1631
1842
|
const defaultValue = currentFieldData.COLUMN_DEFAULT;
|
|
1632
1843
|
const realDefaultValue = ExtractActualDefaultValue(defaultValue);
|
|
1633
|
-
if (!realDefaultValue || realDefaultValue.trim().toLowerCase() !==
|
|
1844
|
+
if (!realDefaultValue || realDefaultValue.trim().toLowerCase() !== this.utcNow().toLowerCase()) {
|
|
1634
1845
|
await this.dropAndCreateDefaultConstraintForSpecialDateField(pool, entity, fieldName);
|
|
1635
1846
|
}
|
|
1636
1847
|
}
|
|
@@ -1649,7 +1860,7 @@ export class ManageMetadataBase {
|
|
|
1649
1860
|
*/
|
|
1650
1861
|
async createDefaultConstraintForSpecialDateField(pool, entity, fieldName) {
|
|
1651
1862
|
try {
|
|
1652
|
-
const sqlAddDefaultConstraint =
|
|
1863
|
+
const sqlAddDefaultConstraint = this.dbProvider.addDefaultConstraintSQL(entity.SchemaName, entity.BaseTable, fieldName, this.utcNow());
|
|
1653
1864
|
await this.LogSQLAndExecute(pool, sqlAddDefaultConstraint, `SQL text to add default constraint for special date field ${fieldName} in entity ${entity.SchemaName}.${entity.BaseTable}`);
|
|
1654
1865
|
}
|
|
1655
1866
|
catch (e) {
|
|
@@ -1675,25 +1886,7 @@ export class ManageMetadataBase {
|
|
|
1675
1886
|
*/
|
|
1676
1887
|
async dropExistingDefaultConstraint(pool, entity, fieldName) {
|
|
1677
1888
|
try {
|
|
1678
|
-
const sqlDropDefaultConstraint =
|
|
1679
|
-
DECLARE @constraintName NVARCHAR(255);
|
|
1680
|
-
|
|
1681
|
-
-- Get the default constraint name
|
|
1682
|
-
SELECT @constraintName = d.name
|
|
1683
|
-
FROM sys.tables t
|
|
1684
|
-
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
1685
|
-
JOIN sys.columns c ON t.object_id = c.object_id
|
|
1686
|
-
JOIN sys.default_constraints d ON c.default_object_id = d.object_id
|
|
1687
|
-
WHERE s.name = '${entity.SchemaName}'
|
|
1688
|
-
AND t.name = '${entity.BaseTable}'
|
|
1689
|
-
AND c.name = '${fieldName}';
|
|
1690
|
-
|
|
1691
|
-
-- Drop the default constraint if it exists
|
|
1692
|
-
IF @constraintName IS NOT NULL
|
|
1693
|
-
BEGIN
|
|
1694
|
-
EXEC('ALTER TABLE [${entity.SchemaName}].[${entity.BaseTable}] DROP CONSTRAINT ' + @constraintName);
|
|
1695
|
-
END
|
|
1696
|
-
`;
|
|
1889
|
+
const sqlDropDefaultConstraint = this.dbProvider.dropDefaultConstraintSQL(entity.SchemaName, entity.BaseTable, fieldName);
|
|
1697
1890
|
await this.LogSQLAndExecute(pool, sqlDropDefaultConstraint, `SQL text to drop default existing default constraints in entity ${entity.SchemaName}.${entity.BaseTable}`);
|
|
1698
1891
|
}
|
|
1699
1892
|
catch (e) {
|
|
@@ -1713,14 +1906,14 @@ export class ManageMetadataBase {
|
|
|
1713
1906
|
if (ag.featureEnabled('EntityDescriptions')) {
|
|
1714
1907
|
// we have the feature enabled, so let's loop through the new entities and generate descriptions for them
|
|
1715
1908
|
for (let e of ManageMetadataBase.newEntityList) {
|
|
1716
|
-
const dataResult = await
|
|
1909
|
+
const dataResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntities')} WHERE Name = '${e}'`);
|
|
1717
1910
|
const data = dataResult.recordset;
|
|
1718
|
-
const fieldsResult = await
|
|
1911
|
+
const fieldsResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntityFields')} WHERE EntityID='${data[0].ID}'`);
|
|
1719
1912
|
const fields = fieldsResult.recordset;
|
|
1720
1913
|
// Use new API to generate entity description
|
|
1721
1914
|
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);
|
|
1722
1915
|
if (result?.entityDescription && result.entityDescription.length > 0) {
|
|
1723
|
-
const sSQL = `UPDATE
|
|
1916
|
+
const sSQL = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET Description = '${result.entityDescription}' WHERE Name = '${e}'`;
|
|
1724
1917
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to update entity description for entity ${e}`);
|
|
1725
1918
|
}
|
|
1726
1919
|
else {
|
|
@@ -1742,9 +1935,9 @@ export class ManageMetadataBase {
|
|
|
1742
1935
|
const sql = `SELECT
|
|
1743
1936
|
ef.ID, ef.Name
|
|
1744
1937
|
FROM
|
|
1745
|
-
|
|
1938
|
+
${this.qs(mj_core_schema(), 'vwEntityFields')} ef
|
|
1746
1939
|
INNER JOIN
|
|
1747
|
-
|
|
1940
|
+
${this.qs(mj_core_schema(), 'vwEntities')} e
|
|
1748
1941
|
ON
|
|
1749
1942
|
ef.EntityID = e.ID
|
|
1750
1943
|
WHERE
|
|
@@ -1752,13 +1945,13 @@ export class ManageMetadataBase {
|
|
|
1752
1945
|
ef.Name <> \'ID\' AND
|
|
1753
1946
|
e.SchemaName NOT IN (${excludeSchemas.map(s => `'${s}'`).join(',')})
|
|
1754
1947
|
`;
|
|
1755
|
-
const fieldsResult = await
|
|
1948
|
+
const fieldsResult = await this.runQuery(pool, sql);
|
|
1756
1949
|
const fields = fieldsResult.recordset;
|
|
1757
1950
|
if (fields && fields.length > 0)
|
|
1758
1951
|
for (const field of fields) {
|
|
1759
1952
|
const sDisplayName = stripTrailingChars(convertCamelCaseToHaveSpaces(field.Name), 'ID', true).trim();
|
|
1760
1953
|
if (sDisplayName.length > 0 && sDisplayName.toLowerCase().trim() !== field.Name.toLowerCase().trim()) {
|
|
1761
|
-
const sSQL = `UPDATE
|
|
1954
|
+
const sSQL = `UPDATE ${this.qs(mj_core_schema(), 'EntityField')} SET ${EntityInfo.UpdatedAtFieldName}=${this.utcNow()}, DisplayName = '${sDisplayName}' WHERE ID = '${field.ID}'`;
|
|
1762
1955
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to update display name for field ${field.Name}`);
|
|
1763
1956
|
}
|
|
1764
1957
|
}
|
|
@@ -1779,7 +1972,7 @@ export class ManageMetadataBase {
|
|
|
1779
1972
|
*/
|
|
1780
1973
|
async setDefaultColumnWidthWhereNeeded(pool, excludeSchemas) {
|
|
1781
1974
|
try {
|
|
1782
|
-
const sSQL =
|
|
1975
|
+
const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spSetDefaultColumnWidthWhereNeeded', [`'${excludeSchemas.join(',')}'`], ['ExcludedSchemaNames']);
|
|
1783
1976
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to set default column width where needed`, true);
|
|
1784
1977
|
return true;
|
|
1785
1978
|
}
|
|
@@ -1801,120 +1994,7 @@ export class ManageMetadataBase {
|
|
|
1801
1994
|
*/
|
|
1802
1995
|
getPendingEntityFieldsSELECTSQL() {
|
|
1803
1996
|
const schema = mj_core_schema();
|
|
1804
|
-
|
|
1805
|
-
-- Materialize system DMV views into temp tables so SQL Server gets real statistics
|
|
1806
|
-
-- instead of expanding nested view-on-view joins with bad cardinality estimates
|
|
1807
|
-
-- Drop first in case a prior run on this connection left them behind
|
|
1808
|
-
IF OBJECT_ID('tempdb..#__mj__CodeGen__vwForeignKeys') IS NOT NULL DROP TABLE #__mj__CodeGen__vwForeignKeys;
|
|
1809
|
-
IF OBJECT_ID('tempdb..#__mj__CodeGen__vwTablePrimaryKeys') IS NOT NULL DROP TABLE #__mj__CodeGen__vwTablePrimaryKeys;
|
|
1810
|
-
IF OBJECT_ID('tempdb..#__mj__CodeGen__vwTableUniqueKeys') IS NOT NULL DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
1811
|
-
|
|
1812
|
-
SELECT [column], [table], [schema_name], referenced_table, referenced_column, [referenced_schema]
|
|
1813
|
-
INTO #__mj__CodeGen__vwForeignKeys
|
|
1814
|
-
FROM [${schema}].vwForeignKeys;
|
|
1815
|
-
|
|
1816
|
-
SELECT TableName, ColumnName, SchemaName
|
|
1817
|
-
INTO #__mj__CodeGen__vwTablePrimaryKeys
|
|
1818
|
-
FROM [${schema}].vwTablePrimaryKeys;
|
|
1819
|
-
|
|
1820
|
-
SELECT TableName, ColumnName, SchemaName
|
|
1821
|
-
INTO #__mj__CodeGen__vwTableUniqueKeys
|
|
1822
|
-
FROM [${schema}].vwTableUniqueKeys;
|
|
1823
|
-
|
|
1824
|
-
WITH MaxSequences AS (
|
|
1825
|
-
-- Calculate the maximum existing sequence for each entity to avoid collisions
|
|
1826
|
-
SELECT
|
|
1827
|
-
EntityID,
|
|
1828
|
-
ISNULL(MAX(Sequence), 0) AS MaxSequence
|
|
1829
|
-
FROM
|
|
1830
|
-
[${schema}].EntityField
|
|
1831
|
-
GROUP BY
|
|
1832
|
-
EntityID
|
|
1833
|
-
),
|
|
1834
|
-
NumberedRows AS (
|
|
1835
|
-
SELECT
|
|
1836
|
-
sf.EntityID,
|
|
1837
|
-
-- Use dynamic offset based on max existing sequence for this entity to prevent collisions
|
|
1838
|
-
-- Add 100000 to ensure we're well above any existing sequences, then add the column sequence
|
|
1839
|
-
ISNULL(ms.MaxSequence, 0) + 100000 + sf.Sequence AS Sequence,
|
|
1840
|
-
sf.FieldName,
|
|
1841
|
-
sf.Description,
|
|
1842
|
-
sf.Type,
|
|
1843
|
-
sf.Length,
|
|
1844
|
-
sf.Precision,
|
|
1845
|
-
sf.Scale,
|
|
1846
|
-
sf.AllowsNull,
|
|
1847
|
-
sf.DefaultValue,
|
|
1848
|
-
sf.AutoIncrement,
|
|
1849
|
-
IIF(sf.IsVirtual = 1, 0, IIF(sf.FieldName = '${EntityInfo.CreatedAtFieldName}' OR
|
|
1850
|
-
sf.FieldName = '${EntityInfo.UpdatedAtFieldName}' OR
|
|
1851
|
-
sf.FieldName = '${EntityInfo.DeletedAtFieldName}' OR
|
|
1852
|
-
pk.ColumnName IS NOT NULL, 0, 1)) AllowUpdateAPI,
|
|
1853
|
-
sf.IsVirtual,
|
|
1854
|
-
e.RelationshipDefaultDisplayType,
|
|
1855
|
-
e.Name EntityName,
|
|
1856
|
-
re.ID RelatedEntityID,
|
|
1857
|
-
fk.referenced_column RelatedEntityFieldName,
|
|
1858
|
-
IIF(sf.FieldName = 'Name', 1, 0) IsNameField,
|
|
1859
|
-
IsPrimaryKey = CASE
|
|
1860
|
-
WHEN pk.ColumnName IS NOT NULL THEN 1
|
|
1861
|
-
ELSE 0
|
|
1862
|
-
END,
|
|
1863
|
-
IsUnique = CASE
|
|
1864
|
-
WHEN pk.ColumnName IS NOT NULL THEN 1
|
|
1865
|
-
ELSE
|
|
1866
|
-
CASE
|
|
1867
|
-
WHEN uk.ColumnName IS NOT NULL THEN 1
|
|
1868
|
-
ELSE 0
|
|
1869
|
-
END
|
|
1870
|
-
END,
|
|
1871
|
-
ROW_NUMBER() OVER (PARTITION BY sf.EntityID, sf.FieldName ORDER BY (SELECT NULL)) AS rn
|
|
1872
|
-
FROM
|
|
1873
|
-
[${schema}].vwSQLColumnsAndEntityFields sf
|
|
1874
|
-
LEFT OUTER JOIN
|
|
1875
|
-
MaxSequences ms
|
|
1876
|
-
ON
|
|
1877
|
-
sf.EntityID = ms.EntityID
|
|
1878
|
-
LEFT OUTER JOIN
|
|
1879
|
-
[${schema}].Entity e
|
|
1880
|
-
ON
|
|
1881
|
-
sf.EntityID = e.ID
|
|
1882
|
-
LEFT OUTER JOIN
|
|
1883
|
-
#__mj__CodeGen__vwForeignKeys fk
|
|
1884
|
-
ON
|
|
1885
|
-
sf.FieldName = fk.[column] AND
|
|
1886
|
-
e.BaseTable = fk.[table] AND
|
|
1887
|
-
e.SchemaName = fk.[schema_name]
|
|
1888
|
-
LEFT OUTER JOIN
|
|
1889
|
-
[${schema}].Entity re -- Related Entity
|
|
1890
|
-
ON
|
|
1891
|
-
re.BaseTable = fk.referenced_table AND
|
|
1892
|
-
re.SchemaName = fk.[referenced_schema]
|
|
1893
|
-
LEFT OUTER JOIN
|
|
1894
|
-
#__mj__CodeGen__vwTablePrimaryKeys pk
|
|
1895
|
-
ON
|
|
1896
|
-
e.BaseTable = pk.TableName AND
|
|
1897
|
-
sf.FieldName = pk.ColumnName AND
|
|
1898
|
-
e.SchemaName = pk.SchemaName
|
|
1899
|
-
LEFT OUTER JOIN
|
|
1900
|
-
#__mj__CodeGen__vwTableUniqueKeys uk
|
|
1901
|
-
ON
|
|
1902
|
-
e.BaseTable = uk.TableName AND
|
|
1903
|
-
sf.FieldName = uk.ColumnName AND
|
|
1904
|
-
e.SchemaName = uk.SchemaName
|
|
1905
|
-
WHERE
|
|
1906
|
-
EntityFieldID IS NULL -- only where we have NOT YET CREATED EntityField records\n${this.createExcludeTablesAndSchemasFilter('sf.')}
|
|
1907
|
-
)
|
|
1908
|
-
SELECT *
|
|
1909
|
-
FROM NumberedRows
|
|
1910
|
-
WHERE rn = 1
|
|
1911
|
-
ORDER BY EntityID, Sequence;
|
|
1912
|
-
|
|
1913
|
-
DROP TABLE #__mj__CodeGen__vwForeignKeys;
|
|
1914
|
-
DROP TABLE #__mj__CodeGen__vwTablePrimaryKeys;
|
|
1915
|
-
DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
1916
|
-
`;
|
|
1917
|
-
return sSQL;
|
|
1997
|
+
return this.dbProvider.getPendingEntityFieldsSQL(schema);
|
|
1918
1998
|
}
|
|
1919
1999
|
/**
|
|
1920
2000
|
* This method builds a SQL Statement that will insert a row into the EntityField table with information about a new field.
|
|
@@ -1946,43 +2026,44 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
1946
2026
|
break;
|
|
1947
2027
|
}
|
|
1948
2028
|
const parsedDefaultValue = this.parseDefaultValue(n.DefaultValue);
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
//
|
|
2029
|
+
// Escape single quotes inside the default value so they don't break the SQL string literal
|
|
2030
|
+
const escapedParsedDefault = parsedDefaultValue?.replace(/'/g, "''");
|
|
2031
|
+
// quotedDefaultValue is NULL when parsedDefaultValue is null/undefined, empty, or the literal string 'NULL' (case insensitive)
|
|
2032
|
+
const quotedDefaultValue = (parsedDefaultValue == null || parsedDefaultValue.trim().length === 0)
|
|
2033
|
+
? 'NULL'
|
|
2034
|
+
: (parsedDefaultValue.trim().toLowerCase() === 'null' ? 'NULL' : `'${escapedParsedDefault}'`);
|
|
2035
|
+
const conflictCheck = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ID = '${newEntityFieldUUID}' OR (EntityID = '${n.EntityID}' AND Name = '${n.FieldName}')`;
|
|
2036
|
+
const guard = this.dbProvider.wrapInsertWithConflictGuard(conflictCheck);
|
|
1952
2037
|
return `
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
WHERE ID = '${newEntityFieldUUID}' OR
|
|
1956
|
-
(EntityID = '${n.EntityID}' AND Name = '${n.FieldName}')
|
|
1957
|
-
-- check to make sure we're not inserting a duplicate entity field metadata record
|
|
1958
|
-
)
|
|
1959
|
-
BEGIN
|
|
1960
|
-
INSERT INTO [${mj_core_schema()}].EntityField
|
|
2038
|
+
${guard.prefix}
|
|
2039
|
+
INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')}
|
|
1961
2040
|
(
|
|
1962
|
-
ID,
|
|
1963
|
-
EntityID,
|
|
1964
|
-
Sequence,
|
|
1965
|
-
Name,
|
|
1966
|
-
DisplayName,
|
|
1967
|
-
Description,
|
|
1968
|
-
Type,
|
|
1969
|
-
Length,
|
|
1970
|
-
Precision,
|
|
1971
|
-
Scale,
|
|
1972
|
-
AllowsNull,
|
|
1973
|
-
DefaultValue,
|
|
1974
|
-
AutoIncrement,
|
|
1975
|
-
AllowUpdateAPI,
|
|
1976
|
-
IsVirtual,
|
|
1977
|
-
RelatedEntityID,
|
|
1978
|
-
RelatedEntityFieldName,
|
|
1979
|
-
IsNameField,
|
|
1980
|
-
IncludeInUserSearchAPI,
|
|
1981
|
-
IncludeRelatedEntityNameFieldInBaseView,
|
|
1982
|
-
DefaultInView,
|
|
1983
|
-
IsPrimaryKey,
|
|
1984
|
-
IsUnique,
|
|
1985
|
-
RelatedEntityDisplayType
|
|
2041
|
+
${this.qi('ID')},
|
|
2042
|
+
${this.qi('EntityID')},
|
|
2043
|
+
${this.qi('Sequence')},
|
|
2044
|
+
${this.qi('Name')},
|
|
2045
|
+
${this.qi('DisplayName')},
|
|
2046
|
+
${this.qi('Description')},
|
|
2047
|
+
${this.qi('Type')},
|
|
2048
|
+
${this.qi('Length')},
|
|
2049
|
+
${this.qi('Precision')},
|
|
2050
|
+
${this.qi('Scale')},
|
|
2051
|
+
${this.qi('AllowsNull')},
|
|
2052
|
+
${this.qi('DefaultValue')},
|
|
2053
|
+
${this.qi('AutoIncrement')},
|
|
2054
|
+
${this.qi('AllowUpdateAPI')},
|
|
2055
|
+
${this.qi('IsVirtual')},
|
|
2056
|
+
${this.qi('RelatedEntityID')},
|
|
2057
|
+
${this.qi('RelatedEntityFieldName')},
|
|
2058
|
+
${this.qi('IsNameField')},
|
|
2059
|
+
${this.qi('IncludeInUserSearchAPI')},
|
|
2060
|
+
${this.qi('IncludeRelatedEntityNameFieldInBaseView')},
|
|
2061
|
+
${this.qi('DefaultInView')},
|
|
2062
|
+
${this.qi('IsPrimaryKey')},
|
|
2063
|
+
${this.qi('IsUnique')},
|
|
2064
|
+
${this.qi('RelatedEntityDisplayType')},
|
|
2065
|
+
${this.qi('__mj_CreatedAt')},
|
|
2066
|
+
${this.qi('__mj_UpdatedAt')}
|
|
1986
2067
|
)
|
|
1987
2068
|
VALUES
|
|
1988
2069
|
(
|
|
@@ -2009,9 +2090,11 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2009
2090
|
${bDefaultInView ? 1 : 0},
|
|
2010
2091
|
${n.IsPrimaryKey},
|
|
2011
2092
|
${n.IsUnique},
|
|
2012
|
-
'${n.RelationshipDefaultDisplayType}'
|
|
2093
|
+
'${n.RelationshipDefaultDisplayType}',
|
|
2094
|
+
${this.utcNow()},
|
|
2095
|
+
${this.utcNow()}
|
|
2013
2096
|
)
|
|
2014
|
-
|
|
2097
|
+
${guard.suffix}`;
|
|
2015
2098
|
}
|
|
2016
2099
|
/**
|
|
2017
2100
|
* This method takes the stored DEFAULT CONSTRAINT value from the database and parses it to retrieve the actual default value. This is necessary because the default value is
|
|
@@ -2023,27 +2106,18 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2023
2106
|
* @returns
|
|
2024
2107
|
*/
|
|
2025
2108
|
parseDefaultValue(sqlDefaultValue) {
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
else
|
|
2031
|
-
sResult = sqlDefaultValue;
|
|
2032
|
-
if (sResult.toUpperCase().startsWith('N\'') && sResult.endsWith('\''))
|
|
2033
|
-
sResult = sResult.substring(2, sResult.length - 1);
|
|
2034
|
-
if (sResult.startsWith('\'') && sResult.endsWith('\''))
|
|
2035
|
-
sResult = sResult.substring(1, sResult.length - 1);
|
|
2036
|
-
}
|
|
2037
|
-
return sResult;
|
|
2109
|
+
if (sqlDefaultValue === null || sqlDefaultValue === undefined) {
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
return this.dbProvider.parseColumnDefaultValue(sqlDefaultValue) ?? null;
|
|
2038
2113
|
}
|
|
2039
2114
|
async createNewEntityFieldsFromSchema(pool) {
|
|
2040
2115
|
try {
|
|
2041
2116
|
const sSQL = this.getPendingEntityFieldsSELECTSQL();
|
|
2042
|
-
const newEntityFieldsResult = await
|
|
2117
|
+
const newEntityFieldsResult = await this.runQuery(pool, sSQL);
|
|
2043
2118
|
const newEntityFields = newEntityFieldsResult.recordset;
|
|
2044
2119
|
if (newEntityFields.length > 0) {
|
|
2045
|
-
const transaction =
|
|
2046
|
-
await transaction.begin();
|
|
2120
|
+
const transaction = await pool.beginTransaction();
|
|
2047
2121
|
try {
|
|
2048
2122
|
// wrap in a transaction so we get all of it or none of it
|
|
2049
2123
|
for (let i = 0; i < newEntityFields.length; ++i) {
|
|
@@ -2091,9 +2165,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2091
2165
|
*/
|
|
2092
2166
|
async updateEntityFieldRelatedEntityNameFieldMap(pool, entityFieldID, relatedEntityNameFieldMap) {
|
|
2093
2167
|
try {
|
|
2094
|
-
const sSQL =
|
|
2095
|
-
@EntityFieldID='${entityFieldID}',
|
|
2096
|
-
@RelatedEntityNameFieldMap='${relatedEntityNameFieldMap}'`;
|
|
2168
|
+
const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spUpdateEntityFieldRelatedEntityNameFieldMap', [`'${entityFieldID}'`, `'${relatedEntityNameFieldMap}'`], ['EntityFieldID', 'RelatedEntityNameFieldMap']);
|
|
2097
2169
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to update entity field related entity name field map for entity field ID ${entityFieldID}`);
|
|
2098
2170
|
return true;
|
|
2099
2171
|
}
|
|
@@ -2104,7 +2176,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2104
2176
|
}
|
|
2105
2177
|
async updateExistingEntitiesFromSchema(pool, excludeSchemas) {
|
|
2106
2178
|
try {
|
|
2107
|
-
const sSQL =
|
|
2179
|
+
const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spUpdateExistingEntitiesFromSchema', [`'${excludeSchemas.join(',')}'`], ['ExcludedSchemaNames']);
|
|
2108
2180
|
const result = await this.LogSQLAndExecute(pool, sSQL, `SQL text to update existing entities from schema`, true);
|
|
2109
2181
|
// result contains the updated entities, and there is a property of each row called Name which has the entity name that was modified
|
|
2110
2182
|
// add these to the modified entity list if they're not already in there
|
|
@@ -2129,7 +2201,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2129
2201
|
}
|
|
2130
2202
|
async updateExistingEntityFieldsFromSchema(pool, excludeSchemas) {
|
|
2131
2203
|
try {
|
|
2132
|
-
const sSQL =
|
|
2204
|
+
const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spUpdateExistingEntityFieldsFromSchema', [`'${excludeSchemas.join(',')}'`], ['ExcludedSchemaNames']);
|
|
2133
2205
|
const result = await this.LogSQLAndExecute(pool, sSQL, `SQL text to update existing entity fields from schema`, true);
|
|
2134
2206
|
// result contains the updated entity fields
|
|
2135
2207
|
// there is a field in there called EntityName. Get a distinct list of entity names from this and add them
|
|
@@ -2154,7 +2226,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2154
2226
|
*/
|
|
2155
2227
|
async updateSchemaInfoFromDatabase(pool, excludeSchemas) {
|
|
2156
2228
|
try {
|
|
2157
|
-
const sSQL =
|
|
2229
|
+
const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spUpdateSchemaInfoFromDatabase', [`'${excludeSchemas.join(',')}'`], ['ExcludedSchemaNames']);
|
|
2158
2230
|
const result = await this.LogSQLAndExecute(pool, sSQL, `SQL text to sync schema info from database schemas`, true);
|
|
2159
2231
|
if (result && result.length > 0) {
|
|
2160
2232
|
logStatus(` > Updated/created ${result.length} SchemaInfo records`);
|
|
@@ -2185,8 +2257,8 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2185
2257
|
*/
|
|
2186
2258
|
async loadSchemaInfoRecords(pool) {
|
|
2187
2259
|
try {
|
|
2188
|
-
const sSQL = `SELECT * FROM
|
|
2189
|
-
const result = await
|
|
2260
|
+
const sSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'SchemaInfo')}`;
|
|
2261
|
+
const result = await this.runQuery(pool, sSQL);
|
|
2190
2262
|
if (result?.recordset?.length > 0) {
|
|
2191
2263
|
this.cacheSchemaInfoRecords(result.recordset);
|
|
2192
2264
|
}
|
|
@@ -2199,7 +2271,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2199
2271
|
}
|
|
2200
2272
|
async deleteUnneededEntityFields(pool, excludeSchemas) {
|
|
2201
2273
|
try {
|
|
2202
|
-
const sSQL =
|
|
2274
|
+
const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spDeleteUnneededEntityFields', [`'${excludeSchemas.join(',')}'`], ['ExcludedSchemaNames']);
|
|
2203
2275
|
const result = await this.LogSQLAndExecute(pool, sSQL, `SQL text to delete unneeded entity fields`, true);
|
|
2204
2276
|
// result contains the DELETED entity fields
|
|
2205
2277
|
// there is a field in there called Entity. Get a distinct list of entity names from this and add them
|
|
@@ -2220,15 +2292,15 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2220
2292
|
// evaluate it to see if it is a simple series of OR statements or not, if it is a simple series of OR statements, we can parse the possible values
|
|
2221
2293
|
// 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
|
|
2222
2294
|
// just ignore it.
|
|
2223
|
-
const filter =
|
|
2224
|
-
const sSQL = `SELECT * FROM
|
|
2225
|
-
const resultResult = await
|
|
2295
|
+
const filter = this.dbProvider.getCheckConstraintsSchemaFilter(excludeSchemas);
|
|
2296
|
+
const sSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntityFieldsWithCheckConstraints')}${filter}`;
|
|
2297
|
+
const resultResult = await this.runQuery(pool, sSQL);
|
|
2226
2298
|
const result = resultResult.recordset;
|
|
2227
|
-
const efvSQL = `SELECT * FROM
|
|
2228
|
-
const allEntityFieldValuesResult = await
|
|
2299
|
+
const efvSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityFieldValue')}`;
|
|
2300
|
+
const allEntityFieldValuesResult = await this.runQuery(pool, efvSQL);
|
|
2229
2301
|
const allEntityFieldValues = allEntityFieldValuesResult.recordset;
|
|
2230
|
-
const efSQL = `SELECT * FROM
|
|
2231
|
-
const allEntityFieldsResult = await
|
|
2302
|
+
const efSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntityFields')} ORDER BY EntityID, Sequence`;
|
|
2303
|
+
const allEntityFieldsResult = await this.runQuery(pool, efSQL);
|
|
2232
2304
|
const allEntityFields = allEntityFieldsResult.recordset;
|
|
2233
2305
|
const generationPromises = [];
|
|
2234
2306
|
const columnLevelResults = result.filter((r) => r.EntityFieldID); // get the column level constraints
|
|
@@ -2249,11 +2321,11 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2249
2321
|
await this.syncEntityFieldValues(pool, r.EntityFieldID, parsedValues, allEntityFieldValues);
|
|
2250
2322
|
// 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.
|
|
2251
2323
|
// check to see if the ValueListType is already set to "List", if not, update it
|
|
2252
|
-
const sSQLCheck = `SELECT ValueListType FROM
|
|
2253
|
-
const checkResultResult = await
|
|
2324
|
+
const sSQLCheck = `SELECT ValueListType FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ID='${r.EntityFieldID}'`;
|
|
2325
|
+
const checkResultResult = await this.runQuery(pool, sSQLCheck);
|
|
2254
2326
|
const checkResult = checkResultResult.recordset;
|
|
2255
2327
|
if (checkResult && checkResult.length > 0 && checkResult[0].ValueListType.trim().toLowerCase() !== 'list') {
|
|
2256
|
-
const sSQL = `UPDATE
|
|
2328
|
+
const sSQL = `UPDATE ${this.qs(mj_core_schema(), 'EntityField')} SET ValueListType='List' WHERE ID='${r.EntityFieldID}'`;
|
|
2257
2329
|
await this.LogSQLAndExecute(pool, sSQL, `SQL text to update ValueListType for entity field ID ${r.EntityFieldID}`);
|
|
2258
2330
|
}
|
|
2259
2331
|
}
|
|
@@ -2381,13 +2453,12 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2381
2453
|
// now, loop through the possible values and add any that are not already in the database
|
|
2382
2454
|
// Step 1: for any existing value that is NOT in the list of possible Values, delete it
|
|
2383
2455
|
let numRemoved = 0;
|
|
2384
|
-
const transaction =
|
|
2385
|
-
await transaction.begin();
|
|
2456
|
+
const transaction = await ds.beginTransaction();
|
|
2386
2457
|
try {
|
|
2387
2458
|
for (const ev of existingValues) {
|
|
2388
2459
|
if (!possibleValues.find(v => v === ev.Value)) {
|
|
2389
2460
|
// delete the value from the database
|
|
2390
|
-
const sSQLDelete = `DELETE FROM
|
|
2461
|
+
const sSQLDelete = `DELETE FROM ${this.qs(mj_core_schema(), 'EntityFieldValue')} WHERE ID='${ev.ID}'`;
|
|
2391
2462
|
await this.LogSQLAndExecute(ds, sSQLDelete, `SQL text to delete entity field value ID ${ev.ID}`);
|
|
2392
2463
|
numRemoved++;
|
|
2393
2464
|
}
|
|
@@ -2399,10 +2470,10 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2399
2470
|
// Generate a UUID for this new EntityFieldValue record
|
|
2400
2471
|
const newId = uuidv4();
|
|
2401
2472
|
// add the value to the database with explicit ID
|
|
2402
|
-
const sSQLInsert = `INSERT INTO
|
|
2403
|
-
(ID, EntityFieldID, Sequence, Value, Code)
|
|
2473
|
+
const sSQLInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityFieldValue')}
|
|
2474
|
+
(${this.qi('ID')}, ${this.qi('EntityFieldID')}, ${this.qi('Sequence')}, ${this.qi('Value')}, ${this.qi('Code')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')})
|
|
2404
2475
|
VALUES
|
|
2405
|
-
('${newId}', '${entityFieldID}', ${1 + possibleValues.indexOf(v)}, '${v}', '${v}')`;
|
|
2476
|
+
('${newId}', '${entityFieldID}', ${1 + possibleValues.indexOf(v)}, '${v}', '${v}', ${this.utcNow()}, ${this.utcNow()})`;
|
|
2406
2477
|
await this.LogSQLAndExecute(ds, sSQLInsert, `SQL text to insert entity field value with ID ${newId}`);
|
|
2407
2478
|
numAdded++;
|
|
2408
2479
|
}
|
|
@@ -2413,7 +2484,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2413
2484
|
const ev = existingValues.find((ev) => ev.Value === v);
|
|
2414
2485
|
if (ev && ev.Sequence !== 1 + possibleValues.indexOf(v)) {
|
|
2415
2486
|
// update the sequence to match the order in the possible values list, if it doesn't already match
|
|
2416
|
-
const sSQLUpdate = `UPDATE
|
|
2487
|
+
const sSQLUpdate = `UPDATE ${this.qs(mj_core_schema(), 'EntityFieldValue')} SET Sequence=${1 + possibleValues.indexOf(v)} WHERE ID='${ev.ID}'`;
|
|
2417
2488
|
await this.LogSQLAndExecute(ds, sSQLUpdate, `SQL text to update entity field value sequence`);
|
|
2418
2489
|
numUpdated++;
|
|
2419
2490
|
}
|
|
@@ -2433,17 +2504,25 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2433
2504
|
}
|
|
2434
2505
|
parseCheckConstraintValues(constraintDefinition, fieldName, entityName) {
|
|
2435
2506
|
// This regex checks for the overall structure including field name and 'OR' sequences
|
|
2436
|
-
//
|
|
2437
|
-
//
|
|
2438
|
-
// Also handles constraints with optional NULL: ([FieldName]='Value1' OR [FieldName]='Value2' OR [FieldName] IS NULL)
|
|
2439
|
-
// Also handles nested NULL pattern: ([FieldName] IS NULL OR ([FieldName]='Value1' OR [FieldName]='Value2'))
|
|
2507
|
+
// SQL Server uses [FieldName]='Value' quoting, PostgreSQL uses "FieldName" or unquoted FieldName
|
|
2508
|
+
// We handle both: [FieldName], "FieldName", or bare FieldName
|
|
2440
2509
|
// Note: Assuming fieldName does not contain regex special characters; otherwise, it needs to be escaped as well.
|
|
2441
2510
|
const processedConstraint = constraintDefinition.replace(/(^|[=(\s])N'([^']*)'/g, "$1'$2'");
|
|
2442
|
-
//
|
|
2443
|
-
|
|
2511
|
+
// PostgreSQL ANY (ARRAY[...]) pattern:
|
|
2512
|
+
// CHECK ((("FieldName")::text = ANY ((ARRAY['Val1'::character varying, 'Val2'::character varying])::text[])))
|
|
2513
|
+
// Also handles simpler forms: ("FieldName" = ANY (ARRAY['Val1', 'Val2']))
|
|
2514
|
+
const pgArrayValues = this.parsePgArrayConstraint(processedConstraint);
|
|
2515
|
+
if (pgArrayValues) {
|
|
2516
|
+
return pgArrayValues;
|
|
2517
|
+
}
|
|
2518
|
+
// Build a regex fragment that matches the field name in any quoting style:
|
|
2519
|
+
// [FieldName] (SQL Server) or "FieldName" (PostgreSQL) or bare FieldName
|
|
2520
|
+
const quotedField = `(?:\\[${fieldName}\\]|"${fieldName}"|${fieldName})`;
|
|
2521
|
+
// Check for nested pattern: (Field IS NULL OR (Field='Value1' OR ...))
|
|
2522
|
+
const nestedNullRegex = new RegExp(`^\\(${quotedField} IS NULL OR \\(${quotedField}='[^']+'(?: OR ${quotedField}='[^']+?')+\\)\\)$`);
|
|
2444
2523
|
if (nestedNullRegex.test(processedConstraint)) {
|
|
2445
2524
|
// Extract values from nested pattern - same extraction logic works
|
|
2446
|
-
const valueRegex = new RegExp(
|
|
2525
|
+
const valueRegex = new RegExp(`${quotedField}='([^']+)\'`, 'g');
|
|
2447
2526
|
let match;
|
|
2448
2527
|
const possibleValues = [];
|
|
2449
2528
|
while ((match = valueRegex.exec(processedConstraint)) !== null) {
|
|
@@ -2457,16 +2536,13 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2457
2536
|
return possibleValues.length > 0 ? possibleValues : null;
|
|
2458
2537
|
}
|
|
2459
2538
|
// Check for standard pattern with optional trailing IS NULL
|
|
2460
|
-
const structureRegex = new RegExp(`^\\(
|
|
2539
|
+
const structureRegex = new RegExp(`^\\(${quotedField}='[^']+'(?: OR ${quotedField}='[^']+?')+(?: OR ${quotedField} IS NULL)?\\)$`);
|
|
2461
2540
|
if (!structureRegex.test(processedConstraint)) {
|
|
2462
|
-
// decided to NOT log these warnings anymore becuase they make it appear to the user that there is a problem but there is NOT, this is normal behvario for all othe types of
|
|
2463
|
-
// check constraints that are not simple OR conditions
|
|
2464
|
-
//logWarning(` Can't extract value list from [${entityName}].[${fieldName}]. The check constraint does not match the simple OR condition pattern or field name does not match: ${constraintDefinition}`);
|
|
2465
2541
|
return null;
|
|
2466
2542
|
}
|
|
2467
2543
|
else {
|
|
2468
2544
|
// Regular expression to match the values within the single quotes specifically for the field
|
|
2469
|
-
const valueRegex = new RegExp(
|
|
2545
|
+
const valueRegex = new RegExp(`${quotedField}='([^']+)\'`, 'g');
|
|
2470
2546
|
let match;
|
|
2471
2547
|
const possibleValues = [];
|
|
2472
2548
|
// Use regex to find matches and extract the values
|
|
@@ -2483,24 +2559,51 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2483
2559
|
return possibleValues;
|
|
2484
2560
|
}
|
|
2485
2561
|
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Parses PostgreSQL ANY (ARRAY[...]) CHECK constraint syntax.
|
|
2564
|
+
* PG's pg_get_constraintdef() returns constraints like:
|
|
2565
|
+
* CHECK ((("Status")::text = ANY ((ARRAY['Confirmed'::character varying, 'Cancelled'::character varying])::text[])))
|
|
2566
|
+
* This method extracts the string values from the ARRAY literal.
|
|
2567
|
+
*/
|
|
2568
|
+
parsePgArrayConstraint(constraintDefinition) {
|
|
2569
|
+
// Match the ARRAY[...] portion, handling optional type casts
|
|
2570
|
+
const arrayMatch = constraintDefinition.match(/\bARRAY\s*\[([^\]]+)\]/i);
|
|
2571
|
+
if (!arrayMatch) {
|
|
2572
|
+
return null;
|
|
2573
|
+
}
|
|
2574
|
+
// Extract individual values from the array content
|
|
2575
|
+
// Each element looks like: 'Value'::character varying or just 'Value'
|
|
2576
|
+
const arrayContent = arrayMatch[1];
|
|
2577
|
+
const valueRegex = /'([^']+)'(?:::[^,\]]*)?/g;
|
|
2578
|
+
const possibleValues = [];
|
|
2579
|
+
let match;
|
|
2580
|
+
while ((match = valueRegex.exec(arrayContent)) !== null) {
|
|
2581
|
+
if (match[1]) {
|
|
2582
|
+
possibleValues.push(match[1]);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
return possibleValues.length > 0 ? possibleValues : null;
|
|
2586
|
+
}
|
|
2486
2587
|
createExcludeTablesAndSchemasFilter(fieldPrefix) {
|
|
2487
2588
|
let sExcludeTables = '';
|
|
2488
2589
|
let sExcludeSchemas = '';
|
|
2590
|
+
const schemaCol = `${fieldPrefix}${this.qi('SchemaName')}`;
|
|
2591
|
+
const tableCol = `${fieldPrefix}${this.qi('TableName')}`;
|
|
2489
2592
|
if (configInfo.excludeTables) {
|
|
2490
2593
|
for (let i = 0; i < configInfo.excludeTables.length; ++i) {
|
|
2491
2594
|
const t = configInfo.excludeTables[i];
|
|
2492
2595
|
sExcludeTables += (sExcludeTables.length > 0 ? ' AND ' : '') +
|
|
2493
|
-
(t.schema.indexOf('%') > -1 ? ` NOT ( ${
|
|
2494
|
-
` NOT ( ${
|
|
2495
|
-
sExcludeTables += (t.table.indexOf('%') > -1 ? ` AND ${
|
|
2496
|
-
` AND ${
|
|
2596
|
+
(t.schema.indexOf('%') > -1 ? ` NOT ( ${schemaCol} LIKE '${t.schema}'` :
|
|
2597
|
+
` NOT ( ${schemaCol} = '${t.schema}'`);
|
|
2598
|
+
sExcludeTables += (t.table.indexOf('%') > -1 ? ` AND ${tableCol} LIKE '${t.table}') ` :
|
|
2599
|
+
` AND ${tableCol} = '${t.table}') `);
|
|
2497
2600
|
}
|
|
2498
2601
|
}
|
|
2499
2602
|
if (configInfo.excludeSchemas) {
|
|
2500
2603
|
for (let i = 0; i < configInfo.excludeSchemas.length; ++i) {
|
|
2501
2604
|
const s = configInfo.excludeSchemas[i];
|
|
2502
2605
|
sExcludeSchemas += (sExcludeSchemas.length > 0 ? ' AND ' : '') +
|
|
2503
|
-
(s.indexOf('%') > -1 ? `${
|
|
2606
|
+
(s.indexOf('%') > -1 ? `${schemaCol} NOT LIKE '${s}'` : `${schemaCol} <> '${s}'`);
|
|
2504
2607
|
}
|
|
2505
2608
|
}
|
|
2506
2609
|
const sWhere = (sExcludeTables.length > 0 || sExcludeSchemas.length > 0 ? ` AND ` : '') +
|
|
@@ -2510,13 +2613,12 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2510
2613
|
}
|
|
2511
2614
|
async createNewEntities(pool, currentUser) {
|
|
2512
2615
|
try {
|
|
2513
|
-
const sSQL = `SELECT * FROM
|
|
2514
|
-
const newEntitiesResult = await
|
|
2616
|
+
const sSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwSQLTablesAndEntities')} WHERE ${this.qi('EntityID')} IS NULL ` + this.createExcludeTablesAndSchemasFilter('');
|
|
2617
|
+
const newEntitiesResult = await this.runQuery(pool, sSQL);
|
|
2515
2618
|
const newEntities = newEntitiesResult.recordset;
|
|
2516
2619
|
if (newEntities && newEntities.length > 0) {
|
|
2517
2620
|
const md = new Metadata();
|
|
2518
|
-
const transaction =
|
|
2519
|
-
await transaction.begin();
|
|
2621
|
+
const transaction = await pool.beginTransaction();
|
|
2520
2622
|
try {
|
|
2521
2623
|
// wrap in a transaction so we get all of it or none of it
|
|
2522
2624
|
for (let i = 0; i < newEntities.length; ++i) {
|
|
@@ -2547,9 +2649,9 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2547
2649
|
// criteria:
|
|
2548
2650
|
// 1) entity has a field that is a primary key
|
|
2549
2651
|
// 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.
|
|
2550
|
-
const query =
|
|
2652
|
+
const query = this.dbProvider.callRoutineSQL(Metadata.Provider.ConfigData.MJCoreSchemaName, 'spGetPrimaryKeyForTable', [`'${newEntity.TableName}'`, `'${newEntity.SchemaName}'`], ['TableName', 'SchemaName']);
|
|
2551
2653
|
try {
|
|
2552
|
-
const resultResult = await ds.
|
|
2654
|
+
const resultResult = await ds.query(query);
|
|
2553
2655
|
const result = resultResult.recordset;
|
|
2554
2656
|
if (result.length === 0) {
|
|
2555
2657
|
// No database PK constraint found - check if there's a soft PK defined in config
|
|
@@ -2784,9 +2886,9 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2784
2886
|
if (configInfo.newEntityDefaults.AddToApplicationWithSchemaName) {
|
|
2785
2887
|
// only do this if the configuration setting is set to add new entities to applications for schema names
|
|
2786
2888
|
for (const appUUID of apps) {
|
|
2787
|
-
const sSQLInsertApplicationEntity = `INSERT INTO ${mj_core_schema()}
|
|
2788
|
-
(ApplicationID, EntityID, Sequence) VALUES
|
|
2789
|
-
('${appUUID}', '${newEntityID}', (SELECT
|
|
2889
|
+
const sSQLInsertApplicationEntity = `INSERT INTO ${this.qs(mj_core_schema(), 'ApplicationEntity')}
|
|
2890
|
+
(${this.qi('ApplicationID')}, ${this.qi('EntityID')}, ${this.qi('Sequence')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')}) VALUES
|
|
2891
|
+
('${appUUID}', '${newEntityID}', (SELECT COALESCE(MAX(${this.qi('Sequence')}),0)+1 FROM ${this.qs(mj_core_schema(), 'ApplicationEntity')} WHERE ${this.qi('ApplicationID')} = '${appUUID}'), ${this.utcNow()}, ${this.utcNow()})`;
|
|
2790
2892
|
await this.LogSQLAndExecute(pool, sSQLInsertApplicationEntity, `SQL generated to add new entity ${newEntityName} to application ID: '${appUUID}'`);
|
|
2791
2893
|
}
|
|
2792
2894
|
}
|
|
@@ -2805,9 +2907,9 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2805
2907
|
for (const p of permissions) {
|
|
2806
2908
|
const RoleID = md.Roles.find(r => r.Name.trim().toLowerCase() === p.RoleName.trim().toLowerCase())?.ID;
|
|
2807
2909
|
if (RoleID) {
|
|
2808
|
-
const sSQLInsertPermission = `INSERT INTO ${mj_core_schema()}
|
|
2809
|
-
(EntityID, RoleID, CanRead, CanCreate, CanUpdate, CanDelete) VALUES
|
|
2810
|
-
('${newEntityID}', '${RoleID}', ${p.CanRead ? 1 : 0}, ${p.CanCreate ? 1 : 0}, ${p.CanUpdate ? 1 : 0}, ${p.CanDelete ? 1 : 0})`;
|
|
2910
|
+
const sSQLInsertPermission = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityPermission')}
|
|
2911
|
+
(${this.qi('EntityID')}, ${this.qi('RoleID')}, ${this.qi('CanRead')}, ${this.qi('CanCreate')}, ${this.qi('CanUpdate')}, ${this.qi('CanDelete')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')}) VALUES
|
|
2912
|
+
('${newEntityID}', '${RoleID}', ${p.CanRead ? 1 : 0}, ${p.CanCreate ? 1 : 0}, ${p.CanUpdate ? 1 : 0}, ${p.CanDelete ? 1 : 0}, ${this.utcNow()}, ${this.utcNow()})`;
|
|
2811
2913
|
await this.LogSQLAndExecute(pool, sSQLInsertPermission, `SQL generated to add new permission for entity ${newEntityName} for role ${p.RoleName}`);
|
|
2812
2914
|
}
|
|
2813
2915
|
else
|
|
@@ -2832,10 +2934,12 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2832
2934
|
}
|
|
2833
2935
|
async isSchemaNew(pool, schemaName) {
|
|
2834
2936
|
// check to see if there are any entities in the db with this schema name
|
|
2835
|
-
|
|
2836
|
-
const
|
|
2937
|
+
// Quote the alias so PostgreSQL preserves PascalCase (unquoted aliases are lowercased in PG)
|
|
2938
|
+
const sSQL = `SELECT COUNT(*) AS ${this.qi('Count')} FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE ${this.qi('SchemaName')} = '${schemaName}'`;
|
|
2939
|
+
const resultResult = await this.runQuery(pool, sSQL);
|
|
2837
2940
|
const result = resultResult.recordset;
|
|
2838
|
-
|
|
2941
|
+
// Use Number() because PG returns COUNT(*) as bigint, which the pg driver delivers as a string
|
|
2942
|
+
return result && result.length > 0 ? Number(result[0].Count) === 0 : true;
|
|
2839
2943
|
}
|
|
2840
2944
|
/**
|
|
2841
2945
|
* Creates a new application using direct SQL INSERT to ensure it's captured in SQL logging.
|
|
@@ -2860,7 +2964,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2860
2964
|
.replace(/[^a-z0-9-]/g, '') // remove special chars
|
|
2861
2965
|
.replace(/-+/g, '-') // collapse multiple hyphens
|
|
2862
2966
|
.replace(/^-|-$/g, ''); // trim hyphens from start/end
|
|
2863
|
-
const sSQL = `INSERT INTO
|
|
2967
|
+
const sSQL = `INSERT INTO ${this.qs(mj_core_schema(), 'Application')} (ID, Name, Description, SchemaAutoAddNewEntities, Path, AutoUpdatePath)
|
|
2864
2968
|
VALUES ('${appID}', '${appName}', 'Generated for schema', '${schemaName}', '${path}', 1)`;
|
|
2865
2969
|
await this.LogSQLAndExecute(pool, sSQL, `SQL generated to create new application ${appName}`);
|
|
2866
2970
|
LogStatus(`Created new application ${appName} with Path: ${path}`);
|
|
@@ -2872,15 +2976,15 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2872
2976
|
}
|
|
2873
2977
|
}
|
|
2874
2978
|
async applicationExists(pool, applicationName) {
|
|
2875
|
-
const sSQL = `SELECT ID FROM
|
|
2876
|
-
const resultResult = await
|
|
2979
|
+
const sSQL = `SELECT ID FROM ${this.qs(mj_core_schema(), 'Application')} WHERE Name = '${applicationName}'`;
|
|
2980
|
+
const resultResult = await this.runQuery(pool, sSQL);
|
|
2877
2981
|
const result = resultResult.recordset;
|
|
2878
2982
|
return result && result.length > 0 ? result[0].ID.length > 0 : false;
|
|
2879
2983
|
}
|
|
2880
2984
|
async getApplicationIDForSchema(pool, schemaName) {
|
|
2881
2985
|
// get all the apps each time from DB as we might be adding, don't use Metadata here for that reason
|
|
2882
|
-
const sSQL = `SELECT ID, Name, SchemaAutoAddNewEntities FROM
|
|
2883
|
-
const resultResult = await
|
|
2986
|
+
const sSQL = `SELECT ID, Name, SchemaAutoAddNewEntities FROM ${this.qs(mj_core_schema(), 'vwApplications')}`;
|
|
2987
|
+
const resultResult = await this.runQuery(pool, sSQL);
|
|
2884
2988
|
const result = resultResult.recordset;
|
|
2885
2989
|
if (!result || result.length === 0) {
|
|
2886
2990
|
// no applications found, return null
|
|
@@ -2921,9 +3025,9 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2921
3025
|
if (apps && apps.length > 0) {
|
|
2922
3026
|
if (configInfo.newEntityDefaults.AddToApplicationWithSchemaName) {
|
|
2923
3027
|
for (const appUUID of apps) {
|
|
2924
|
-
const sSQLInsert = `INSERT INTO ${mj_core_schema()}
|
|
2925
|
-
(ApplicationID, EntityID, Sequence) VALUES
|
|
2926
|
-
('${appUUID}', '${entityId}', (SELECT
|
|
3028
|
+
const sSQLInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'ApplicationEntity')}
|
|
3029
|
+
(${this.qi('ApplicationID')}, ${this.qi('EntityID')}, ${this.qi('Sequence')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')}) VALUES
|
|
3030
|
+
('${appUUID}', '${entityId}', (SELECT COALESCE(MAX(${this.qi('Sequence')}),0)+1 FROM ${this.qs(mj_core_schema(), 'ApplicationEntity')} WHERE ${this.qi('ApplicationID')} = '${appUUID}'), ${this.utcNow()}, ${this.utcNow()})`;
|
|
2927
3031
|
await this.LogSQLAndExecute(pool, sSQLInsert, `SQL generated to add entity ${entityName} to application ID: '${appUUID}'`);
|
|
2928
3032
|
}
|
|
2929
3033
|
}
|
|
@@ -2945,9 +3049,9 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2945
3049
|
for (const p of permissions) {
|
|
2946
3050
|
const RoleID = md.Roles.find(r => r.Name.trim().toLowerCase() === p.RoleName.trim().toLowerCase())?.ID;
|
|
2947
3051
|
if (RoleID) {
|
|
2948
|
-
const sSQLInsert = `INSERT INTO ${mj_core_schema()}
|
|
2949
|
-
(EntityID, RoleID, CanRead, CanCreate, CanUpdate, CanDelete) VALUES
|
|
2950
|
-
('${entityId}', '${RoleID}', ${p.CanRead ? 1 : 0}, ${p.CanCreate ? 1 : 0}, ${p.CanUpdate ? 1 : 0}, ${p.CanDelete ? 1 : 0})`;
|
|
3052
|
+
const sSQLInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityPermission')}
|
|
3053
|
+
(${this.qi('EntityID')}, ${this.qi('RoleID')}, ${this.qi('CanRead')}, ${this.qi('CanCreate')}, ${this.qi('CanUpdate')}, ${this.qi('CanDelete')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')}) VALUES
|
|
3054
|
+
('${entityId}', '${RoleID}', ${p.CanRead ? 1 : 0}, ${p.CanCreate ? 1 : 0}, ${p.CanUpdate ? 1 : 0}, ${p.CanDelete ? 1 : 0}, ${this.utcNow()}, ${this.utcNow()})`;
|
|
2951
3055
|
await this.LogSQLAndExecute(pool, sSQLInsert, `SQL generated to add permission for entity ${entityName} for role ${p.RoleName}`);
|
|
2952
3056
|
}
|
|
2953
3057
|
else {
|
|
@@ -2957,27 +3061,30 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2957
3061
|
}
|
|
2958
3062
|
createNewEntityInsertSQL(newEntityUUID, newEntityName, newEntity, newEntitySuffix, newEntityDisplayName) {
|
|
2959
3063
|
const newEntityDefaults = configInfo.newEntityDefaults;
|
|
2960
|
-
const newEntityDescriptionEscaped = newEntity.
|
|
3064
|
+
const newEntityDescriptionEscaped = newEntity.EntityDescription ? `'${newEntity.EntityDescription.replace(/'/g, "''")}'` : null;
|
|
3065
|
+
const q = (name) => this.qi(name);
|
|
2961
3066
|
const sSQLInsert = `
|
|
2962
|
-
INSERT INTO
|
|
2963
|
-
ID,
|
|
2964
|
-
Name,
|
|
2965
|
-
DisplayName,
|
|
2966
|
-
Description,
|
|
2967
|
-
NameSuffix,
|
|
2968
|
-
BaseTable,
|
|
2969
|
-
BaseView,
|
|
2970
|
-
SchemaName,
|
|
2971
|
-
IncludeInAPI,
|
|
2972
|
-
AllowUserSearchAPI
|
|
2973
|
-
${newEntityDefaults.TrackRecordChanges === undefined ? '' : ', TrackRecordChanges'}
|
|
2974
|
-
${newEntityDefaults.AuditRecordAccess === undefined ? '' : ', AuditRecordAccess'}
|
|
2975
|
-
${newEntityDefaults.AuditViewRuns === undefined ? '' : ', AuditViewRuns'}
|
|
2976
|
-
${newEntityDefaults.AllowAllRowsAPI === undefined ? '' : ', AllowAllRowsAPI'}
|
|
2977
|
-
${newEntityDefaults.AllowCreateAPI === undefined ? '' : ', AllowCreateAPI'}
|
|
2978
|
-
${newEntityDefaults.AllowUpdateAPI === undefined ? '' : ', AllowUpdateAPI'}
|
|
2979
|
-
${newEntityDefaults.AllowDeleteAPI === undefined ? '' : ', AllowDeleteAPI'}
|
|
2980
|
-
${newEntityDefaults.UserViewMaxRows === undefined ? '' : ', UserViewMaxRows'}
|
|
3067
|
+
INSERT INTO ${this.qs(mj_core_schema(), 'Entity')} (
|
|
3068
|
+
${q('ID')},
|
|
3069
|
+
${q('Name')},
|
|
3070
|
+
${q('DisplayName')},
|
|
3071
|
+
${q('Description')},
|
|
3072
|
+
${q('NameSuffix')},
|
|
3073
|
+
${q('BaseTable')},
|
|
3074
|
+
${q('BaseView')},
|
|
3075
|
+
${q('SchemaName')},
|
|
3076
|
+
${q('IncludeInAPI')},
|
|
3077
|
+
${q('AllowUserSearchAPI')}
|
|
3078
|
+
${newEntityDefaults.TrackRecordChanges === undefined ? '' : ', ' + q('TrackRecordChanges')}
|
|
3079
|
+
${newEntityDefaults.AuditRecordAccess === undefined ? '' : ', ' + q('AuditRecordAccess')}
|
|
3080
|
+
${newEntityDefaults.AuditViewRuns === undefined ? '' : ', ' + q('AuditViewRuns')}
|
|
3081
|
+
${newEntityDefaults.AllowAllRowsAPI === undefined ? '' : ', ' + q('AllowAllRowsAPI')}
|
|
3082
|
+
${newEntityDefaults.AllowCreateAPI === undefined ? '' : ', ' + q('AllowCreateAPI')}
|
|
3083
|
+
${newEntityDefaults.AllowUpdateAPI === undefined ? '' : ', ' + q('AllowUpdateAPI')}
|
|
3084
|
+
${newEntityDefaults.AllowDeleteAPI === undefined ? '' : ', ' + q('AllowDeleteAPI')}
|
|
3085
|
+
${newEntityDefaults.UserViewMaxRows === undefined ? '' : ', ' + q('UserViewMaxRows')}
|
|
3086
|
+
, ${q('__mj_CreatedAt')}
|
|
3087
|
+
, ${q('__mj_UpdatedAt')}
|
|
2981
3088
|
)
|
|
2982
3089
|
VALUES (
|
|
2983
3090
|
'${newEntityUUID}',
|
|
@@ -2998,6 +3105,8 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
2998
3105
|
${newEntityDefaults.AllowUpdateAPI === undefined ? '' : ', ' + (newEntityDefaults.AllowUpdateAPI ? '1' : '0')}
|
|
2999
3106
|
${newEntityDefaults.AllowDeleteAPI === undefined ? '' : ', ' + (newEntityDefaults.AllowDeleteAPI ? '1' : '0')}
|
|
3000
3107
|
${newEntityDefaults.UserViewMaxRows === undefined ? '' : ', ' + (newEntityDefaults.UserViewMaxRows)}
|
|
3108
|
+
, ${this.utcNow()}
|
|
3109
|
+
, ${this.utcNow()}
|
|
3001
3110
|
)
|
|
3002
3111
|
`;
|
|
3003
3112
|
return sSQLInsert;
|
|
@@ -3051,13 +3160,13 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
3051
3160
|
e.BaseTable,
|
|
3052
3161
|
e.ParentID
|
|
3053
3162
|
FROM
|
|
3054
|
-
|
|
3163
|
+
${this.qs(mj_core_schema(), 'vwEntities')} e
|
|
3055
3164
|
WHERE
|
|
3056
3165
|
${whereClause}
|
|
3057
3166
|
ORDER BY
|
|
3058
3167
|
e.Name
|
|
3059
3168
|
`;
|
|
3060
|
-
const entitiesResult = await
|
|
3169
|
+
const entitiesResult = await this.runQuery(pool, entitiesSQL);
|
|
3061
3170
|
const entities = entitiesResult.recordset;
|
|
3062
3171
|
if (entities.length === 0) {
|
|
3063
3172
|
return true;
|
|
@@ -3089,14 +3198,14 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
3089
3198
|
ef.DefaultInView,
|
|
3090
3199
|
ef.IncludeInUserSearchAPI
|
|
3091
3200
|
FROM
|
|
3092
|
-
|
|
3201
|
+
${this.qs(mj_core_schema(), 'vwEntityFields')} ef
|
|
3093
3202
|
WHERE
|
|
3094
3203
|
ef.EntityID IN (${entityIds})
|
|
3095
3204
|
ORDER BY
|
|
3096
3205
|
ef.EntityID,
|
|
3097
3206
|
ef.Sequence
|
|
3098
3207
|
`;
|
|
3099
|
-
const fieldsResult = await
|
|
3208
|
+
const fieldsResult = await this.runQuery(pool, fieldsSQL);
|
|
3100
3209
|
const allFields = fieldsResult.recordset;
|
|
3101
3210
|
// Get EntitySettings for all entities (for existing category icons/info)
|
|
3102
3211
|
const settingsSQL = `
|
|
@@ -3105,12 +3214,12 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
3105
3214
|
es.Name,
|
|
3106
3215
|
es.Value
|
|
3107
3216
|
FROM
|
|
3108
|
-
|
|
3217
|
+
${this.qs(mj_core_schema(), 'EntitySetting')} es
|
|
3109
3218
|
WHERE
|
|
3110
3219
|
es.EntityID IN (${entityIds})
|
|
3111
3220
|
AND es.Name = 'FieldCategoryInfo'
|
|
3112
3221
|
`;
|
|
3113
|
-
const settingsResult = await
|
|
3222
|
+
const settingsResult = await this.runQuery(pool, settingsSQL);
|
|
3114
3223
|
const allSettings = settingsResult.recordset;
|
|
3115
3224
|
// Group settings by entity
|
|
3116
3225
|
const settingsByEntity = {};
|
|
@@ -3175,7 +3284,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
3175
3284
|
async processEntityAdvancedGeneration(pool, entity, allFields, ag, currentUser) {
|
|
3176
3285
|
try {
|
|
3177
3286
|
// Filter fields for this entity (client-side filtering)
|
|
3178
|
-
const fields = allFields.filter((f) => f.EntityID
|
|
3287
|
+
const fields = allFields.filter((f) => UUIDsEqual(f.EntityID, entity.ID));
|
|
3179
3288
|
// Determine if this is a new entity (for DefaultForNewUser decision)
|
|
3180
3289
|
const isNewEntity = ManageMetadataBase.newEntityList.includes(entity.Name);
|
|
3181
3290
|
// Smart Field Identification
|
|
@@ -3236,7 +3345,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
3236
3345
|
if (visited.has(currentParentID))
|
|
3237
3346
|
break; // circular reference guard
|
|
3238
3347
|
visited.add(currentParentID);
|
|
3239
|
-
const parentEntity = allEntities.find(e => e.ID
|
|
3348
|
+
const parentEntity = allEntities.find(e => UUIDsEqual(e.ID, currentParentID));
|
|
3240
3349
|
if (!parentEntity)
|
|
3241
3350
|
break;
|
|
3242
3351
|
parentChain.push({ entityID: parentEntity.ID, entityName: parentEntity.Name });
|
|
@@ -3264,7 +3373,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
3264
3373
|
*/
|
|
3265
3374
|
findFieldSourceParent(fieldName, parentChain, allEntities) {
|
|
3266
3375
|
for (const parent of parentChain) {
|
|
3267
|
-
const parentEntity = allEntities.find(e => e.ID
|
|
3376
|
+
const parentEntity = allEntities.find(e => UUIDsEqual(e.ID, parent.entityID));
|
|
3268
3377
|
if (!parentEntity)
|
|
3269
3378
|
continue;
|
|
3270
3379
|
// Check if this parent has a non-virtual field with this name
|
|
@@ -3284,7 +3393,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
3284
3393
|
const nameField = fields.find(f => f.Name === result.nameField);
|
|
3285
3394
|
if (nameField && nameField.AutoUpdateIsNameField && nameField.ID && !nameField.IsNameField /*don't waste SQL to set the value if IsNameField already set */) {
|
|
3286
3395
|
sqlStatements.push(`
|
|
3287
|
-
UPDATE
|
|
3396
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
3288
3397
|
SET IsNameField = 1
|
|
3289
3398
|
WHERE ID = '${nameField.ID}'
|
|
3290
3399
|
AND AutoUpdateIsNameField = 1
|
|
@@ -3305,7 +3414,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
3305
3414
|
if (!field.DefaultInView) {
|
|
3306
3415
|
// only set these when DefaultInView not already on, otherwise wasteful
|
|
3307
3416
|
sqlStatements.push(`
|
|
3308
|
-
UPDATE
|
|
3417
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
3309
3418
|
SET DefaultInView = 1
|
|
3310
3419
|
WHERE ID = '${field.ID}'
|
|
3311
3420
|
AND AutoUpdateDefaultInView = 1
|
|
@@ -3325,7 +3434,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
3325
3434
|
if (!field.IncludeInUserSearchAPI) {
|
|
3326
3435
|
// only set this if IncludeInUserSearchAPI isn't already set
|
|
3327
3436
|
sqlStatements.push(`
|
|
3328
|
-
UPDATE
|
|
3437
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
3329
3438
|
SET IncludeInUserSearchAPI = 1
|
|
3330
3439
|
WHERE ID = '${field.ID}'
|
|
3331
3440
|
AND AutoUpdateIncludeInUserSearchAPI = 1
|
|
@@ -3429,7 +3538,7 @@ DROP TABLE #__mj__CodeGen__vwTableUniqueKeys;
|
|
|
3429
3538
|
}
|
|
3430
3539
|
if (setClauses.length > 0) {
|
|
3431
3540
|
// only generate an UPDATE if we have 1+ set clause
|
|
3432
|
-
sqlStatements.push(`\n-- UPDATE Entity Field Category Info ${entity.Name}.${field.Name} \nUPDATE
|
|
3541
|
+
sqlStatements.push(`\n-- UPDATE Entity Field Category Info ${entity.Name}.${field.Name} \nUPDATE ${this.qs(mj_core_schema(), 'EntityField')}
|
|
3433
3542
|
SET
|
|
3434
3543
|
${setClauses.join(',\n ')}
|
|
3435
3544
|
WHERE
|
|
@@ -3455,15 +3564,15 @@ WHERE
|
|
|
3455
3564
|
async applyEntityIcon(pool, entityId, entityIcon) {
|
|
3456
3565
|
if (!entityIcon || entityIcon.trim().length === 0)
|
|
3457
3566
|
return;
|
|
3458
|
-
const checkSQL = `SELECT Icon FROM
|
|
3459
|
-
const entityCheck = await
|
|
3567
|
+
const checkSQL = `SELECT Icon FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE ID = '${entityId}'`;
|
|
3568
|
+
const entityCheck = await this.runQuery(pool, checkSQL);
|
|
3460
3569
|
if (entityCheck.recordset.length > 0) {
|
|
3461
3570
|
const currentIcon = entityCheck.recordset[0].Icon;
|
|
3462
3571
|
if (!currentIcon || currentIcon.trim().length === 0) {
|
|
3463
3572
|
const escapedIcon = entityIcon.replace(/'/g, "''");
|
|
3464
3573
|
const updateSQL = `
|
|
3465
|
-
UPDATE
|
|
3466
|
-
SET Icon = '${escapedIcon}', __mj_UpdatedAt =
|
|
3574
|
+
UPDATE ${this.qs(mj_core_schema(), 'Entity')}
|
|
3575
|
+
SET Icon = '${escapedIcon}', __mj_UpdatedAt = ${this.utcNow()}
|
|
3467
3576
|
WHERE ID = '${entityId}'
|
|
3468
3577
|
`;
|
|
3469
3578
|
try {
|
|
@@ -3484,13 +3593,13 @@ WHERE
|
|
|
3484
3593
|
return;
|
|
3485
3594
|
const infoJSON = JSON.stringify(categoryInfo).replace(/'/g, "''");
|
|
3486
3595
|
// Upsert FieldCategoryInfo (new format)
|
|
3487
|
-
const checkNewSQL = `SELECT ID FROM
|
|
3488
|
-
const existingNew = await
|
|
3596
|
+
const checkNewSQL = `SELECT ID FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryInfo'`;
|
|
3597
|
+
const existingNew = await this.runQuery(pool, checkNewSQL);
|
|
3489
3598
|
if (existingNew.recordset.length > 0) {
|
|
3490
3599
|
try {
|
|
3491
3600
|
await this.LogSQLAndExecute(pool, `
|
|
3492
|
-
UPDATE
|
|
3493
|
-
SET Value = '${infoJSON}', __mj_UpdatedAt =
|
|
3601
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntitySetting')}
|
|
3602
|
+
SET Value = '${infoJSON}', __mj_UpdatedAt = ${this.utcNow()}
|
|
3494
3603
|
WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryInfo'
|
|
3495
3604
|
`, `Update FieldCategoryInfo setting for entity`, false);
|
|
3496
3605
|
}
|
|
@@ -3502,8 +3611,8 @@ WHERE
|
|
|
3502
3611
|
const newId = uuidv4();
|
|
3503
3612
|
try {
|
|
3504
3613
|
await this.LogSQLAndExecute(pool, `
|
|
3505
|
-
INSERT INTO
|
|
3506
|
-
VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}',
|
|
3614
|
+
INSERT INTO ${this.qs(mj_core_schema(), 'EntitySetting')} (ID, EntityID, Name, Value, __mj_CreatedAt, __mj_UpdatedAt)
|
|
3615
|
+
VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}', ${this.utcNow()}, ${this.utcNow()})
|
|
3507
3616
|
`, `Insert FieldCategoryInfo setting for entity`, false);
|
|
3508
3617
|
}
|
|
3509
3618
|
catch (ex) {
|
|
@@ -3518,13 +3627,13 @@ WHERE
|
|
|
3518
3627
|
}
|
|
3519
3628
|
}
|
|
3520
3629
|
const iconsJSON = JSON.stringify(iconsOnly).replace(/'/g, "''");
|
|
3521
|
-
const checkLegacySQL = `SELECT ID FROM
|
|
3522
|
-
const existingLegacy = await
|
|
3630
|
+
const checkLegacySQL = `SELECT ID FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryIcons'`;
|
|
3631
|
+
const existingLegacy = await this.runQuery(pool, checkLegacySQL);
|
|
3523
3632
|
if (existingLegacy.recordset.length > 0) {
|
|
3524
3633
|
try {
|
|
3525
3634
|
await this.LogSQLAndExecute(pool, `
|
|
3526
|
-
UPDATE
|
|
3527
|
-
SET Value = '${iconsJSON}', __mj_UpdatedAt =
|
|
3635
|
+
UPDATE ${this.qs(mj_core_schema(), 'EntitySetting')}
|
|
3636
|
+
SET Value = '${iconsJSON}', __mj_UpdatedAt = ${this.utcNow()}
|
|
3528
3637
|
WHERE EntityID = '${entityId}' AND Name = 'FieldCategoryIcons'
|
|
3529
3638
|
`, `Update FieldCategoryIcons setting (legacy)`, false);
|
|
3530
3639
|
}
|
|
@@ -3536,8 +3645,8 @@ WHERE
|
|
|
3536
3645
|
const newId = uuidv4();
|
|
3537
3646
|
try {
|
|
3538
3647
|
await this.LogSQLAndExecute(pool, `
|
|
3539
|
-
INSERT INTO
|
|
3540
|
-
VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}',
|
|
3648
|
+
INSERT INTO ${this.qs(mj_core_schema(), 'EntitySetting')} (ID, EntityID, Name, Value, __mj_CreatedAt, __mj_UpdatedAt)
|
|
3649
|
+
VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}', ${this.utcNow()}, ${this.utcNow()})
|
|
3541
3650
|
`, `Insert FieldCategoryIcons setting (legacy)`, false);
|
|
3542
3651
|
}
|
|
3543
3652
|
catch (ex) {
|
|
@@ -3552,8 +3661,8 @@ WHERE
|
|
|
3552
3661
|
async applyEntityImportance(pool, entityId, importance) {
|
|
3553
3662
|
const defaultForNewUser = importance.defaultForNewUser ? 1 : 0;
|
|
3554
3663
|
const updateSQL = `
|
|
3555
|
-
UPDATE
|
|
3556
|
-
SET DefaultForNewUser = ${defaultForNewUser}, __mj_UpdatedAt =
|
|
3664
|
+
UPDATE ${this.qs(mj_core_schema(), 'ApplicationEntity')}
|
|
3665
|
+
SET DefaultForNewUser = ${defaultForNewUser}, __mj_UpdatedAt = ${this.utcNow()}
|
|
3557
3666
|
WHERE EntityID = '${entityId}'
|
|
3558
3667
|
`;
|
|
3559
3668
|
try {
|
|
@@ -3576,7 +3685,7 @@ WHERE
|
|
|
3576
3685
|
* @returns - The result of the query execution.
|
|
3577
3686
|
*/
|
|
3578
3687
|
async LogSQLAndExecute(pool, query, description, isRecurringScript = false) {
|
|
3579
|
-
return await SQLLogging.LogSQLAndExecute(pool, query, description, isRecurringScript);
|
|
3688
|
+
return await SQLLogging.LogSQLAndExecute(pool, this.qsql(query), description, isRecurringScript);
|
|
3580
3689
|
}
|
|
3581
3690
|
}
|
|
3582
3691
|
//# sourceMappingURL=manage-metadata.js.map
|