@memberjunction/codegen-lib 2.48.0 → 2.49.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/dist/Angular/angular-codegen.d.ts +164 -6
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +177 -13
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/join-grid-related-entity-component.d.ts +52 -3
- package/dist/Angular/join-grid-related-entity-component.d.ts.map +1 -1
- package/dist/Angular/join-grid-related-entity-component.js +58 -3
- package/dist/Angular/join-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/related-entity-components.d.ts +99 -42
- package/dist/Angular/related-entity-components.d.ts.map +1 -1
- package/dist/Angular/related-entity-components.js +116 -26
- package/dist/Angular/related-entity-components.js.map +1 -1
- package/dist/Angular/timeline-related-entity-component.d.ts +46 -7
- package/dist/Angular/timeline-related-entity-component.d.ts.map +1 -1
- package/dist/Angular/timeline-related-entity-component.js +64 -7
- package/dist/Angular/timeline-related-entity-component.js.map +1 -1
- package/dist/Angular/user-view-grid-related-entity-component.d.ts +33 -1
- package/dist/Angular/user-view-grid-related-entity-component.d.ts.map +1 -1
- package/dist/Angular/user-view-grid-related-entity-component.js +33 -1
- package/dist/Angular/user-view-grid-related-entity-component.js.map +1 -1
- package/dist/Config/config.d.ts +369 -45
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +136 -2
- package/dist/Config/config.js.map +1 -1
- package/dist/Config/db-connection.d.ts +17 -3
- package/dist/Config/db-connection.d.ts.map +1 -1
- package/dist/Config/db-connection.js +31 -19
- package/dist/Config/db-connection.js.map +1 -1
- package/dist/Database/dbSchema.d.ts +44 -1
- package/dist/Database/dbSchema.d.ts.map +1 -1
- package/dist/Database/dbSchema.js +44 -1
- package/dist/Database/dbSchema.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +52 -46
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +221 -167
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/reorder-columns.d.ts +2 -2
- package/dist/Database/reorder-columns.d.ts.map +1 -1
- package/dist/Database/reorder-columns.js +23 -17
- package/dist/Database/reorder-columns.js.map +1 -1
- package/dist/Database/sql.d.ts +4 -4
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +2 -2
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +15 -15
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +184 -112
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.js +81 -81
- package/dist/Misc/advanced_generation.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts +5 -5
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +10 -8
- 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 +33 -28
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +2 -2
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +4 -3
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/Misc/status_logging.d.ts +37 -0
- package/dist/Misc/status_logging.d.ts.map +1 -1
- package/dist/Misc/status_logging.js +145 -3
- package/dist/Misc/status_logging.js.map +1 -1
- package/dist/Misc/system_integrity.d.ts +9 -9
- package/dist/Misc/system_integrity.d.ts.map +1 -1
- package/dist/Misc/system_integrity.js +23 -21
- package/dist/Misc/system_integrity.js.map +1 -1
- package/dist/index.d.ts +45 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -7
- package/dist/index.js.map +1 -1
- package/dist/runCodeGen.d.ts +84 -6
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +242 -82
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +14 -14
|
@@ -49,33 +49,36 @@ exports.SPType = {
|
|
|
49
49
|
* of that abstract base class and other databases will be sub-classes of the abstract base class as well.
|
|
50
50
|
*/
|
|
51
51
|
class SQLCodeGenBase {
|
|
52
|
-
|
|
53
|
-
this._sqlUtilityObject = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(sql_1.SQLUtilityBase);
|
|
54
|
-
this.__specialUUIDValue = '00000000-0000-0000-0000-000000000000';
|
|
55
|
-
}
|
|
52
|
+
_sqlUtilityObject = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(sql_1.SQLUtilityBase);
|
|
56
53
|
get SQLUtilityObject() {
|
|
57
54
|
return this._sqlUtilityObject;
|
|
58
55
|
}
|
|
59
|
-
async manageSQLScriptsAndExecution(
|
|
56
|
+
async manageSQLScriptsAndExecution(pool, entities, directory, currentUser) {
|
|
60
57
|
try {
|
|
61
58
|
// STEP 1 - execute any custom SQL scripts for object creation that need to happen first - for example, if
|
|
62
59
|
// we have custom base views, need to have them defined before we do
|
|
63
60
|
// the rest as the generated stuff might use custom base views in compiled
|
|
64
61
|
// objects like spCreate for a given entity might reference the vw for that entity
|
|
62
|
+
(0, status_logging_1.startSpinner)('Running custom SQL scripts...');
|
|
65
63
|
const startTime = new Date();
|
|
66
|
-
if (!await this.runCustomSQLScripts(
|
|
64
|
+
if (!await this.runCustomSQLScripts(pool, 'before-sql')) {
|
|
65
|
+
(0, status_logging_1.failSpinner)('Failed to run custom SQL scripts');
|
|
67
66
|
return false;
|
|
68
|
-
|
|
67
|
+
}
|
|
68
|
+
(0, status_logging_1.succeedSpinner)(`Custom SQL scripts completed (${(new Date().getTime() - startTime.getTime()) / 1000}s)`);
|
|
69
69
|
// ALWAYS use the first filter where we only include entities that have IncludeInAPI = 1
|
|
70
70
|
const baselineEntities = entities.filter(e => e.IncludeInAPI);
|
|
71
71
|
const includedEntities = baselineEntities.filter(e => config_1.configInfo.excludeSchemas.find(s => s.toLowerCase() === e.SchemaName.toLowerCase()) === undefined); //only include entities that are NOT in the excludeSchemas list
|
|
72
72
|
const excludedEntities = baselineEntities.filter(e => config_1.configInfo.excludeSchemas.find(s => s.toLowerCase() === e.SchemaName.toLowerCase()) !== undefined); //only include entities that ARE in the excludeSchemas list in this array
|
|
73
73
|
// STEP 2(a) - clean out all *.generated.sql and *.permissions.generated.sql files from the directory
|
|
74
|
+
(0, status_logging_1.startSpinner)('Cleaning generated files...');
|
|
74
75
|
this.deleteGeneratedEntityFiles(directory, baselineEntities);
|
|
76
|
+
(0, status_logging_1.succeedSpinner)('Cleaned generated files');
|
|
75
77
|
// STEP 2(b) - generate all the SQL files and execute them
|
|
78
|
+
(0, status_logging_1.startSpinner)(`Generating SQL for ${includedEntities.length} entities...`);
|
|
76
79
|
const step2StartTime = new Date();
|
|
77
80
|
const genResult = await this.generateAndExecuteEntitySQLToSeparateFiles({
|
|
78
|
-
|
|
81
|
+
pool,
|
|
79
82
|
entities: includedEntities,
|
|
80
83
|
directory,
|
|
81
84
|
onlyPermissions: false,
|
|
@@ -85,12 +88,13 @@ class SQLCodeGenBase {
|
|
|
85
88
|
enableSQLLoggingForNewOrModifiedEntities: true
|
|
86
89
|
}); // enable sql logging for NEW entities....
|
|
87
90
|
if (!genResult.Success) {
|
|
88
|
-
(0, status_logging_1.
|
|
91
|
+
(0, status_logging_1.failSpinner)('Failed to generate entity SQL files');
|
|
89
92
|
return false;
|
|
90
93
|
}
|
|
91
94
|
// STEP 2(c) - for the excludedEntities, while we don't want to generate SQL, we do want to generate the permissions files for them
|
|
95
|
+
(0, status_logging_1.updateSpinner)(`Generating permissions for ${excludedEntities.length} excluded entities...`);
|
|
92
96
|
const genResult2 = await this.generateAndExecuteEntitySQLToSeparateFiles({
|
|
93
|
-
|
|
97
|
+
pool,
|
|
94
98
|
entities: excludedEntities,
|
|
95
99
|
directory,
|
|
96
100
|
onlyPermissions: true,
|
|
@@ -100,41 +104,50 @@ class SQLCodeGenBase {
|
|
|
100
104
|
enableSQLLoggingForNewOrModifiedEntities: false /*don't log this stuff, it is just permissions for excluded entities*/
|
|
101
105
|
});
|
|
102
106
|
if (!genResult2.Success) {
|
|
103
|
-
(0, status_logging_1.
|
|
107
|
+
(0, status_logging_1.failSpinner)('Failed to generate permissions for excluded entities');
|
|
104
108
|
return false;
|
|
105
109
|
}
|
|
106
|
-
(0, status_logging_1.
|
|
110
|
+
(0, status_logging_1.succeedSpinner)(`Entity generation completed (${(new Date().getTime() - step2StartTime.getTime()) / 1000}s)`);
|
|
107
111
|
// 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
|
|
112
|
+
(0, status_logging_1.startSpinner)('Creating combined SQL files...');
|
|
108
113
|
const allEntityFiles = this.createCombinedEntitySQLFiles(directory, baselineEntities);
|
|
114
|
+
(0, status_logging_1.succeedSpinner)(`Created combined SQL files for ${allEntityFiles.length} schemas`);
|
|
109
115
|
// STEP 2(e) ---- FINALLY, we now execute all the combined files by schema;
|
|
116
|
+
(0, status_logging_1.startSpinner)('Executing combined entity SQL files...');
|
|
110
117
|
const step2eStartTime = new Date();
|
|
111
|
-
if (!await this.SQLUtilityObject.executeSQLFiles(allEntityFiles,
|
|
112
|
-
(0, status_logging_1.
|
|
118
|
+
if (!await this.SQLUtilityObject.executeSQLFiles(allEntityFiles, config_1.configInfo?.verboseOutput ?? false)) {
|
|
119
|
+
(0, status_logging_1.failSpinner)('Failed to execute combined entity SQL files');
|
|
113
120
|
return false;
|
|
114
121
|
}
|
|
115
122
|
const step2eEndTime = new Date();
|
|
116
|
-
(0, status_logging_1.
|
|
123
|
+
(0, status_logging_1.succeedSpinner)(`SQL execution completed (${(step2eEndTime.getTime() - step2eStartTime.getTime()) / 1000}s)`);
|
|
117
124
|
const manageMD = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(manage_metadata_1.ManageMetadataBase);
|
|
118
125
|
// 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
|
|
119
126
|
// we CAN skip the entity field values part because that wouldn't change from the first time we ran it
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
(0, status_logging_1.startSpinner)('Managing entity fields metadata...');
|
|
128
|
+
if (!await manageMD.manageEntityFields(pool, config_1.configInfo.excludeSchemas, true, true, currentUser)) {
|
|
129
|
+
(0, status_logging_1.failSpinner)('Failed to manage entity fields');
|
|
122
130
|
return false;
|
|
123
131
|
}
|
|
132
|
+
(0, status_logging_1.succeedSpinner)('Entity fields metadata updated');
|
|
124
133
|
// 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
|
|
125
134
|
// STEP 4- Apply permissions, executing all .permissions files
|
|
135
|
+
(0, status_logging_1.startSpinner)('Applying permissions...');
|
|
126
136
|
const step4StartTime = new Date();
|
|
127
|
-
if (!await this.applyPermissions(
|
|
128
|
-
(0, status_logging_1.
|
|
137
|
+
if (!await this.applyPermissions(pool, directory, baselineEntities)) {
|
|
138
|
+
(0, status_logging_1.failSpinner)('Failed to apply permissions');
|
|
129
139
|
return false;
|
|
130
140
|
}
|
|
131
|
-
(0, status_logging_1.
|
|
141
|
+
(0, status_logging_1.succeedSpinner)(`Permissions applied (${(new Date().getTime() - step4StartTime.getTime()) / 1000}s)`);
|
|
132
142
|
// STEP 5 - execute any custom SQL scripts that should run afterwards
|
|
143
|
+
(0, status_logging_1.startSpinner)('Running post-generation SQL scripts...');
|
|
133
144
|
const step5StartTime = new Date();
|
|
134
|
-
if (!await this.runCustomSQLScripts(
|
|
145
|
+
if (!await this.runCustomSQLScripts(pool, 'after-sql')) {
|
|
146
|
+
(0, status_logging_1.failSpinner)('Failed to run post-generation SQL scripts');
|
|
135
147
|
return false;
|
|
136
|
-
|
|
137
|
-
(0, status_logging_1.
|
|
148
|
+
}
|
|
149
|
+
(0, status_logging_1.succeedSpinner)(`Post-generation scripts completed (${(new Date().getTime() - step5StartTime.getTime()) / 1000}s)`);
|
|
150
|
+
(0, status_logging_1.succeedSpinner)(`CodeGen completed successfully (${((new Date().getTime() - startTime.getTime()) / 1000)}s total)`);
|
|
138
151
|
// now - we need to tell our metadata object to refresh itself
|
|
139
152
|
const md = new core_1.Metadata();
|
|
140
153
|
await md.Refresh();
|
|
@@ -145,7 +158,7 @@ class SQLCodeGenBase {
|
|
|
145
158
|
return false;
|
|
146
159
|
}
|
|
147
160
|
}
|
|
148
|
-
async runCustomSQLScripts(
|
|
161
|
+
async runCustomSQLScripts(pool, when) {
|
|
149
162
|
try {
|
|
150
163
|
const scripts = (0, config_1.customSqlScripts)(when);
|
|
151
164
|
let bSuccess = true;
|
|
@@ -165,7 +178,7 @@ class SQLCodeGenBase {
|
|
|
165
178
|
return false;
|
|
166
179
|
}
|
|
167
180
|
}
|
|
168
|
-
async applyPermissions(
|
|
181
|
+
async applyPermissions(pool, directory, entities, batchSize = 5) {
|
|
169
182
|
try {
|
|
170
183
|
let bSuccess = true;
|
|
171
184
|
for (let i = 0; i < entities.length; i += batchSize) {
|
|
@@ -180,7 +193,7 @@ class SQLCodeGenBase {
|
|
|
180
193
|
const fileBuffer = fs.readFileSync(fullPath);
|
|
181
194
|
const fileContents = fileBuffer.toString();
|
|
182
195
|
try {
|
|
183
|
-
await
|
|
196
|
+
await pool.request().query(fileContents);
|
|
184
197
|
}
|
|
185
198
|
catch (e) {
|
|
186
199
|
(0, status_logging_1.logError)(`Error executing permissions file ${fullPath} for entity ${e.Name}: ${e}`);
|
|
@@ -231,7 +244,7 @@ class SQLCodeGenBase {
|
|
|
231
244
|
return { Success: false, Files: [] };
|
|
232
245
|
}
|
|
233
246
|
return this.generateAndExecuteSingleEntitySQLToSeparateFiles({
|
|
234
|
-
|
|
247
|
+
pool: options.pool,
|
|
235
248
|
entity: e,
|
|
236
249
|
directory: options.directory,
|
|
237
250
|
onlyPermissions: options.onlyPermissions,
|
|
@@ -304,7 +317,7 @@ class SQLCodeGenBase {
|
|
|
304
317
|
const { sql, permissionsSQL, files } = await this.generateSingleEntitySQLToSeparateFiles(options); // this creates the files and returns a single string with all the SQL we can then execute
|
|
305
318
|
if (!options.skipExecution) {
|
|
306
319
|
return {
|
|
307
|
-
Success: await this.SQLUtilityObject.executeSQLScript(options.
|
|
320
|
+
Success: await this.SQLUtilityObject.executeSQLScript(options.pool, sql + "\n\nGO\n\n" + permissionsSQL, true), // combine the SQL and permissions and execute it,
|
|
308
321
|
Files: files
|
|
309
322
|
};
|
|
310
323
|
}
|
|
@@ -317,9 +330,26 @@ class SQLCodeGenBase {
|
|
|
317
330
|
}
|
|
318
331
|
}
|
|
319
332
|
logSQLForNewOrModifiedEntity(entity, sql, description, logSql = false) {
|
|
320
|
-
if
|
|
321
|
-
|
|
322
|
-
|
|
333
|
+
// Check if we should log this SQL
|
|
334
|
+
let shouldLog = false;
|
|
335
|
+
if (logSql) {
|
|
336
|
+
// Check if entity is in new or modified lists
|
|
337
|
+
const isNewOrModified = !!manage_metadata_1.ManageMetadataBase.newEntityList.find(e => e === entity.Name) ||
|
|
338
|
+
!!manage_metadata_1.ManageMetadataBase.modifiedEntityList.find(e => e === entity.Name);
|
|
339
|
+
// Check if force regeneration is enabled for relevant SQL types
|
|
340
|
+
const isForceRegeneration = config_1.configInfo.forceRegeneration?.enabled && ((description.toLowerCase().includes('base view') && config_1.configInfo.forceRegeneration.baseViews) ||
|
|
341
|
+
(description.toLowerCase().includes('spcreate') && config_1.configInfo.forceRegeneration.spCreate) ||
|
|
342
|
+
(description.toLowerCase().includes('spupdate') && config_1.configInfo.forceRegeneration.spUpdate) ||
|
|
343
|
+
(description.toLowerCase().includes('spdelete') && config_1.configInfo.forceRegeneration.spDelete) ||
|
|
344
|
+
(description.toLowerCase().includes('index') && config_1.configInfo.forceRegeneration.indexes) ||
|
|
345
|
+
(description.toLowerCase().includes('full text search') && config_1.configInfo.forceRegeneration.fullTextSearch) ||
|
|
346
|
+
(config_1.configInfo.forceRegeneration.allStoredProcedures &&
|
|
347
|
+
(description.toLowerCase().includes('spcreate') ||
|
|
348
|
+
description.toLowerCase().includes('spupdate') ||
|
|
349
|
+
description.toLowerCase().includes('spdelete'))));
|
|
350
|
+
shouldLog = isNewOrModified || isForceRegeneration;
|
|
351
|
+
}
|
|
352
|
+
if (shouldLog) {
|
|
323
353
|
sql_logging_1.SQLLogging.appendToSQLLogFile(sql, description);
|
|
324
354
|
}
|
|
325
355
|
(0, util_1.logIf)(config_1.configInfo.verboseOutput, `SQL Generated for ${entity.Name}: ${description}`);
|
|
@@ -338,7 +368,8 @@ class SQLCodeGenBase {
|
|
|
338
368
|
let permissionsSQL = '';
|
|
339
369
|
// Indexes for Fkeys for the table
|
|
340
370
|
if (!options.onlyPermissions) {
|
|
341
|
-
const
|
|
371
|
+
const shouldGenerateIndexes = (0, config_1.autoIndexForeignKeys)() || (config_1.configInfo.forceRegeneration?.enabled && config_1.configInfo.forceRegeneration?.indexes);
|
|
372
|
+
const indexSQL = shouldGenerateIndexes ? this.generateIndexesForForeignKeys(options.pool, options.entity) : ''; // generate indexes if auto-indexing is on OR force regeneration is enabled
|
|
342
373
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, 'Index for Foreign Keys') + indexSQL;
|
|
343
374
|
if (options.writeFiles) {
|
|
344
375
|
const filePath = path_1.default.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('index', options.entity.SchemaName, options.entity.BaseTable, false, true));
|
|
@@ -349,9 +380,11 @@ class SQLCodeGenBase {
|
|
|
349
380
|
sRet += s + '\nGO\n';
|
|
350
381
|
}
|
|
351
382
|
// BASE VIEW
|
|
352
|
-
if (!options.onlyPermissions &&
|
|
383
|
+
if (!options.onlyPermissions &&
|
|
384
|
+
(options.entity.BaseViewGenerated || (config_1.configInfo.forceRegeneration?.enabled && config_1.configInfo.forceRegeneration?.baseViews)) &&
|
|
385
|
+
!options.entity.VirtualEntity) {
|
|
353
386
|
// generate the base view
|
|
354
|
-
const s = this.generateSingleEntitySQLFileHeader(options.entity, options.entity.BaseView) + await this.generateBaseView(options.
|
|
387
|
+
const s = this.generateSingleEntitySQLFileHeader(options.entity, options.entity.BaseView) + await this.generateBaseView(options.pool, options.entity);
|
|
355
388
|
const filePath = path_1.default.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('view', options.entity.SchemaName, options.entity.BaseView, false, true));
|
|
356
389
|
if (options.writeFiles) {
|
|
357
390
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `Base View SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
@@ -377,7 +410,9 @@ class SQLCodeGenBase {
|
|
|
377
410
|
// CREATE SP
|
|
378
411
|
if (options.entity.AllowCreateAPI && !options.entity.VirtualEntity) {
|
|
379
412
|
const spName = this.getSPName(options.entity, exports.SPType.Create);
|
|
380
|
-
if (!options.onlyPermissions &&
|
|
413
|
+
if (!options.onlyPermissions &&
|
|
414
|
+
(options.entity.spCreateGenerated ||
|
|
415
|
+
(config_1.configInfo.forceRegeneration?.enabled && (config_1.configInfo.forceRegeneration?.spCreate || config_1.configInfo.forceRegeneration?.allStoredProcedures)))) {
|
|
381
416
|
// generate the create SP
|
|
382
417
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPCreate(options.entity);
|
|
383
418
|
if (options.writeFiles) {
|
|
@@ -405,7 +440,9 @@ class SQLCodeGenBase {
|
|
|
405
440
|
// UPDATE SP
|
|
406
441
|
if (options.entity.AllowUpdateAPI && !options.entity.VirtualEntity) {
|
|
407
442
|
const spName = this.getSPName(options.entity, exports.SPType.Update);
|
|
408
|
-
if (!options.onlyPermissions &&
|
|
443
|
+
if (!options.onlyPermissions &&
|
|
444
|
+
(options.entity.spUpdateGenerated ||
|
|
445
|
+
(config_1.configInfo.forceRegeneration?.enabled && (config_1.configInfo.forceRegeneration?.spUpdate || config_1.configInfo.forceRegeneration?.allStoredProcedures)))) {
|
|
409
446
|
// generate the update SP
|
|
410
447
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPUpdate(options.entity);
|
|
411
448
|
if (options.writeFiles) {
|
|
@@ -433,7 +470,9 @@ class SQLCodeGenBase {
|
|
|
433
470
|
// DELETE SP
|
|
434
471
|
if (options.entity.AllowDeleteAPI && !options.entity.VirtualEntity) {
|
|
435
472
|
const spName = this.getSPName(options.entity, exports.SPType.Delete);
|
|
436
|
-
if (!options.onlyPermissions &&
|
|
473
|
+
if (!options.onlyPermissions &&
|
|
474
|
+
(options.entity.spDeleteGenerated ||
|
|
475
|
+
(config_1.configInfo.forceRegeneration?.enabled && (config_1.configInfo.forceRegeneration?.spDelete || config_1.configInfo.forceRegeneration?.allStoredProcedures)))) {
|
|
437
476
|
// generate the delete SP
|
|
438
477
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, spName) + this.generateSPDelete(options.entity);
|
|
439
478
|
if (options.writeFiles) {
|
|
@@ -459,9 +498,9 @@ class SQLCodeGenBase {
|
|
|
459
498
|
sRet += s + '\nGO\n';
|
|
460
499
|
}
|
|
461
500
|
// check to see if the options.entity supports full text search or not
|
|
462
|
-
if (options.entity.FullTextSearchEnabled) {
|
|
501
|
+
if (options.entity.FullTextSearchEnabled || (config_1.configInfo.forceRegeneration?.enabled && config_1.configInfo.forceRegeneration?.fullTextSearch)) {
|
|
463
502
|
// always generate the code so we can get the function name from the below function call
|
|
464
|
-
const ft = await this.generateEntityFullTextSearchSQL(options.
|
|
503
|
+
const ft = await this.generateEntityFullTextSearchSQL(options.pool, options.entity);
|
|
465
504
|
if (!options.onlyPermissions) {
|
|
466
505
|
// only write the actual sql out if we're not only generating permissions
|
|
467
506
|
const filePath = path_1.default.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('full_text_search_function', options.entity.SchemaName, options.entity.BaseTable, false, true));
|
|
@@ -528,11 +567,11 @@ class SQLCodeGenBase {
|
|
|
528
567
|
* @param entity
|
|
529
568
|
* @returns
|
|
530
569
|
*/
|
|
531
|
-
async generateEntitySQL(
|
|
570
|
+
async generateEntitySQL(pool, entity) {
|
|
532
571
|
let sOutput = '';
|
|
533
572
|
if (entity.BaseViewGenerated && !entity.VirtualEntity)
|
|
534
573
|
// generated the base view (will include permissions)
|
|
535
|
-
sOutput += await this.generateBaseView(
|
|
574
|
+
sOutput += await this.generateBaseView(pool, entity) + '\n\n';
|
|
536
575
|
else
|
|
537
576
|
// still generate the permissions for the view even if a custom view
|
|
538
577
|
sOutput += this.generateViewPermissions(entity) + '\n\n';
|
|
@@ -562,11 +601,11 @@ class SQLCodeGenBase {
|
|
|
562
601
|
}
|
|
563
602
|
// check to see if the entity supports full text search or not
|
|
564
603
|
if (entity.FullTextSearchEnabled) {
|
|
565
|
-
sOutput += await this.generateEntityFullTextSearchSQL(
|
|
604
|
+
sOutput += await this.generateEntityFullTextSearchSQL(pool, entity) + '\n\n';
|
|
566
605
|
}
|
|
567
606
|
return sOutput;
|
|
568
607
|
}
|
|
569
|
-
async generateEntityFullTextSearchSQL(
|
|
608
|
+
async generateEntityFullTextSearchSQL(pool, entity) {
|
|
570
609
|
let sql = '';
|
|
571
610
|
const catalogName = entity.FullTextCatalog && entity.FullTextCatalog.length > 0 ? entity.FullTextCatalog : config_1.dbDatabase + '_FullTextCatalog';
|
|
572
611
|
if (entity.FullTextCatalogGenerated) {
|
|
@@ -586,7 +625,7 @@ class SQLCodeGenBase {
|
|
|
586
625
|
if (fullTextFields.length === 0)
|
|
587
626
|
throw new Error(`FullTextIndexGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
|
|
588
627
|
// drop and recreate the full text index
|
|
589
|
-
const entity_pk_name = await this.getEntityPrimaryKeyIndexName(
|
|
628
|
+
const entity_pk_name = await this.getEntityPrimaryKeyIndexName(pool, entity);
|
|
590
629
|
sql += ` -- DROP AND RECREATE THE FULL TEXT INDEX
|
|
591
630
|
IF EXISTS (
|
|
592
631
|
SELECT *
|
|
@@ -653,7 +692,7 @@ class SQLCodeGenBase {
|
|
|
653
692
|
}
|
|
654
693
|
return { sql, functionName };
|
|
655
694
|
}
|
|
656
|
-
async getEntityPrimaryKeyIndexName(
|
|
695
|
+
async getEntityPrimaryKeyIndexName(pool, entity) {
|
|
657
696
|
const sSQL = ` SELECT
|
|
658
697
|
i.name AS IndexName
|
|
659
698
|
FROM
|
|
@@ -668,7 +707,8 @@ class SQLCodeGenBase {
|
|
|
668
707
|
o.schema_id = SCHEMA_ID('${entity.SchemaName}') AND
|
|
669
708
|
kc.type = 'PK';
|
|
670
709
|
`;
|
|
671
|
-
const
|
|
710
|
+
const resultResult = await pool.request().query(sSQL);
|
|
711
|
+
const result = resultResult.recordset;
|
|
672
712
|
if (result && result.length > 0)
|
|
673
713
|
return result[0].IndexName;
|
|
674
714
|
else
|
|
@@ -700,10 +740,10 @@ class SQLCodeGenBase {
|
|
|
700
740
|
}
|
|
701
741
|
/**
|
|
702
742
|
* Generates the SQL for creating indexes for the foreign keys in the entity
|
|
703
|
-
* @param
|
|
743
|
+
* @param pool
|
|
704
744
|
* @param entity
|
|
705
745
|
*/
|
|
706
|
-
generateIndexesForForeignKeys(
|
|
746
|
+
generateIndexesForForeignKeys(pool, entity) {
|
|
707
747
|
// iterate through all of the fields in the entity that are foreign keys and generate an index for each one
|
|
708
748
|
let sOutput = '';
|
|
709
749
|
for (const f of entity.Fields) {
|
|
@@ -726,10 +766,10 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
|
|
|
726
766
|
}
|
|
727
767
|
return sOutput;
|
|
728
768
|
}
|
|
729
|
-
async generateBaseView(
|
|
769
|
+
async generateBaseView(pool, entity) {
|
|
730
770
|
const viewName = entity.BaseView ? entity.BaseView : `vw${entity.CodeName}`;
|
|
731
771
|
const classNameFirstChar = entity.ClassName.charAt(0).toLowerCase();
|
|
732
|
-
const relatedFieldsString = await this.generateBaseViewRelatedFieldsString(
|
|
772
|
+
const relatedFieldsString = await this.generateBaseViewRelatedFieldsString(pool, entity.Fields);
|
|
733
773
|
const relatedFieldsJoinString = this.generateBaseViewJoins(entity, entity.Fields);
|
|
734
774
|
const permissions = this.generateViewPermissions(entity);
|
|
735
775
|
const whereClause = entity.DeleteType === 'Soft' ? `WHERE
|
|
@@ -776,7 +816,7 @@ ${whereClause}GO${permissions}
|
|
|
776
816
|
}
|
|
777
817
|
return sOutput;
|
|
778
818
|
}
|
|
779
|
-
async generateBaseViewRelatedFieldsString(
|
|
819
|
+
async generateBaseViewRelatedFieldsString(pool, entityFields) {
|
|
780
820
|
let sOutput = '';
|
|
781
821
|
let fieldCount = 0;
|
|
782
822
|
const manageMD = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(manage_metadata_1.ManageMetadataBase);
|
|
@@ -808,7 +848,7 @@ ${whereClause}GO${permissions}
|
|
|
808
848
|
// and it also reflects what the DB will hold
|
|
809
849
|
ef.RelatedEntityNameFieldMap = ef._RelatedEntityNameFieldMap;
|
|
810
850
|
// then update the database itself
|
|
811
|
-
await manageMD.updateEntityFieldRelatedEntityNameFieldMap(
|
|
851
|
+
await manageMD.updateEntityFieldRelatedEntityNameFieldMap(pool, ef.ID, ef.RelatedEntityNameFieldMap);
|
|
812
852
|
}
|
|
813
853
|
fieldCount++;
|
|
814
854
|
}
|
|
@@ -863,8 +903,8 @@ ${whereClause}GO${permissions}
|
|
|
863
903
|
const spName = entity.spCreate ? entity.spCreate : `spCreate${entity.ClassName}`;
|
|
864
904
|
const firstKey = entity.FirstPrimaryKey;
|
|
865
905
|
//double exclamations used on the firstKey.DefaultValue property otherwise the type of this variable is 'number | ""';
|
|
866
|
-
const primaryKeyAutomatic = firstKey.AutoIncrement
|
|
867
|
-
const efString = this.createEntityFieldsParamString(entity.Fields,
|
|
906
|
+
const primaryKeyAutomatic = firstKey.AutoIncrement; // Only exclude auto-increment fields, allow manual override for all other PKs including UUIDs with defaults
|
|
907
|
+
const efString = this.createEntityFieldsParamString(entity.Fields, false); // Always pass false for isUpdate since this is generateSPCreate
|
|
868
908
|
const permissions = this.generateSPPermissions(entity, spName, exports.SPType.Create);
|
|
869
909
|
let preInsertCode = '';
|
|
870
910
|
let outputCode = '';
|
|
@@ -874,29 +914,58 @@ ${whereClause}GO${permissions}
|
|
|
874
914
|
if (entity.FirstPrimaryKey.AutoIncrement) {
|
|
875
915
|
selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE [${entity.FirstPrimaryKey.Name}] = SCOPE_IDENTITY()`;
|
|
876
916
|
}
|
|
877
|
-
else if (entity.FirstPrimaryKey.Type.toLowerCase().trim() === 'uniqueidentifier') {
|
|
878
|
-
// our primary key is a uniqueidentifier.
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
917
|
+
else if (entity.FirstPrimaryKey.Type.toLowerCase().trim() === 'uniqueidentifier' && entity.PrimaryKeys.length === 1) {
|
|
918
|
+
// our primary key is a uniqueidentifier. Now we support optional override:
|
|
919
|
+
// - If PKEY is provided (not NULL), use it
|
|
920
|
+
// - If PKEY is NULL and there's a default value, let the database use it
|
|
921
|
+
// - If PKEY is NULL and no default value, generate NEWID()
|
|
882
922
|
const hasDefaultValue = entity.FirstPrimaryKey.DefaultValue && entity.FirstPrimaryKey.DefaultValue.trim().length > 0;
|
|
883
|
-
// if we have a default value, then we do NOT want to insert a new value, let the database use the default
|
|
884
923
|
if (hasDefaultValue) {
|
|
885
|
-
//
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
924
|
+
// Has default value - use conditional logic to either include the field or let DB use default
|
|
925
|
+
preInsertCode = `DECLARE @InsertedRow TABLE ([${entity.FirstPrimaryKey.Name}] UNIQUEIDENTIFIER)
|
|
926
|
+
|
|
927
|
+
IF @${entity.FirstPrimaryKey.Name} IS NOT NULL
|
|
928
|
+
BEGIN
|
|
929
|
+
-- User provided a value, use it
|
|
930
|
+
INSERT INTO [${entity.SchemaName}].[${entity.BaseTable}]
|
|
931
|
+
(
|
|
932
|
+
[${entity.FirstPrimaryKey.Name}],
|
|
933
|
+
${this.createEntityFieldsInsertString(entity, entity.Fields, '', true)}
|
|
934
|
+
)
|
|
935
|
+
OUTPUT INSERTED.[${entity.FirstPrimaryKey.Name}] INTO @InsertedRow
|
|
936
|
+
VALUES
|
|
937
|
+
(
|
|
938
|
+
@${entity.FirstPrimaryKey.Name},
|
|
939
|
+
${this.createEntityFieldsInsertString(entity, entity.Fields, '@', true)}
|
|
940
|
+
)
|
|
941
|
+
END
|
|
942
|
+
ELSE
|
|
943
|
+
BEGIN
|
|
944
|
+
-- No value provided, let database use its default (e.g., NEWSEQUENTIALID())
|
|
945
|
+
INSERT INTO [${entity.SchemaName}].[${entity.BaseTable}]
|
|
946
|
+
(
|
|
947
|
+
${this.createEntityFieldsInsertString(entity, entity.Fields, '', true)}
|
|
948
|
+
)
|
|
949
|
+
OUTPUT INSERTED.[${entity.FirstPrimaryKey.Name}] INTO @InsertedRow
|
|
950
|
+
VALUES
|
|
951
|
+
(
|
|
952
|
+
${this.createEntityFieldsInsertString(entity, entity.Fields, '@', true)}
|
|
953
|
+
)
|
|
954
|
+
END`;
|
|
955
|
+
// Clear these as we're handling the INSERT in preInsertCode
|
|
956
|
+
additionalFieldList = '';
|
|
957
|
+
additionalValueList = '';
|
|
958
|
+
outputCode = '';
|
|
891
959
|
selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE [${entity.FirstPrimaryKey.Name}] = (SELECT [${entity.FirstPrimaryKey.Name}] FROM @InsertedRow)`;
|
|
892
960
|
}
|
|
893
961
|
else {
|
|
894
|
-
//
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
962
|
+
// No default value - we calculate the ID upfront, so no need for OUTPUT clause
|
|
963
|
+
preInsertCode = `DECLARE @ActualID UNIQUEIDENTIFIER = ISNULL(@${entity.FirstPrimaryKey.Name}, NEWID())`;
|
|
964
|
+
additionalFieldList = ',\n [' + entity.FirstPrimaryKey.Name + ']';
|
|
965
|
+
additionalValueList = ',\n @ActualID';
|
|
966
|
+
outputCode = ''; // No OUTPUT clause needed
|
|
967
|
+
// We already know the ID, so just select using it directly
|
|
968
|
+
selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE [${entity.FirstPrimaryKey.Name}] = @ActualID`;
|
|
900
969
|
}
|
|
901
970
|
}
|
|
902
971
|
else {
|
|
@@ -921,7 +990,7 @@ CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
|
|
|
921
990
|
AS
|
|
922
991
|
BEGIN
|
|
923
992
|
SET NOCOUNT ON;
|
|
924
|
-
${preInsertCode}
|
|
993
|
+
${preInsertCode}${preInsertCode.includes('INSERT INTO') ? '' : `
|
|
925
994
|
INSERT INTO
|
|
926
995
|
[${entity.SchemaName}].[${entity.BaseTable}]
|
|
927
996
|
(
|
|
@@ -930,7 +999,7 @@ BEGIN
|
|
|
930
999
|
${outputCode}VALUES
|
|
931
1000
|
(
|
|
932
1001
|
${this.createEntityFieldsInsertString(entity, entity.Fields, '@')}${additionalValueList}
|
|
933
|
-
)
|
|
1002
|
+
)`}
|
|
934
1003
|
-- return the new record from the base view, which might have some calculated fields
|
|
935
1004
|
${selectInsertedRecord}
|
|
936
1005
|
END
|
|
@@ -1007,12 +1076,13 @@ GO
|
|
|
1007
1076
|
${updatedAtTrigger}
|
|
1008
1077
|
`;
|
|
1009
1078
|
}
|
|
1079
|
+
__specialUUIDValue = '00000000-0000-0000-0000-000000000000';
|
|
1010
1080
|
createEntityFieldsParamString(entityFields, isUpdate) {
|
|
1011
1081
|
let sOutput = '', isFirst = true;
|
|
1012
1082
|
for (let i = 0; i < entityFields.length; ++i) {
|
|
1013
1083
|
const ef = entityFields[i];
|
|
1014
|
-
const autoGeneratedPrimaryKey = ef.AutoIncrement
|
|
1015
|
-
if ((ef.AllowUpdateAPI || (ef.IsPrimaryKey && isUpdate)) &&
|
|
1084
|
+
const autoGeneratedPrimaryKey = ef.AutoIncrement; // Only exclude auto-increment fields from params
|
|
1085
|
+
if ((ef.AllowUpdateAPI || (ef.IsPrimaryKey && isUpdate) || (ef.IsPrimaryKey && !autoGeneratedPrimaryKey && !isUpdate)) &&
|
|
1016
1086
|
!ef.IsVirtual &&
|
|
1017
1087
|
(!ef.IsPrimaryKey || !autoGeneratedPrimaryKey || isUpdate) &&
|
|
1018
1088
|
!ef.IsSpecialDateField) {
|
|
@@ -1020,58 +1090,60 @@ ${updatedAtTrigger}
|
|
|
1020
1090
|
sOutput += ',\n ';
|
|
1021
1091
|
else
|
|
1022
1092
|
isFirst = false;
|
|
1023
|
-
//
|
|
1024
|
-
// to the sproc param with '00000000-0000-0000-0000-000000000000' so that the sproc can be called without passing in a value
|
|
1025
|
-
// within the sproc body for spCreate, we will look for this special value and substitute it with the actual default value for the column (typically newid() or newsequentialid())
|
|
1093
|
+
// Check if we need a default value
|
|
1026
1094
|
let defaultParamValue = '';
|
|
1027
|
-
if (!isUpdate && ef.
|
|
1028
|
-
|
|
1095
|
+
if (!isUpdate && ef.IsPrimaryKey && !ef.AutoIncrement) {
|
|
1096
|
+
// For primary keys (non-auto-increment), make them optional with NULL default
|
|
1097
|
+
// This allows callers to omit the PK and let the DB/sproc handle it
|
|
1098
|
+
defaultParamValue = ' = NULL';
|
|
1029
1099
|
}
|
|
1030
1100
|
sOutput += `@${ef.CodeName} ${ef.SQLFullType}${defaultParamValue}`;
|
|
1031
1101
|
}
|
|
1032
1102
|
}
|
|
1033
1103
|
return sOutput;
|
|
1034
1104
|
}
|
|
1035
|
-
createEntityFieldsInsertString(entity, entityFields, prefix) {
|
|
1036
|
-
const autoGeneratedPrimaryKey = entity.FirstPrimaryKey.AutoIncrement
|
|
1105
|
+
createEntityFieldsInsertString(entity, entityFields, prefix, excludePrimaryKey = false) {
|
|
1106
|
+
const autoGeneratedPrimaryKey = entity.FirstPrimaryKey.AutoIncrement; // Only exclude auto-increment PKs from insert
|
|
1037
1107
|
let sOutput = '', isFirst = true;
|
|
1038
1108
|
const filteredFields = entityFields.filter(f => f.AllowUpdateAPI);
|
|
1039
1109
|
for (let i = 0; i < entityFields.length; ++i) {
|
|
1040
1110
|
const ef = entityFields[i];
|
|
1041
1111
|
const quotes = ef.NeedsQuotes ? "'" : "";
|
|
1042
1112
|
// we only want fields that are (a) not primary keys, or if a pkey, not an auto-increment pkey and (b) not virtual fields and (c) updateable fields and (d) not auto-increment fields if they're not pkeys)
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1113
|
+
// ALSO: if excludePrimaryKey is true, skip all primary key fields
|
|
1114
|
+
if ((excludePrimaryKey && ef.IsPrimaryKey) || (ef.IsPrimaryKey && autoGeneratedPrimaryKey) || ef.IsVirtual || !ef.AllowUpdateAPI || ef.AutoIncrement) {
|
|
1115
|
+
continue; // skip this field
|
|
1116
|
+
}
|
|
1117
|
+
if (!isFirst)
|
|
1118
|
+
sOutput += ',\n ';
|
|
1119
|
+
else
|
|
1120
|
+
isFirst = false;
|
|
1121
|
+
if (prefix !== '' && ef.IsSpecialDateField) {
|
|
1122
|
+
if (ef.IsCreatedAtField || ef.IsUpdatedAtField)
|
|
1123
|
+
sOutput += `GETUTCDATE()`; // we set the inserted row value to the current date for created and updated at fields
|
|
1046
1124
|
else
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
// next check to make sure ef.DefaultValue does not contain quotes around the value if it is a string type, if it does, we need to remove them
|
|
1059
|
-
let defValue = ef.DefaultValue;
|
|
1060
|
-
if (ef.TSType === core_1.EntityFieldTSType.String) {
|
|
1061
|
-
if (defValue.startsWith("'") && defValue.endsWith("'")) {
|
|
1062
|
-
defValue = defValue.substring(1, defValue.length - 1).trim(); // remove the quotes
|
|
1063
|
-
}
|
|
1125
|
+
sOutput += `NULL`; // we don't set the deleted at field on an insert, only on a delete
|
|
1126
|
+
}
|
|
1127
|
+
else if ((prefix && prefix !== '') && !ef.IsPrimaryKey && ef.IsUniqueIdentifier && ef.HasDefaultValue) {
|
|
1128
|
+
// this is the VALUE side (prefix not null/blank), is NOT a primary key, and is a uniqueidentifier column, and has a default value specified
|
|
1129
|
+
// in this situation we need to check if the value being passed in is the special value '00000000-0000-0000-0000-000000000000' (which is in __specialUUIDValue) if it is, we substitute it with the actual default value
|
|
1130
|
+
// otherwise we use the value passed in
|
|
1131
|
+
// next check to make sure ef.DefaultValue does not contain quotes around the value if it is a string type, if it does, we need to remove them
|
|
1132
|
+
let defValue = ef.DefaultValue;
|
|
1133
|
+
if (ef.TSType === core_1.EntityFieldTSType.String) {
|
|
1134
|
+
if (defValue.startsWith("'") && defValue.endsWith("'")) {
|
|
1135
|
+
defValue = defValue.substring(1, defValue.length - 1).trim(); // remove the quotes
|
|
1064
1136
|
}
|
|
1065
|
-
sOutput += `CASE @${ef.CodeName} WHEN '${this.__specialUUIDValue}' THEN ${quotes}${defValue}${quotes} ELSE @${ef.CodeName} END`;
|
|
1066
|
-
}
|
|
1067
|
-
else {
|
|
1068
|
-
let sVal = '';
|
|
1069
|
-
if (!prefix || prefix.length === 0)
|
|
1070
|
-
sVal = '[' + ef.Name + ']'; // always put field names in brackets so that if reserved words are being used for field names in a table like "USER" and so on, they still work
|
|
1071
|
-
else
|
|
1072
|
-
sVal = prefix + ef.CodeName;
|
|
1073
|
-
sOutput += sVal;
|
|
1074
1137
|
}
|
|
1138
|
+
sOutput += `CASE @${ef.CodeName} WHEN '${this.__specialUUIDValue}' THEN ${quotes}${defValue}${quotes} ELSE @${ef.CodeName} END`;
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
let sVal = '';
|
|
1142
|
+
if (!prefix || prefix.length === 0)
|
|
1143
|
+
sVal = '[' + ef.Name + ']'; // always put field names in brackets so that if reserved words are being used for field names in a table like "USER" and so on, they still work
|
|
1144
|
+
else
|
|
1145
|
+
sVal = prefix + ef.CodeName;
|
|
1146
|
+
sOutput += sVal;
|
|
1075
1147
|
}
|
|
1076
1148
|
}
|
|
1077
1149
|
return sOutput;
|