@memberjunction/codegen-lib 3.4.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +828 -630
- package/dist/Angular/angular-codegen.d.ts +10 -3
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +164 -199
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.d.ts +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.d.ts.map +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.js +8 -10
- package/dist/Angular/entity-data-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/join-grid-related-entity-component.d.ts +1 -1
- package/dist/Angular/join-grid-related-entity-component.js +8 -36
- package/dist/Angular/join-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/related-entity-components.js +13 -79
- package/dist/Angular/related-entity-components.js.map +1 -1
- package/dist/Angular/timeline-related-entity-component.d.ts +1 -1
- package/dist/Angular/timeline-related-entity-component.js +14 -38
- package/dist/Angular/timeline-related-entity-component.js.map +1 -1
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +171 -177
- package/dist/Config/config.js.map +1 -1
- package/dist/Config/db-connection.d.ts +1 -1
- package/dist/Config/db-connection.d.ts.map +1 -1
- package/dist/Config/db-connection.js +6 -33
- package/dist/Config/db-connection.js.map +1 -1
- package/dist/Database/dbSchema.js +28 -35
- package/dist/Database/dbSchema.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +302 -6
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +1294 -445
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/reorder-columns.d.ts +1 -1
- package/dist/Database/reorder-columns.d.ts.map +1 -1
- package/dist/Database/reorder-columns.js +1 -5
- package/dist/Database/reorder-columns.js.map +1 -1
- package/dist/Database/sql.d.ts +1 -1
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +67 -98
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +15 -2
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +282 -253
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +11 -0
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.js +43 -41
- package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +15 -26
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.d.ts +69 -7
- package/dist/Misc/advanced_generation.d.ts.map +1 -1
- package/dist/Misc/advanced_generation.js +114 -75
- package/dist/Misc/advanced_generation.js.map +1 -1
- package/dist/Misc/createNewUser.d.ts +1 -1
- package/dist/Misc/createNewUser.js +22 -26
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts +7 -2
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +56 -45
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +39 -42
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/runCommand.d.ts +1 -1
- package/dist/Misc/runCommand.js +13 -20
- package/dist/Misc/runCommand.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +1 -1
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +21 -51
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/Misc/status_logging.js +45 -60
- package/dist/Misc/status_logging.js.map +1 -1
- package/dist/Misc/system_integrity.d.ts +1 -1
- package/dist/Misc/system_integrity.d.ts.map +1 -1
- package/dist/Misc/system_integrity.js +12 -16
- package/dist/Misc/system_integrity.js.map +1 -1
- package/dist/Misc/temp_batch_file.js +15 -22
- package/dist/Misc/temp_batch_file.js.map +1 -1
- package/dist/Misc/util.d.ts.map +1 -1
- package/dist/Misc/util.js +17 -28
- package/dist/Misc/util.js.map +1 -1
- package/dist/__tests__/metadataConfig.test.d.ts +12 -0
- package/dist/__tests__/metadataConfig.test.d.ts.map +1 -0
- package/dist/__tests__/metadataConfig.test.js +604 -0
- package/dist/__tests__/metadataConfig.test.js.map +1 -0
- package/dist/index.d.ts +21 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -41
- package/dist/index.js.map +1 -1
- package/dist/runCodeGen.d.ts +1 -0
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +150 -178
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +29 -23
|
@@ -1,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 SQL GENERATION: Entity ${e.Name} has no primary key field in metadata. If using soft primary keys, ensure metadata was refreshed after applySoftPKFKConfig().`);
|
|
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,20 +404,20 @@ 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'))));
|
|
@@ -478,10 +451,10 @@ class SQLCodeGenBase {
|
|
|
478
451
|
}
|
|
479
452
|
}
|
|
480
453
|
if (shouldLog) {
|
|
481
|
-
|
|
482
|
-
|
|
454
|
+
SQLLogging.appendToSQLLogFile(sql, description);
|
|
455
|
+
TempBatchFile.appendToTempBatchFile(sql, entity.SchemaName);
|
|
483
456
|
}
|
|
484
|
-
|
|
457
|
+
logIf(configInfo.verboseOutput, `SQL Generated for ${entity.Name}: ${description}`);
|
|
485
458
|
}
|
|
486
459
|
async generateSingleEntitySQLToSeparateFiles(options) {
|
|
487
460
|
const files = [];
|
|
@@ -490,18 +463,18 @@ class SQLCodeGenBase {
|
|
|
490
463
|
if (options.writeFiles && !fs.existsSync(options.directory))
|
|
491
464
|
fs.mkdirSync(options.directory, { recursive: true });
|
|
492
465
|
// now do the same thing for the /schema directory within the provided directory
|
|
493
|
-
const schemaDirectory =
|
|
466
|
+
const schemaDirectory = path.join(options.directory, options.entity.SchemaName);
|
|
494
467
|
if (options.writeFiles && !fs.existsSync(schemaDirectory))
|
|
495
468
|
fs.mkdirSync(schemaDirectory, { recursive: true }); // create the directory if it doesn't exist
|
|
496
469
|
let sRet = '';
|
|
497
470
|
let permissionsSQL = '';
|
|
498
|
-
// Indexes for Fkeys for the table
|
|
499
|
-
if (!options.onlyPermissions) {
|
|
500
|
-
const shouldGenerateIndexes =
|
|
471
|
+
// Indexes for Fkeys for the table (skip for virtual entities — views can't have indexes)
|
|
472
|
+
if (!options.onlyPermissions && !options.entity.VirtualEntity) {
|
|
473
|
+
const shouldGenerateIndexes = autoIndexForeignKeys() || (configInfo.forceRegeneration?.enabled && configInfo.forceRegeneration?.indexes);
|
|
501
474
|
const indexSQL = shouldGenerateIndexes ? this.generateIndexesForForeignKeys(options.pool, options.entity) : ''; // generate indexes if auto-indexing is on OR force regeneration is enabled
|
|
502
475
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, 'Index for Foreign Keys') + indexSQL;
|
|
503
476
|
if (options.writeFiles) {
|
|
504
|
-
const filePath =
|
|
477
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('index', options.entity.SchemaName, options.entity.BaseTable, false, true));
|
|
505
478
|
this.logSQLForNewOrModifiedEntity(options.entity, s, 'Index for Foreign Keys for ' + options.entity.BaseTable, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
506
479
|
fs.writeFileSync(filePath, s);
|
|
507
480
|
files.push(filePath);
|
|
@@ -523,7 +496,7 @@ class SQLCodeGenBase {
|
|
|
523
496
|
const functionName = `fn${options.entity.BaseTable}${field.Name}_GetRootID`;
|
|
524
497
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, functionName) +
|
|
525
498
|
this.generateRootIDFunction(options.entity, field);
|
|
526
|
-
const filePath =
|
|
499
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('function', options.entity.SchemaName, functionName, false, true));
|
|
527
500
|
if (options.writeFiles) {
|
|
528
501
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `Root ID Function SQL for ${options.entity.Name}.${field.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
529
502
|
fs.writeFileSync(filePath, s);
|
|
@@ -535,7 +508,7 @@ class SQLCodeGenBase {
|
|
|
535
508
|
}
|
|
536
509
|
// Generate the base view (which may reference the TVFs created above)
|
|
537
510
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, options.entity.BaseView) + await this.generateBaseView(options.pool, options.entity);
|
|
538
|
-
const filePath =
|
|
511
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('view', options.entity.SchemaName, options.entity.BaseView, false, true));
|
|
539
512
|
if (options.writeFiles) {
|
|
540
513
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `Base View SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
541
514
|
fs.writeFileSync(filePath, s);
|
|
@@ -548,7 +521,7 @@ class SQLCodeGenBase {
|
|
|
548
521
|
if (s.length > 0)
|
|
549
522
|
permissionsSQL += s + '\nGO\n';
|
|
550
523
|
if (options.writeFiles) {
|
|
551
|
-
const filePath =
|
|
524
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('view', options.entity.SchemaName, options.entity.BaseView, true, true));
|
|
552
525
|
fs.writeFileSync(filePath, s);
|
|
553
526
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `Base View Permissions SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
554
527
|
files.push(filePath);
|
|
@@ -559,25 +532,25 @@ class SQLCodeGenBase {
|
|
|
559
532
|
sRet += s + '\nGO\n';
|
|
560
533
|
// CREATE SP
|
|
561
534
|
if (options.entity.AllowCreateAPI && !options.entity.VirtualEntity) {
|
|
562
|
-
const spName = this.getSPName(options.entity,
|
|
535
|
+
const spName = this.getSPName(options.entity, SPType.Create);
|
|
563
536
|
// Only generate if spCreateGenerated is true (respects custom SPs where it's false)
|
|
564
537
|
// forceRegeneration only forces regeneration of SPs where spCreateGenerated=true
|
|
565
538
|
if (!options.onlyPermissions && options.entity.spCreateGenerated) {
|
|
566
539
|
// generate the create SP
|
|
567
540
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPCreate(options.entity);
|
|
568
541
|
if (options.writeFiles) {
|
|
569
|
-
const filePath =
|
|
542
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, false, true));
|
|
570
543
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spCreate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
571
544
|
fs.writeFileSync(filePath, s);
|
|
572
545
|
files.push(filePath);
|
|
573
546
|
}
|
|
574
547
|
sRet += s + '\nGO\n';
|
|
575
548
|
}
|
|
576
|
-
const s = this.generateSPPermissions(options.entity, spName,
|
|
549
|
+
const s = this.generateSPPermissions(options.entity, spName, SPType.Create) + '\n\n';
|
|
577
550
|
if (s.length > 0)
|
|
578
551
|
permissionsSQL += s + '\nGO\n';
|
|
579
552
|
if (options.writeFiles) {
|
|
580
|
-
const filePath =
|
|
553
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
|
|
581
554
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spCreate Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
582
555
|
fs.writeFileSync(filePath, s);
|
|
583
556
|
files.push(filePath);
|
|
@@ -589,25 +562,25 @@ class SQLCodeGenBase {
|
|
|
589
562
|
}
|
|
590
563
|
// UPDATE SP
|
|
591
564
|
if (options.entity.AllowUpdateAPI && !options.entity.VirtualEntity) {
|
|
592
|
-
const spName = this.getSPName(options.entity,
|
|
565
|
+
const spName = this.getSPName(options.entity, SPType.Update);
|
|
593
566
|
// Only generate if spUpdateGenerated is true (respects custom SPs where it's false)
|
|
594
567
|
// forceRegeneration only forces regeneration of SPs where spUpdateGenerated=true
|
|
595
568
|
if (!options.onlyPermissions && options.entity.spUpdateGenerated) {
|
|
596
569
|
// generate the update SP
|
|
597
570
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPUpdate(options.entity);
|
|
598
571
|
if (options.writeFiles) {
|
|
599
|
-
const filePath =
|
|
572
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, false, true));
|
|
600
573
|
fs.writeFileSync(filePath, s);
|
|
601
574
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spUpdate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
602
575
|
files.push(filePath);
|
|
603
576
|
}
|
|
604
577
|
sRet += s + '\nGO\n';
|
|
605
578
|
}
|
|
606
|
-
const s = this.generateSPPermissions(options.entity, spName,
|
|
579
|
+
const s = this.generateSPPermissions(options.entity, spName, SPType.Update) + '\n\n';
|
|
607
580
|
if (s.length > 0)
|
|
608
581
|
permissionsSQL += s + '\nGO\n';
|
|
609
582
|
if (options.writeFiles) {
|
|
610
|
-
const filePath =
|
|
583
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
|
|
611
584
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spUpdate Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
612
585
|
fs.writeFileSync(filePath, s);
|
|
613
586
|
files.push(filePath);
|
|
@@ -619,7 +592,7 @@ class SQLCodeGenBase {
|
|
|
619
592
|
}
|
|
620
593
|
// DELETE SP
|
|
621
594
|
if (options.entity.AllowDeleteAPI && !options.entity.VirtualEntity) {
|
|
622
|
-
const spName = this.getSPName(options.entity,
|
|
595
|
+
const spName = this.getSPName(options.entity, SPType.Delete);
|
|
623
596
|
// Only generate if spDeleteGenerated is true (respects custom SPs where it's false)
|
|
624
597
|
// OR if this entity has cascade delete dependencies that require regeneration
|
|
625
598
|
// forceRegeneration only forces regeneration of SPs where spDeleteGenerated=true
|
|
@@ -628,22 +601,22 @@ class SQLCodeGenBase {
|
|
|
628
601
|
this.entitiesNeedingDeleteSPRegeneration.has(options.entity.ID))) {
|
|
629
602
|
// generate the delete SP
|
|
630
603
|
if (this.entitiesNeedingDeleteSPRegeneration.has(options.entity.ID)) {
|
|
631
|
-
|
|
604
|
+
logStatus(` Regenerating ${spName} due to cascade dependency changes`);
|
|
632
605
|
}
|
|
633
606
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPDelete(options.entity);
|
|
634
607
|
if (options.writeFiles) {
|
|
635
|
-
const filePath =
|
|
608
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, false, true));
|
|
636
609
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spDelete SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
637
610
|
fs.writeFileSync(filePath, s);
|
|
638
611
|
files.push(filePath);
|
|
639
612
|
}
|
|
640
613
|
sRet += s + '\nGO\n';
|
|
641
614
|
}
|
|
642
|
-
const s = this.generateSPPermissions(options.entity, spName,
|
|
615
|
+
const s = this.generateSPPermissions(options.entity, spName, SPType.Delete) + '\n\n';
|
|
643
616
|
if (s.length > 0)
|
|
644
617
|
permissionsSQL += s + '\nGO\n';
|
|
645
618
|
if (options.writeFiles) {
|
|
646
|
-
const filePath =
|
|
619
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
|
|
647
620
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spDelete Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
648
621
|
fs.writeFileSync(filePath, s);
|
|
649
622
|
files.push(filePath);
|
|
@@ -654,12 +627,12 @@ class SQLCodeGenBase {
|
|
|
654
627
|
sRet += s + '\nGO\n';
|
|
655
628
|
}
|
|
656
629
|
// check to see if the options.entity supports full text search or not
|
|
657
|
-
if (options.entity.FullTextSearchEnabled || (
|
|
630
|
+
if (options.entity.FullTextSearchEnabled || (configInfo.forceRegeneration?.enabled && configInfo.forceRegeneration?.fullTextSearch)) {
|
|
658
631
|
// always generate the code so we can get the function name from the below function call
|
|
659
632
|
const ft = await this.generateEntityFullTextSearchSQL(options.pool, options.entity);
|
|
660
633
|
if (!options.onlyPermissions) {
|
|
661
634
|
// only write the actual sql out if we're not only generating permissions
|
|
662
|
-
const filePath =
|
|
635
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('full_text_search_function', options.entity.SchemaName, options.entity.BaseTable, false, true));
|
|
663
636
|
if (options.writeFiles) {
|
|
664
637
|
this.logSQLForNewOrModifiedEntity(options.entity, ft.sql, `Full Text Search SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
665
638
|
fs.writeFileSync(filePath, ft.sql);
|
|
@@ -670,7 +643,7 @@ class SQLCodeGenBase {
|
|
|
670
643
|
const sP = this.generateFullTextSearchFunctionPermissions(options.entity, ft.functionName) + '\n\n';
|
|
671
644
|
if (sP.length > 0)
|
|
672
645
|
permissionsSQL += sP + '\nGO\n';
|
|
673
|
-
const filePath =
|
|
646
|
+
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('full_text_search_function', options.entity.SchemaName, options.entity.BaseTable, true, true));
|
|
674
647
|
if (options.writeFiles) {
|
|
675
648
|
this.logSQLForNewOrModifiedEntity(options.entity, sP, `Full Text Search Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
676
649
|
fs.writeFileSync(filePath, sP);
|
|
@@ -684,17 +657,17 @@ class SQLCodeGenBase {
|
|
|
684
657
|
return { sql: sRet, permissionsSQL: permissionsSQL, files: files };
|
|
685
658
|
}
|
|
686
659
|
catch (err) {
|
|
687
|
-
|
|
660
|
+
logError(err);
|
|
688
661
|
return null;
|
|
689
662
|
}
|
|
690
663
|
}
|
|
691
664
|
getSPName(entity, type) {
|
|
692
665
|
switch (type) {
|
|
693
|
-
case
|
|
666
|
+
case SPType.Create:
|
|
694
667
|
return entity.spCreate && entity.spCreate.length > 0 ? entity.spCreate : 'spCreate' + entity.BaseTableCodeName;
|
|
695
|
-
case
|
|
668
|
+
case SPType.Update:
|
|
696
669
|
return entity.spUpdate && entity.spUpdate.length > 0 ? entity.spUpdate : 'spUpdate' + entity.BaseTableCodeName;
|
|
697
|
-
case
|
|
670
|
+
case SPType.Delete:
|
|
698
671
|
return entity.spDelete && entity.spDelete.length > 0 ? entity.spDelete : 'spDelete' + entity.BaseTableCodeName;
|
|
699
672
|
}
|
|
700
673
|
}
|
|
@@ -706,11 +679,11 @@ class SQLCodeGenBase {
|
|
|
706
679
|
if (!entity.VirtualEntity) {
|
|
707
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
|
|
708
681
|
if (entity.AllowCreateAPI)
|
|
709
|
-
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));
|
|
710
683
|
if (entity.AllowUpdateAPI)
|
|
711
|
-
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));
|
|
712
685
|
if (entity.AllowDeleteAPI)
|
|
713
|
-
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));
|
|
714
687
|
}
|
|
715
688
|
if (entity.FullTextSearchEnabled)
|
|
716
689
|
files.push(this.SQLUtilityObject.getDBObjectFileName('full_text_search_function', entity.SchemaName, entity.BaseTable, true, true));
|
|
@@ -737,7 +710,7 @@ class SQLCodeGenBase {
|
|
|
737
710
|
sOutput += this.generateSPCreate(entity) + '\n\n';
|
|
738
711
|
else
|
|
739
712
|
// custom SP, still generate the permissions
|
|
740
|
-
sOutput += this.generateSPPermissions(entity, entity.spCreate,
|
|
713
|
+
sOutput += this.generateSPPermissions(entity, entity.spCreate, SPType.Create) + '\n\n';
|
|
741
714
|
}
|
|
742
715
|
if (entity.AllowUpdateAPI && !entity.VirtualEntity) {
|
|
743
716
|
if (entity.spUpdateGenerated)
|
|
@@ -745,7 +718,7 @@ class SQLCodeGenBase {
|
|
|
745
718
|
sOutput += this.generateSPUpdate(entity) + '\n\n';
|
|
746
719
|
else
|
|
747
720
|
// custom SP, still generate the permissions
|
|
748
|
-
sOutput += this.generateSPPermissions(entity, entity.spUpdate,
|
|
721
|
+
sOutput += this.generateSPPermissions(entity, entity.spUpdate, SPType.Update) + '\n\n';
|
|
749
722
|
}
|
|
750
723
|
if (entity.AllowDeleteAPI && !entity.VirtualEntity) {
|
|
751
724
|
if (entity.spDeleteGenerated)
|
|
@@ -753,7 +726,7 @@ class SQLCodeGenBase {
|
|
|
753
726
|
sOutput += this.generateSPDelete(entity) + '\n\n';
|
|
754
727
|
else
|
|
755
728
|
// custom SP, still generate the permissions
|
|
756
|
-
sOutput += this.generateSPPermissions(entity, entity.spDelete,
|
|
729
|
+
sOutput += this.generateSPPermissions(entity, entity.spDelete, SPType.Delete) + '\n\n';
|
|
757
730
|
}
|
|
758
731
|
// check to see if the entity supports full text search or not
|
|
759
732
|
if (entity.FullTextSearchEnabled) {
|
|
@@ -763,7 +736,7 @@ class SQLCodeGenBase {
|
|
|
763
736
|
}
|
|
764
737
|
async generateEntityFullTextSearchSQL(pool, entity) {
|
|
765
738
|
let sql = '';
|
|
766
|
-
const catalogName = entity.FullTextCatalog && entity.FullTextCatalog.length > 0 ? entity.FullTextCatalog :
|
|
739
|
+
const catalogName = entity.FullTextCatalog && entity.FullTextCatalog.length > 0 ? entity.FullTextCatalog : dbDatabase + '_FullTextCatalog';
|
|
767
740
|
if (entity.FullTextCatalogGenerated) {
|
|
768
741
|
// this situation means we have a generated catalog and the user has provided a name specific to THIS entity
|
|
769
742
|
sql += ` -- CREATE THE FULL TEXT CATALOG FOR THE ENTITY, IF NOT ALREADY CREATED
|
|
@@ -816,8 +789,8 @@ class SQLCodeGenBase {
|
|
|
816
789
|
throw new Error(`FullTextSearchFunctionGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
|
|
817
790
|
if (!entity.FullTextSearchFunction || entity.FullTextSearchFunction.length === 0) {
|
|
818
791
|
// update this in the DB
|
|
819
|
-
const md = new
|
|
820
|
-
const u =
|
|
792
|
+
const md = new Metadata();
|
|
793
|
+
const u = UserCache.Instance.Users[0];
|
|
821
794
|
if (!u)
|
|
822
795
|
throw new Error('Could not find the first user in the cache, cant generate the full text search function without a user');
|
|
823
796
|
const e = await md.GetEntityObject('Entities', u);
|
|
@@ -906,8 +879,8 @@ class SQLCodeGenBase {
|
|
|
906
879
|
if (f.RelatedEntity && f.RelatedEntity.length > 0) {
|
|
907
880
|
// we have an fkey, so generate the create index
|
|
908
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
|
|
909
|
-
if (indexName.length >
|
|
910
|
-
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
|
|
911
884
|
if (sOutput.length > 0)
|
|
912
885
|
sOutput += '\n\n'; // do this way so we don't end up with a trailing newline at end of the string/file
|
|
913
886
|
sOutput += `-- Index for foreign key ${f.Name} in table ${entity.BaseTable}
|
|
@@ -1046,6 +1019,56 @@ GO
|
|
|
1046
1019
|
generateRecursiveCTEJoins(recursiveFKs, classNameFirstChar, entity) {
|
|
1047
1020
|
return this.generateRootIDJoins(recursiveFKs, classNameFirstChar, entity);
|
|
1048
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Generates SELECT field expressions for IS-A parent entity columns.
|
|
1024
|
+
* Walks the ParentID chain upward, joining to each parent's base table, and includes
|
|
1025
|
+
* non-PK, non-timestamp, non-virtual fields from each parent table.
|
|
1026
|
+
* Returns a string starting with ',\n' if there are parent fields, or '' if none.
|
|
1027
|
+
*/
|
|
1028
|
+
generateParentEntityFieldSelects(entity) {
|
|
1029
|
+
if (!entity.IsChildType)
|
|
1030
|
+
return '';
|
|
1031
|
+
const parentChain = entity.ParentChain;
|
|
1032
|
+
if (parentChain.length === 0)
|
|
1033
|
+
return '';
|
|
1034
|
+
const fieldExpressions = [];
|
|
1035
|
+
for (let i = 0; i < parentChain.length; i++) {
|
|
1036
|
+
const parent = parentChain[i];
|
|
1037
|
+
const alias = `__mj_isa_p${i + 1}`;
|
|
1038
|
+
for (const field of parent.Fields) {
|
|
1039
|
+
// Skip PKs (shared with child), timestamps, and virtual fields (view-computed)
|
|
1040
|
+
if (field.IsPrimaryKey || field.Name.startsWith('__mj_') || field.IsVirtual)
|
|
1041
|
+
continue;
|
|
1042
|
+
fieldExpressions.push(` ${alias}.[${field.Name}]`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (fieldExpressions.length === 0)
|
|
1046
|
+
return '';
|
|
1047
|
+
return ',\n' + fieldExpressions.join(',\n');
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Generates INNER JOIN clauses for IS-A parent entity base tables.
|
|
1051
|
+
* Chains joins from child -> parent -> grandparent using PK-to-PK conditions.
|
|
1052
|
+
* Each parent is joined via its base table (not view) to avoid view dependency ordering issues.
|
|
1053
|
+
*/
|
|
1054
|
+
generateParentEntityJoins(entity, classNameFirstChar) {
|
|
1055
|
+
if (!entity.IsChildType)
|
|
1056
|
+
return '';
|
|
1057
|
+
const parentChain = entity.ParentChain;
|
|
1058
|
+
if (parentChain.length === 0)
|
|
1059
|
+
return '';
|
|
1060
|
+
const joins = [];
|
|
1061
|
+
for (let i = 0; i < parentChain.length; i++) {
|
|
1062
|
+
const parent = parentChain[i];
|
|
1063
|
+
const parentAlias = `__mj_isa_p${i + 1}`;
|
|
1064
|
+
// First parent joins to child table alias; deeper parents chain to previous parent alias
|
|
1065
|
+
const sourceAlias = i === 0 ? classNameFirstChar : `__mj_isa_p${i}`;
|
|
1066
|
+
// Build PK-to-PK join condition (supports composite keys)
|
|
1067
|
+
const joinConditions = entity.PrimaryKeys.map(pk => `[${sourceAlias}].[${pk.Name}] = ${parentAlias}.[${pk.Name}]`).join(' AND ');
|
|
1068
|
+
joins.push(`INNER JOIN\n [${parent.SchemaName}].[${parent.BaseTable}] AS ${parentAlias}\n ON\n ${joinConditions}`);
|
|
1069
|
+
}
|
|
1070
|
+
return joins.join('\n');
|
|
1071
|
+
}
|
|
1049
1072
|
async generateBaseView(pool, entity) {
|
|
1050
1073
|
const viewName = entity.BaseView ? entity.BaseView : `vw${entity.CodeName}`;
|
|
1051
1074
|
const classNameFirstChar = entity.ClassName.charAt(0).toLowerCase();
|
|
@@ -1053,12 +1076,15 @@ GO
|
|
|
1053
1076
|
const relatedFieldsJoinString = this.generateBaseViewJoins(entity, entity.Fields);
|
|
1054
1077
|
const permissions = this.generateViewPermissions(entity);
|
|
1055
1078
|
const whereClause = entity.DeleteType === 'Soft' ? `WHERE
|
|
1056
|
-
${classNameFirstChar}.[${
|
|
1079
|
+
${classNameFirstChar}.[${EntityInfo.DeletedAtFieldName}] IS NULL
|
|
1057
1080
|
` : '';
|
|
1058
1081
|
// Detect recursive foreign keys and generate TVF joins and root field selects
|
|
1059
1082
|
const recursiveFKs = this.detectRecursiveForeignKeys(entity);
|
|
1060
1083
|
const rootFields = recursiveFKs.length > 0 ? this.generateRootFieldSelects(recursiveFKs, classNameFirstChar) : '';
|
|
1061
1084
|
const rootJoins = recursiveFKs.length > 0 ? this.generateRootIDJoins(recursiveFKs, classNameFirstChar, entity) : '';
|
|
1085
|
+
// IS-A parent entity JOINs — walk ParentID chain, JOIN to each parent's base table
|
|
1086
|
+
const parentFieldsString = this.generateParentEntityFieldSelects(entity);
|
|
1087
|
+
const parentJoinsString = this.generateParentEntityJoins(entity, classNameFirstChar);
|
|
1062
1088
|
return `
|
|
1063
1089
|
------------------------------------------------------------
|
|
1064
1090
|
----- BASE VIEW FOR ENTITY: ${entity.Name}
|
|
@@ -1073,9 +1099,9 @@ GO
|
|
|
1073
1099
|
CREATE VIEW [${entity.SchemaName}].[${viewName}]
|
|
1074
1100
|
AS
|
|
1075
1101
|
SELECT
|
|
1076
|
-
${classNameFirstChar}.*${relatedFieldsString.length > 0 ? ',' : ''}${relatedFieldsString}${rootFields}
|
|
1102
|
+
${classNameFirstChar}.*${parentFieldsString}${relatedFieldsString.length > 0 ? ',' : ''}${relatedFieldsString}${rootFields}
|
|
1077
1103
|
FROM
|
|
1078
|
-
[${entity.SchemaName}].[${entity.BaseTable}] AS ${classNameFirstChar}${relatedFieldsJoinString ? '\n' + relatedFieldsJoinString : ''}${rootJoins}
|
|
1104
|
+
[${entity.SchemaName}].[${entity.BaseTable}] AS ${classNameFirstChar}${parentJoinsString ? '\n' + parentJoinsString : ''}${relatedFieldsJoinString ? '\n' + relatedFieldsJoinString : ''}${rootJoins}
|
|
1079
1105
|
${whereClause}GO${permissions}
|
|
1080
1106
|
`;
|
|
1081
1107
|
}
|
|
@@ -1109,8 +1135,8 @@ ${whereClause}GO${permissions}
|
|
|
1109
1135
|
async generateBaseViewRelatedFieldsString(pool, entityFields) {
|
|
1110
1136
|
let sOutput = '';
|
|
1111
1137
|
let fieldCount = 0;
|
|
1112
|
-
const manageMD =
|
|
1113
|
-
const md = new
|
|
1138
|
+
const manageMD = MJGlobal.Instance.ClassFactory.CreateInstance(ManageMetadataBase);
|
|
1139
|
+
const md = new Metadata();
|
|
1114
1140
|
const allGeneratedAliases = [];
|
|
1115
1141
|
// Get fields that are related entities with join field configuration.
|
|
1116
1142
|
//
|
|
@@ -1145,7 +1171,7 @@ ${whereClause}GO${permissions}
|
|
|
1145
1171
|
// This happens in table-per-type inheritance where child.ID is FK to parent.ID
|
|
1146
1172
|
// stripID("ID") returns "" which would generate invalid SQL: AS []
|
|
1147
1173
|
if (candidateName.trim().length === 0) {
|
|
1148
|
-
|
|
1174
|
+
logStatus(` Skipping related entity name field for ${ef.Name} in entity - stripID returned empty string (likely inheritance pattern)`);
|
|
1149
1175
|
}
|
|
1150
1176
|
else {
|
|
1151
1177
|
// 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)
|
|
@@ -1184,12 +1210,12 @@ ${whereClause}GO${permissions}
|
|
|
1184
1210
|
const alias = fieldConfig.alias || this.generateDefaultAlias(ef.Name, fieldName);
|
|
1185
1211
|
// Validate field exists on related entity
|
|
1186
1212
|
if (!this.validateFieldExists(ef.RelatedEntity, fieldName)) {
|
|
1187
|
-
|
|
1213
|
+
logError(`RelatedEntityJoinFields: Field '${fieldName}' not found on entity '${ef.RelatedEntity}' (FK: ${ef.Name})`);
|
|
1188
1214
|
continue;
|
|
1189
1215
|
}
|
|
1190
1216
|
// Check for alias collisions
|
|
1191
1217
|
if (currentEntity && this.hasAliasCollision(currentEntity, alias, allGeneratedAliases)) {
|
|
1192
|
-
|
|
1218
|
+
logError(`RelatedEntityJoinFields: Alias '${alias}' for field '${fieldName}' would collide with an existing field or alias in entity '${currentEntity.Name}'`);
|
|
1193
1219
|
continue;
|
|
1194
1220
|
}
|
|
1195
1221
|
// Get field metadata from related entity to check if virtual
|
|
@@ -1219,7 +1245,7 @@ ${whereClause}GO${permissions}
|
|
|
1219
1245
|
return sOutput;
|
|
1220
1246
|
}
|
|
1221
1247
|
getIsNameFieldForSingleEntity(entityName) {
|
|
1222
|
-
const md = new
|
|
1248
|
+
const md = new Metadata(); // use the full metadata entity list, not the filtered version that we receive
|
|
1223
1249
|
const e = md.Entities.find(e => e.Name === entityName);
|
|
1224
1250
|
if (e) {
|
|
1225
1251
|
const ef = e.NameField;
|
|
@@ -1227,7 +1253,7 @@ ${whereClause}GO${permissions}
|
|
|
1227
1253
|
return { nameField: ef.Name, nameFieldIsVirtual: ef.IsVirtual };
|
|
1228
1254
|
}
|
|
1229
1255
|
else
|
|
1230
|
-
|
|
1256
|
+
logStatus(`ERROR: Could not find entity with name ${entityName}`);
|
|
1231
1257
|
return { nameField: '', nameFieldIsVirtual: false };
|
|
1232
1258
|
}
|
|
1233
1259
|
stripID(name) {
|
|
@@ -1244,7 +1270,7 @@ ${whereClause}GO${permissions}
|
|
|
1244
1270
|
return baseName + relatedFieldName;
|
|
1245
1271
|
}
|
|
1246
1272
|
validateFieldExists(entityName, fieldName) {
|
|
1247
|
-
const md = new
|
|
1273
|
+
const md = new Metadata();
|
|
1248
1274
|
const entity = md.Entities.find(e => e.Name === entityName);
|
|
1249
1275
|
if (!entity)
|
|
1250
1276
|
return false;
|
|
@@ -1258,7 +1284,7 @@ ${whereClause}GO${permissions}
|
|
|
1258
1284
|
if (generatedAliases.some(a => a.toLowerCase() === alias.toLowerCase()))
|
|
1259
1285
|
return true;
|
|
1260
1286
|
// Check against system fields
|
|
1261
|
-
const systemFields = ['__mj_CreatedAt', '__mj_UpdatedAt',
|
|
1287
|
+
const systemFields = ['__mj_CreatedAt', '__mj_UpdatedAt', EntityInfo.DeletedAtFieldName];
|
|
1262
1288
|
if (systemFields.some(sf => sf?.toLowerCase() === alias.toLowerCase()))
|
|
1263
1289
|
return true;
|
|
1264
1290
|
return false;
|
|
@@ -1267,9 +1293,9 @@ ${whereClause}GO${permissions}
|
|
|
1267
1293
|
let sOutput = '';
|
|
1268
1294
|
for (let i = 0; i < entity.Permissions.length; i++) {
|
|
1269
1295
|
const ep = entity.Permissions[i];
|
|
1270
|
-
if ((type ==
|
|
1271
|
-
(type ==
|
|
1272
|
-
(type ==
|
|
1296
|
+
if ((type == SPType.Create && ep.CanCreate) ||
|
|
1297
|
+
(type == SPType.Update && ep.CanUpdate) ||
|
|
1298
|
+
(type == SPType.Delete && ep.CanDelete)) {
|
|
1273
1299
|
if (ep.RoleSQLName && ep.RoleSQLName.length > 0) {
|
|
1274
1300
|
sOutput += (sOutput === '' ? `GRANT EXECUTE ON [${entity.SchemaName}].[${spName}] TO ` : ', ') + `[${ep.RoleSQLName}]`;
|
|
1275
1301
|
}
|
|
@@ -1295,7 +1321,7 @@ ${whereClause}GO${permissions}
|
|
|
1295
1321
|
//double exclamations used on the firstKey.DefaultValue property otherwise the type of this variable is 'number | ""';
|
|
1296
1322
|
const primaryKeyAutomatic = firstKey.AutoIncrement; // Only exclude auto-increment fields, allow manual override for all other PKs including UUIDs with defaults
|
|
1297
1323
|
const efString = this.createEntityFieldsParamString(entity.Fields, false); // Always pass false for isUpdate since this is generateSPCreate
|
|
1298
|
-
const permissions = this.generateSPPermissions(entity, spName,
|
|
1324
|
+
const permissions = this.generateSPPermissions(entity, spName, SPType.Create);
|
|
1299
1325
|
let preInsertCode = '';
|
|
1300
1326
|
let outputCode = '';
|
|
1301
1327
|
let selectInsertedRecord = '';
|
|
@@ -1398,12 +1424,12 @@ GO${permissions}
|
|
|
1398
1424
|
`;
|
|
1399
1425
|
}
|
|
1400
1426
|
generateUpdatedAtTrigger(entity) {
|
|
1401
|
-
const updatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() ===
|
|
1427
|
+
const updatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() === EntityInfo.UpdatedAtFieldName.toLowerCase().trim());
|
|
1402
1428
|
if (!updatedAtField)
|
|
1403
1429
|
return '';
|
|
1404
1430
|
const triggerStatement = `
|
|
1405
1431
|
------------------------------------------------------------
|
|
1406
|
-
----- TRIGGER FOR ${
|
|
1432
|
+
----- TRIGGER FOR ${EntityInfo.UpdatedAtFieldName} field for the ${entity.BaseTable} table
|
|
1407
1433
|
------------------------------------------------------------
|
|
1408
1434
|
IF OBJECT_ID('[${entity.SchemaName}].[trgUpdate${entity.ClassName}]', 'TR') IS NOT NULL
|
|
1409
1435
|
DROP TRIGGER [${entity.SchemaName}].[trgUpdate${entity.ClassName}];
|
|
@@ -1417,7 +1443,7 @@ BEGIN
|
|
|
1417
1443
|
UPDATE
|
|
1418
1444
|
[${entity.SchemaName}].[${entity.BaseTable}]
|
|
1419
1445
|
SET
|
|
1420
|
-
${
|
|
1446
|
+
${EntityInfo.UpdatedAtFieldName} = GETUTCDATE()
|
|
1421
1447
|
FROM
|
|
1422
1448
|
[${entity.SchemaName}].[${entity.BaseTable}] AS _organicTable
|
|
1423
1449
|
INNER JOIN
|
|
@@ -1430,8 +1456,8 @@ GO`;
|
|
|
1430
1456
|
generateSPUpdate(entity) {
|
|
1431
1457
|
const spName = entity.spUpdate ? entity.spUpdate : `spUpdate${entity.BaseTableCodeName}`;
|
|
1432
1458
|
const efParamString = this.createEntityFieldsParamString(entity.Fields, true);
|
|
1433
|
-
const permissions = this.generateSPPermissions(entity, spName,
|
|
1434
|
-
const hasUpdatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() ===
|
|
1459
|
+
const permissions = this.generateSPPermissions(entity, spName, SPType.Update);
|
|
1460
|
+
const hasUpdatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() === EntityInfo.UpdatedAtFieldName.trim().toLowerCase()) !== undefined;
|
|
1435
1461
|
const updatedAtTrigger = hasUpdatedAtField ? this.generateUpdatedAtTrigger(entity) : '';
|
|
1436
1462
|
let selectUpdatedRecord = `SELECT
|
|
1437
1463
|
*
|
|
@@ -1625,7 +1651,7 @@ ${updatedAtTrigger}
|
|
|
1625
1651
|
generateSPDelete(entity) {
|
|
1626
1652
|
const spName = entity.spDelete ? entity.spDelete : `spDelete${entity.BaseTableCodeName}`;
|
|
1627
1653
|
const sCascadeDeletes = this.generateCascadeDeletes(entity);
|
|
1628
|
-
const permissions = this.generateSPPermissions(entity, spName,
|
|
1654
|
+
const permissions = this.generateSPPermissions(entity, spName, SPType.Delete);
|
|
1629
1655
|
let sVariables = '';
|
|
1630
1656
|
let sSelect = '';
|
|
1631
1657
|
for (let k of entity.PrimaryKeys) {
|
|
@@ -1650,8 +1676,8 @@ ${deleteCode}`;
|
|
|
1650
1676
|
deleteCode = ` UPDATE
|
|
1651
1677
|
[${entity.SchemaName}].[${entity.BaseTable}]
|
|
1652
1678
|
SET
|
|
1653
|
-
${
|
|
1654
|
-
${deleteCode} AND ${
|
|
1679
|
+
${EntityInfo.DeletedAtFieldName} = GETUTCDATE()
|
|
1680
|
+
${deleteCode} AND ${EntityInfo.DeletedAtFieldName} IS NULL -- don't update the record if it's already been deleted via a soft delete`;
|
|
1655
1681
|
}
|
|
1656
1682
|
// Build the NULL select statement for when no rows are affected
|
|
1657
1683
|
let sNullSelect = '';
|
|
@@ -1688,7 +1714,7 @@ GO${permissions}
|
|
|
1688
1714
|
generateCascadeDeletes(entity) {
|
|
1689
1715
|
let sOutput = '';
|
|
1690
1716
|
if (entity.CascadeDeletes) {
|
|
1691
|
-
const md = new
|
|
1717
|
+
const md = new Metadata();
|
|
1692
1718
|
// Find all fields in other entities that are foreign keys to this entity
|
|
1693
1719
|
for (const e of md.Entities) {
|
|
1694
1720
|
for (const ef of e.Fields) {
|
|
@@ -1718,7 +1744,7 @@ GO${permissions}
|
|
|
1718
1744
|
// Nullable FK but no update API - this is a configuration error
|
|
1719
1745
|
const sqlComment = `WARNING: ${relatedEntity.BaseTable} has nullable FK to ${parentEntity.BaseTable} but doesn't allow update API - cascade operation will fail`;
|
|
1720
1746
|
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`;
|
|
1721
|
-
|
|
1747
|
+
logWarning(consoleMsg);
|
|
1722
1748
|
return `
|
|
1723
1749
|
-- ${sqlComment}
|
|
1724
1750
|
-- This will cause the delete operation to fail due to referential integrity`;
|
|
@@ -1727,7 +1753,7 @@ GO${permissions}
|
|
|
1727
1753
|
// Entity doesn't allow delete API, so we can't cascade delete
|
|
1728
1754
|
const sqlComment = `WARNING: ${relatedEntity.BaseTable} has non-nullable FK to ${parentEntity.BaseTable} but doesn't allow delete API - cascade operation will fail`;
|
|
1729
1755
|
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`;
|
|
1730
|
-
|
|
1756
|
+
logWarning(consoleMsg);
|
|
1731
1757
|
return `
|
|
1732
1758
|
-- ${sqlComment}
|
|
1733
1759
|
-- This will cause a referential integrity violation`;
|
|
@@ -1738,43 +1764,47 @@ GO${permissions}
|
|
|
1738
1764
|
// Build the WHERE clause for matching foreign key(s)
|
|
1739
1765
|
// TODO: Future enhancement to support composite foreign keys
|
|
1740
1766
|
const whereClause = `[${fkField.CodeName}] = @${parentEntity.FirstPrimaryKey.CodeName}`;
|
|
1741
|
-
// Generate unique cursor name using entity code
|
|
1742
|
-
|
|
1767
|
+
// Generate unique cursor name using entity code name AND FK field name
|
|
1768
|
+
// This ensures uniqueness when an entity has multiple FKs pointing to the same parent
|
|
1769
|
+
// (e.g., AIPromptRun.ParentID and AIPromptRun.RerunFromPromptRunID both reference AIPromptRun)
|
|
1770
|
+
const cursorName = `cascade_${operation}_${relatedEntity.CodeName}_${fkField.CodeName}_cursor`;
|
|
1771
|
+
// Use a combined prefix that includes both entity name and FK field name to ensure unique variable names
|
|
1772
|
+
const variablePrefix = `${relatedEntity.CodeName}_${fkField.CodeName}`;
|
|
1743
1773
|
// Determine which SP to call
|
|
1744
|
-
const spType = operation === 'delete' ?
|
|
1774
|
+
const spType = operation === 'delete' ? SPType.Delete : SPType.Update;
|
|
1745
1775
|
const spName = this.getSPName(relatedEntity, spType);
|
|
1746
1776
|
if (operation === 'update') {
|
|
1747
1777
|
// For update, we need to include all updateable fields
|
|
1748
|
-
// Use the
|
|
1749
|
-
const updateParams = this.buildUpdateCursorParameters(relatedEntity, fkField,
|
|
1778
|
+
// Use the combined prefix to ensure uniqueness across multiple FKs to same entity
|
|
1779
|
+
const updateParams = this.buildUpdateCursorParameters(relatedEntity, fkField, variablePrefix);
|
|
1750
1780
|
const spCallParams = updateParams.allParams;
|
|
1751
1781
|
return `
|
|
1752
1782
|
-- Cascade update on ${relatedEntity.BaseTable} using cursor to call ${spName}
|
|
1753
1783
|
${updateParams.declarations}
|
|
1754
|
-
DECLARE ${cursorName} CURSOR FOR
|
|
1784
|
+
DECLARE ${cursorName} CURSOR FOR
|
|
1755
1785
|
SELECT ${updateParams.selectFields}
|
|
1756
1786
|
FROM [${relatedEntity.SchemaName}].[${relatedEntity.BaseTable}]
|
|
1757
1787
|
WHERE ${whereClause}
|
|
1758
|
-
|
|
1788
|
+
|
|
1759
1789
|
OPEN ${cursorName}
|
|
1760
1790
|
FETCH NEXT FROM ${cursorName} INTO ${updateParams.fetchInto}
|
|
1761
|
-
|
|
1791
|
+
|
|
1762
1792
|
WHILE @@FETCH_STATUS = 0
|
|
1763
1793
|
BEGIN
|
|
1764
1794
|
-- Set the FK field to NULL
|
|
1765
|
-
SET @${
|
|
1766
|
-
|
|
1795
|
+
SET @${variablePrefix}_${fkField.CodeName} = NULL
|
|
1796
|
+
|
|
1767
1797
|
-- Call the update SP for the related entity
|
|
1768
1798
|
EXEC [${relatedEntity.SchemaName}].[${spName}] ${spCallParams}
|
|
1769
|
-
|
|
1799
|
+
|
|
1770
1800
|
FETCH NEXT FROM ${cursorName} INTO ${updateParams.fetchInto}
|
|
1771
1801
|
END
|
|
1772
|
-
|
|
1802
|
+
|
|
1773
1803
|
CLOSE ${cursorName}
|
|
1774
1804
|
DEALLOCATE ${cursorName}`;
|
|
1775
1805
|
}
|
|
1776
1806
|
// For delete operation, use a simpler prefix for primary keys only
|
|
1777
|
-
const pkComponents = this.buildPrimaryKeyComponents(relatedEntity,
|
|
1807
|
+
const pkComponents = this.buildPrimaryKeyComponents(relatedEntity, variablePrefix);
|
|
1778
1808
|
return `
|
|
1779
1809
|
-- Cascade delete from ${relatedEntity.BaseTable} using cursor to call ${spName}
|
|
1780
1810
|
DECLARE ${pkComponents.varDeclarations}
|
|
@@ -1835,7 +1865,7 @@ GO${permissions}
|
|
|
1835
1865
|
fetchInto = pkComponents.fetchInto;
|
|
1836
1866
|
allParams = pkComponents.spParams;
|
|
1837
1867
|
// Then, add all updateable fields with the same prefix
|
|
1838
|
-
const sortedFields =
|
|
1868
|
+
const sortedFields = sortBySequenceAndCreatedAt(entity.Fields);
|
|
1839
1869
|
for (const ef of sortedFields) {
|
|
1840
1870
|
if (!ef.IsPrimaryKey && !ef.IsVirtual && ef.AllowUpdateAPI && !ef.AutoIncrement && !ef.IsSpecialDateField) {
|
|
1841
1871
|
if (declarations !== '')
|
|
@@ -1861,7 +1891,7 @@ GO${permissions}
|
|
|
1861
1891
|
*/
|
|
1862
1892
|
analyzeCascadeDeleteDependencies(entity) {
|
|
1863
1893
|
if (entity.CascadeDeletes) {
|
|
1864
|
-
const md = new
|
|
1894
|
+
const md = new Metadata();
|
|
1865
1895
|
// Find all fields in other entities that are foreign keys to this entity
|
|
1866
1896
|
for (const e of md.Entities) {
|
|
1867
1897
|
for (const ef of e.Fields) {
|
|
@@ -1903,7 +1933,7 @@ GO${permissions}
|
|
|
1903
1933
|
async buildCascadeDeleteDependencies(entities) {
|
|
1904
1934
|
// Clear existing dependencies
|
|
1905
1935
|
this.cascadeDeleteDependencies.clear();
|
|
1906
|
-
|
|
1936
|
+
logStatus(`Building cascade delete dependencies...`);
|
|
1907
1937
|
// Analyze cascade deletes for each entity with CascadeDeletes=true
|
|
1908
1938
|
// This will populate the cascadeDeleteDependencies map WITHOUT generating SQL
|
|
1909
1939
|
let entitiesWithCascadeDeletes = 0;
|
|
@@ -1911,23 +1941,23 @@ GO${permissions}
|
|
|
1911
1941
|
if (entity.CascadeDeletes) {
|
|
1912
1942
|
entitiesWithCascadeDeletes++;
|
|
1913
1943
|
if (entity.spDeleteGenerated) {
|
|
1914
|
-
|
|
1944
|
+
logStatus(` Analyzing cascade deletes for ${entity.Name}...`);
|
|
1915
1945
|
this.analyzeCascadeDeleteDependencies(entity);
|
|
1916
1946
|
}
|
|
1917
1947
|
else {
|
|
1918
|
-
|
|
1948
|
+
logStatus(` Skipping ${entity.Name} - has CascadeDeletes but spDeleteGenerated=false`);
|
|
1919
1949
|
}
|
|
1920
1950
|
}
|
|
1921
1951
|
}
|
|
1922
|
-
|
|
1952
|
+
logStatus(`Total entities with CascadeDeletes=true: ${entitiesWithCascadeDeletes}`);
|
|
1923
1953
|
// Log the dependency map
|
|
1924
|
-
|
|
1954
|
+
logStatus(`Cascade delete dependency map built:`);
|
|
1925
1955
|
for (const [dependedOnEntityId, dependentEntityIds] of this.cascadeDeleteDependencies) {
|
|
1926
1956
|
const dependedOnEntity = entities.find(e => e.ID === dependedOnEntityId);
|
|
1927
1957
|
const dependentNames = Array.from(dependentEntityIds)
|
|
1928
1958
|
.map(id => entities.find(e => e.ID === id)?.Name || id)
|
|
1929
1959
|
.join(', ');
|
|
1930
|
-
|
|
1960
|
+
logStatus(` ${dependedOnEntity?.Name || dependedOnEntityId} is depended on by: ${dependentNames}`);
|
|
1931
1961
|
}
|
|
1932
1962
|
}
|
|
1933
1963
|
/**
|
|
@@ -1937,8 +1967,8 @@ GO${permissions}
|
|
|
1937
1967
|
async getModifiedEntitiesWithUpdateAPI(entities) {
|
|
1938
1968
|
const modifiedEntitiesMap = new Map();
|
|
1939
1969
|
// Get the list of modified entity names from the metadata management phase
|
|
1940
|
-
const modifiedEntityNames =
|
|
1941
|
-
|
|
1970
|
+
const modifiedEntityNames = ManageMetadataBase.modifiedEntityList;
|
|
1971
|
+
logStatus(`Modified entities from metadata phase: ${modifiedEntityNames.join(', ')}`);
|
|
1942
1972
|
// Convert entity names to IDs and filter for those with update API
|
|
1943
1973
|
for (const entityName of modifiedEntityNames) {
|
|
1944
1974
|
const entity = entities.find(e => e.Name === entityName &&
|
|
@@ -1946,15 +1976,15 @@ GO${permissions}
|
|
|
1946
1976
|
e.spUpdateGenerated);
|
|
1947
1977
|
if (entity) {
|
|
1948
1978
|
modifiedEntitiesMap.set(entity.Name, entity.ID);
|
|
1949
|
-
|
|
1979
|
+
logStatus(` - ${entity.Name} (${entity.ID}) has update API and will be tracked`);
|
|
1950
1980
|
}
|
|
1951
1981
|
else {
|
|
1952
1982
|
const nonUpdateEntity = entities.find(e => e.Name === entityName);
|
|
1953
1983
|
if (nonUpdateEntity) {
|
|
1954
|
-
|
|
1984
|
+
logStatus(` - ${entityName} found but AllowUpdateAPI=${nonUpdateEntity.AllowUpdateAPI}, spUpdateGenerated=${nonUpdateEntity.spUpdateGenerated}`);
|
|
1955
1985
|
}
|
|
1956
1986
|
else {
|
|
1957
|
-
|
|
1987
|
+
logStatus(` - ${entityName} not found in entities list`);
|
|
1958
1988
|
}
|
|
1959
1989
|
}
|
|
1960
1990
|
}
|
|
@@ -1988,38 +2018,38 @@ GO${permissions}
|
|
|
1988
2018
|
// Get entities that were modified during metadata management
|
|
1989
2019
|
const modifiedEntitiesMap = await this.getModifiedEntitiesWithUpdateAPI(entities);
|
|
1990
2020
|
if (modifiedEntitiesMap.size > 0) {
|
|
1991
|
-
|
|
2021
|
+
logStatus(`Found ${modifiedEntitiesMap.size} entities with schema changes affecting update SPs`);
|
|
1992
2022
|
// Convert map values to set of IDs
|
|
1993
2023
|
const changedEntityIds = new Set(modifiedEntitiesMap.values());
|
|
1994
2024
|
// Find entities that need delete SP regeneration
|
|
1995
2025
|
const entitiesNeedingRegeneration = await this.getEntitiesRequiringCascadeDeleteRegeneration(pool, changedEntityIds);
|
|
1996
2026
|
if (entitiesNeedingRegeneration.size > 0) {
|
|
1997
|
-
|
|
2027
|
+
logStatus(`Identified ${entitiesNeedingRegeneration.size} entities requiring delete SP regeneration due to cascade dependencies`);
|
|
1998
2028
|
// Store the entity IDs that need regeneration (only if spDeleteGenerated=true)
|
|
1999
2029
|
for (const entityId of entitiesNeedingRegeneration) {
|
|
2000
2030
|
const entity = entities.find(e => e.ID === entityId);
|
|
2001
2031
|
if (entity && entity.spDeleteGenerated) {
|
|
2002
2032
|
this.entitiesNeedingDeleteSPRegeneration.add(entityId);
|
|
2003
|
-
|
|
2033
|
+
logStatus(` - Marked ${entity.Name} for delete SP regeneration (cascade dependency)`);
|
|
2004
2034
|
}
|
|
2005
2035
|
else if (entity && !entity.spDeleteGenerated) {
|
|
2006
|
-
|
|
2036
|
+
logStatus(` - Skipping ${entity.Name} - has cascade dependency but spDeleteGenerated=false (custom SP)`);
|
|
2007
2037
|
}
|
|
2008
2038
|
}
|
|
2009
2039
|
// Order entities by dependencies for proper regeneration
|
|
2010
2040
|
this.orderedEntitiesForDeleteSPRegeneration = this.orderEntitiesByDependencies(entities, this.entitiesNeedingDeleteSPRegeneration);
|
|
2011
2041
|
if (this.orderedEntitiesForDeleteSPRegeneration.length > 0) {
|
|
2012
|
-
|
|
2042
|
+
logStatus(`Ordered entities for delete SP regeneration:`);
|
|
2013
2043
|
this.orderedEntitiesForDeleteSPRegeneration.forEach((entityId, index) => {
|
|
2014
2044
|
const entity = entities.find(e => e.ID === entityId);
|
|
2015
|
-
|
|
2045
|
+
logStatus(` ${index + 1}. ${entity?.Name || entityId}`);
|
|
2016
2046
|
});
|
|
2017
2047
|
}
|
|
2018
2048
|
}
|
|
2019
2049
|
}
|
|
2020
2050
|
}
|
|
2021
2051
|
catch (error) {
|
|
2022
|
-
|
|
2052
|
+
logError(`Error in cascade delete dependency analysis: ${error}`);
|
|
2023
2053
|
// Continue with normal processing even if dependency analysis fails
|
|
2024
2054
|
}
|
|
2025
2055
|
}
|
|
@@ -2060,7 +2090,7 @@ GO${permissions}
|
|
|
2060
2090
|
if (visiting.has(entityId)) {
|
|
2061
2091
|
// Circular dependency detected - mark it but don't fail
|
|
2062
2092
|
const entity = entities.find(e => e.ID === entityId);
|
|
2063
|
-
|
|
2093
|
+
logStatus(`Warning: Circular cascade delete dependency detected involving ${entity?.Name || entityId}`);
|
|
2064
2094
|
circularDeps.add(entityId);
|
|
2065
2095
|
return false; // Signal circular dependency but continue processing
|
|
2066
2096
|
}
|
|
@@ -2086,17 +2116,16 @@ GO${permissions}
|
|
|
2086
2116
|
if (!success && circularDeps.has(entityId)) {
|
|
2087
2117
|
// Entity is part of circular dependency - add it anyway in arbitrary order
|
|
2088
2118
|
// The SQL will still be generated, just not in perfect dependency order
|
|
2089
|
-
|
|
2119
|
+
logStatus(` - Adding ${entities.find(e => e.ID === entityId)?.Name || entityId} despite circular dependency`);
|
|
2090
2120
|
visited.add(entityId);
|
|
2091
2121
|
ordered.push(entityId);
|
|
2092
2122
|
}
|
|
2093
2123
|
}
|
|
2094
2124
|
}
|
|
2095
2125
|
if (circularDeps.size > 0) {
|
|
2096
|
-
|
|
2126
|
+
logStatus(`Note: ${circularDeps.size} entities have circular cascade delete dependencies and will be regenerated in arbitrary order.`);
|
|
2097
2127
|
}
|
|
2098
2128
|
return ordered;
|
|
2099
2129
|
}
|
|
2100
2130
|
}
|
|
2101
|
-
exports.SQLCodeGenBase = SQLCodeGenBase;
|
|
2102
2131
|
//# sourceMappingURL=sql_codegen.js.map
|