@memberjunction/codegen-lib 3.3.0 → 4.0.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 +56 -1
- package/dist/Angular/angular-codegen.d.ts +1 -1
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +80 -136
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.d.ts +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.js +6 -9
- package/dist/Angular/entity-data-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/join-grid-related-entity-component.d.ts +1 -1
- package/dist/Angular/join-grid-related-entity-component.js +8 -36
- package/dist/Angular/join-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/related-entity-components.js +13 -79
- package/dist/Angular/related-entity-components.js.map +1 -1
- package/dist/Angular/timeline-related-entity-component.d.ts +1 -1
- package/dist/Angular/timeline-related-entity-component.js +14 -38
- package/dist/Angular/timeline-related-entity-component.js.map +1 -1
- package/dist/Angular/user-view-grid-related-entity-component.d.ts +43 -0
- package/dist/Angular/user-view-grid-related-entity-component.d.ts.map +1 -0
- package/dist/Angular/user-view-grid-related-entity-component.js +85 -0
- package/dist/Angular/user-view-grid-related-entity-component.js.map +1 -0
- package/dist/Config/config.d.ts +79 -0
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +174 -172
- package/dist/Config/config.js.map +1 -1
- package/dist/Config/db-connection.d.ts +1 -1
- package/dist/Config/db-connection.d.ts.map +1 -1
- package/dist/Config/db-connection.js +6 -33
- package/dist/Config/db-connection.js.map +1 -1
- package/dist/Database/dbSchema.js +28 -35
- package/dist/Database/dbSchema.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +25 -9
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +438 -316
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/reorder-columns.d.ts +1 -1
- package/dist/Database/reorder-columns.d.ts.map +1 -1
- package/dist/Database/reorder-columns.js +1 -5
- package/dist/Database/reorder-columns.js.map +1 -1
- package/dist/Database/sql.d.ts +1 -1
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +67 -98
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +5 -2
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +344 -268
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +110 -0
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -0
- package/dist/Manifest/GenerateClassRegistrationsManifest.js +632 -0
- package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -0
- package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +15 -26
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.d.ts +1 -1
- package/dist/Misc/advanced_generation.js +34 -40
- package/dist/Misc/advanced_generation.js.map +1 -1
- package/dist/Misc/createNewUser.d.ts +1 -1
- package/dist/Misc/createNewUser.js +22 -26
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts +2 -2
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +33 -40
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +36 -41
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/runCommand.d.ts +1 -1
- package/dist/Misc/runCommand.js +13 -20
- package/dist/Misc/runCommand.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +7 -1
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +40 -53
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/Misc/status_logging.js +45 -60
- package/dist/Misc/status_logging.js.map +1 -1
- package/dist/Misc/system_integrity.d.ts +1 -1
- package/dist/Misc/system_integrity.d.ts.map +1 -1
- package/dist/Misc/system_integrity.js +12 -16
- package/dist/Misc/system_integrity.js.map +1 -1
- package/dist/Misc/temp_batch_file.js +15 -22
- package/dist/Misc/temp_batch_file.js.map +1 -1
- package/dist/Misc/util.d.ts.map +1 -1
- package/dist/Misc/util.js +17 -28
- package/dist/Misc/util.js.map +1 -1
- package/dist/index.d.ts +21 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -40
- package/dist/index.js.map +1 -1
- package/dist/runCodeGen.d.ts +1 -0
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +151 -178
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +24 -21
|
@@ -1,45 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
-
};
|
|
28
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.SQLCodeGenBase = exports.SPType = void 0;
|
|
30
|
-
const core_1 = require("@memberjunction/core");
|
|
31
|
-
const status_logging_1 = require("../Misc/status_logging");
|
|
32
|
-
const fs = __importStar(require("fs"));
|
|
33
|
-
const path_1 = __importDefault(require("path"));
|
|
34
|
-
const sql_1 = require("./sql");
|
|
35
|
-
const config_1 = require("../Config/config");
|
|
36
|
-
const manage_metadata_1 = require("./manage-metadata");
|
|
37
|
-
const sqlserver_dataprovider_1 = require("@memberjunction/sqlserver-dataprovider");
|
|
38
|
-
const util_1 = require("../Misc/util");
|
|
39
|
-
const global_1 = require("@memberjunction/global");
|
|
40
|
-
const sql_logging_1 = require("../Misc/sql_logging");
|
|
41
|
-
const temp_batch_file_1 = require("../Misc/temp_batch_file");
|
|
42
|
-
exports.SPType = {
|
|
1
|
+
import { EntityInfo, Metadata } from '@memberjunction/core';
|
|
2
|
+
import { logError, logStatus, logWarning, startSpinner, updateSpinner, succeedSpinner, failSpinner } from '../Misc/status_logging.js';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { SQLUtilityBase } from './sql.js';
|
|
6
|
+
import { autoIndexForeignKeys, configInfo, customSqlScripts, dbDatabase, mjCoreSchema, MAX_INDEX_NAME_LENGTH } from '../Config/config.js';
|
|
7
|
+
import { ManageMetadataBase } from './manage-metadata.js';
|
|
8
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
9
|
+
import { combineFiles, logIf, sortBySequenceAndCreatedAt } from '../Misc/util.js';
|
|
10
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
11
|
+
import { SQLLogging } from '../Misc/sql_logging.js';
|
|
12
|
+
import { TempBatchFile } from '../Misc/temp_batch_file.js';
|
|
13
|
+
export const SPType = {
|
|
43
14
|
Create: 'Create',
|
|
44
15
|
Update: 'Update',
|
|
45
16
|
Delete: 'Delete',
|
|
@@ -49,53 +20,55 @@ exports.SPType = {
|
|
|
49
20
|
* databases. The base class implements support for SQL Server. In future versions of MJ, we will break out an abstract base class that has the skeleton of the logic and then the SQL Server version will be a sub-class
|
|
50
21
|
* of that abstract base class and other databases will be sub-classes of the abstract base class as well.
|
|
51
22
|
*/
|
|
52
|
-
class SQLCodeGenBase {
|
|
53
|
-
|
|
23
|
+
export class SQLCodeGenBase {
|
|
24
|
+
constructor() {
|
|
25
|
+
this._sqlUtilityObject = MJGlobal.Instance.ClassFactory.CreateInstance(SQLUtilityBase);
|
|
26
|
+
/**
|
|
27
|
+
* Array of entity names that qualify for forced regeneration based on the whereClause filter
|
|
28
|
+
*/
|
|
29
|
+
this.entitiesQualifiedForForcedRegeneration = [];
|
|
30
|
+
/**
|
|
31
|
+
* Flag indicating whether to filter entities for forced regeneration based on entityWhereClause
|
|
32
|
+
*/
|
|
33
|
+
this.filterEntitiesQualifiedForRegeneration = false;
|
|
34
|
+
/**
|
|
35
|
+
* Tracks cascade delete dependencies between entities.
|
|
36
|
+
* Key: Entity ID whose update/delete SP is called by other entities' delete SPs
|
|
37
|
+
* Value: Set of Entity IDs that have CascadeDeletes=true and call this entity's update/delete SP
|
|
38
|
+
*/
|
|
39
|
+
this.cascadeDeleteDependencies = new Map();
|
|
40
|
+
/**
|
|
41
|
+
* Tracks entities that need their delete stored procedures regenerated due to cascade dependencies
|
|
42
|
+
*/
|
|
43
|
+
this.entitiesNeedingDeleteSPRegeneration = new Set();
|
|
44
|
+
/**
|
|
45
|
+
* Ordered list of entity IDs for delete SP regeneration (dependency order)
|
|
46
|
+
*/
|
|
47
|
+
this.orderedEntitiesForDeleteSPRegeneration = [];
|
|
48
|
+
}
|
|
54
49
|
get SQLUtilityObject() {
|
|
55
50
|
return this._sqlUtilityObject;
|
|
56
51
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Array of entity names that qualify for forced regeneration based on the whereClause filter
|
|
59
|
-
*/
|
|
60
|
-
entitiesQualifiedForForcedRegeneration = [];
|
|
61
|
-
/**
|
|
62
|
-
* Flag indicating whether to filter entities for forced regeneration based on entityWhereClause
|
|
63
|
-
*/
|
|
64
|
-
filterEntitiesQualifiedForRegeneration = false;
|
|
65
|
-
/**
|
|
66
|
-
* Tracks cascade delete dependencies between entities.
|
|
67
|
-
* Key: Entity ID whose update/delete SP is called by other entities' delete SPs
|
|
68
|
-
* Value: Set of Entity IDs that have CascadeDeletes=true and call this entity's update/delete SP
|
|
69
|
-
*/
|
|
70
|
-
cascadeDeleteDependencies = new Map();
|
|
71
|
-
/**
|
|
72
|
-
* Tracks entities that need their delete stored procedures regenerated due to cascade dependencies
|
|
73
|
-
*/
|
|
74
|
-
entitiesNeedingDeleteSPRegeneration = new Set();
|
|
75
|
-
/**
|
|
76
|
-
* Ordered list of entity IDs for delete SP regeneration (dependency order)
|
|
77
|
-
*/
|
|
78
|
-
orderedEntitiesForDeleteSPRegeneration = [];
|
|
79
52
|
async manageSQLScriptsAndExecution(pool, entities, directory, currentUser) {
|
|
80
53
|
try {
|
|
81
54
|
// Build list of entities qualified for forced regeneration if entityWhereClause is provided
|
|
82
|
-
if (
|
|
55
|
+
if (configInfo.forceRegeneration?.enabled && configInfo.forceRegeneration?.entityWhereClause) {
|
|
83
56
|
this.filterEntitiesQualifiedForRegeneration = true; // Enable filtering
|
|
84
57
|
try {
|
|
85
|
-
const whereClause =
|
|
58
|
+
const whereClause = configInfo.forceRegeneration.entityWhereClause;
|
|
86
59
|
const query = `
|
|
87
60
|
SELECT Name
|
|
88
|
-
FROM [${
|
|
61
|
+
FROM [${mjCoreSchema}].[Entity]
|
|
89
62
|
WHERE ${whereClause}
|
|
90
63
|
`;
|
|
91
64
|
const result = await pool.request().query(query);
|
|
92
65
|
this.entitiesQualifiedForForcedRegeneration = result.recordset.map((r) => r.Name);
|
|
93
|
-
|
|
66
|
+
logStatus(`Force regeneration filter enabled: ${this.entitiesQualifiedForForcedRegeneration.length} entities qualified based on entityWhereClause: ${whereClause}`);
|
|
94
67
|
}
|
|
95
68
|
catch (error) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
69
|
+
logError(`CRITICAL ERROR: Failed to execute forceRegeneration.entityWhereClause query: ${error}`);
|
|
70
|
+
logError(`WHERE clause: ${configInfo.forceRegeneration.entityWhereClause}`);
|
|
71
|
+
logError(`Stopping execution due to invalid entityWhereClause configuration`);
|
|
99
72
|
throw new Error(`Invalid forceRegeneration.entityWhereClause: ${error}`);
|
|
100
73
|
}
|
|
101
74
|
}
|
|
@@ -103,32 +76,32 @@ class SQLCodeGenBase {
|
|
|
103
76
|
// we have custom base views, need to have them defined before we do
|
|
104
77
|
// the rest as the generated stuff might use custom base views in compiled
|
|
105
78
|
// objects like spCreate for a given entity might reference the vw for that entity
|
|
106
|
-
|
|
79
|
+
startSpinner('Running custom SQL scripts...');
|
|
107
80
|
const startTime = new Date();
|
|
108
81
|
if (!await this.runCustomSQLScripts(pool, 'before-sql')) {
|
|
109
|
-
|
|
82
|
+
failSpinner('Failed to run custom SQL scripts');
|
|
110
83
|
return false;
|
|
111
84
|
}
|
|
112
|
-
|
|
85
|
+
succeedSpinner(`Custom SQL scripts completed (${(new Date().getTime() - startTime.getTime()) / 1000}s)`);
|
|
113
86
|
// ALWAYS use the first filter where we only include entities that have IncludeInAPI = 1
|
|
114
87
|
// Entities are already sorted by name in PostProcessEntityMetadata (see providerBase.ts)
|
|
115
88
|
const baselineEntities = entities.filter(e => e.IncludeInAPI);
|
|
116
|
-
const includedEntities = baselineEntities.filter(e =>
|
|
117
|
-
const excludedEntities = baselineEntities.filter(e =>
|
|
89
|
+
const includedEntities = baselineEntities.filter(e => configInfo.excludeSchemas.find(s => s.toLowerCase() === e.SchemaName.toLowerCase()) === undefined); //only include entities that are NOT in the excludeSchemas list
|
|
90
|
+
const excludedEntities = baselineEntities.filter(e => configInfo.excludeSchemas.find(s => s.toLowerCase() === e.SchemaName.toLowerCase()) !== undefined); //only include entities that ARE in the excludeSchemas list in this array
|
|
118
91
|
// Initialize temp batch files for each schema
|
|
119
92
|
// These will be populated as SQL is generated and will be used for actual execution
|
|
120
93
|
const schemas = Array.from(new Set(baselineEntities.map(e => e.SchemaName)));
|
|
121
|
-
|
|
94
|
+
TempBatchFile.initialize(directory, schemas);
|
|
122
95
|
// STEP 1.5 - Check for cascade delete dependencies that require regeneration
|
|
123
|
-
|
|
96
|
+
startSpinner('Analyzing cascade delete dependencies...');
|
|
124
97
|
await this.markEntitiesForCascadeDeleteRegeneration(pool, includedEntities);
|
|
125
|
-
|
|
98
|
+
succeedSpinner('Cascade delete dependency analysis completed');
|
|
126
99
|
// STEP 2(a) - clean out all *.generated.sql and *.permissions.generated.sql files from the directory
|
|
127
|
-
|
|
100
|
+
startSpinner('Cleaning generated files...');
|
|
128
101
|
this.deleteGeneratedEntityFiles(directory, baselineEntities);
|
|
129
|
-
|
|
102
|
+
succeedSpinner('Cleaned generated files');
|
|
130
103
|
// STEP 2(b) - generate all the SQL files and execute them
|
|
131
|
-
|
|
104
|
+
startSpinner(`Generating SQL for ${includedEntities.length} entities...`);
|
|
132
105
|
const step2StartTime = new Date();
|
|
133
106
|
// First, separate entities that need cascade delete regeneration from others
|
|
134
107
|
const entitiesWithoutCascadeRegeneration = includedEntities.filter(e => !this.entitiesNeedingDeleteSPRegeneration.has(e.ID));
|
|
@@ -147,12 +120,12 @@ class SQLCodeGenBase {
|
|
|
147
120
|
enableSQLLoggingForNewOrModifiedEntities: true
|
|
148
121
|
}); // enable sql logging for NEW entities....
|
|
149
122
|
if (!genResult.Success) {
|
|
150
|
-
|
|
123
|
+
failSpinner('Failed to generate entity SQL files');
|
|
151
124
|
return false;
|
|
152
125
|
}
|
|
153
126
|
// Generate SQL for cascade delete regenerations in dependency order (sequentially)
|
|
154
127
|
if (entitiesForCascadeRegeneration.length > 0) {
|
|
155
|
-
|
|
128
|
+
updateSpinner(`Regenerating ${entitiesForCascadeRegeneration.length} delete SPs in dependency order...`);
|
|
156
129
|
const cascadeGenResult = await this.generateAndExecuteEntitySQLToSeparateFiles({
|
|
157
130
|
pool,
|
|
158
131
|
entities: entitiesForCascadeRegeneration,
|
|
@@ -164,13 +137,13 @@ class SQLCodeGenBase {
|
|
|
164
137
|
enableSQLLoggingForNewOrModifiedEntities: true
|
|
165
138
|
});
|
|
166
139
|
if (!cascadeGenResult.Success) {
|
|
167
|
-
|
|
140
|
+
failSpinner('Failed to regenerate cascade delete SPs');
|
|
168
141
|
return false;
|
|
169
142
|
}
|
|
170
143
|
genResult.Files.push(...cascadeGenResult.Files);
|
|
171
144
|
}
|
|
172
145
|
// STEP 2(c) - for the excludedEntities, while we don't want to generate SQL, we do want to generate the permissions files for them
|
|
173
|
-
|
|
146
|
+
updateSpinner(`Generating permissions for ${excludedEntities.length} excluded entities...`);
|
|
174
147
|
const genResult2 = await this.generateAndExecuteEntitySQLToSeparateFiles({
|
|
175
148
|
pool,
|
|
176
149
|
entities: excludedEntities,
|
|
@@ -182,88 +155,88 @@ class SQLCodeGenBase {
|
|
|
182
155
|
enableSQLLoggingForNewOrModifiedEntities: false /*don't log this stuff, it is just permissions for excluded entities*/
|
|
183
156
|
});
|
|
184
157
|
if (!genResult2.Success) {
|
|
185
|
-
|
|
158
|
+
failSpinner('Failed to generate permissions for excluded entities');
|
|
186
159
|
return false;
|
|
187
160
|
}
|
|
188
|
-
|
|
161
|
+
succeedSpinner(`Entity generation completed (${(new Date().getTime() - step2StartTime.getTime()) / 1000}s)`);
|
|
189
162
|
// STEP 2(d) now that we've generated the SQL, let's create a combined file in each schema sub-directory for convenience for a DBA
|
|
190
|
-
|
|
163
|
+
startSpinner('Creating combined SQL files...');
|
|
191
164
|
const allEntityFiles = this.createCombinedEntitySQLFiles(directory, baselineEntities);
|
|
192
|
-
|
|
165
|
+
succeedSpinner(`Created combined SQL files for ${allEntityFiles.length} schemas`);
|
|
193
166
|
// STEP 2(e) ---- FINALLY, we execute SQL in proper dependency order
|
|
194
167
|
// Use temp batch files (which maintain CodeGen log order) if available, otherwise fall back to combined files
|
|
195
|
-
|
|
168
|
+
startSpinner('Executing entity SQL files...');
|
|
196
169
|
const step2eStartTime = new Date();
|
|
197
170
|
let executionSuccess = false;
|
|
198
|
-
if (
|
|
171
|
+
if (TempBatchFile.hasContent()) {
|
|
199
172
|
// Execute temp batch files in dependency order (matches CodeGen run log)
|
|
200
|
-
const tempFiles =
|
|
201
|
-
|
|
202
|
-
executionSuccess = await this.SQLUtilityObject.executeSQLFiles(tempFiles,
|
|
173
|
+
const tempFiles = TempBatchFile.getTempFilePaths();
|
|
174
|
+
logIf(configInfo?.verboseOutput ?? false, `Executing ${tempFiles.length} temp batch file(s) in dependency order`);
|
|
175
|
+
executionSuccess = await this.SQLUtilityObject.executeSQLFiles(tempFiles, configInfo?.verboseOutput ?? false);
|
|
203
176
|
// Clean up temp files after execution
|
|
204
|
-
|
|
177
|
+
TempBatchFile.cleanup();
|
|
205
178
|
}
|
|
206
179
|
else {
|
|
207
180
|
// Fall back to combined files (for backward compatibility or if temp files weren't created)
|
|
208
|
-
|
|
209
|
-
executionSuccess = await this.SQLUtilityObject.executeSQLFiles(allEntityFiles,
|
|
181
|
+
logIf(configInfo?.verboseOutput ?? false, `Executing ${allEntityFiles.length} combined file(s)`);
|
|
182
|
+
executionSuccess = await this.SQLUtilityObject.executeSQLFiles(allEntityFiles, configInfo?.verboseOutput ?? false);
|
|
210
183
|
}
|
|
211
184
|
if (!executionSuccess) {
|
|
212
|
-
|
|
213
|
-
|
|
185
|
+
failSpinner('Failed to execute entity SQL files');
|
|
186
|
+
TempBatchFile.cleanup(); // Cleanup on error
|
|
214
187
|
return false;
|
|
215
188
|
}
|
|
216
189
|
const step2eEndTime = new Date();
|
|
217
|
-
|
|
218
|
-
const manageMD =
|
|
190
|
+
succeedSpinner(`SQL execution completed (${(step2eEndTime.getTime() - step2eStartTime.getTime()) / 1000}s)`);
|
|
191
|
+
const manageMD = MJGlobal.Instance.ClassFactory.CreateInstance(ManageMetadataBase);
|
|
219
192
|
// STEP 3 - re-run the process to manage entity fields since the Step 1 and 2 above might have resulted in differences in base view columns compared to what we had at first
|
|
220
193
|
// we CAN skip the entity field values part because that wouldn't change from the first time we ran it
|
|
221
194
|
// Run advanced generation here in case new virtual fields added, so we do NOT run advanced geneartion in the main manageMetadata() call
|
|
222
|
-
|
|
223
|
-
if (!await manageMD.manageEntityFields(pool,
|
|
224
|
-
|
|
195
|
+
startSpinner('Managing entity fields metadata...');
|
|
196
|
+
if (!await manageMD.manageEntityFields(pool, configInfo.excludeSchemas, true, true, currentUser, false)) {
|
|
197
|
+
failSpinner('Failed to manage entity fields');
|
|
225
198
|
return false;
|
|
226
199
|
}
|
|
227
|
-
|
|
200
|
+
succeedSpinner('Entity fields metadata updated');
|
|
228
201
|
// no logStatus/timer for this because manageEntityFields() has its own internal logging for this including the total, so it is redundant to log it here
|
|
229
202
|
// STEP 4- Apply permissions, executing all .permissions files
|
|
230
|
-
|
|
203
|
+
startSpinner('Applying permissions...');
|
|
231
204
|
const step4StartTime = new Date();
|
|
232
205
|
if (!await this.applyPermissions(pool, directory, baselineEntities)) {
|
|
233
|
-
|
|
206
|
+
failSpinner('Failed to apply permissions');
|
|
234
207
|
return false;
|
|
235
208
|
}
|
|
236
|
-
|
|
209
|
+
succeedSpinner(`Permissions applied (${(new Date().getTime() - step4StartTime.getTime()) / 1000}s)`);
|
|
237
210
|
// STEP 5 - execute any custom SQL scripts that should run afterwards
|
|
238
|
-
|
|
211
|
+
startSpinner('Running post-generation SQL scripts...');
|
|
239
212
|
const step5StartTime = new Date();
|
|
240
213
|
if (!await this.runCustomSQLScripts(pool, 'after-sql')) {
|
|
241
|
-
|
|
214
|
+
failSpinner('Failed to run post-generation SQL scripts');
|
|
242
215
|
return false;
|
|
243
216
|
}
|
|
244
|
-
|
|
245
|
-
|
|
217
|
+
succeedSpinner(`Post-generation scripts completed (${(new Date().getTime() - step5StartTime.getTime()) / 1000}s)`);
|
|
218
|
+
succeedSpinner(`SQL CodeGen completed successfully (${((new Date().getTime() - startTime.getTime()) / 1000)}s total)`);
|
|
246
219
|
// now - we need to tell our metadata object to refresh itself
|
|
247
|
-
const md = new
|
|
220
|
+
const md = new Metadata();
|
|
248
221
|
await md.Refresh();
|
|
249
222
|
return true;
|
|
250
223
|
}
|
|
251
224
|
catch (err) {
|
|
252
|
-
|
|
225
|
+
logError(err);
|
|
253
226
|
// Clean up temp batch files on error
|
|
254
|
-
|
|
227
|
+
TempBatchFile.cleanup();
|
|
255
228
|
return false;
|
|
256
229
|
}
|
|
257
230
|
}
|
|
258
231
|
async runCustomSQLScripts(pool, when) {
|
|
259
232
|
try {
|
|
260
|
-
const scripts =
|
|
233
|
+
const scripts = customSqlScripts(when);
|
|
261
234
|
let bSuccess = true;
|
|
262
235
|
if (scripts) {
|
|
263
236
|
for (let i = 0; i < scripts.length; ++i) {
|
|
264
237
|
const s = scripts[i];
|
|
265
238
|
if (!await this.SQLUtilityObject.executeSQLFile(s.scriptFile)) {
|
|
266
|
-
|
|
239
|
+
logError(`Error executing custom '${when}' SQL script ${s.scriptFile}`);
|
|
267
240
|
bSuccess = false; // keep going if we have more scripts, but make sure we return false
|
|
268
241
|
}
|
|
269
242
|
}
|
|
@@ -271,7 +244,7 @@ class SQLCodeGenBase {
|
|
|
271
244
|
return bSuccess;
|
|
272
245
|
}
|
|
273
246
|
catch (e) {
|
|
274
|
-
|
|
247
|
+
logError(e);
|
|
275
248
|
return false;
|
|
276
249
|
}
|
|
277
250
|
}
|
|
@@ -285,7 +258,7 @@ class SQLCodeGenBase {
|
|
|
285
258
|
const files = this.getEntityPermissionFileNames(e);
|
|
286
259
|
let innerSuccess = true;
|
|
287
260
|
for (const f of files) {
|
|
288
|
-
const fullPath =
|
|
261
|
+
const fullPath = path.join(directory, f);
|
|
289
262
|
if (fs.existsSync(fullPath)) {
|
|
290
263
|
const fileBuffer = fs.readFileSync(fullPath);
|
|
291
264
|
const fileContents = fileBuffer.toString();
|
|
@@ -293,28 +266,28 @@ class SQLCodeGenBase {
|
|
|
293
266
|
await pool.request().query(fileContents);
|
|
294
267
|
}
|
|
295
268
|
catch (e) {
|
|
296
|
-
|
|
269
|
+
logError(`Error executing permissions file ${fullPath} for entity ${e.Name}: ${e}`);
|
|
297
270
|
innerSuccess = false;
|
|
298
271
|
}
|
|
299
272
|
}
|
|
300
273
|
else {
|
|
301
274
|
// we don't have the file, so we can't execute it, but we should log it as an error
|
|
302
275
|
// and then keep going
|
|
303
|
-
|
|
276
|
+
logError(`Permissions file ${fullPath} does not exist for entity ${e.Name}`);
|
|
304
277
|
}
|
|
305
278
|
}
|
|
306
279
|
return innerSuccess;
|
|
307
280
|
});
|
|
308
281
|
const results = await Promise.all(promises);
|
|
309
282
|
if (results.includes(false)) {
|
|
310
|
-
|
|
283
|
+
logError(`Error executing one or more permissions files in batch starting from index ${i}`);
|
|
311
284
|
bSuccess = false; // keep going, but will return false at the end
|
|
312
285
|
}
|
|
313
286
|
}
|
|
314
287
|
return bSuccess;
|
|
315
288
|
}
|
|
316
289
|
catch (err) {
|
|
317
|
-
|
|
290
|
+
logError(err);
|
|
318
291
|
return false;
|
|
319
292
|
}
|
|
320
293
|
}
|
|
@@ -337,7 +310,7 @@ class SQLCodeGenBase {
|
|
|
337
310
|
const promises = batch.map(async (e) => {
|
|
338
311
|
const pkeyField = e.Fields.find(f => f.IsPrimaryKey);
|
|
339
312
|
if (!pkeyField) {
|
|
340
|
-
|
|
313
|
+
logError(`SKIPPING ENTITY: Entity ${e.Name}, because it does not have a primary key field defined. A table must have a primary key defined to quality to be a MemberJunction entity`);
|
|
341
314
|
return { Success: false, Files: [] };
|
|
342
315
|
}
|
|
343
316
|
return this.generateAndExecuteSingleEntitySQLToSeparateFiles({
|
|
@@ -360,7 +333,7 @@ class SQLCodeGenBase {
|
|
|
360
333
|
return { Success: !bFail, Files: files };
|
|
361
334
|
}
|
|
362
335
|
catch (err) {
|
|
363
|
-
|
|
336
|
+
logError(err);
|
|
364
337
|
return { Success: false, Files: files };
|
|
365
338
|
}
|
|
366
339
|
}
|
|
@@ -369,7 +342,7 @@ class SQLCodeGenBase {
|
|
|
369
342
|
// for the schemas associated with the specified entities, clean out all the generated files
|
|
370
343
|
const schemaNames = entities.map(e => e.SchemaName).filter((value, index, self) => self.indexOf(value) === index);
|
|
371
344
|
for (const s of schemaNames) {
|
|
372
|
-
const fullPath =
|
|
345
|
+
const fullPath = path.join(directory, s);
|
|
373
346
|
// now, within each schema directory, clean out all the generated files
|
|
374
347
|
// the generated files map this pattern: *.generated.sql or *.permissions.generated.sql
|
|
375
348
|
let stats;
|
|
@@ -383,14 +356,14 @@ class SQLCodeGenBase {
|
|
|
383
356
|
if (stats?.isDirectory()) {
|
|
384
357
|
const files = fs.readdirSync(fullPath).filter(f => f.endsWith('.generated.sql') || f.endsWith('.permissions.generated.sql'));
|
|
385
358
|
for (const f of files) {
|
|
386
|
-
const filePath =
|
|
359
|
+
const filePath = path.join(fullPath, f);
|
|
387
360
|
fs.unlinkSync(filePath);
|
|
388
361
|
}
|
|
389
362
|
}
|
|
390
363
|
}
|
|
391
364
|
}
|
|
392
365
|
catch (e) {
|
|
393
|
-
|
|
366
|
+
logError(e);
|
|
394
367
|
}
|
|
395
368
|
}
|
|
396
369
|
createCombinedEntitySQLFiles(directory, entities) {
|
|
@@ -399,12 +372,12 @@ class SQLCodeGenBase {
|
|
|
399
372
|
const schemaNames = entities.map(e => e.SchemaName).filter((value, index, self) => self.indexOf(value) === index);
|
|
400
373
|
for (const s of schemaNames) {
|
|
401
374
|
// generate the all-entities.sql file and all-entities.permissions.sql file in each schema folder
|
|
402
|
-
const fullPath =
|
|
375
|
+
const fullPath = path.join(directory, s);
|
|
403
376
|
if (fs.statSync(fullPath).isDirectory()) {
|
|
404
|
-
|
|
405
|
-
files.push(
|
|
406
|
-
|
|
407
|
-
files.push(
|
|
377
|
+
combineFiles(fullPath, '_all_entities.sql', '*.generated.sql', true);
|
|
378
|
+
files.push(path.join(fullPath, '_all_entities.sql'));
|
|
379
|
+
combineFiles(fullPath, '_all_entities.permissions.sql', '*.permissions.generated.sql', true);
|
|
380
|
+
files.push(path.join(fullPath, '_all_entities.permissions.sql'));
|
|
408
381
|
}
|
|
409
382
|
}
|
|
410
383
|
return files;
|
|
@@ -422,7 +395,7 @@ class SQLCodeGenBase {
|
|
|
422
395
|
return { Success: true, Files: files };
|
|
423
396
|
}
|
|
424
397
|
catch (err) {
|
|
425
|
-
|
|
398
|
+
logError(err);
|
|
426
399
|
return { Success: false, Files: [] };
|
|
427
400
|
}
|
|
428
401
|
}
|
|
@@ -431,28 +404,35 @@ class SQLCodeGenBase {
|
|
|
431
404
|
let shouldLog = false;
|
|
432
405
|
if (logSql) {
|
|
433
406
|
// Check if entity is in new or modified lists
|
|
434
|
-
const isNewOrModified = !!
|
|
435
|
-
!!
|
|
407
|
+
const isNewOrModified = !!ManageMetadataBase.newEntityList.find(e => e === entity.Name) ||
|
|
408
|
+
!!ManageMetadataBase.modifiedEntityList.find(e => e === entity.Name);
|
|
436
409
|
// Check if entity is being regenerated due to cascade dependencies
|
|
437
410
|
const isCascadeDependencyRegeneration = description.toLowerCase().includes('spdelete') &&
|
|
438
411
|
this.entitiesNeedingDeleteSPRegeneration.has(entity.ID);
|
|
439
412
|
// Check if force regeneration is enabled for relevant SQL types
|
|
440
|
-
const isForceRegeneration =
|
|
441
|
-
(description.toLowerCase().includes('root id function') &&
|
|
442
|
-
(description.toLowerCase().includes('spcreate') &&
|
|
443
|
-
(description.toLowerCase().includes('spupdate') &&
|
|
444
|
-
(description.toLowerCase().includes('spdelete') &&
|
|
445
|
-
(description.toLowerCase().includes('index') &&
|
|
446
|
-
(description.toLowerCase().includes('full text search') &&
|
|
447
|
-
(
|
|
413
|
+
const isForceRegeneration = configInfo.forceRegeneration?.enabled && ((description.toLowerCase().includes('base view') && configInfo.forceRegeneration.baseViews) ||
|
|
414
|
+
(description.toLowerCase().includes('root id function') && configInfo.forceRegeneration.baseViews) || // TVFs are part of base view infrastructure
|
|
415
|
+
(description.toLowerCase().includes('spcreate') && configInfo.forceRegeneration.spCreate) ||
|
|
416
|
+
(description.toLowerCase().includes('spupdate') && configInfo.forceRegeneration.spUpdate) ||
|
|
417
|
+
(description.toLowerCase().includes('spdelete') && configInfo.forceRegeneration.spDelete) ||
|
|
418
|
+
(description.toLowerCase().includes('index') && configInfo.forceRegeneration.indexes) ||
|
|
419
|
+
(description.toLowerCase().includes('full text search') && configInfo.forceRegeneration.fullTextSearch) ||
|
|
420
|
+
(configInfo.forceRegeneration.allStoredProcedures &&
|
|
448
421
|
(description.toLowerCase().includes('spcreate') ||
|
|
449
422
|
description.toLowerCase().includes('spupdate') ||
|
|
450
423
|
description.toLowerCase().includes('spdelete'))));
|
|
424
|
+
// Check if entity has RelatedEntityJoinFields configured (requires view regeneration for metadata-only changes)
|
|
425
|
+
const hasRelatedEntityJoinFields = description.toLowerCase().includes('base view') &&
|
|
426
|
+
entity.Fields.some(f => f.RelatedEntityJoinFieldsConfig !== null);
|
|
451
427
|
// Determine if we should log based on entity state and force regeneration settings
|
|
452
428
|
if (isNewOrModified) {
|
|
453
429
|
// Always log new or modified entities
|
|
454
430
|
shouldLog = true;
|
|
455
431
|
}
|
|
432
|
+
else if (hasRelatedEntityJoinFields) {
|
|
433
|
+
// Always regenerate base views for entities with RelatedEntityJoinFields configuration
|
|
434
|
+
shouldLog = true;
|
|
435
|
+
}
|
|
456
436
|
else if (isCascadeDependencyRegeneration) {
|
|
457
437
|
// Always log cascade dependency regenerations
|
|
458
438
|
shouldLog = true;
|
|
@@ -471,11 +451,10 @@ class SQLCodeGenBase {
|
|
|
471
451
|
}
|
|
472
452
|
}
|
|
473
453
|
if (shouldLog) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
temp_batch_file_1.TempBatchFile.appendToTempBatchFile(sql, entity.SchemaName);
|
|
454
|
+
SQLLogging.appendToSQLLogFile(sql, description);
|
|
455
|
+
TempBatchFile.appendToTempBatchFile(sql, entity.SchemaName);
|
|
477
456
|
}
|
|
478
|
-
|
|
457
|
+
logIf(configInfo.verboseOutput, `SQL Generated for ${entity.Name}: ${description}`);
|
|
479
458
|
}
|
|
480
459
|
async generateSingleEntitySQLToSeparateFiles(options) {
|
|
481
460
|
const files = [];
|
|
@@ -484,18 +463,18 @@ class SQLCodeGenBase {
|
|
|
484
463
|
if (options.writeFiles && !fs.existsSync(options.directory))
|
|
485
464
|
fs.mkdirSync(options.directory, { recursive: true });
|
|
486
465
|
// now do the same thing for the /schema directory within the provided directory
|
|
487
|
-
const schemaDirectory =
|
|
466
|
+
const schemaDirectory = path.join(options.directory, options.entity.SchemaName);
|
|
488
467
|
if (options.writeFiles && !fs.existsSync(schemaDirectory))
|
|
489
468
|
fs.mkdirSync(schemaDirectory, { recursive: true }); // create the directory if it doesn't exist
|
|
490
469
|
let sRet = '';
|
|
491
470
|
let permissionsSQL = '';
|
|
492
471
|
// Indexes for Fkeys for the table
|
|
493
472
|
if (!options.onlyPermissions) {
|
|
494
|
-
const shouldGenerateIndexes =
|
|
473
|
+
const shouldGenerateIndexes = autoIndexForeignKeys() || (configInfo.forceRegeneration?.enabled && configInfo.forceRegeneration?.indexes);
|
|
495
474
|
const indexSQL = shouldGenerateIndexes ? this.generateIndexesForForeignKeys(options.pool, options.entity) : ''; // generate indexes if auto-indexing is on OR force regeneration is enabled
|
|
496
475
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, 'Index for Foreign Keys') + indexSQL;
|
|
497
476
|
if (options.writeFiles) {
|
|
498
|
-
const filePath =
|
|
477
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('index', options.entity.SchemaName, options.entity.BaseTable, false, true));
|
|
499
478
|
this.logSQLForNewOrModifiedEntity(options.entity, s, 'Index for Foreign Keys for ' + options.entity.BaseTable, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
500
479
|
fs.writeFileSync(filePath, s);
|
|
501
480
|
files.push(filePath);
|
|
@@ -517,7 +496,7 @@ class SQLCodeGenBase {
|
|
|
517
496
|
const functionName = `fn${options.entity.BaseTable}${field.Name}_GetRootID`;
|
|
518
497
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, functionName) +
|
|
519
498
|
this.generateRootIDFunction(options.entity, field);
|
|
520
|
-
const filePath =
|
|
499
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('function', options.entity.SchemaName, functionName, false, true));
|
|
521
500
|
if (options.writeFiles) {
|
|
522
501
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `Root ID Function SQL for ${options.entity.Name}.${field.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
523
502
|
fs.writeFileSync(filePath, s);
|
|
@@ -529,7 +508,7 @@ class SQLCodeGenBase {
|
|
|
529
508
|
}
|
|
530
509
|
// Generate the base view (which may reference the TVFs created above)
|
|
531
510
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, options.entity.BaseView) + await this.generateBaseView(options.pool, options.entity);
|
|
532
|
-
const filePath =
|
|
511
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('view', options.entity.SchemaName, options.entity.BaseView, false, true));
|
|
533
512
|
if (options.writeFiles) {
|
|
534
513
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `Base View SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
535
514
|
fs.writeFileSync(filePath, s);
|
|
@@ -542,7 +521,7 @@ class SQLCodeGenBase {
|
|
|
542
521
|
if (s.length > 0)
|
|
543
522
|
permissionsSQL += s + '\nGO\n';
|
|
544
523
|
if (options.writeFiles) {
|
|
545
|
-
const filePath =
|
|
524
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('view', options.entity.SchemaName, options.entity.BaseView, true, true));
|
|
546
525
|
fs.writeFileSync(filePath, s);
|
|
547
526
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `Base View Permissions SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
548
527
|
files.push(filePath);
|
|
@@ -553,25 +532,25 @@ class SQLCodeGenBase {
|
|
|
553
532
|
sRet += s + '\nGO\n';
|
|
554
533
|
// CREATE SP
|
|
555
534
|
if (options.entity.AllowCreateAPI && !options.entity.VirtualEntity) {
|
|
556
|
-
const spName = this.getSPName(options.entity,
|
|
535
|
+
const spName = this.getSPName(options.entity, SPType.Create);
|
|
557
536
|
// Only generate if spCreateGenerated is true (respects custom SPs where it's false)
|
|
558
537
|
// forceRegeneration only forces regeneration of SPs where spCreateGenerated=true
|
|
559
538
|
if (!options.onlyPermissions && options.entity.spCreateGenerated) {
|
|
560
539
|
// generate the create SP
|
|
561
540
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPCreate(options.entity);
|
|
562
541
|
if (options.writeFiles) {
|
|
563
|
-
const filePath =
|
|
542
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, false, true));
|
|
564
543
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spCreate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
565
544
|
fs.writeFileSync(filePath, s);
|
|
566
545
|
files.push(filePath);
|
|
567
546
|
}
|
|
568
547
|
sRet += s + '\nGO\n';
|
|
569
548
|
}
|
|
570
|
-
const s = this.generateSPPermissions(options.entity, spName,
|
|
549
|
+
const s = this.generateSPPermissions(options.entity, spName, SPType.Create) + '\n\n';
|
|
571
550
|
if (s.length > 0)
|
|
572
551
|
permissionsSQL += s + '\nGO\n';
|
|
573
552
|
if (options.writeFiles) {
|
|
574
|
-
const filePath =
|
|
553
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
|
|
575
554
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spCreate Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
576
555
|
fs.writeFileSync(filePath, s);
|
|
577
556
|
files.push(filePath);
|
|
@@ -583,25 +562,25 @@ class SQLCodeGenBase {
|
|
|
583
562
|
}
|
|
584
563
|
// UPDATE SP
|
|
585
564
|
if (options.entity.AllowUpdateAPI && !options.entity.VirtualEntity) {
|
|
586
|
-
const spName = this.getSPName(options.entity,
|
|
565
|
+
const spName = this.getSPName(options.entity, SPType.Update);
|
|
587
566
|
// Only generate if spUpdateGenerated is true (respects custom SPs where it's false)
|
|
588
567
|
// forceRegeneration only forces regeneration of SPs where spUpdateGenerated=true
|
|
589
568
|
if (!options.onlyPermissions && options.entity.spUpdateGenerated) {
|
|
590
569
|
// generate the update SP
|
|
591
570
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPUpdate(options.entity);
|
|
592
571
|
if (options.writeFiles) {
|
|
593
|
-
const filePath =
|
|
572
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, false, true));
|
|
594
573
|
fs.writeFileSync(filePath, s);
|
|
595
574
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spUpdate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
596
575
|
files.push(filePath);
|
|
597
576
|
}
|
|
598
577
|
sRet += s + '\nGO\n';
|
|
599
578
|
}
|
|
600
|
-
const s = this.generateSPPermissions(options.entity, spName,
|
|
579
|
+
const s = this.generateSPPermissions(options.entity, spName, SPType.Update) + '\n\n';
|
|
601
580
|
if (s.length > 0)
|
|
602
581
|
permissionsSQL += s + '\nGO\n';
|
|
603
582
|
if (options.writeFiles) {
|
|
604
|
-
const filePath =
|
|
583
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
|
|
605
584
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spUpdate Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
606
585
|
fs.writeFileSync(filePath, s);
|
|
607
586
|
files.push(filePath);
|
|
@@ -613,7 +592,7 @@ class SQLCodeGenBase {
|
|
|
613
592
|
}
|
|
614
593
|
// DELETE SP
|
|
615
594
|
if (options.entity.AllowDeleteAPI && !options.entity.VirtualEntity) {
|
|
616
|
-
const spName = this.getSPName(options.entity,
|
|
595
|
+
const spName = this.getSPName(options.entity, SPType.Delete);
|
|
617
596
|
// Only generate if spDeleteGenerated is true (respects custom SPs where it's false)
|
|
618
597
|
// OR if this entity has cascade delete dependencies that require regeneration
|
|
619
598
|
// forceRegeneration only forces regeneration of SPs where spDeleteGenerated=true
|
|
@@ -622,22 +601,22 @@ class SQLCodeGenBase {
|
|
|
622
601
|
this.entitiesNeedingDeleteSPRegeneration.has(options.entity.ID))) {
|
|
623
602
|
// generate the delete SP
|
|
624
603
|
if (this.entitiesNeedingDeleteSPRegeneration.has(options.entity.ID)) {
|
|
625
|
-
|
|
604
|
+
logStatus(` Regenerating ${spName} due to cascade dependency changes`);
|
|
626
605
|
}
|
|
627
606
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPDelete(options.entity);
|
|
628
607
|
if (options.writeFiles) {
|
|
629
|
-
const filePath =
|
|
608
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, false, true));
|
|
630
609
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spDelete SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
631
610
|
fs.writeFileSync(filePath, s);
|
|
632
611
|
files.push(filePath);
|
|
633
612
|
}
|
|
634
613
|
sRet += s + '\nGO\n';
|
|
635
614
|
}
|
|
636
|
-
const s = this.generateSPPermissions(options.entity, spName,
|
|
615
|
+
const s = this.generateSPPermissions(options.entity, spName, SPType.Delete) + '\n\n';
|
|
637
616
|
if (s.length > 0)
|
|
638
617
|
permissionsSQL += s + '\nGO\n';
|
|
639
618
|
if (options.writeFiles) {
|
|
640
|
-
const filePath =
|
|
619
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
|
|
641
620
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spDelete Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
642
621
|
fs.writeFileSync(filePath, s);
|
|
643
622
|
files.push(filePath);
|
|
@@ -648,12 +627,12 @@ class SQLCodeGenBase {
|
|
|
648
627
|
sRet += s + '\nGO\n';
|
|
649
628
|
}
|
|
650
629
|
// check to see if the options.entity supports full text search or not
|
|
651
|
-
if (options.entity.FullTextSearchEnabled || (
|
|
630
|
+
if (options.entity.FullTextSearchEnabled || (configInfo.forceRegeneration?.enabled && configInfo.forceRegeneration?.fullTextSearch)) {
|
|
652
631
|
// always generate the code so we can get the function name from the below function call
|
|
653
632
|
const ft = await this.generateEntityFullTextSearchSQL(options.pool, options.entity);
|
|
654
633
|
if (!options.onlyPermissions) {
|
|
655
634
|
// only write the actual sql out if we're not only generating permissions
|
|
656
|
-
const filePath =
|
|
635
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('full_text_search_function', options.entity.SchemaName, options.entity.BaseTable, false, true));
|
|
657
636
|
if (options.writeFiles) {
|
|
658
637
|
this.logSQLForNewOrModifiedEntity(options.entity, ft.sql, `Full Text Search SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
659
638
|
fs.writeFileSync(filePath, ft.sql);
|
|
@@ -664,7 +643,7 @@ class SQLCodeGenBase {
|
|
|
664
643
|
const sP = this.generateFullTextSearchFunctionPermissions(options.entity, ft.functionName) + '\n\n';
|
|
665
644
|
if (sP.length > 0)
|
|
666
645
|
permissionsSQL += sP + '\nGO\n';
|
|
667
|
-
const filePath =
|
|
646
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('full_text_search_function', options.entity.SchemaName, options.entity.BaseTable, true, true));
|
|
668
647
|
if (options.writeFiles) {
|
|
669
648
|
this.logSQLForNewOrModifiedEntity(options.entity, sP, `Full Text Search Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
670
649
|
fs.writeFileSync(filePath, sP);
|
|
@@ -678,17 +657,17 @@ class SQLCodeGenBase {
|
|
|
678
657
|
return { sql: sRet, permissionsSQL: permissionsSQL, files: files };
|
|
679
658
|
}
|
|
680
659
|
catch (err) {
|
|
681
|
-
|
|
660
|
+
logError(err);
|
|
682
661
|
return null;
|
|
683
662
|
}
|
|
684
663
|
}
|
|
685
664
|
getSPName(entity, type) {
|
|
686
665
|
switch (type) {
|
|
687
|
-
case
|
|
666
|
+
case SPType.Create:
|
|
688
667
|
return entity.spCreate && entity.spCreate.length > 0 ? entity.spCreate : 'spCreate' + entity.BaseTableCodeName;
|
|
689
|
-
case
|
|
668
|
+
case SPType.Update:
|
|
690
669
|
return entity.spUpdate && entity.spUpdate.length > 0 ? entity.spUpdate : 'spUpdate' + entity.BaseTableCodeName;
|
|
691
|
-
case
|
|
670
|
+
case SPType.Delete:
|
|
692
671
|
return entity.spDelete && entity.spDelete.length > 0 ? entity.spDelete : 'spDelete' + entity.BaseTableCodeName;
|
|
693
672
|
}
|
|
694
673
|
}
|
|
@@ -700,11 +679,11 @@ class SQLCodeGenBase {
|
|
|
700
679
|
if (!entity.VirtualEntity) {
|
|
701
680
|
// only add each SP file if the Allow flags are set to true, doesn't matter if the SPs are generated or not, we always generate permissions
|
|
702
681
|
if (entity.AllowCreateAPI)
|
|
703
|
-
files.push(this.SQLUtilityObject.getDBObjectFileName('sp', entity.SchemaName, this.getSPName(entity,
|
|
682
|
+
files.push(this.SQLUtilityObject.getDBObjectFileName('sp', entity.SchemaName, this.getSPName(entity, SPType.Create), true, true));
|
|
704
683
|
if (entity.AllowUpdateAPI)
|
|
705
|
-
files.push(this.SQLUtilityObject.getDBObjectFileName('sp', entity.SchemaName, this.getSPName(entity,
|
|
684
|
+
files.push(this.SQLUtilityObject.getDBObjectFileName('sp', entity.SchemaName, this.getSPName(entity, SPType.Update), true, true));
|
|
706
685
|
if (entity.AllowDeleteAPI)
|
|
707
|
-
files.push(this.SQLUtilityObject.getDBObjectFileName('sp', entity.SchemaName, this.getSPName(entity,
|
|
686
|
+
files.push(this.SQLUtilityObject.getDBObjectFileName('sp', entity.SchemaName, this.getSPName(entity, SPType.Delete), true, true));
|
|
708
687
|
}
|
|
709
688
|
if (entity.FullTextSearchEnabled)
|
|
710
689
|
files.push(this.SQLUtilityObject.getDBObjectFileName('full_text_search_function', entity.SchemaName, entity.BaseTable, true, true));
|
|
@@ -731,7 +710,7 @@ class SQLCodeGenBase {
|
|
|
731
710
|
sOutput += this.generateSPCreate(entity) + '\n\n';
|
|
732
711
|
else
|
|
733
712
|
// custom SP, still generate the permissions
|
|
734
|
-
sOutput += this.generateSPPermissions(entity, entity.spCreate,
|
|
713
|
+
sOutput += this.generateSPPermissions(entity, entity.spCreate, SPType.Create) + '\n\n';
|
|
735
714
|
}
|
|
736
715
|
if (entity.AllowUpdateAPI && !entity.VirtualEntity) {
|
|
737
716
|
if (entity.spUpdateGenerated)
|
|
@@ -739,7 +718,7 @@ class SQLCodeGenBase {
|
|
|
739
718
|
sOutput += this.generateSPUpdate(entity) + '\n\n';
|
|
740
719
|
else
|
|
741
720
|
// custom SP, still generate the permissions
|
|
742
|
-
sOutput += this.generateSPPermissions(entity, entity.spUpdate,
|
|
721
|
+
sOutput += this.generateSPPermissions(entity, entity.spUpdate, SPType.Update) + '\n\n';
|
|
743
722
|
}
|
|
744
723
|
if (entity.AllowDeleteAPI && !entity.VirtualEntity) {
|
|
745
724
|
if (entity.spDeleteGenerated)
|
|
@@ -747,7 +726,7 @@ class SQLCodeGenBase {
|
|
|
747
726
|
sOutput += this.generateSPDelete(entity) + '\n\n';
|
|
748
727
|
else
|
|
749
728
|
// custom SP, still generate the permissions
|
|
750
|
-
sOutput += this.generateSPPermissions(entity, entity.spDelete,
|
|
729
|
+
sOutput += this.generateSPPermissions(entity, entity.spDelete, SPType.Delete) + '\n\n';
|
|
751
730
|
}
|
|
752
731
|
// check to see if the entity supports full text search or not
|
|
753
732
|
if (entity.FullTextSearchEnabled) {
|
|
@@ -757,7 +736,7 @@ class SQLCodeGenBase {
|
|
|
757
736
|
}
|
|
758
737
|
async generateEntityFullTextSearchSQL(pool, entity) {
|
|
759
738
|
let sql = '';
|
|
760
|
-
const catalogName = entity.FullTextCatalog && entity.FullTextCatalog.length > 0 ? entity.FullTextCatalog :
|
|
739
|
+
const catalogName = entity.FullTextCatalog && entity.FullTextCatalog.length > 0 ? entity.FullTextCatalog : dbDatabase + '_FullTextCatalog';
|
|
761
740
|
if (entity.FullTextCatalogGenerated) {
|
|
762
741
|
// this situation means we have a generated catalog and the user has provided a name specific to THIS entity
|
|
763
742
|
sql += ` -- CREATE THE FULL TEXT CATALOG FOR THE ENTITY, IF NOT ALREADY CREATED
|
|
@@ -810,8 +789,8 @@ class SQLCodeGenBase {
|
|
|
810
789
|
throw new Error(`FullTextSearchFunctionGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
|
|
811
790
|
if (!entity.FullTextSearchFunction || entity.FullTextSearchFunction.length === 0) {
|
|
812
791
|
// update this in the DB
|
|
813
|
-
const md = new
|
|
814
|
-
const u =
|
|
792
|
+
const md = new Metadata();
|
|
793
|
+
const u = UserCache.Instance.Users[0];
|
|
815
794
|
if (!u)
|
|
816
795
|
throw new Error('Could not find the first user in the cache, cant generate the full text search function without a user');
|
|
817
796
|
const e = await md.GetEntityObject('Entities', u);
|
|
@@ -900,8 +879,8 @@ class SQLCodeGenBase {
|
|
|
900
879
|
if (f.RelatedEntity && f.RelatedEntity.length > 0) {
|
|
901
880
|
// we have an fkey, so generate the create index
|
|
902
881
|
let indexName = `IDX_AUTO_MJ_FKEY_${entity.BaseTableCodeName}_${f.CodeName}`; // use code names in case the table and/or field names have special characters or spaces/etc
|
|
903
|
-
if (indexName.length >
|
|
904
|
-
indexName = indexName.substring(0,
|
|
882
|
+
if (indexName.length > MAX_INDEX_NAME_LENGTH)
|
|
883
|
+
indexName = indexName.substring(0, MAX_INDEX_NAME_LENGTH); // truncate to max length if necessary
|
|
905
884
|
if (sOutput.length > 0)
|
|
906
885
|
sOutput += '\n\n'; // do this way so we don't end up with a trailing newline at end of the string/file
|
|
907
886
|
sOutput += `-- Index for foreign key ${f.Name} in table ${entity.BaseTable}
|
|
@@ -1047,7 +1026,7 @@ GO
|
|
|
1047
1026
|
const relatedFieldsJoinString = this.generateBaseViewJoins(entity, entity.Fields);
|
|
1048
1027
|
const permissions = this.generateViewPermissions(entity);
|
|
1049
1028
|
const whereClause = entity.DeleteType === 'Soft' ? `WHERE
|
|
1050
|
-
${classNameFirstChar}.[${
|
|
1029
|
+
${classNameFirstChar}.[${EntityInfo.DeletedAtFieldName}] IS NULL
|
|
1051
1030
|
` : '';
|
|
1052
1031
|
// Detect recursive foreign keys and generate TVF joins and root field selects
|
|
1053
1032
|
const recursiveFKs = this.detectRecursiveForeignKeys(entity);
|
|
@@ -1088,7 +1067,12 @@ ${whereClause}GO${permissions}
|
|
|
1088
1067
|
const classNameFirstChar = entity.ClassName.charAt(0).toLowerCase();
|
|
1089
1068
|
for (let i = 0; i < entityFields.length; i++) {
|
|
1090
1069
|
const ef = entityFields[i];
|
|
1091
|
-
|
|
1070
|
+
// Generate SQL JOIN for related entities that have configured join fields
|
|
1071
|
+
// _RelatedEntityJoinFieldMappings is populated during field analysis if:
|
|
1072
|
+
// - IncludeRelatedEntityNameFieldInBaseView is true (legacy), OR
|
|
1073
|
+
// - RelatedEntityJoinFieldsConfig specifies fields to join
|
|
1074
|
+
// This generates the JOIN clause; the actual field aliases are added separately in generateBaseViewFields()
|
|
1075
|
+
if (ef.RelatedEntityID && ef._RelatedEntityJoinFieldMappings && ef._RelatedEntityJoinFieldMappings.length > 0 && ef._RelatedEntityTableAlias) {
|
|
1092
1076
|
sOutput += sOutput == '' ? '' : '\n';
|
|
1093
1077
|
sOutput += `${ef.AllowsNull ? 'LEFT OUTER' : 'INNER'} JOIN\n ${'[' + ef.RelatedEntitySchemaName + '].'}[${ef._RelatedEntityNameFieldIsVirtual ? ef.RelatedEntityBaseView : ef.RelatedEntityBaseTable}] AS ${ef._RelatedEntityTableAlias}\n ON\n [${classNameFirstChar}].[${ef.Name}] = ${ef._RelatedEntityTableAlias}.[${ef.RelatedEntityFieldName}]`;
|
|
1094
1078
|
}
|
|
@@ -1098,51 +1082,117 @@ ${whereClause}GO${permissions}
|
|
|
1098
1082
|
async generateBaseViewRelatedFieldsString(pool, entityFields) {
|
|
1099
1083
|
let sOutput = '';
|
|
1100
1084
|
let fieldCount = 0;
|
|
1101
|
-
const manageMD =
|
|
1102
|
-
|
|
1103
|
-
const
|
|
1085
|
+
const manageMD = MJGlobal.Instance.ClassFactory.CreateInstance(ManageMetadataBase);
|
|
1086
|
+
const md = new Metadata();
|
|
1087
|
+
const allGeneratedAliases = [];
|
|
1088
|
+
// Get fields that are related entities with join field configuration.
|
|
1089
|
+
//
|
|
1090
|
+
// BACKWARD COMPATIBILITY LOGIC:
|
|
1091
|
+
// This handles two cases:
|
|
1092
|
+
// 1. Legacy behavior: IncludeRelatedEntityNameFieldInBaseView=true, no RelatedEntityJoinFieldsConfig
|
|
1093
|
+
// → Automatically defaults to { mode: 'extend' } and joins the related entity's NameField (as before)
|
|
1094
|
+
// 2. New behavior: RelatedEntityJoinFieldsConfig specified
|
|
1095
|
+
// → Can 'extend' the NameField with additional fields, 'override' it completely, or 'disable' joins
|
|
1096
|
+
//
|
|
1097
|
+
// Result: _RelatedEntityJoinFieldMappings is populated with all fields to be joined from the related entity.
|
|
1098
|
+
// If both old and new configs are set, they work together (new fields extend or replace the NameField).
|
|
1099
|
+
const qualifyingFields = entityFields.filter(f => f.RelatedEntityID && (f.IncludeRelatedEntityNameFieldInBaseView || f.RelatedEntityJoinFieldsConfig));
|
|
1104
1100
|
for (const ef of qualifyingFields) {
|
|
1105
|
-
const {
|
|
1106
|
-
if (
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1101
|
+
const config = ef.RelatedEntityJoinFieldsConfig || { mode: 'extend' };
|
|
1102
|
+
if (config.mode === 'disable') {
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
ef._RelatedEntityJoinFieldMappings = [];
|
|
1106
|
+
let anyFieldIsVirtual = false;
|
|
1107
|
+
// 1. Handle NameField (if not overridden)
|
|
1108
|
+
// In 'extend' mode: include the NameField (backward compatible with IncludeRelatedEntityNameFieldInBaseView)
|
|
1109
|
+
// In 'override' mode: skip the NameField, only use explicitly configured fields
|
|
1110
|
+
if (config.mode !== 'override' && ef.IncludeRelatedEntityNameFieldInBaseView) {
|
|
1111
|
+
const { nameField, nameFieldIsVirtual } = this.getIsNameFieldForSingleEntity(ef.RelatedEntity);
|
|
1112
|
+
if (nameField !== '') {
|
|
1113
|
+
// only add to the output, if we found a name field for the related entity.
|
|
1114
|
+
ef._RelatedEntityTableAlias = ef.RelatedEntityClassName + '_' + ef.Name;
|
|
1115
|
+
// This next section generates a field name for the new virtual field and makes sure it doesn't collide with a field in the base table
|
|
1116
|
+
const candidateName = this.stripID(ef.Name);
|
|
1117
|
+
// Skip if candidateName is empty (e.g., field named exactly "ID")
|
|
1118
|
+
// This happens in table-per-type inheritance where child.ID is FK to parent.ID
|
|
1119
|
+
// stripID("ID") returns "" which would generate invalid SQL: AS []
|
|
1120
|
+
if (candidateName.trim().length === 0) {
|
|
1121
|
+
logStatus(` Skipping related entity name field for ${ef.Name} in entity - stripID returned empty string (likely inheritance pattern)`);
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
// check to make sure candidateName is not already a field name in the base table (other than a virtual field of course, as that is what we're creating)
|
|
1125
|
+
// because if it is, we need to change it to something else
|
|
1126
|
+
const bFound = entityFields.find(f => f.IsVirtual === false && f.Name.trim().toLowerCase() === candidateName.trim().toLowerCase()) !== undefined ||
|
|
1127
|
+
allGeneratedAliases.some(a => a.toLowerCase() === candidateName.trim().toLowerCase());
|
|
1128
|
+
const safeAlias = bFound ? candidateName + '_Virtual' : candidateName;
|
|
1129
|
+
ef._RelatedEntityNameFieldMap = safeAlias;
|
|
1130
|
+
ef._RelatedEntityJoinFieldMappings.push({
|
|
1131
|
+
sourceField: nameField,
|
|
1132
|
+
alias: safeAlias,
|
|
1133
|
+
isVirtual: nameFieldIsVirtual
|
|
1134
|
+
});
|
|
1135
|
+
allGeneratedAliases.push(safeAlias);
|
|
1136
|
+
if (nameFieldIsVirtual)
|
|
1137
|
+
anyFieldIsVirtual = true;
|
|
1138
|
+
// check to see if the database already knows about the RelatedEntityNameFieldMap or not
|
|
1139
|
+
if (ef.RelatedEntityNameFieldMap === null ||
|
|
1140
|
+
ef.RelatedEntityNameFieldMap === undefined ||
|
|
1141
|
+
ef.RelatedEntityNameFieldMap.trim().length === 0) {
|
|
1142
|
+
// the database doesn't yet know about this RelatedEntityNameFieldMap, so we need to update it
|
|
1143
|
+
// first update the actul field in the metadata object so it can be used from this point forward
|
|
1144
|
+
// and it also reflects what the DB will hold
|
|
1145
|
+
ef.RelatedEntityNameFieldMap = ef._RelatedEntityNameFieldMap;
|
|
1146
|
+
// then update the database itself
|
|
1147
|
+
await manageMD.updateEntityFieldRelatedEntityNameFieldMap(pool, ef.ID, ef.RelatedEntityNameFieldMap);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1118
1150
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
ef.
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
//
|
|
1137
|
-
|
|
1151
|
+
}
|
|
1152
|
+
// 2. Handle configured additional fields
|
|
1153
|
+
if (config.fields && config.fields.length > 0) {
|
|
1154
|
+
const currentEntity = md.Entities.find(e => e.ID === ef.EntityID);
|
|
1155
|
+
for (const fieldConfig of config.fields) {
|
|
1156
|
+
const fieldName = fieldConfig.field;
|
|
1157
|
+
const alias = fieldConfig.alias || this.generateDefaultAlias(ef.Name, fieldName);
|
|
1158
|
+
// Validate field exists on related entity
|
|
1159
|
+
if (!this.validateFieldExists(ef.RelatedEntity, fieldName)) {
|
|
1160
|
+
logError(`RelatedEntityJoinFields: Field '${fieldName}' not found on entity '${ef.RelatedEntity}' (FK: ${ef.Name})`);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
// Check for alias collisions
|
|
1164
|
+
if (currentEntity && this.hasAliasCollision(currentEntity, alias, allGeneratedAliases)) {
|
|
1165
|
+
logError(`RelatedEntityJoinFields: Alias '${alias}' for field '${fieldName}' would collide with an existing field or alias in entity '${currentEntity.Name}'`);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
// Get field metadata from related entity to check if virtual
|
|
1169
|
+
const relatedEntity = md.Entities.find(e => e.Name === ef.RelatedEntity);
|
|
1170
|
+
const relatedField = relatedEntity?.Fields.find(f => f.Name.toLowerCase() === fieldName.toLowerCase());
|
|
1171
|
+
const isVirtual = relatedField?.IsVirtual || false;
|
|
1172
|
+
ef._RelatedEntityJoinFieldMappings.push({
|
|
1173
|
+
sourceField: fieldName,
|
|
1174
|
+
alias: alias,
|
|
1175
|
+
isVirtual: isVirtual
|
|
1176
|
+
});
|
|
1177
|
+
allGeneratedAliases.push(alias);
|
|
1178
|
+
if (isVirtual)
|
|
1179
|
+
anyFieldIsVirtual = true;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
// 3. Generate SQL for the mappings
|
|
1183
|
+
if (ef._RelatedEntityJoinFieldMappings.length > 0) {
|
|
1184
|
+
ef._RelatedEntityTableAlias = ef.RelatedEntityClassName + '_' + ef.Name;
|
|
1185
|
+
ef._RelatedEntityNameFieldIsVirtual = anyFieldIsVirtual;
|
|
1186
|
+
for (const mapping of ef._RelatedEntityJoinFieldMappings) {
|
|
1187
|
+
sOutput += `${fieldCount === 0 ? '' : ','}\n ${ef._RelatedEntityTableAlias}.[${mapping.sourceField}] AS [${mapping.alias}]`;
|
|
1188
|
+
fieldCount++;
|
|
1138
1189
|
}
|
|
1139
|
-
fieldCount++;
|
|
1140
1190
|
}
|
|
1141
1191
|
}
|
|
1142
1192
|
return sOutput;
|
|
1143
1193
|
}
|
|
1144
1194
|
getIsNameFieldForSingleEntity(entityName) {
|
|
1145
|
-
const md = new
|
|
1195
|
+
const md = new Metadata(); // use the full metadata entity list, not the filtered version that we receive
|
|
1146
1196
|
const e = md.Entities.find(e => e.Name === entityName);
|
|
1147
1197
|
if (e) {
|
|
1148
1198
|
const ef = e.NameField;
|
|
@@ -1150,7 +1200,7 @@ ${whereClause}GO${permissions}
|
|
|
1150
1200
|
return { nameField: ef.Name, nameFieldIsVirtual: ef.IsVirtual };
|
|
1151
1201
|
}
|
|
1152
1202
|
else
|
|
1153
|
-
|
|
1203
|
+
logStatus(`ERROR: Could not find entity with name ${entityName}`);
|
|
1154
1204
|
return { nameField: '', nameFieldIsVirtual: false };
|
|
1155
1205
|
}
|
|
1156
1206
|
stripID(name) {
|
|
@@ -1159,13 +1209,40 @@ ${whereClause}GO${permissions}
|
|
|
1159
1209
|
else
|
|
1160
1210
|
return name;
|
|
1161
1211
|
}
|
|
1212
|
+
generateDefaultAlias(fkFieldName, relatedFieldName) {
|
|
1213
|
+
const baseName = this.stripID(fkFieldName);
|
|
1214
|
+
if (baseName.toLowerCase() === relatedFieldName.toLowerCase()) {
|
|
1215
|
+
return baseName;
|
|
1216
|
+
}
|
|
1217
|
+
return baseName + relatedFieldName;
|
|
1218
|
+
}
|
|
1219
|
+
validateFieldExists(entityName, fieldName) {
|
|
1220
|
+
const md = new Metadata();
|
|
1221
|
+
const entity = md.Entities.find(e => e.Name === entityName);
|
|
1222
|
+
if (!entity)
|
|
1223
|
+
return false;
|
|
1224
|
+
return entity.Fields.some(f => f.Name.toLowerCase() === fieldName.toLowerCase());
|
|
1225
|
+
}
|
|
1226
|
+
hasAliasCollision(entity, alias, generatedAliases) {
|
|
1227
|
+
// Check against existing fields in the entity (non-virtual fields first)
|
|
1228
|
+
if (entity.Fields.some(f => !f.IsVirtual && f.Name.toLowerCase() === alias.toLowerCase()))
|
|
1229
|
+
return true;
|
|
1230
|
+
// Check against other generated aliases in this view
|
|
1231
|
+
if (generatedAliases.some(a => a.toLowerCase() === alias.toLowerCase()))
|
|
1232
|
+
return true;
|
|
1233
|
+
// Check against system fields
|
|
1234
|
+
const systemFields = ['__mj_CreatedAt', '__mj_UpdatedAt', EntityInfo.DeletedAtFieldName];
|
|
1235
|
+
if (systemFields.some(sf => sf?.toLowerCase() === alias.toLowerCase()))
|
|
1236
|
+
return true;
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1162
1239
|
generateSPPermissions(entity, spName, type) {
|
|
1163
1240
|
let sOutput = '';
|
|
1164
1241
|
for (let i = 0; i < entity.Permissions.length; i++) {
|
|
1165
1242
|
const ep = entity.Permissions[i];
|
|
1166
|
-
if ((type ==
|
|
1167
|
-
(type ==
|
|
1168
|
-
(type ==
|
|
1243
|
+
if ((type == SPType.Create && ep.CanCreate) ||
|
|
1244
|
+
(type == SPType.Update && ep.CanUpdate) ||
|
|
1245
|
+
(type == SPType.Delete && ep.CanDelete)) {
|
|
1169
1246
|
if (ep.RoleSQLName && ep.RoleSQLName.length > 0) {
|
|
1170
1247
|
sOutput += (sOutput === '' ? `GRANT EXECUTE ON [${entity.SchemaName}].[${spName}] TO ` : ', ') + `[${ep.RoleSQLName}]`;
|
|
1171
1248
|
}
|
|
@@ -1191,7 +1268,7 @@ ${whereClause}GO${permissions}
|
|
|
1191
1268
|
//double exclamations used on the firstKey.DefaultValue property otherwise the type of this variable is 'number | ""';
|
|
1192
1269
|
const primaryKeyAutomatic = firstKey.AutoIncrement; // Only exclude auto-increment fields, allow manual override for all other PKs including UUIDs with defaults
|
|
1193
1270
|
const efString = this.createEntityFieldsParamString(entity.Fields, false); // Always pass false for isUpdate since this is generateSPCreate
|
|
1194
|
-
const permissions = this.generateSPPermissions(entity, spName,
|
|
1271
|
+
const permissions = this.generateSPPermissions(entity, spName, SPType.Create);
|
|
1195
1272
|
let preInsertCode = '';
|
|
1196
1273
|
let outputCode = '';
|
|
1197
1274
|
let selectInsertedRecord = '';
|
|
@@ -1294,12 +1371,12 @@ GO${permissions}
|
|
|
1294
1371
|
`;
|
|
1295
1372
|
}
|
|
1296
1373
|
generateUpdatedAtTrigger(entity) {
|
|
1297
|
-
const updatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() ===
|
|
1374
|
+
const updatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() === EntityInfo.UpdatedAtFieldName.toLowerCase().trim());
|
|
1298
1375
|
if (!updatedAtField)
|
|
1299
1376
|
return '';
|
|
1300
1377
|
const triggerStatement = `
|
|
1301
1378
|
------------------------------------------------------------
|
|
1302
|
-
----- TRIGGER FOR ${
|
|
1379
|
+
----- TRIGGER FOR ${EntityInfo.UpdatedAtFieldName} field for the ${entity.BaseTable} table
|
|
1303
1380
|
------------------------------------------------------------
|
|
1304
1381
|
IF OBJECT_ID('[${entity.SchemaName}].[trgUpdate${entity.ClassName}]', 'TR') IS NOT NULL
|
|
1305
1382
|
DROP TRIGGER [${entity.SchemaName}].[trgUpdate${entity.ClassName}];
|
|
@@ -1313,7 +1390,7 @@ BEGIN
|
|
|
1313
1390
|
UPDATE
|
|
1314
1391
|
[${entity.SchemaName}].[${entity.BaseTable}]
|
|
1315
1392
|
SET
|
|
1316
|
-
${
|
|
1393
|
+
${EntityInfo.UpdatedAtFieldName} = GETUTCDATE()
|
|
1317
1394
|
FROM
|
|
1318
1395
|
[${entity.SchemaName}].[${entity.BaseTable}] AS _organicTable
|
|
1319
1396
|
INNER JOIN
|
|
@@ -1326,8 +1403,8 @@ GO`;
|
|
|
1326
1403
|
generateSPUpdate(entity) {
|
|
1327
1404
|
const spName = entity.spUpdate ? entity.spUpdate : `spUpdate${entity.BaseTableCodeName}`;
|
|
1328
1405
|
const efParamString = this.createEntityFieldsParamString(entity.Fields, true);
|
|
1329
|
-
const permissions = this.generateSPPermissions(entity, spName,
|
|
1330
|
-
const hasUpdatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() ===
|
|
1406
|
+
const permissions = this.generateSPPermissions(entity, spName, SPType.Update);
|
|
1407
|
+
const hasUpdatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() === EntityInfo.UpdatedAtFieldName.trim().toLowerCase()) !== undefined;
|
|
1331
1408
|
const updatedAtTrigger = hasUpdatedAtField ? this.generateUpdatedAtTrigger(entity) : '';
|
|
1332
1409
|
let selectUpdatedRecord = `SELECT
|
|
1333
1410
|
*
|
|
@@ -1521,7 +1598,7 @@ ${updatedAtTrigger}
|
|
|
1521
1598
|
generateSPDelete(entity) {
|
|
1522
1599
|
const spName = entity.spDelete ? entity.spDelete : `spDelete${entity.BaseTableCodeName}`;
|
|
1523
1600
|
const sCascadeDeletes = this.generateCascadeDeletes(entity);
|
|
1524
|
-
const permissions = this.generateSPPermissions(entity, spName,
|
|
1601
|
+
const permissions = this.generateSPPermissions(entity, spName, SPType.Delete);
|
|
1525
1602
|
let sVariables = '';
|
|
1526
1603
|
let sSelect = '';
|
|
1527
1604
|
for (let k of entity.PrimaryKeys) {
|
|
@@ -1546,8 +1623,8 @@ ${deleteCode}`;
|
|
|
1546
1623
|
deleteCode = ` UPDATE
|
|
1547
1624
|
[${entity.SchemaName}].[${entity.BaseTable}]
|
|
1548
1625
|
SET
|
|
1549
|
-
${
|
|
1550
|
-
${deleteCode} AND ${
|
|
1626
|
+
${EntityInfo.DeletedAtFieldName} = GETUTCDATE()
|
|
1627
|
+
${deleteCode} AND ${EntityInfo.DeletedAtFieldName} IS NULL -- don't update the record if it's already been deleted via a soft delete`;
|
|
1551
1628
|
}
|
|
1552
1629
|
// Build the NULL select statement for when no rows are affected
|
|
1553
1630
|
let sNullSelect = '';
|
|
@@ -1584,7 +1661,7 @@ GO${permissions}
|
|
|
1584
1661
|
generateCascadeDeletes(entity) {
|
|
1585
1662
|
let sOutput = '';
|
|
1586
1663
|
if (entity.CascadeDeletes) {
|
|
1587
|
-
const md = new
|
|
1664
|
+
const md = new Metadata();
|
|
1588
1665
|
// Find all fields in other entities that are foreign keys to this entity
|
|
1589
1666
|
for (const e of md.Entities) {
|
|
1590
1667
|
for (const ef of e.Fields) {
|
|
@@ -1614,7 +1691,7 @@ GO${permissions}
|
|
|
1614
1691
|
// Nullable FK but no update API - this is a configuration error
|
|
1615
1692
|
const sqlComment = `WARNING: ${relatedEntity.BaseTable} has nullable FK to ${parentEntity.BaseTable} but doesn't allow update API - cascade operation will fail`;
|
|
1616
1693
|
const consoleMsg = `WARNING in spDelete${parentEntity.BaseTableCodeName} generation: ${relatedEntity.BaseTable} has nullable FK to ${parentEntity.BaseTable} but doesn't allow update API - cascade operation will fail`;
|
|
1617
|
-
|
|
1694
|
+
logWarning(consoleMsg);
|
|
1618
1695
|
return `
|
|
1619
1696
|
-- ${sqlComment}
|
|
1620
1697
|
-- This will cause the delete operation to fail due to referential integrity`;
|
|
@@ -1623,7 +1700,7 @@ GO${permissions}
|
|
|
1623
1700
|
// Entity doesn't allow delete API, so we can't cascade delete
|
|
1624
1701
|
const sqlComment = `WARNING: ${relatedEntity.BaseTable} has non-nullable FK to ${parentEntity.BaseTable} but doesn't allow delete API - cascade operation will fail`;
|
|
1625
1702
|
const consoleMsg = `WARNING in spDelete${parentEntity.BaseTableCodeName} generation: ${relatedEntity.BaseTable} has non-nullable FK to ${parentEntity.BaseTable} but doesn't allow delete API - cascade operation will fail`;
|
|
1626
|
-
|
|
1703
|
+
logWarning(consoleMsg);
|
|
1627
1704
|
return `
|
|
1628
1705
|
-- ${sqlComment}
|
|
1629
1706
|
-- This will cause a referential integrity violation`;
|
|
@@ -1637,7 +1714,7 @@ GO${permissions}
|
|
|
1637
1714
|
// Generate unique cursor name using entity code names
|
|
1638
1715
|
const cursorName = `cascade_${operation}_${relatedEntity.CodeName}_cursor`;
|
|
1639
1716
|
// Determine which SP to call
|
|
1640
|
-
const spType = operation === 'delete' ?
|
|
1717
|
+
const spType = operation === 'delete' ? SPType.Delete : SPType.Update;
|
|
1641
1718
|
const spName = this.getSPName(relatedEntity, spType);
|
|
1642
1719
|
if (operation === 'update') {
|
|
1643
1720
|
// For update, we need to include all updateable fields
|
|
@@ -1731,7 +1808,7 @@ GO${permissions}
|
|
|
1731
1808
|
fetchInto = pkComponents.fetchInto;
|
|
1732
1809
|
allParams = pkComponents.spParams;
|
|
1733
1810
|
// Then, add all updateable fields with the same prefix
|
|
1734
|
-
const sortedFields =
|
|
1811
|
+
const sortedFields = sortBySequenceAndCreatedAt(entity.Fields);
|
|
1735
1812
|
for (const ef of sortedFields) {
|
|
1736
1813
|
if (!ef.IsPrimaryKey && !ef.IsVirtual && ef.AllowUpdateAPI && !ef.AutoIncrement && !ef.IsSpecialDateField) {
|
|
1737
1814
|
if (declarations !== '')
|
|
@@ -1757,7 +1834,7 @@ GO${permissions}
|
|
|
1757
1834
|
*/
|
|
1758
1835
|
analyzeCascadeDeleteDependencies(entity) {
|
|
1759
1836
|
if (entity.CascadeDeletes) {
|
|
1760
|
-
const md = new
|
|
1837
|
+
const md = new Metadata();
|
|
1761
1838
|
// Find all fields in other entities that are foreign keys to this entity
|
|
1762
1839
|
for (const e of md.Entities) {
|
|
1763
1840
|
for (const ef of e.Fields) {
|
|
@@ -1799,7 +1876,7 @@ GO${permissions}
|
|
|
1799
1876
|
async buildCascadeDeleteDependencies(entities) {
|
|
1800
1877
|
// Clear existing dependencies
|
|
1801
1878
|
this.cascadeDeleteDependencies.clear();
|
|
1802
|
-
|
|
1879
|
+
logStatus(`Building cascade delete dependencies...`);
|
|
1803
1880
|
// Analyze cascade deletes for each entity with CascadeDeletes=true
|
|
1804
1881
|
// This will populate the cascadeDeleteDependencies map WITHOUT generating SQL
|
|
1805
1882
|
let entitiesWithCascadeDeletes = 0;
|
|
@@ -1807,23 +1884,23 @@ GO${permissions}
|
|
|
1807
1884
|
if (entity.CascadeDeletes) {
|
|
1808
1885
|
entitiesWithCascadeDeletes++;
|
|
1809
1886
|
if (entity.spDeleteGenerated) {
|
|
1810
|
-
|
|
1887
|
+
logStatus(` Analyzing cascade deletes for ${entity.Name}...`);
|
|
1811
1888
|
this.analyzeCascadeDeleteDependencies(entity);
|
|
1812
1889
|
}
|
|
1813
1890
|
else {
|
|
1814
|
-
|
|
1891
|
+
logStatus(` Skipping ${entity.Name} - has CascadeDeletes but spDeleteGenerated=false`);
|
|
1815
1892
|
}
|
|
1816
1893
|
}
|
|
1817
1894
|
}
|
|
1818
|
-
|
|
1895
|
+
logStatus(`Total entities with CascadeDeletes=true: ${entitiesWithCascadeDeletes}`);
|
|
1819
1896
|
// Log the dependency map
|
|
1820
|
-
|
|
1897
|
+
logStatus(`Cascade delete dependency map built:`);
|
|
1821
1898
|
for (const [dependedOnEntityId, dependentEntityIds] of this.cascadeDeleteDependencies) {
|
|
1822
1899
|
const dependedOnEntity = entities.find(e => e.ID === dependedOnEntityId);
|
|
1823
1900
|
const dependentNames = Array.from(dependentEntityIds)
|
|
1824
1901
|
.map(id => entities.find(e => e.ID === id)?.Name || id)
|
|
1825
1902
|
.join(', ');
|
|
1826
|
-
|
|
1903
|
+
logStatus(` ${dependedOnEntity?.Name || dependedOnEntityId} is depended on by: ${dependentNames}`);
|
|
1827
1904
|
}
|
|
1828
1905
|
}
|
|
1829
1906
|
/**
|
|
@@ -1833,8 +1910,8 @@ GO${permissions}
|
|
|
1833
1910
|
async getModifiedEntitiesWithUpdateAPI(entities) {
|
|
1834
1911
|
const modifiedEntitiesMap = new Map();
|
|
1835
1912
|
// Get the list of modified entity names from the metadata management phase
|
|
1836
|
-
const modifiedEntityNames =
|
|
1837
|
-
|
|
1913
|
+
const modifiedEntityNames = ManageMetadataBase.modifiedEntityList;
|
|
1914
|
+
logStatus(`Modified entities from metadata phase: ${modifiedEntityNames.join(', ')}`);
|
|
1838
1915
|
// Convert entity names to IDs and filter for those with update API
|
|
1839
1916
|
for (const entityName of modifiedEntityNames) {
|
|
1840
1917
|
const entity = entities.find(e => e.Name === entityName &&
|
|
@@ -1842,15 +1919,15 @@ GO${permissions}
|
|
|
1842
1919
|
e.spUpdateGenerated);
|
|
1843
1920
|
if (entity) {
|
|
1844
1921
|
modifiedEntitiesMap.set(entity.Name, entity.ID);
|
|
1845
|
-
|
|
1922
|
+
logStatus(` - ${entity.Name} (${entity.ID}) has update API and will be tracked`);
|
|
1846
1923
|
}
|
|
1847
1924
|
else {
|
|
1848
1925
|
const nonUpdateEntity = entities.find(e => e.Name === entityName);
|
|
1849
1926
|
if (nonUpdateEntity) {
|
|
1850
|
-
|
|
1927
|
+
logStatus(` - ${entityName} found but AllowUpdateAPI=${nonUpdateEntity.AllowUpdateAPI}, spUpdateGenerated=${nonUpdateEntity.spUpdateGenerated}`);
|
|
1851
1928
|
}
|
|
1852
1929
|
else {
|
|
1853
|
-
|
|
1930
|
+
logStatus(` - ${entityName} not found in entities list`);
|
|
1854
1931
|
}
|
|
1855
1932
|
}
|
|
1856
1933
|
}
|
|
@@ -1884,38 +1961,38 @@ GO${permissions}
|
|
|
1884
1961
|
// Get entities that were modified during metadata management
|
|
1885
1962
|
const modifiedEntitiesMap = await this.getModifiedEntitiesWithUpdateAPI(entities);
|
|
1886
1963
|
if (modifiedEntitiesMap.size > 0) {
|
|
1887
|
-
|
|
1964
|
+
logStatus(`Found ${modifiedEntitiesMap.size} entities with schema changes affecting update SPs`);
|
|
1888
1965
|
// Convert map values to set of IDs
|
|
1889
1966
|
const changedEntityIds = new Set(modifiedEntitiesMap.values());
|
|
1890
1967
|
// Find entities that need delete SP regeneration
|
|
1891
1968
|
const entitiesNeedingRegeneration = await this.getEntitiesRequiringCascadeDeleteRegeneration(pool, changedEntityIds);
|
|
1892
1969
|
if (entitiesNeedingRegeneration.size > 0) {
|
|
1893
|
-
|
|
1970
|
+
logStatus(`Identified ${entitiesNeedingRegeneration.size} entities requiring delete SP regeneration due to cascade dependencies`);
|
|
1894
1971
|
// Store the entity IDs that need regeneration (only if spDeleteGenerated=true)
|
|
1895
1972
|
for (const entityId of entitiesNeedingRegeneration) {
|
|
1896
1973
|
const entity = entities.find(e => e.ID === entityId);
|
|
1897
1974
|
if (entity && entity.spDeleteGenerated) {
|
|
1898
1975
|
this.entitiesNeedingDeleteSPRegeneration.add(entityId);
|
|
1899
|
-
|
|
1976
|
+
logStatus(` - Marked ${entity.Name} for delete SP regeneration (cascade dependency)`);
|
|
1900
1977
|
}
|
|
1901
1978
|
else if (entity && !entity.spDeleteGenerated) {
|
|
1902
|
-
|
|
1979
|
+
logStatus(` - Skipping ${entity.Name} - has cascade dependency but spDeleteGenerated=false (custom SP)`);
|
|
1903
1980
|
}
|
|
1904
1981
|
}
|
|
1905
1982
|
// Order entities by dependencies for proper regeneration
|
|
1906
1983
|
this.orderedEntitiesForDeleteSPRegeneration = this.orderEntitiesByDependencies(entities, this.entitiesNeedingDeleteSPRegeneration);
|
|
1907
1984
|
if (this.orderedEntitiesForDeleteSPRegeneration.length > 0) {
|
|
1908
|
-
|
|
1985
|
+
logStatus(`Ordered entities for delete SP regeneration:`);
|
|
1909
1986
|
this.orderedEntitiesForDeleteSPRegeneration.forEach((entityId, index) => {
|
|
1910
1987
|
const entity = entities.find(e => e.ID === entityId);
|
|
1911
|
-
|
|
1988
|
+
logStatus(` ${index + 1}. ${entity?.Name || entityId}`);
|
|
1912
1989
|
});
|
|
1913
1990
|
}
|
|
1914
1991
|
}
|
|
1915
1992
|
}
|
|
1916
1993
|
}
|
|
1917
1994
|
catch (error) {
|
|
1918
|
-
|
|
1995
|
+
logError(`Error in cascade delete dependency analysis: ${error}`);
|
|
1919
1996
|
// Continue with normal processing even if dependency analysis fails
|
|
1920
1997
|
}
|
|
1921
1998
|
}
|
|
@@ -1956,7 +2033,7 @@ GO${permissions}
|
|
|
1956
2033
|
if (visiting.has(entityId)) {
|
|
1957
2034
|
// Circular dependency detected - mark it but don't fail
|
|
1958
2035
|
const entity = entities.find(e => e.ID === entityId);
|
|
1959
|
-
|
|
2036
|
+
logStatus(`Warning: Circular cascade delete dependency detected involving ${entity?.Name || entityId}`);
|
|
1960
2037
|
circularDeps.add(entityId);
|
|
1961
2038
|
return false; // Signal circular dependency but continue processing
|
|
1962
2039
|
}
|
|
@@ -1982,17 +2059,16 @@ GO${permissions}
|
|
|
1982
2059
|
if (!success && circularDeps.has(entityId)) {
|
|
1983
2060
|
// Entity is part of circular dependency - add it anyway in arbitrary order
|
|
1984
2061
|
// The SQL will still be generated, just not in perfect dependency order
|
|
1985
|
-
|
|
2062
|
+
logStatus(` - Adding ${entities.find(e => e.ID === entityId)?.Name || entityId} despite circular dependency`);
|
|
1986
2063
|
visited.add(entityId);
|
|
1987
2064
|
ordered.push(entityId);
|
|
1988
2065
|
}
|
|
1989
2066
|
}
|
|
1990
2067
|
}
|
|
1991
2068
|
if (circularDeps.size > 0) {
|
|
1992
|
-
|
|
2069
|
+
logStatus(`Note: ${circularDeps.size} entities have circular cascade delete dependencies and will be regenerated in arbitrary order.`);
|
|
1993
2070
|
}
|
|
1994
2071
|
return ordered;
|
|
1995
2072
|
}
|
|
1996
2073
|
}
|
|
1997
|
-
exports.SQLCodeGenBase = SQLCodeGenBase;
|
|
1998
2074
|
//# sourceMappingURL=sql_codegen.js.map
|