@memberjunction/codegen-lib 5.4.1 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -2
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +26 -12
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/related-entity-components.js +2 -2
- package/dist/Angular/related-entity-components.js.map +1 -1
- package/dist/Config/config.d.ts +10 -0
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +10 -0
- package/dist/Config/config.js.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts +544 -0
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -0
- package/dist/Database/codeGenDatabaseProvider.js +29 -0
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -0
- package/dist/Database/manage-metadata.d.ts +165 -60
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +592 -483
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.d.ts +53 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js +112 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js.map +1 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +344 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +1567 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.d.ts +42 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js +84 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +372 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +1483 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -0
- package/dist/Database/reorder-columns.d.ts +2 -2
- package/dist/Database/reorder-columns.d.ts.map +1 -1
- package/dist/Database/reorder-columns.js +9 -9
- package/dist/Database/reorder-columns.js.map +1 -1
- package/dist/Database/sql.d.ts +10 -5
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +44 -228
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +31 -29
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +209 -842
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +3 -2
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts +4 -4
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts +6 -1
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +33 -35
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +2 -2
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +1 -1
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/Misc/system_integrity.d.ts +6 -6
- package/dist/Misc/system_integrity.d.ts.map +1 -1
- package/dist/Misc/system_integrity.js +33 -8
- package/dist/Misc/system_integrity.js.map +1 -1
- package/dist/Misc/temp_batch_file.d.ts.map +1 -1
- package/dist/Misc/temp_batch_file.js +4 -1
- package/dist/Misc/temp_batch_file.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/runCodeGen.d.ts +30 -75
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +123 -215
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +18 -15
|
@@ -3,12 +3,13 @@ import { logError, logStatus, logWarning, startSpinner, updateSpinner, succeedSp
|
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { SQLUtilityBase } from './sql.js';
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { CodeGenDatabaseProvider } from './codeGenDatabaseProvider.js';
|
|
7
|
+
import { SQLServerCodeGenProvider } from './providers/sqlserver/SQLServerCodeGenProvider.js';
|
|
8
|
+
import { autoIndexForeignKeys, configInfo, customSqlScripts, mjCoreSchema } from '../Config/config.js';
|
|
8
9
|
import { ManageMetadataBase } from './manage-metadata.js';
|
|
9
10
|
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
10
|
-
import { combineFiles, logIf
|
|
11
|
-
import { MJGlobal } from '@memberjunction/global';
|
|
11
|
+
import { combineFiles, logIf } from '../Misc/util.js';
|
|
12
|
+
import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
|
|
12
13
|
import { SQLLogging } from '../Misc/sql_logging.js';
|
|
13
14
|
import { TempBatchFile } from '../Misc/temp_batch_file.js';
|
|
14
15
|
export const SPType = {
|
|
@@ -21,9 +22,44 @@ export const SPType = {
|
|
|
21
22
|
* 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
|
|
22
23
|
* of that abstract base class and other databases will be sub-classes of the abstract base class as well.
|
|
23
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* Creates the appropriate CodeGen database provider based on the configured database type.
|
|
27
|
+
* Falls back to SQLServerCodeGenProvider for backward compatibility.
|
|
28
|
+
*
|
|
29
|
+
* @param dbType The database platform type from configuration
|
|
30
|
+
* @returns A CodeGenDatabaseProvider instance for the specified platform
|
|
31
|
+
*/
|
|
32
|
+
function createCodeGenProvider(dbType) {
|
|
33
|
+
switch (dbType) {
|
|
34
|
+
case 'postgresql':
|
|
35
|
+
// Dynamic import is avoided - the PostgreSQL provider should be
|
|
36
|
+
// registered via MJGlobal ClassFactory for proper decoupling.
|
|
37
|
+
// For now, attempt to resolve from ClassFactory.
|
|
38
|
+
try {
|
|
39
|
+
const pgProvider = MJGlobal.Instance.ClassFactory.CreateInstance(CodeGenDatabaseProvider, 'PostgreSQLCodeGenProvider');
|
|
40
|
+
if (pgProvider) {
|
|
41
|
+
return pgProvider;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Fall through to error
|
|
46
|
+
}
|
|
47
|
+
throw new Error('PostgreSQL CodeGen provider not found. Ensure @memberjunction/postgresql-dataprovider ' +
|
|
48
|
+
'is installed and its CodeGen provider is registered before running CodeGen.');
|
|
49
|
+
case 'mssql':
|
|
50
|
+
default:
|
|
51
|
+
return new SQLServerCodeGenProvider();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
24
54
|
export class SQLCodeGenBase {
|
|
25
55
|
constructor() {
|
|
26
56
|
this._sqlUtilityObject = MJGlobal.Instance.ClassFactory.CreateInstance(SQLUtilityBase);
|
|
57
|
+
/**
|
|
58
|
+
* The database-specific code generation provider. Defaults to SQL Server.
|
|
59
|
+
* Override this property or set it before calling generation methods to use
|
|
60
|
+
* a different database platform (e.g., PostgreSQL).
|
|
61
|
+
*/
|
|
62
|
+
this._dbProvider = createCodeGenProvider(configInfo.dbType);
|
|
27
63
|
/**
|
|
28
64
|
* Array of entity names that qualify for forced regeneration based on the whereClause filter
|
|
29
65
|
*/
|
|
@@ -50,6 +86,12 @@ export class SQLCodeGenBase {
|
|
|
50
86
|
get SQLUtilityObject() {
|
|
51
87
|
return this._sqlUtilityObject;
|
|
52
88
|
}
|
|
89
|
+
get DBProvider() {
|
|
90
|
+
return this._dbProvider;
|
|
91
|
+
}
|
|
92
|
+
set DBProvider(value) {
|
|
93
|
+
this._dbProvider = value;
|
|
94
|
+
}
|
|
53
95
|
async manageSQLScriptsAndExecution(pool, entities, directory, currentUser) {
|
|
54
96
|
try {
|
|
55
97
|
// Build list of entities qualified for forced regeneration if entityWhereClause is provided
|
|
@@ -57,12 +99,15 @@ export class SQLCodeGenBase {
|
|
|
57
99
|
this.filterEntitiesQualifiedForRegeneration = true; // Enable filtering
|
|
58
100
|
try {
|
|
59
101
|
const whereClause = configInfo.forceRegeneration.entityWhereClause;
|
|
102
|
+
const qs = this._dbProvider.Dialect.QuoteSchema.bind(this._dbProvider.Dialect);
|
|
103
|
+
const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
|
|
104
|
+
const quotedWhereClause = this._dbProvider.quoteSQLForExecution(whereClause);
|
|
60
105
|
const query = `
|
|
61
|
-
SELECT Name
|
|
62
|
-
FROM
|
|
63
|
-
WHERE ${
|
|
106
|
+
SELECT ${qi('Name')}
|
|
107
|
+
FROM ${qs(mjCoreSchema, 'Entity')}
|
|
108
|
+
WHERE ${quotedWhereClause}
|
|
64
109
|
`;
|
|
65
|
-
const result = await pool.
|
|
110
|
+
const result = await pool.query(query);
|
|
66
111
|
this.entitiesQualifiedForForcedRegeneration = result.recordset.map((r) => r.Name);
|
|
67
112
|
logStatus(`Force regeneration filter enabled: ${this.entitiesQualifiedForForcedRegeneration.length} entities qualified based on entityWhereClause: ${whereClause}`);
|
|
68
113
|
}
|
|
@@ -107,7 +152,7 @@ export class SQLCodeGenBase {
|
|
|
107
152
|
// First, separate entities that need cascade delete regeneration from others
|
|
108
153
|
const entitiesWithoutCascadeRegeneration = includedEntities.filter(e => !this.entitiesNeedingDeleteSPRegeneration.has(e.ID));
|
|
109
154
|
const entitiesForCascadeRegeneration = this.orderedEntitiesForDeleteSPRegeneration
|
|
110
|
-
.map(id => includedEntities.find(e => e.ID
|
|
155
|
+
.map(id => includedEntities.find(e => UUIDsEqual(e.ID, id)))
|
|
111
156
|
.filter(e => e !== undefined);
|
|
112
157
|
// Generate SQL for entities that don't need cascade delete regeneration
|
|
113
158
|
const genResult = await this.generateAndExecuteEntitySQLToSeparateFiles({
|
|
@@ -208,6 +253,17 @@ export class SQLCodeGenBase {
|
|
|
208
253
|
succeedSpinner('Entity fields metadata updated');
|
|
209
254
|
}
|
|
210
255
|
// 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
|
|
256
|
+
// STEP 3.5 - Clean up stale entity relationships now that entity fields are fully synced.
|
|
257
|
+
// This must run AFTER manageEntityFields deletes stale fields (for dropped columns),
|
|
258
|
+
// otherwise the stale EntityField records make the relationships appear valid.
|
|
259
|
+
startSpinner('Cleaning up stale entity relationships...');
|
|
260
|
+
if (!await manageMD.cleanupStaleEntityRelationships(pool, configInfo.excludeSchemas)) {
|
|
261
|
+
failSpinner('Failed to clean up stale entity relationships');
|
|
262
|
+
overallSuccess = false;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
succeedSpinner('Stale entity relationships cleaned up');
|
|
266
|
+
}
|
|
211
267
|
// STEP 4- Apply permissions, executing all .permissions files
|
|
212
268
|
startSpinner('Applying permissions...');
|
|
213
269
|
const step4StartTime = new Date();
|
|
@@ -278,7 +334,7 @@ export class SQLCodeGenBase {
|
|
|
278
334
|
const fileBuffer = fs.readFileSync(fullPath);
|
|
279
335
|
const fileContents = fileBuffer.toString();
|
|
280
336
|
try {
|
|
281
|
-
await pool.
|
|
337
|
+
await pool.query(fileContents);
|
|
282
338
|
}
|
|
283
339
|
catch (e) {
|
|
284
340
|
logError(`Error executing permissions file ${fullPath} for entity ${e.Name}: ${e}`);
|
|
@@ -447,7 +503,8 @@ export class SQLCodeGenBase {
|
|
|
447
503
|
}
|
|
448
504
|
try {
|
|
449
505
|
const viewName = entity.BaseView ? entity.BaseView : `vw${entity.CodeName}`;
|
|
450
|
-
const
|
|
506
|
+
const viewDefSQL = this._dbProvider.getViewDefinitionSQL(entity.SchemaName, viewName);
|
|
507
|
+
const result = await pool.query(viewDefSQL);
|
|
451
508
|
const dbDefinition = result.recordset?.[0]?.ViewDefinition;
|
|
452
509
|
if (!dbDefinition) {
|
|
453
510
|
// View doesn't exist in DB yet — it's new, will be handled by existing new entity logic
|
|
@@ -482,7 +539,8 @@ export class SQLCodeGenBase {
|
|
|
482
539
|
// Start from the SELECT keyword
|
|
483
540
|
let body = viewSQL.substring(asMatch.index + asMatch[0].indexOf('SELECT'));
|
|
484
541
|
// Trim trailing GO and anything after it (permissions, etc.)
|
|
485
|
-
const
|
|
542
|
+
const batchSep = this._dbProvider.BatchSeparator;
|
|
543
|
+
const goMatch = body.match(new RegExp(`\\r?\\n${batchSep}\\b`, 'i'));
|
|
486
544
|
if (goMatch && goMatch.index !== undefined) {
|
|
487
545
|
body = body.substring(0, goMatch.index);
|
|
488
546
|
}
|
|
@@ -573,7 +631,7 @@ export class SQLCodeGenBase {
|
|
|
573
631
|
this.logSQLForNewOrModifiedEntity(options.entity, s, 'Index for Foreign Keys for ' + options.entity.BaseTable, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
574
632
|
files.push(filePath);
|
|
575
633
|
}
|
|
576
|
-
sRet += s + '\
|
|
634
|
+
sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
577
635
|
}
|
|
578
636
|
// BASE VIEW AND RELATED TVFs
|
|
579
637
|
// Only generate if BaseViewGenerated is true (respects custom views where it's false)
|
|
@@ -597,7 +655,7 @@ export class SQLCodeGenBase {
|
|
|
597
655
|
files.push(filePath);
|
|
598
656
|
}
|
|
599
657
|
// Add function SQL to output BEFORE the view
|
|
600
|
-
sRet += s + '\
|
|
658
|
+
sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
601
659
|
}
|
|
602
660
|
}
|
|
603
661
|
// Generate the base view (which may reference the TVFs created above)
|
|
@@ -612,12 +670,12 @@ export class SQLCodeGenBase {
|
|
|
612
670
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `Base View SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
|
|
613
671
|
files.push(filePath);
|
|
614
672
|
}
|
|
615
|
-
sRet += s + '\
|
|
673
|
+
sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
616
674
|
}
|
|
617
675
|
// always generate permissions for the base view
|
|
618
676
|
const s = this.generateSingleEntitySQLFileHeader(options.entity, 'Permissions for ' + options.entity.BaseView) + this.generateViewPermissions(options.entity);
|
|
619
677
|
if (s.length > 0)
|
|
620
|
-
permissionsSQL += s + '\
|
|
678
|
+
permissionsSQL += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
621
679
|
if (options.writeFiles) {
|
|
622
680
|
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('view', options.entity.SchemaName, options.entity.BaseView, true, true));
|
|
623
681
|
this.writeFileIfChanged(filePath, s);
|
|
@@ -627,7 +685,7 @@ export class SQLCodeGenBase {
|
|
|
627
685
|
// now, append the permissions to the return string IF we did NOT generate the base view - because if we generated the base view, that
|
|
628
686
|
// means we already generated the permissions for it above and it is part of sRet already, but we always save it to a file, (per above line)
|
|
629
687
|
if (!options.entity.BaseViewGenerated)
|
|
630
|
-
sRet += s + '\
|
|
688
|
+
sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
631
689
|
// CREATE SP
|
|
632
690
|
if (options.entity.AllowCreateAPI && !options.entity.VirtualEntity) {
|
|
633
691
|
const spName = this.getSPName(options.entity, SPType.Create);
|
|
@@ -642,11 +700,11 @@ export class SQLCodeGenBase {
|
|
|
642
700
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spCreate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
|
|
643
701
|
files.push(filePath);
|
|
644
702
|
}
|
|
645
|
-
sRet += s + '\
|
|
703
|
+
sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
646
704
|
}
|
|
647
705
|
const s = this.generateSPPermissions(options.entity, spName, SPType.Create) + '\n\n';
|
|
648
706
|
if (s.length > 0)
|
|
649
|
-
permissionsSQL += s + '\
|
|
707
|
+
permissionsSQL += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
650
708
|
if (options.writeFiles) {
|
|
651
709
|
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
|
|
652
710
|
this.writeFileIfChanged(filePath, s);
|
|
@@ -656,7 +714,7 @@ export class SQLCodeGenBase {
|
|
|
656
714
|
// now, append the permissions to the return string IF we did NOT generate the proc - because if we generated the proc, that
|
|
657
715
|
// means we already generated the permissions for it above and it is part of sRet already, but we always save it to a file, (per above line)
|
|
658
716
|
if (!options.entity.spCreateGenerated)
|
|
659
|
-
sRet += s + '\
|
|
717
|
+
sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
660
718
|
}
|
|
661
719
|
// UPDATE SP
|
|
662
720
|
if (options.entity.AllowUpdateAPI && !options.entity.VirtualEntity) {
|
|
@@ -672,11 +730,11 @@ export class SQLCodeGenBase {
|
|
|
672
730
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spUpdate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
|
|
673
731
|
files.push(filePath);
|
|
674
732
|
}
|
|
675
|
-
sRet += s + '\
|
|
733
|
+
sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
676
734
|
}
|
|
677
735
|
const s = this.generateSPPermissions(options.entity, spName, SPType.Update) + '\n\n';
|
|
678
736
|
if (s.length > 0)
|
|
679
|
-
permissionsSQL += s + '\
|
|
737
|
+
permissionsSQL += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
680
738
|
if (options.writeFiles) {
|
|
681
739
|
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
|
|
682
740
|
this.writeFileIfChanged(filePath, s);
|
|
@@ -686,7 +744,7 @@ export class SQLCodeGenBase {
|
|
|
686
744
|
// now, append the permissions to the return string IF we did NOT generate the proc - because if we generated the proc, that
|
|
687
745
|
// means we already generated the permissions for it above and it is part of sRet already, but we always save it to a file, (per above line)
|
|
688
746
|
if (!options.entity.spUpdateGenerated)
|
|
689
|
-
sRet += s + '\
|
|
747
|
+
sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
690
748
|
}
|
|
691
749
|
// DELETE SP
|
|
692
750
|
if (options.entity.AllowDeleteAPI && !options.entity.VirtualEntity) {
|
|
@@ -708,11 +766,11 @@ export class SQLCodeGenBase {
|
|
|
708
766
|
this.logSQLForNewOrModifiedEntity(options.entity, s, `spDelete SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
|
|
709
767
|
files.push(filePath);
|
|
710
768
|
}
|
|
711
|
-
sRet += s + '\
|
|
769
|
+
sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
712
770
|
}
|
|
713
771
|
const s = this.generateSPPermissions(options.entity, spName, SPType.Delete) + '\n\n';
|
|
714
772
|
if (s.length > 0)
|
|
715
|
-
permissionsSQL += s + '\
|
|
773
|
+
permissionsSQL += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
716
774
|
if (options.writeFiles) {
|
|
717
775
|
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
|
|
718
776
|
this.writeFileIfChanged(filePath, s);
|
|
@@ -722,7 +780,7 @@ export class SQLCodeGenBase {
|
|
|
722
780
|
// now, append the permissions to the return string IF we did NOT generate the proc - because if we generated the proc, that
|
|
723
781
|
// means we already generated the permissions for it above and it is part of sRet already, but we always save it to a file, (per above line)
|
|
724
782
|
if (!options.entity.spDeleteGenerated)
|
|
725
|
-
sRet += s + '\
|
|
783
|
+
sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
726
784
|
}
|
|
727
785
|
// check to see if the options.entity supports full text search or not
|
|
728
786
|
if (options.entity.FullTextSearchEnabled || (configInfo.forceRegeneration?.enabled && configInfo.forceRegeneration?.fullTextSearch)) {
|
|
@@ -736,11 +794,11 @@ export class SQLCodeGenBase {
|
|
|
736
794
|
this.logSQLForNewOrModifiedEntity(options.entity, ft.sql, `Full Text Search SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
|
|
737
795
|
files.push(filePath);
|
|
738
796
|
}
|
|
739
|
-
sRet += ft.sql + '\
|
|
797
|
+
sRet += ft.sql + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
740
798
|
}
|
|
741
799
|
const sP = this.generateFullTextSearchFunctionPermissions(options.entity, ft.functionName) + '\n\n';
|
|
742
800
|
if (sP.length > 0)
|
|
743
|
-
permissionsSQL += sP + '\
|
|
801
|
+
permissionsSQL += sP + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
744
802
|
const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('full_text_search_function', options.entity.SchemaName, options.entity.BaseTable, true, true));
|
|
745
803
|
if (options.writeFiles) {
|
|
746
804
|
this.writeFileIfChanged(filePath, sP);
|
|
@@ -750,7 +808,7 @@ export class SQLCodeGenBase {
|
|
|
750
808
|
// now, append the permissions to the return string IF we did NOT generate the function - because if we generated the function, that
|
|
751
809
|
// means we already generated the permissions for it above and it is part of sRet already, but we always save it to a file, (per above line)
|
|
752
810
|
if (!options.entity.FullTextSearchFunctionGenerated)
|
|
753
|
-
sRet += sP + '\
|
|
811
|
+
sRet += sP + '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
754
812
|
}
|
|
755
813
|
return { sql: sRet, permissionsSQL: permissionsSQL, files: files };
|
|
756
814
|
}
|
|
@@ -760,14 +818,7 @@ export class SQLCodeGenBase {
|
|
|
760
818
|
}
|
|
761
819
|
}
|
|
762
820
|
getSPName(entity, type) {
|
|
763
|
-
|
|
764
|
-
case SPType.Create:
|
|
765
|
-
return entity.spCreate && entity.spCreate.length > 0 ? entity.spCreate : 'spCreate' + entity.BaseTableCodeName;
|
|
766
|
-
case SPType.Update:
|
|
767
|
-
return entity.spUpdate && entity.spUpdate.length > 0 ? entity.spUpdate : 'spUpdate' + entity.BaseTableCodeName;
|
|
768
|
-
case SPType.Delete:
|
|
769
|
-
return entity.spDelete && entity.spDelete.length > 0 ? entity.spDelete : 'spDelete' + entity.BaseTableCodeName;
|
|
770
|
-
}
|
|
821
|
+
return this._dbProvider.getCRUDRoutineName(entity, type);
|
|
771
822
|
}
|
|
772
823
|
getEntityPermissionFileNames(entity) {
|
|
773
824
|
const files = [];
|
|
@@ -833,108 +884,43 @@ export class SQLCodeGenBase {
|
|
|
833
884
|
return sOutput;
|
|
834
885
|
}
|
|
835
886
|
async generateEntityFullTextSearchSQL(pool, entity) {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (entity.
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
WHERE name = '${catalogName}'
|
|
845
|
-
)
|
|
846
|
-
CREATE FULLTEXT CATALOG ${catalogName};
|
|
847
|
-
GO
|
|
848
|
-
`;
|
|
849
|
-
}
|
|
887
|
+
const searchFields = entity.Fields.filter(f => f.FullTextSearchEnabled);
|
|
888
|
+
// Validate search fields exist when needed
|
|
889
|
+
if (entity.FullTextIndexGenerated && searchFields.length === 0)
|
|
890
|
+
throw new Error(`FullTextIndexGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
|
|
891
|
+
if (entity.FullTextSearchFunctionGenerated && searchFields.length === 0)
|
|
892
|
+
throw new Error(`FullTextSearchFunctionGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
|
|
893
|
+
// Get the primary key index name (needed for full text index)
|
|
894
|
+
let primaryKeyIndexName = '';
|
|
850
895
|
if (entity.FullTextIndexGenerated) {
|
|
851
|
-
|
|
852
|
-
if (fullTextFields.length === 0)
|
|
853
|
-
throw new Error(`FullTextIndexGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
|
|
854
|
-
// drop and recreate the full text index
|
|
855
|
-
const entity_pk_name = await this.getEntityPrimaryKeyIndexName(pool, entity);
|
|
856
|
-
sql += ` -- DROP AND RECREATE THE FULL TEXT INDEX
|
|
857
|
-
IF EXISTS (
|
|
858
|
-
SELECT *
|
|
859
|
-
FROM sys.fulltext_indexes
|
|
860
|
-
WHERE object_id = OBJECT_ID('${entity.SchemaName}.${entity.BaseTable}')
|
|
861
|
-
)
|
|
862
|
-
BEGIN
|
|
863
|
-
DROP FULLTEXT INDEX ON [${entity.SchemaName}].[${entity.BaseTable}];
|
|
864
|
-
END
|
|
865
|
-
GO
|
|
866
|
-
|
|
867
|
-
IF NOT EXISTS (
|
|
868
|
-
SELECT *
|
|
869
|
-
FROM sys.fulltext_indexes
|
|
870
|
-
WHERE object_id = OBJECT_ID('${entity.SchemaName}.${entity.BaseTable}')
|
|
871
|
-
)
|
|
872
|
-
BEGIN
|
|
873
|
-
CREATE FULLTEXT INDEX ON [${entity.SchemaName}].[${entity.BaseTable}]
|
|
874
|
-
(
|
|
875
|
-
${fullTextFields}
|
|
876
|
-
)
|
|
877
|
-
KEY INDEX ${entity_pk_name}
|
|
878
|
-
ON ${catalogName};
|
|
879
|
-
END
|
|
880
|
-
GO
|
|
881
|
-
`;
|
|
896
|
+
primaryKeyIndexName = await this.getEntityPrimaryKeyIndexName(pool, entity);
|
|
882
897
|
}
|
|
883
|
-
|
|
898
|
+
// If the function name is not set, save it to the DB
|
|
899
|
+
const functionName = entity.FullTextSearchFunction && entity.FullTextSearchFunction.length > 0
|
|
900
|
+
? entity.FullTextSearchFunction
|
|
901
|
+
: `fnSearch${entity.CodeName}`;
|
|
902
|
+
if (entity.FullTextSearchFunctionGenerated && (!entity.FullTextSearchFunction || entity.FullTextSearchFunction.length === 0)) {
|
|
903
|
+
const md = new Metadata();
|
|
904
|
+
const u = UserCache.Instance.Users[0];
|
|
905
|
+
if (!u)
|
|
906
|
+
throw new Error('Could not find the first user in the cache, cant generate the full text search function without a user');
|
|
907
|
+
const e = await md.GetEntityObject('MJ: Entities', u);
|
|
908
|
+
await e.Load(entity.ID);
|
|
909
|
+
e.FullTextSearchFunction = functionName;
|
|
910
|
+
if (!await e.Save())
|
|
911
|
+
throw new Error(`Could not update the FullTextSearchFunction for entity ${entity.Name}`);
|
|
912
|
+
}
|
|
913
|
+
// Delegate SQL generation to the provider
|
|
914
|
+
const result = this._dbProvider.generateFullTextSearch(entity, searchFields, primaryKeyIndexName);
|
|
915
|
+
// Add permissions if function was generated
|
|
884
916
|
if (entity.FullTextSearchFunctionGenerated) {
|
|
885
|
-
|
|
886
|
-
if (fullTextFieldsSimple.length === 0)
|
|
887
|
-
throw new Error(`FullTextSearchFunctionGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
|
|
888
|
-
if (!entity.FullTextSearchFunction || entity.FullTextSearchFunction.length === 0) {
|
|
889
|
-
// update this in the DB
|
|
890
|
-
const md = new Metadata();
|
|
891
|
-
const u = UserCache.Instance.Users[0];
|
|
892
|
-
if (!u)
|
|
893
|
-
throw new Error('Could not find the first user in the cache, cant generate the full text search function without a user');
|
|
894
|
-
const e = await md.GetEntityObject('MJ: Entities', u);
|
|
895
|
-
await e.Load(entity.ID);
|
|
896
|
-
e.FullTextSearchFunction = functionName;
|
|
897
|
-
if (!await e.Save())
|
|
898
|
-
throw new Error(`Could not update the FullTextSearchFunction for entity ${entity.Name}`);
|
|
899
|
-
}
|
|
900
|
-
const pkeyList = entity.PrimaryKeys.map(pk => '[' + pk.Name + ']').join(', ');
|
|
901
|
-
// drop and recreate the full text search function
|
|
902
|
-
sql += ` -- DROP AND RECREATE THE FULL TEXT SEARCH FUNCTION
|
|
903
|
-
-- Create an inline table-valued function to perform full-text search
|
|
904
|
-
-- Drop the function if it already exists
|
|
905
|
-
IF OBJECT_ID('${entity.SchemaName}.${functionName}', 'IF') IS NOT NULL
|
|
906
|
-
DROP FUNCTION ${entity.SchemaName}.${functionName};
|
|
907
|
-
GO
|
|
908
|
-
CREATE FUNCTION ${entity.SchemaName}.${functionName} (@searchTerm NVARCHAR(255))
|
|
909
|
-
RETURNS TABLE
|
|
910
|
-
AS
|
|
911
|
-
RETURN (
|
|
912
|
-
SELECT ${pkeyList}
|
|
913
|
-
FROM [${entity.SchemaName}].[${entity.BaseTable}]
|
|
914
|
-
WHERE CONTAINS((${fullTextFieldsSimple}), @searchTerm)
|
|
915
|
-
)
|
|
916
|
-
GO
|
|
917
|
-
`;
|
|
918
|
-
sql += this.generateFullTextSearchFunctionPermissions(entity, functionName) + '\n\nGO\n';
|
|
917
|
+
result.sql += this.generateFullTextSearchFunctionPermissions(entity, functionName) + '\n\n' + this._dbProvider.BatchSeparator + '\n';
|
|
919
918
|
}
|
|
920
|
-
return
|
|
919
|
+
return result;
|
|
921
920
|
}
|
|
922
921
|
async getEntityPrimaryKeyIndexName(pool, entity) {
|
|
923
|
-
const sSQL =
|
|
924
|
-
|
|
925
|
-
FROM
|
|
926
|
-
sys.indexes i
|
|
927
|
-
INNER JOIN
|
|
928
|
-
sys.objects o ON i.object_id = o.object_id
|
|
929
|
-
INNER JOIN
|
|
930
|
-
sys.key_constraints kc ON i.object_id = kc.parent_object_id AND
|
|
931
|
-
i.index_id = kc.unique_index_id
|
|
932
|
-
WHERE
|
|
933
|
-
o.name = '${entity.BaseTable}' AND
|
|
934
|
-
o.schema_id = SCHEMA_ID('${entity.SchemaName}') AND
|
|
935
|
-
kc.type = 'PK';
|
|
936
|
-
`;
|
|
937
|
-
const resultResult = await pool.request().query(sSQL);
|
|
922
|
+
const sSQL = this._dbProvider.getPrimaryKeyIndexNameSQL(entity.SchemaName, entity.BaseTable);
|
|
923
|
+
const resultResult = await pool.query(sSQL);
|
|
938
924
|
const result = resultResult.recordset;
|
|
939
925
|
if (result && result.length > 0)
|
|
940
926
|
return result[0].IndexName;
|
|
@@ -942,56 +928,18 @@ export class SQLCodeGenBase {
|
|
|
942
928
|
throw new Error(`Could not find primary key index for entity ${entity.Name}`);
|
|
943
929
|
}
|
|
944
930
|
generateAllEntitiesSQLFileHeader() {
|
|
945
|
-
return
|
|
946
|
-
-- SQL Code Generation for Entities
|
|
947
|
-
--
|
|
948
|
-
-- This file contains the SQL code for the entities in the database
|
|
949
|
-
-- that are included in the API and have generated SQL elements like views and
|
|
950
|
-
-- stored procedures.
|
|
951
|
-
--
|
|
952
|
-
-- It is generated by the MemberJunction CodeGen tool.
|
|
953
|
-
-- It is not intended to be edited by hand.
|
|
954
|
-
-----------------------------------------------------------------
|
|
955
|
-
`;
|
|
931
|
+
return this._dbProvider.generateAllEntitiesSQLFileHeader();
|
|
956
932
|
}
|
|
957
933
|
generateSingleEntitySQLFileHeader(entity, itemName) {
|
|
958
|
-
return
|
|
959
|
-
-- SQL Code Generation
|
|
960
|
-
-- Entity: ${entity.Name}
|
|
961
|
-
-- Item: ${itemName}
|
|
962
|
-
--
|
|
963
|
-
-- This was generated by the MemberJunction CodeGen tool.
|
|
964
|
-
-- This file should NOT be edited by hand.
|
|
965
|
-
-----------------------------------------------------------------
|
|
966
|
-
`;
|
|
934
|
+
return this._dbProvider.generateSQLFileHeader(entity, itemName);
|
|
967
935
|
}
|
|
968
936
|
/**
|
|
969
|
-
* Generates the SQL for creating indexes for the foreign keys in the entity
|
|
970
|
-
*
|
|
971
|
-
* @param entity
|
|
937
|
+
* Generates the SQL for creating indexes for the foreign keys in the entity.
|
|
938
|
+
* Delegates to the database provider for platform-specific index DDL.
|
|
972
939
|
*/
|
|
973
940
|
generateIndexesForForeignKeys(pool, entity) {
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
for (const f of entity.Fields) {
|
|
977
|
-
if (f.RelatedEntity && f.RelatedEntity.length > 0) {
|
|
978
|
-
// we have an fkey, so generate the create index
|
|
979
|
-
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
|
|
980
|
-
if (indexName.length > MAX_INDEX_NAME_LENGTH)
|
|
981
|
-
indexName = indexName.substring(0, MAX_INDEX_NAME_LENGTH); // truncate to max length if necessary
|
|
982
|
-
if (sOutput.length > 0)
|
|
983
|
-
sOutput += '\n\n'; // do this way so we don't end up with a trailing newline at end of the string/file
|
|
984
|
-
sOutput += `-- Index for foreign key ${f.Name} in table ${entity.BaseTable}
|
|
985
|
-
IF NOT EXISTS (
|
|
986
|
-
SELECT 1
|
|
987
|
-
FROM sys.indexes
|
|
988
|
-
WHERE name = '${indexName}'
|
|
989
|
-
AND object_id = OBJECT_ID('[${entity.SchemaName}].[${entity.BaseTable}]')
|
|
990
|
-
)
|
|
991
|
-
CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.Name}]);`;
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
return sOutput;
|
|
941
|
+
const indexStatements = this._dbProvider.generateForeignKeyIndexes(entity);
|
|
942
|
+
return indexStatements.join('\n\n');
|
|
995
943
|
}
|
|
996
944
|
/**
|
|
997
945
|
* Detects self-referential foreign keys in an entity (e.g., ParentTaskID pointing back to Task table)
|
|
@@ -999,7 +947,7 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
|
|
|
999
947
|
*/
|
|
1000
948
|
detectRecursiveForeignKeys(entity) {
|
|
1001
949
|
return entity.Fields.filter(field => field.RelatedEntityID != null &&
|
|
1002
|
-
field.RelatedEntityID
|
|
950
|
+
UUIDsEqual(field.RelatedEntityID, entity.ID));
|
|
1003
951
|
}
|
|
1004
952
|
/**
|
|
1005
953
|
* Generates an inline Table Value Function for calculating root ID for a recursive FK field
|
|
@@ -1007,67 +955,7 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
|
|
|
1007
955
|
* Returns SQL to create the function including DROP IF EXISTS
|
|
1008
956
|
*/
|
|
1009
957
|
generateRootIDFunction(entity, field) {
|
|
1010
|
-
|
|
1011
|
-
const primaryKeyType = entity.FirstPrimaryKey.SQLFullType;
|
|
1012
|
-
const schemaName = entity.SchemaName;
|
|
1013
|
-
const tableName = entity.BaseTable;
|
|
1014
|
-
const fieldName = field.Name;
|
|
1015
|
-
const functionName = `fn${entity.BaseTable}${fieldName}_GetRootID`;
|
|
1016
|
-
return `------------------------------------------------------------
|
|
1017
|
-
----- ROOT ID FUNCTION FOR: [${tableName}].[${fieldName}]
|
|
1018
|
-
------------------------------------------------------------
|
|
1019
|
-
IF OBJECT_ID('[${schemaName}].[${functionName}]', 'IF') IS NOT NULL
|
|
1020
|
-
DROP FUNCTION [${schemaName}].[${functionName}];
|
|
1021
|
-
GO
|
|
1022
|
-
|
|
1023
|
-
CREATE FUNCTION [${schemaName}].[${functionName}]
|
|
1024
|
-
(
|
|
1025
|
-
@RecordID ${primaryKeyType},
|
|
1026
|
-
@ParentID ${primaryKeyType}
|
|
1027
|
-
)
|
|
1028
|
-
RETURNS TABLE
|
|
1029
|
-
AS
|
|
1030
|
-
RETURN
|
|
1031
|
-
(
|
|
1032
|
-
WITH CTE_RootParent AS (
|
|
1033
|
-
-- Anchor: Start from @ParentID if not null, otherwise start from @RecordID
|
|
1034
|
-
SELECT
|
|
1035
|
-
[${primaryKey}],
|
|
1036
|
-
[${fieldName}],
|
|
1037
|
-
[${primaryKey}] AS [RootParentID],
|
|
1038
|
-
0 AS [Depth]
|
|
1039
|
-
FROM
|
|
1040
|
-
[${schemaName}].[${tableName}]
|
|
1041
|
-
WHERE
|
|
1042
|
-
[${primaryKey}] = COALESCE(@ParentID, @RecordID)
|
|
1043
|
-
|
|
1044
|
-
UNION ALL
|
|
1045
|
-
|
|
1046
|
-
-- Recursive: Keep going up the hierarchy until ${fieldName} is NULL
|
|
1047
|
-
-- Includes depth counter to prevent infinite loops from circular references
|
|
1048
|
-
SELECT
|
|
1049
|
-
c.[${primaryKey}],
|
|
1050
|
-
c.[${fieldName}],
|
|
1051
|
-
c.[${primaryKey}] AS [RootParentID],
|
|
1052
|
-
p.[Depth] + 1 AS [Depth]
|
|
1053
|
-
FROM
|
|
1054
|
-
[${schemaName}].[${tableName}] c
|
|
1055
|
-
INNER JOIN
|
|
1056
|
-
CTE_RootParent p ON c.[${primaryKey}] = p.[${fieldName}]
|
|
1057
|
-
WHERE
|
|
1058
|
-
p.[Depth] < 100 -- Prevent infinite loops, max 100 levels
|
|
1059
|
-
)
|
|
1060
|
-
SELECT TOP 1
|
|
1061
|
-
[RootParentID] AS RootID
|
|
1062
|
-
FROM
|
|
1063
|
-
CTE_RootParent
|
|
1064
|
-
WHERE
|
|
1065
|
-
[${fieldName}] IS NULL
|
|
1066
|
-
ORDER BY
|
|
1067
|
-
[RootParentID]
|
|
1068
|
-
);
|
|
1069
|
-
GO
|
|
1070
|
-
`;
|
|
958
|
+
return this._dbProvider.generateRootIDFunction(entity, field);
|
|
1071
959
|
}
|
|
1072
960
|
/**
|
|
1073
961
|
* Generates all inline Table Value Functions for an entity's recursive foreign keys
|
|
@@ -1085,30 +973,33 @@ GO
|
|
|
1085
973
|
* Generates the SELECT clause additions for root fields from TVFs
|
|
1086
974
|
* Example: , root_ParentID.RootID AS [RootParentID]
|
|
1087
975
|
*/
|
|
1088
|
-
generateRootFieldSelects(recursiveFKs, classNameFirstChar) {
|
|
1089
|
-
|
|
976
|
+
generateRootFieldSelects(entity, recursiveFKs, classNameFirstChar) {
|
|
977
|
+
if (recursiveFKs.length === 0)
|
|
978
|
+
return '';
|
|
979
|
+
let sOutput = '';
|
|
980
|
+
for (let i = 0; i < recursiveFKs.length; i++) {
|
|
981
|
+
const field = recursiveFKs[i];
|
|
1090
982
|
const alias = `root_${field.Name}`;
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
983
|
+
sOutput += `,\n ${this._dbProvider.generateRootFieldSelect(entity, field, alias)}`;
|
|
984
|
+
}
|
|
985
|
+
return sOutput;
|
|
1094
986
|
}
|
|
1095
987
|
/**
|
|
1096
988
|
* Generates OUTER APPLY joins to inline Table Value Functions for root ID calculation
|
|
1097
989
|
* Each recursive FK gets an OUTER APPLY that calls its corresponding function
|
|
1098
990
|
*/
|
|
1099
991
|
generateRootIDJoins(recursiveFKs, classNameFirstChar, entity) {
|
|
1100
|
-
if (recursiveFKs.length === 0)
|
|
992
|
+
if (recursiveFKs.length === 0)
|
|
1101
993
|
return '';
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const tableName = entity.BaseTable;
|
|
1106
|
-
const joins = recursiveFKs.map(field => {
|
|
1107
|
-
const functionName = `fn${tableName}${field.Name}_GetRootID`;
|
|
994
|
+
let sOutput = '';
|
|
995
|
+
for (let i = 0; i < recursiveFKs.length; i++) {
|
|
996
|
+
const field = recursiveFKs[i];
|
|
1108
997
|
const alias = `root_${field.Name}`;
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
998
|
+
if (sOutput.length > 0)
|
|
999
|
+
sOutput += '\n';
|
|
1000
|
+
sOutput += this._dbProvider.generateRootFieldJoin(entity, field, alias);
|
|
1001
|
+
}
|
|
1002
|
+
return '\n' + sOutput;
|
|
1112
1003
|
}
|
|
1113
1004
|
/**
|
|
1114
1005
|
* @deprecated Use generateRootIDJoins instead - kept for backward compatibility during migration
|
|
@@ -1137,7 +1028,7 @@ GO
|
|
|
1137
1028
|
// Skip PKs (shared with child), timestamps, and virtual fields (view-computed)
|
|
1138
1029
|
if (field.IsPrimaryKey || field.Name.startsWith('__mj_') || field.IsVirtual)
|
|
1139
1030
|
continue;
|
|
1140
|
-
fieldExpressions.push(` ${alias}.
|
|
1031
|
+
fieldExpressions.push(` ${alias}.${this._dbProvider.Dialect.QuoteIdentifier(field.Name)}`);
|
|
1141
1032
|
}
|
|
1142
1033
|
}
|
|
1143
1034
|
if (fieldExpressions.length === 0)
|
|
@@ -1162,8 +1053,10 @@ GO
|
|
|
1162
1053
|
// First parent joins to child table alias; deeper parents chain to previous parent alias
|
|
1163
1054
|
const sourceAlias = i === 0 ? classNameFirstChar : `__mj_isa_p${i}`;
|
|
1164
1055
|
// Build PK-to-PK join condition (supports composite keys)
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1056
|
+
const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
|
|
1057
|
+
const joinConditions = entity.PrimaryKeys.map(pk => `${qi(sourceAlias)}.${qi(pk.Name)} = ${parentAlias}.${qi(pk.Name)}`).join(' AND ');
|
|
1058
|
+
const qs = this._dbProvider.Dialect.QuoteSchema.bind(this._dbProvider.Dialect);
|
|
1059
|
+
joins.push(`INNER JOIN\n ${qs(parent.SchemaName, parent.BaseTable)} AS ${parentAlias}\n ON\n ${joinConditions}`);
|
|
1167
1060
|
}
|
|
1168
1061
|
return joins.join('\n');
|
|
1169
1062
|
}
|
|
@@ -1173,45 +1066,30 @@ GO
|
|
|
1173
1066
|
const relatedFieldsString = await this.generateBaseViewRelatedFieldsString(pool, entity.Fields);
|
|
1174
1067
|
const relatedFieldsJoinString = this.generateBaseViewJoins(entity, entity.Fields);
|
|
1175
1068
|
const permissions = this.generateViewPermissions(entity);
|
|
1069
|
+
const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
|
|
1176
1070
|
const whereClause = entity.DeleteType === 'Soft' ? `WHERE
|
|
1177
|
-
${classNameFirstChar}
|
|
1071
|
+
${qi(classNameFirstChar)}.${qi(EntityInfo.DeletedAtFieldName)} IS NULL
|
|
1178
1072
|
` : '';
|
|
1179
1073
|
// Detect recursive foreign keys and generate TVF joins and root field selects
|
|
1180
1074
|
const recursiveFKs = this.detectRecursiveForeignKeys(entity);
|
|
1181
|
-
const rootFields = recursiveFKs.length > 0 ? this.generateRootFieldSelects(recursiveFKs, classNameFirstChar) : '';
|
|
1075
|
+
const rootFields = recursiveFKs.length > 0 ? this.generateRootFieldSelects(entity, recursiveFKs, classNameFirstChar) : '';
|
|
1182
1076
|
const rootJoins = recursiveFKs.length > 0 ? this.generateRootIDJoins(recursiveFKs, classNameFirstChar, entity) : '';
|
|
1183
1077
|
// IS-A parent entity JOINs — walk ParentID chain, JOIN to each parent's base table
|
|
1184
1078
|
const parentFieldsString = this.generateParentEntityFieldSelects(entity);
|
|
1185
1079
|
const parentJoinsString = this.generateParentEntityJoins(entity, classNameFirstChar);
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
CREATE VIEW [${entity.SchemaName}].[${viewName}]
|
|
1198
|
-
AS
|
|
1199
|
-
SELECT
|
|
1200
|
-
${classNameFirstChar}.*${parentFieldsString}${relatedFieldsString.length > 0 ? ',' : ''}${relatedFieldsString}${rootFields}
|
|
1201
|
-
FROM
|
|
1202
|
-
[${entity.SchemaName}].[${entity.BaseTable}] AS ${classNameFirstChar}${parentJoinsString ? '\n' + parentJoinsString : ''}${relatedFieldsJoinString ? '\n' + relatedFieldsJoinString : ''}${rootJoins}
|
|
1203
|
-
${whereClause}GO${permissions}
|
|
1204
|
-
`;
|
|
1080
|
+
const context = {
|
|
1081
|
+
entity,
|
|
1082
|
+
relatedFieldsSelect: relatedFieldsString,
|
|
1083
|
+
relatedFieldsJoins: relatedFieldsJoinString,
|
|
1084
|
+
parentFieldsSelect: parentFieldsString,
|
|
1085
|
+
parentJoins: parentJoinsString,
|
|
1086
|
+
rootFieldsSelect: rootFields,
|
|
1087
|
+
rootJoins: rootJoins,
|
|
1088
|
+
};
|
|
1089
|
+
return this._dbProvider.generateBaseView(context) + permissions;
|
|
1205
1090
|
}
|
|
1206
1091
|
generateViewPermissions(entity) {
|
|
1207
|
-
|
|
1208
|
-
for (let i = 0; i < entity.Permissions.length; i++) {
|
|
1209
|
-
const ep = entity.Permissions[i];
|
|
1210
|
-
if (ep.RoleSQLName && ep.RoleSQLName.length > 0) {
|
|
1211
|
-
sOutput += (sOutput === '' ? `GRANT SELECT ON [${entity.SchemaName}].[${entity.BaseView}] TO ` : ', ') + `[${ep.RoleSQLName}]`;
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
return (sOutput == '' ? '' : '\n') + sOutput;
|
|
1092
|
+
return this._dbProvider.generateViewPermissions(entity);
|
|
1215
1093
|
}
|
|
1216
1094
|
generateBaseViewJoins(entity, entityFields) {
|
|
1217
1095
|
let sOutput = '';
|
|
@@ -1225,7 +1103,10 @@ ${whereClause}GO${permissions}
|
|
|
1225
1103
|
// This generates the JOIN clause; the actual field aliases are added separately in generateBaseViewFields()
|
|
1226
1104
|
if (ef.RelatedEntityID && ef._RelatedEntityJoinFieldMappings && ef._RelatedEntityJoinFieldMappings.length > 0 && ef._RelatedEntityTableAlias) {
|
|
1227
1105
|
sOutput += sOutput == '' ? '' : '\n';
|
|
1228
|
-
|
|
1106
|
+
const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
|
|
1107
|
+
const qs = this._dbProvider.Dialect.QuoteSchema.bind(this._dbProvider.Dialect);
|
|
1108
|
+
const relatedTable = ef._RelatedEntityNameFieldIsVirtual ? ef.RelatedEntityBaseView : ef.RelatedEntityBaseTable;
|
|
1109
|
+
sOutput += `${ef.AllowsNull ? 'LEFT OUTER' : 'INNER'} JOIN\n ${qs(ef.RelatedEntitySchemaName, relatedTable)} AS ${ef._RelatedEntityTableAlias}\n ON\n ${qi(classNameFirstChar)}.${qi(ef.Name)} = ${ef._RelatedEntityTableAlias}.${qi(ef.RelatedEntityFieldName)}`;
|
|
1229
1110
|
}
|
|
1230
1111
|
}
|
|
1231
1112
|
return sOutput;
|
|
@@ -1302,7 +1183,7 @@ ${whereClause}GO${permissions}
|
|
|
1302
1183
|
}
|
|
1303
1184
|
// 2. Handle configured additional fields
|
|
1304
1185
|
if (config.fields && config.fields.length > 0) {
|
|
1305
|
-
const currentEntity = md.Entities.find(e => e.ID
|
|
1186
|
+
const currentEntity = md.Entities.find(e => UUIDsEqual(e.ID, ef.EntityID));
|
|
1306
1187
|
for (const fieldConfig of config.fields) {
|
|
1307
1188
|
const fieldName = fieldConfig.field;
|
|
1308
1189
|
const alias = fieldConfig.alias || this.generateDefaultAlias(ef.Name, fieldName);
|
|
@@ -1335,7 +1216,8 @@ ${whereClause}GO${permissions}
|
|
|
1335
1216
|
ef._RelatedEntityTableAlias = ef.RelatedEntityClassName + '_' + ef.Name;
|
|
1336
1217
|
ef._RelatedEntityNameFieldIsVirtual = anyFieldIsVirtual;
|
|
1337
1218
|
for (const mapping of ef._RelatedEntityJoinFieldMappings) {
|
|
1338
|
-
|
|
1219
|
+
const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
|
|
1220
|
+
sOutput += `${fieldCount === 0 ? '' : ','}\n ${ef._RelatedEntityTableAlias}.${qi(mapping.sourceField)} AS ${qi(mapping.alias)}`;
|
|
1339
1221
|
fieldCount++;
|
|
1340
1222
|
}
|
|
1341
1223
|
}
|
|
@@ -1388,215 +1270,19 @@ ${whereClause}GO${permissions}
|
|
|
1388
1270
|
return false;
|
|
1389
1271
|
}
|
|
1390
1272
|
generateSPPermissions(entity, spName, type) {
|
|
1391
|
-
|
|
1392
|
-
for (let i = 0; i < entity.Permissions.length; i++) {
|
|
1393
|
-
const ep = entity.Permissions[i];
|
|
1394
|
-
if ((type == SPType.Create && ep.CanCreate) ||
|
|
1395
|
-
(type == SPType.Update && ep.CanUpdate) ||
|
|
1396
|
-
(type == SPType.Delete && ep.CanDelete)) {
|
|
1397
|
-
if (ep.RoleSQLName && ep.RoleSQLName.length > 0) {
|
|
1398
|
-
sOutput += (sOutput === '' ? `GRANT EXECUTE ON [${entity.SchemaName}].[${spName}] TO ` : ', ') + `[${ep.RoleSQLName}]`;
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
return (sOutput == '' ? '' : '\n') + sOutput;
|
|
1273
|
+
return this._dbProvider.generateCRUDPermissions(entity, spName, type);
|
|
1403
1274
|
}
|
|
1404
1275
|
generateFullTextSearchFunctionPermissions(entity, functionName) {
|
|
1405
|
-
|
|
1406
|
-
for (let i = 0; i < entity.Permissions.length; i++) {
|
|
1407
|
-
const ep = entity.Permissions[i];
|
|
1408
|
-
if (ep.CanRead) {
|
|
1409
|
-
if (ep.RoleSQLName && ep.RoleSQLName.length > 0) {
|
|
1410
|
-
sOutput += (sOutput === '' ? `GRANT SELECT ON [${entity.SchemaName}].[${functionName}] TO ` : ', ') + `[${ep.RoleSQLName}]`;
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
return (sOutput == '' ? '' : '\n') + sOutput;
|
|
1276
|
+
return this._dbProvider.generateFullTextSearchPermissions(entity, functionName);
|
|
1415
1277
|
}
|
|
1416
1278
|
generateSPCreate(entity) {
|
|
1417
|
-
|
|
1418
|
-
const firstKey = entity.FirstPrimaryKey;
|
|
1419
|
-
//double exclamations used on the firstKey.DefaultValue property otherwise the type of this variable is 'number | ""';
|
|
1420
|
-
const primaryKeyAutomatic = firstKey.AutoIncrement; // Only exclude auto-increment fields, allow manual override for all other PKs including UUIDs with defaults
|
|
1421
|
-
const efString = this.createEntityFieldsParamString(entity.Fields, false); // Always pass false for isUpdate since this is generateSPCreate
|
|
1422
|
-
const permissions = this.generateSPPermissions(entity, spName, SPType.Create);
|
|
1423
|
-
let preInsertCode = '';
|
|
1424
|
-
let outputCode = '';
|
|
1425
|
-
let selectInsertedRecord = '';
|
|
1426
|
-
let additionalFieldList = '';
|
|
1427
|
-
let additionalValueList = '';
|
|
1428
|
-
if (entity.FirstPrimaryKey.AutoIncrement) {
|
|
1429
|
-
selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE [${entity.FirstPrimaryKey.Name}] = SCOPE_IDENTITY()`;
|
|
1430
|
-
}
|
|
1431
|
-
else if (entity.FirstPrimaryKey.Type.toLowerCase().trim() === 'uniqueidentifier' && entity.PrimaryKeys.length === 1) {
|
|
1432
|
-
// our primary key is a uniqueidentifier. Now we support optional override:
|
|
1433
|
-
// - If PKEY is provided (not NULL), use it
|
|
1434
|
-
// - If PKEY is NULL and there's a default value, let the database use it
|
|
1435
|
-
// - If PKEY is NULL and no default value, generate NEWID()
|
|
1436
|
-
const hasDefaultValue = entity.FirstPrimaryKey.DefaultValue && entity.FirstPrimaryKey.DefaultValue.trim().length > 0;
|
|
1437
|
-
if (hasDefaultValue) {
|
|
1438
|
-
// Has default value - use conditional logic to either include the field or let DB use default
|
|
1439
|
-
preInsertCode = `DECLARE @InsertedRow TABLE ([${entity.FirstPrimaryKey.Name}] UNIQUEIDENTIFIER)
|
|
1440
|
-
|
|
1441
|
-
IF @${entity.FirstPrimaryKey.Name} IS NOT NULL
|
|
1442
|
-
BEGIN
|
|
1443
|
-
-- User provided a value, use it
|
|
1444
|
-
INSERT INTO [${entity.SchemaName}].[${entity.BaseTable}]
|
|
1445
|
-
(
|
|
1446
|
-
[${entity.FirstPrimaryKey.Name}],
|
|
1447
|
-
${this.createEntityFieldsInsertString(entity, entity.Fields, '', true)}
|
|
1448
|
-
)
|
|
1449
|
-
OUTPUT INSERTED.[${entity.FirstPrimaryKey.Name}] INTO @InsertedRow
|
|
1450
|
-
VALUES
|
|
1451
|
-
(
|
|
1452
|
-
@${entity.FirstPrimaryKey.Name},
|
|
1453
|
-
${this.createEntityFieldsInsertString(entity, entity.Fields, '@', true)}
|
|
1454
|
-
)
|
|
1455
|
-
END
|
|
1456
|
-
ELSE
|
|
1457
|
-
BEGIN
|
|
1458
|
-
-- No value provided, let database use its default (e.g., NEWSEQUENTIALID())
|
|
1459
|
-
INSERT INTO [${entity.SchemaName}].[${entity.BaseTable}]
|
|
1460
|
-
(
|
|
1461
|
-
${this.createEntityFieldsInsertString(entity, entity.Fields, '', true)}
|
|
1462
|
-
)
|
|
1463
|
-
OUTPUT INSERTED.[${entity.FirstPrimaryKey.Name}] INTO @InsertedRow
|
|
1464
|
-
VALUES
|
|
1465
|
-
(
|
|
1466
|
-
${this.createEntityFieldsInsertString(entity, entity.Fields, '@', true)}
|
|
1467
|
-
)
|
|
1468
|
-
END`;
|
|
1469
|
-
// Clear these as we're handling the INSERT in preInsertCode
|
|
1470
|
-
additionalFieldList = '';
|
|
1471
|
-
additionalValueList = '';
|
|
1472
|
-
outputCode = '';
|
|
1473
|
-
selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE [${entity.FirstPrimaryKey.Name}] = (SELECT [${entity.FirstPrimaryKey.Name}] FROM @InsertedRow)`;
|
|
1474
|
-
}
|
|
1475
|
-
else {
|
|
1476
|
-
// No default value - we calculate the ID upfront, so no need for OUTPUT clause
|
|
1477
|
-
preInsertCode = `DECLARE @ActualID UNIQUEIDENTIFIER = ISNULL(@${entity.FirstPrimaryKey.Name}, NEWID())`;
|
|
1478
|
-
additionalFieldList = ',\n [' + entity.FirstPrimaryKey.Name + ']';
|
|
1479
|
-
additionalValueList = ',\n @ActualID';
|
|
1480
|
-
outputCode = ''; // No OUTPUT clause needed
|
|
1481
|
-
// We already know the ID, so just select using it directly
|
|
1482
|
-
selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE [${entity.FirstPrimaryKey.Name}] = @ActualID`;
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
else {
|
|
1486
|
-
selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE `;
|
|
1487
|
-
let isFirst = true;
|
|
1488
|
-
for (let k of entity.PrimaryKeys) {
|
|
1489
|
-
if (!isFirst)
|
|
1490
|
-
selectInsertedRecord += ' AND ';
|
|
1491
|
-
selectInsertedRecord += `[${k.Name}] = @${k.CodeName}`;
|
|
1492
|
-
isFirst = false;
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
return `
|
|
1496
|
-
------------------------------------------------------------
|
|
1497
|
-
----- CREATE PROCEDURE FOR ${entity.BaseTable}
|
|
1498
|
-
------------------------------------------------------------
|
|
1499
|
-
IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
|
|
1500
|
-
DROP PROCEDURE [${entity.SchemaName}].[${spName}];
|
|
1501
|
-
GO
|
|
1502
|
-
|
|
1503
|
-
CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
|
|
1504
|
-
${efString}
|
|
1505
|
-
AS
|
|
1506
|
-
BEGIN
|
|
1507
|
-
SET NOCOUNT ON;
|
|
1508
|
-
${preInsertCode}${preInsertCode.includes('INSERT INTO') ? '' : `
|
|
1509
|
-
INSERT INTO
|
|
1510
|
-
[${entity.SchemaName}].[${entity.BaseTable}]
|
|
1511
|
-
(
|
|
1512
|
-
${this.createEntityFieldsInsertString(entity, entity.Fields, '')}${additionalFieldList}
|
|
1513
|
-
)
|
|
1514
|
-
${outputCode}VALUES
|
|
1515
|
-
(
|
|
1516
|
-
${this.createEntityFieldsInsertString(entity, entity.Fields, '@')}${additionalValueList}
|
|
1517
|
-
)`}
|
|
1518
|
-
-- return the new record from the base view, which might have some calculated fields
|
|
1519
|
-
${selectInsertedRecord}
|
|
1520
|
-
END
|
|
1521
|
-
GO${permissions}
|
|
1522
|
-
`;
|
|
1279
|
+
return this._dbProvider.generateCRUDCreate(entity);
|
|
1523
1280
|
}
|
|
1524
1281
|
generateUpdatedAtTrigger(entity) {
|
|
1525
|
-
|
|
1526
|
-
if (!updatedAtField)
|
|
1527
|
-
return '';
|
|
1528
|
-
const triggerStatement = `
|
|
1529
|
-
------------------------------------------------------------
|
|
1530
|
-
----- TRIGGER FOR ${EntityInfo.UpdatedAtFieldName} field for the ${entity.BaseTable} table
|
|
1531
|
-
------------------------------------------------------------
|
|
1532
|
-
IF OBJECT_ID('[${entity.SchemaName}].[trgUpdate${entity.BaseTableCodeName}]', 'TR') IS NOT NULL
|
|
1533
|
-
DROP TRIGGER [${entity.SchemaName}].[trgUpdate${entity.BaseTableCodeName}];
|
|
1534
|
-
GO
|
|
1535
|
-
CREATE TRIGGER [${entity.SchemaName}].trgUpdate${entity.BaseTableCodeName}
|
|
1536
|
-
ON [${entity.SchemaName}].[${entity.BaseTable}]
|
|
1537
|
-
AFTER UPDATE
|
|
1538
|
-
AS
|
|
1539
|
-
BEGIN
|
|
1540
|
-
SET NOCOUNT ON;
|
|
1541
|
-
UPDATE
|
|
1542
|
-
[${entity.SchemaName}].[${entity.BaseTable}]
|
|
1543
|
-
SET
|
|
1544
|
-
${EntityInfo.UpdatedAtFieldName} = GETUTCDATE()
|
|
1545
|
-
FROM
|
|
1546
|
-
[${entity.SchemaName}].[${entity.BaseTable}] AS _organicTable
|
|
1547
|
-
INNER JOIN
|
|
1548
|
-
INSERTED AS I ON
|
|
1549
|
-
${entity.PrimaryKeys.map(k => `_organicTable.[${k.Name}] = I.[${k.Name}]`).join(' AND ')};
|
|
1550
|
-
END;
|
|
1551
|
-
GO`;
|
|
1552
|
-
return triggerStatement;
|
|
1282
|
+
return this._dbProvider.generateTimestampTrigger(entity);
|
|
1553
1283
|
}
|
|
1554
1284
|
generateSPUpdate(entity) {
|
|
1555
|
-
|
|
1556
|
-
const efParamString = this.createEntityFieldsParamString(entity.Fields, true);
|
|
1557
|
-
const permissions = this.generateSPPermissions(entity, spName, SPType.Update);
|
|
1558
|
-
const hasUpdatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() === EntityInfo.UpdatedAtFieldName.trim().toLowerCase()) !== undefined;
|
|
1559
|
-
const updatedAtTrigger = hasUpdatedAtField ? this.generateUpdatedAtTrigger(entity) : '';
|
|
1560
|
-
let selectUpdatedRecord = `SELECT
|
|
1561
|
-
*
|
|
1562
|
-
FROM
|
|
1563
|
-
[${entity.SchemaName}].[${entity.BaseView}]
|
|
1564
|
-
WHERE
|
|
1565
|
-
${entity.PrimaryKeys.map(k => `[${k.Name}] = @${k.CodeName}`).join(' AND ')}
|
|
1566
|
-
`;
|
|
1567
|
-
return `
|
|
1568
|
-
------------------------------------------------------------
|
|
1569
|
-
----- UPDATE PROCEDURE FOR ${entity.BaseTable}
|
|
1570
|
-
------------------------------------------------------------
|
|
1571
|
-
IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
|
|
1572
|
-
DROP PROCEDURE [${entity.SchemaName}].[${spName}];
|
|
1573
|
-
GO
|
|
1574
|
-
|
|
1575
|
-
CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
|
|
1576
|
-
${efParamString}
|
|
1577
|
-
AS
|
|
1578
|
-
BEGIN
|
|
1579
|
-
SET NOCOUNT ON;
|
|
1580
|
-
UPDATE
|
|
1581
|
-
[${entity.SchemaName}].[${entity.BaseTable}]
|
|
1582
|
-
SET
|
|
1583
|
-
${this.createEntityFieldsUpdateString(entity.Fields)}
|
|
1584
|
-
WHERE
|
|
1585
|
-
${entity.PrimaryKeys.map(k => `[${k.Name}] = @${k.CodeName}`).join(' AND ')}
|
|
1586
|
-
|
|
1587
|
-
-- Check if the update was successful
|
|
1588
|
-
IF @@ROWCOUNT = 0
|
|
1589
|
-
-- Nothing was updated, return no rows, but column structure from base view intact, semantically correct this way.
|
|
1590
|
-
SELECT TOP 0 * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE 1=0
|
|
1591
|
-
ELSE
|
|
1592
|
-
-- Return the updated record so the caller can see the updated values and any calculated fields
|
|
1593
|
-
${selectUpdatedRecord}
|
|
1594
|
-
END
|
|
1595
|
-
GO
|
|
1596
|
-
${permissions}
|
|
1597
|
-
GO
|
|
1598
|
-
${updatedAtTrigger}
|
|
1599
|
-
`;
|
|
1285
|
+
return this._dbProvider.generateCRUDUpdate(entity);
|
|
1600
1286
|
}
|
|
1601
1287
|
/**
|
|
1602
1288
|
* Formats a default value for use in SQL, handling special cases like SQL functions
|
|
@@ -1605,209 +1291,20 @@ ${updatedAtTrigger}
|
|
|
1605
1291
|
* @returns Properly formatted default value for SQL
|
|
1606
1292
|
*/
|
|
1607
1293
|
formatDefaultValue(defaultValue, needsQuotes) {
|
|
1608
|
-
|
|
1609
|
-
return 'NULL';
|
|
1610
|
-
}
|
|
1611
|
-
let trimmedValue = defaultValue.trim();
|
|
1612
|
-
const lowerValue = trimmedValue.toLowerCase();
|
|
1613
|
-
// SQL functions that should not be quoted
|
|
1614
|
-
const sqlFunctions = [
|
|
1615
|
-
'newid()',
|
|
1616
|
-
'newsequentialid()',
|
|
1617
|
-
'getdate()',
|
|
1618
|
-
'getutcdate()',
|
|
1619
|
-
'sysdatetime()',
|
|
1620
|
-
'sysdatetimeoffset()',
|
|
1621
|
-
'current_timestamp',
|
|
1622
|
-
'user_name()',
|
|
1623
|
-
'suser_name()',
|
|
1624
|
-
'system_user'
|
|
1625
|
-
];
|
|
1626
|
-
// Check if this is a SQL function
|
|
1627
|
-
for (const func of sqlFunctions) {
|
|
1628
|
-
if (lowerValue.includes(func)) {
|
|
1629
|
-
// Remove outer parentheses if they exist (e.g., "(getutcdate())" -> "getutcdate()")
|
|
1630
|
-
if (trimmedValue.startsWith('(') && trimmedValue.endsWith(')')) {
|
|
1631
|
-
trimmedValue = trimmedValue.substring(1, trimmedValue.length - 1);
|
|
1632
|
-
}
|
|
1633
|
-
return trimmedValue;
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
// If the value already has quotes, remove them first
|
|
1637
|
-
let cleanValue = trimmedValue;
|
|
1638
|
-
if (cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
|
|
1639
|
-
cleanValue = cleanValue.substring(1, cleanValue.length - 1);
|
|
1640
|
-
}
|
|
1641
|
-
// Add quotes if needed
|
|
1642
|
-
if (needsQuotes) {
|
|
1643
|
-
return `'${cleanValue}'`;
|
|
1644
|
-
}
|
|
1645
|
-
return cleanValue;
|
|
1294
|
+
return this._dbProvider.formatDefaultValue(defaultValue, needsQuotes);
|
|
1646
1295
|
}
|
|
1647
1296
|
createEntityFieldsParamString(entityFields, isUpdate) {
|
|
1648
|
-
|
|
1649
|
-
for (let i = 0; i < entityFields.length; ++i) {
|
|
1650
|
-
const ef = entityFields[i];
|
|
1651
|
-
const autoGeneratedPrimaryKey = ef.AutoIncrement; // Only exclude auto-increment fields from params
|
|
1652
|
-
if ((ef.AllowUpdateAPI || (ef.IsPrimaryKey && isUpdate) || (ef.IsPrimaryKey && !autoGeneratedPrimaryKey && !isUpdate)) &&
|
|
1653
|
-
!ef.IsVirtual &&
|
|
1654
|
-
(!ef.IsPrimaryKey || !autoGeneratedPrimaryKey || isUpdate) &&
|
|
1655
|
-
!ef.IsSpecialDateField) {
|
|
1656
|
-
if (!isFirst)
|
|
1657
|
-
sOutput += ',\n ';
|
|
1658
|
-
else
|
|
1659
|
-
isFirst = false;
|
|
1660
|
-
// Check if we need a default value
|
|
1661
|
-
let defaultParamValue = '';
|
|
1662
|
-
if (!isUpdate && ef.IsPrimaryKey && !ef.AutoIncrement) {
|
|
1663
|
-
// For primary keys (non-auto-increment), make them optional with NULL default
|
|
1664
|
-
// This allows callers to omit the PK and let the DB/sproc handle it
|
|
1665
|
-
defaultParamValue = ' = NULL';
|
|
1666
|
-
}
|
|
1667
|
-
else if (!isUpdate && ef.HasDefaultValue && !ef.AllowsNull) {
|
|
1668
|
-
// For non-nullable fields with database defaults, make the parameter optional
|
|
1669
|
-
// This allows callers to pass NULL and let the database default be used
|
|
1670
|
-
defaultParamValue = ' = NULL';
|
|
1671
|
-
}
|
|
1672
|
-
sOutput += `@${ef.CodeName} ${ef.SQLFullType}${defaultParamValue}`;
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
return sOutput;
|
|
1297
|
+
return this._dbProvider.generateCRUDParamString(entityFields, isUpdate);
|
|
1676
1298
|
}
|
|
1677
1299
|
createEntityFieldsInsertString(entity, entityFields, prefix, excludePrimaryKey = false) {
|
|
1678
|
-
|
|
1679
|
-
let sOutput = '', isFirst = true;
|
|
1680
|
-
for (let i = 0; i < entityFields.length; ++i) {
|
|
1681
|
-
const ef = entityFields[i];
|
|
1682
|
-
// 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)
|
|
1683
|
-
// ALSO: if excludePrimaryKey is true, skip all primary key fields
|
|
1684
|
-
if ((excludePrimaryKey && ef.IsPrimaryKey) || (ef.IsPrimaryKey && autoGeneratedPrimaryKey) || ef.IsVirtual || !ef.AllowUpdateAPI || ef.AutoIncrement) {
|
|
1685
|
-
continue; // skip this field
|
|
1686
|
-
}
|
|
1687
|
-
if (!isFirst)
|
|
1688
|
-
sOutput += ',\n ';
|
|
1689
|
-
else
|
|
1690
|
-
isFirst = false;
|
|
1691
|
-
if (prefix !== '' && ef.IsSpecialDateField) {
|
|
1692
|
-
if (ef.IsCreatedAtField || ef.IsUpdatedAtField)
|
|
1693
|
-
sOutput += `GETUTCDATE()`; // we set the inserted row value to the current date for created and updated at fields
|
|
1694
|
-
else
|
|
1695
|
-
sOutput += `NULL`; // we don't set the deleted at field on an insert, only on a delete
|
|
1696
|
-
}
|
|
1697
|
-
else if ((prefix && prefix !== '') && !ef.IsPrimaryKey && ef.IsUniqueIdentifier && ef.HasDefaultValue && !ef.AllowsNull) {
|
|
1698
|
-
// this is the VALUE side (prefix not null/blank), is NOT a primary key, and is a uniqueidentifier column with a default value and does NOT allow NULL
|
|
1699
|
-
// We need to handle both NULL and the special value '00000000-0000-0000-0000-000000000000' for backward compatibility
|
|
1700
|
-
// Existing code uses the special value to indicate "use the default", so we preserve that behavior
|
|
1701
|
-
const formattedDefault = this.formatDefaultValue(ef.DefaultValue, ef.NeedsQuotes);
|
|
1702
|
-
sOutput += `CASE @${ef.CodeName} WHEN '00000000-0000-0000-0000-000000000000' THEN ${formattedDefault} ELSE ISNULL(@${ef.CodeName}, ${formattedDefault}) END`;
|
|
1703
|
-
}
|
|
1704
|
-
else {
|
|
1705
|
-
let sVal = '';
|
|
1706
|
-
if (!prefix || prefix.length === 0) {
|
|
1707
|
-
// Column name side
|
|
1708
|
-
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
|
|
1709
|
-
}
|
|
1710
|
-
else {
|
|
1711
|
-
// Value/parameter side
|
|
1712
|
-
sVal = prefix + ef.CodeName;
|
|
1713
|
-
// If this field has a default value and doesn't allow NULL, wrap with ISNULL
|
|
1714
|
-
// For UniqueIdentifier fields, also handle the special value '00000000-0000-0000-0000-000000000000' for backward compatibility
|
|
1715
|
-
if (ef.HasDefaultValue && !ef.AllowsNull) {
|
|
1716
|
-
const formattedDefault = this.formatDefaultValue(ef.DefaultValue, ef.NeedsQuotes);
|
|
1717
|
-
if (ef.IsUniqueIdentifier) {
|
|
1718
|
-
// Handle both NULL and the special UUID value for backward compatibility with existing code
|
|
1719
|
-
sVal = `CASE ${sVal} WHEN '00000000-0000-0000-0000-000000000000' THEN ${formattedDefault} ELSE ISNULL(${sVal}, ${formattedDefault}) END`;
|
|
1720
|
-
}
|
|
1721
|
-
else {
|
|
1722
|
-
sVal = `ISNULL(${sVal}, ${formattedDefault})`;
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
sOutput += sVal;
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
return sOutput;
|
|
1300
|
+
return this._dbProvider.generateInsertFieldString(entity, entityFields, prefix, excludePrimaryKey);
|
|
1730
1301
|
}
|
|
1731
1302
|
createEntityFieldsUpdateString(entityFields) {
|
|
1732
|
-
|
|
1733
|
-
for (let i = 0; i < entityFields.length; ++i) {
|
|
1734
|
-
const ef = entityFields[i];
|
|
1735
|
-
if (!ef.IsPrimaryKey &&
|
|
1736
|
-
!ef.IsVirtual &&
|
|
1737
|
-
ef.AllowUpdateAPI &&
|
|
1738
|
-
!ef.AutoIncrement &&
|
|
1739
|
-
!ef.IsSpecialDateField) {
|
|
1740
|
-
if (!isFirst)
|
|
1741
|
-
sOutput += ',\n ';
|
|
1742
|
-
else
|
|
1743
|
-
isFirst = false;
|
|
1744
|
-
sOutput += `[${ef.Name}] = @${ef.CodeName}`; // always put field names in brackets for field names that have spaces or use reserved words. Also, we use CodeName for the param name, which is the field name unless it has spaces
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
return sOutput;
|
|
1303
|
+
return this._dbProvider.generateUpdateFieldString(entityFields);
|
|
1748
1304
|
}
|
|
1749
1305
|
async generateSPDelete(entity, pool) {
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1752
|
-
const permissions = this.generateSPPermissions(entity, spName, SPType.Delete);
|
|
1753
|
-
let sVariables = '';
|
|
1754
|
-
let sSelect = '';
|
|
1755
|
-
for (let k of entity.PrimaryKeys) {
|
|
1756
|
-
if (sVariables !== '')
|
|
1757
|
-
sVariables += ', ';
|
|
1758
|
-
sVariables += `@${k.CodeName} ${k.SQLFullType}`;
|
|
1759
|
-
if (sSelect !== '')
|
|
1760
|
-
sSelect += ', ';
|
|
1761
|
-
sSelect += `@${k.CodeName} AS [${k.Name}]`;
|
|
1762
|
-
}
|
|
1763
|
-
// next up, create the delete code which is based on the type of delete the entity is set to
|
|
1764
|
-
// start off by creating the where clause first and then prepend the delete or update statement to it
|
|
1765
|
-
let deleteCode = ` WHERE
|
|
1766
|
-
${entity.PrimaryKeys.map(k => `[${k.Name}] = @${k.CodeName}`).join(' AND ')}
|
|
1767
|
-
`;
|
|
1768
|
-
if (entity.DeleteType === 'Hard') {
|
|
1769
|
-
deleteCode = ` DELETE FROM
|
|
1770
|
-
[${entity.SchemaName}].[${entity.BaseTable}]
|
|
1771
|
-
${deleteCode}`;
|
|
1772
|
-
}
|
|
1773
|
-
else {
|
|
1774
|
-
deleteCode = ` UPDATE
|
|
1775
|
-
[${entity.SchemaName}].[${entity.BaseTable}]
|
|
1776
|
-
SET
|
|
1777
|
-
${EntityInfo.DeletedAtFieldName} = GETUTCDATE()
|
|
1778
|
-
${deleteCode} AND ${EntityInfo.DeletedAtFieldName} IS NULL -- don't update the record if it's already been deleted via a soft delete`;
|
|
1779
|
-
}
|
|
1780
|
-
// Build the NULL select statement for when no rows are affected
|
|
1781
|
-
let sNullSelect = '';
|
|
1782
|
-
for (let k of entity.PrimaryKeys) {
|
|
1783
|
-
if (sNullSelect !== '')
|
|
1784
|
-
sNullSelect += ', ';
|
|
1785
|
-
sNullSelect += `NULL AS [${k.Name}]`;
|
|
1786
|
-
}
|
|
1787
|
-
return `
|
|
1788
|
-
------------------------------------------------------------
|
|
1789
|
-
----- DELETE PROCEDURE FOR ${entity.BaseTable}
|
|
1790
|
-
------------------------------------------------------------
|
|
1791
|
-
IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
|
|
1792
|
-
DROP PROCEDURE [${entity.SchemaName}].[${spName}];
|
|
1793
|
-
GO
|
|
1794
|
-
|
|
1795
|
-
CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
|
|
1796
|
-
${sVariables}
|
|
1797
|
-
AS
|
|
1798
|
-
BEGIN
|
|
1799
|
-
SET NOCOUNT ON;${sCascadeDeletes}
|
|
1800
|
-
|
|
1801
|
-
${deleteCode}
|
|
1802
|
-
|
|
1803
|
-
-- Check if the delete was successful
|
|
1804
|
-
IF @@ROWCOUNT = 0
|
|
1805
|
-
SELECT ${sNullSelect} -- Return NULL for all primary key fields to indicate no record was deleted
|
|
1806
|
-
ELSE
|
|
1807
|
-
SELECT ${sSelect} -- Return the primary key values to indicate we successfully deleted the record
|
|
1808
|
-
END
|
|
1809
|
-
GO${permissions}
|
|
1810
|
-
`;
|
|
1306
|
+
const cascadeSQL = await this.generateCascadeDeletes(entity, pool);
|
|
1307
|
+
return this._dbProvider.generateCRUDDelete(entity, cascadeSQL);
|
|
1811
1308
|
}
|
|
1812
1309
|
async generateCascadeDeletes(entity, pool) {
|
|
1813
1310
|
let sOutput = '';
|
|
@@ -1816,7 +1313,7 @@ GO${permissions}
|
|
|
1816
1313
|
// Find all fields in other entities that are foreign keys to this entity
|
|
1817
1314
|
for (const e of md.Entities) {
|
|
1818
1315
|
for (const ef of e.Fields) {
|
|
1819
|
-
if (ef.RelatedEntityID
|
|
1316
|
+
if (UUIDsEqual(ef.RelatedEntityID, entity.ID) && ef.IsVirtual === false) {
|
|
1820
1317
|
const cascadeSql = await this.generateSingleCascadeOperation(entity, e, ef, pool);
|
|
1821
1318
|
if (cascadeSql !== '') {
|
|
1822
1319
|
if (sOutput !== '')
|
|
@@ -1878,129 +1375,26 @@ GO${permissions}
|
|
|
1878
1375
|
return '';
|
|
1879
1376
|
}
|
|
1880
1377
|
generateCascadeCursorOperation(parentEntity, relatedEntity, fkField, operation) {
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
const cursorName = `cascade_${operation}_${relatedEntity.CodeName}_${fkField.CodeName}_cursor`;
|
|
1888
|
-
// Use a combined prefix that includes both entity name and FK field name to ensure unique variable names
|
|
1889
|
-
const variablePrefix = `${relatedEntity.CodeName}_${fkField.CodeName}`;
|
|
1890
|
-
// Determine which SP to call
|
|
1891
|
-
const spType = operation === 'delete' ? SPType.Delete : SPType.Update;
|
|
1892
|
-
const spName = this.getSPName(relatedEntity, spType);
|
|
1893
|
-
if (operation === 'update') {
|
|
1894
|
-
// For update, we need to include all updateable fields
|
|
1895
|
-
// Use the combined prefix to ensure uniqueness across multiple FKs to same entity
|
|
1896
|
-
const updateParams = this.buildUpdateCursorParameters(relatedEntity, fkField, variablePrefix);
|
|
1897
|
-
const spCallParams = updateParams.allParams;
|
|
1898
|
-
return `
|
|
1899
|
-
-- Cascade update on ${relatedEntity.BaseTable} using cursor to call ${spName}
|
|
1900
|
-
${updateParams.declarations}
|
|
1901
|
-
DECLARE ${cursorName} CURSOR FOR
|
|
1902
|
-
SELECT ${updateParams.selectFields}
|
|
1903
|
-
FROM [${relatedEntity.SchemaName}].[${relatedEntity.BaseTable}]
|
|
1904
|
-
WHERE ${whereClause}
|
|
1905
|
-
|
|
1906
|
-
OPEN ${cursorName}
|
|
1907
|
-
FETCH NEXT FROM ${cursorName} INTO ${updateParams.fetchInto}
|
|
1908
|
-
|
|
1909
|
-
WHILE @@FETCH_STATUS = 0
|
|
1910
|
-
BEGIN
|
|
1911
|
-
-- Set the FK field to NULL
|
|
1912
|
-
SET @${variablePrefix}_${fkField.CodeName} = NULL
|
|
1913
|
-
|
|
1914
|
-
-- Call the update SP for the related entity
|
|
1915
|
-
EXEC [${relatedEntity.SchemaName}].[${spName}] ${spCallParams}
|
|
1916
|
-
|
|
1917
|
-
FETCH NEXT FROM ${cursorName} INTO ${updateParams.fetchInto}
|
|
1918
|
-
END
|
|
1919
|
-
|
|
1920
|
-
CLOSE ${cursorName}
|
|
1921
|
-
DEALLOCATE ${cursorName}`;
|
|
1922
|
-
}
|
|
1923
|
-
// For delete operation, use a simpler prefix for primary keys only
|
|
1924
|
-
const pkComponents = this.buildPrimaryKeyComponents(relatedEntity, variablePrefix);
|
|
1925
|
-
return `
|
|
1926
|
-
-- Cascade delete from ${relatedEntity.BaseTable} using cursor to call ${spName}
|
|
1927
|
-
DECLARE ${pkComponents.varDeclarations}
|
|
1928
|
-
DECLARE ${cursorName} CURSOR FOR
|
|
1929
|
-
SELECT ${pkComponents.selectFields}
|
|
1930
|
-
FROM [${relatedEntity.SchemaName}].[${relatedEntity.BaseTable}]
|
|
1931
|
-
WHERE ${whereClause}
|
|
1932
|
-
|
|
1933
|
-
OPEN ${cursorName}
|
|
1934
|
-
FETCH NEXT FROM ${cursorName} INTO ${pkComponents.fetchInto}
|
|
1935
|
-
|
|
1936
|
-
WHILE @@FETCH_STATUS = 0
|
|
1937
|
-
BEGIN
|
|
1938
|
-
-- Call the delete SP for the related entity, which handles its own cascades
|
|
1939
|
-
EXEC [${relatedEntity.SchemaName}].[${spName}] ${pkComponents.spParams}
|
|
1940
|
-
|
|
1941
|
-
FETCH NEXT FROM ${cursorName} INTO ${pkComponents.fetchInto}
|
|
1942
|
-
END
|
|
1943
|
-
|
|
1944
|
-
CLOSE ${cursorName}
|
|
1945
|
-
DEALLOCATE ${cursorName}`;
|
|
1378
|
+
return this._dbProvider.generateSingleCascadeOperation({
|
|
1379
|
+
parentEntity,
|
|
1380
|
+
relatedEntity,
|
|
1381
|
+
fkField,
|
|
1382
|
+
operation,
|
|
1383
|
+
});
|
|
1946
1384
|
}
|
|
1947
1385
|
buildPrimaryKeyComponents(entity, prefix = '') {
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
varDeclarations += ', ';
|
|
1956
|
-
varDeclarations += `@${varPrefix}${pk.CodeName} ${pk.SQLFullType}`;
|
|
1957
|
-
if (selectFields !== '')
|
|
1958
|
-
selectFields += ', ';
|
|
1959
|
-
selectFields += `[${pk.Name}]`;
|
|
1960
|
-
if (fetchInto !== '')
|
|
1961
|
-
fetchInto += ', ';
|
|
1962
|
-
fetchInto += `@${varPrefix}${pk.CodeName}`;
|
|
1963
|
-
if (spParams !== '')
|
|
1964
|
-
spParams += ', ';
|
|
1965
|
-
// Use named parameters: @ParamName = @VariableValue
|
|
1966
|
-
spParams += `@${pk.CodeName} = @${varPrefix}${pk.CodeName}`;
|
|
1967
|
-
}
|
|
1968
|
-
return { varDeclarations, selectFields, fetchInto, spParams };
|
|
1386
|
+
const result = this._dbProvider.buildPrimaryKeyComponents(entity, prefix || undefined);
|
|
1387
|
+
return {
|
|
1388
|
+
varDeclarations: result.varDeclarations,
|
|
1389
|
+
selectFields: result.selectFields,
|
|
1390
|
+
fetchInto: result.fetchInto,
|
|
1391
|
+
spParams: result.routineParams,
|
|
1392
|
+
};
|
|
1969
1393
|
}
|
|
1970
1394
|
buildUpdateCursorParameters(entity, _fkField, prefix = '') {
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
let allParams = '';
|
|
1975
|
-
const varPrefix = prefix || entity.CodeName;
|
|
1976
|
-
// First, handle primary keys with the entity-specific prefix
|
|
1977
|
-
const pkComponents = this.buildPrimaryKeyComponents(entity, varPrefix);
|
|
1978
|
-
// Add primary key declarations to the declarations string
|
|
1979
|
-
// Need to add DECLARE keyword since buildPrimaryKeyComponents doesn't include it
|
|
1980
|
-
declarations = pkComponents.varDeclarations.split(', ').map(decl => `DECLARE ${decl}`).join('\n ');
|
|
1981
|
-
selectFields = pkComponents.selectFields;
|
|
1982
|
-
fetchInto = pkComponents.fetchInto;
|
|
1983
|
-
allParams = pkComponents.spParams;
|
|
1984
|
-
// Then, add all updateable fields with the same prefix
|
|
1985
|
-
const sortedFields = sortBySequenceAndCreatedAt(entity.Fields);
|
|
1986
|
-
for (const ef of sortedFields) {
|
|
1987
|
-
if (!ef.IsPrimaryKey && !ef.IsVirtual && ef.AllowUpdateAPI && !ef.AutoIncrement && !ef.IsSpecialDateField) {
|
|
1988
|
-
if (declarations !== '')
|
|
1989
|
-
declarations += '\n ';
|
|
1990
|
-
declarations += `DECLARE @${varPrefix}_${ef.CodeName} ${ef.SQLFullType}`;
|
|
1991
|
-
if (selectFields !== '')
|
|
1992
|
-
selectFields += ', ';
|
|
1993
|
-
selectFields += `[${ef.Name}]`;
|
|
1994
|
-
if (fetchInto !== '')
|
|
1995
|
-
fetchInto += ', ';
|
|
1996
|
-
fetchInto += `@${varPrefix}_${ef.CodeName}`;
|
|
1997
|
-
if (allParams !== '')
|
|
1998
|
-
allParams += ', ';
|
|
1999
|
-
// Use named parameters: @ParamName = @VariableValue
|
|
2000
|
-
allParams += `@${ef.CodeName} = @${varPrefix}_${ef.CodeName}`;
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
return { declarations, selectFields, fetchInto, allParams };
|
|
1395
|
+
// This method is now only called by generateCascadeCursorOperation which
|
|
1396
|
+
// delegates to the provider. Kept for backward compatibility.
|
|
1397
|
+
throw new Error('buildUpdateCursorParameters should be handled by the database provider');
|
|
2004
1398
|
}
|
|
2005
1399
|
/**
|
|
2006
1400
|
* Checks if a field is part of a composite (multi-column) unique constraint.
|
|
@@ -2009,36 +1403,9 @@ GO${permissions}
|
|
|
2009
1403
|
* during cascade operations, since setting to NULL could violate uniqueness.
|
|
2010
1404
|
*/
|
|
2011
1405
|
async isFieldInCompositeUniqueConstraint(pool, schemaName, tableName, columnName) {
|
|
2012
|
-
const query =
|
|
2013
|
-
SELECT i.index_id
|
|
2014
|
-
FROM sys.indexes i
|
|
2015
|
-
INNER JOIN sys.tables t ON i.object_id = t.object_id
|
|
2016
|
-
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
2017
|
-
WHERE i.is_unique = 1
|
|
2018
|
-
AND i.is_primary_key = 0
|
|
2019
|
-
AND s.name = @schemaName
|
|
2020
|
-
AND t.name = @tableName
|
|
2021
|
-
-- This index contains the specified column
|
|
2022
|
-
AND EXISTS (
|
|
2023
|
-
SELECT 1
|
|
2024
|
-
FROM sys.index_columns ic
|
|
2025
|
-
INNER JOIN sys.columns c ON ic.object_id = c.object_id AND c.column_id = ic.column_id
|
|
2026
|
-
WHERE ic.object_id = i.object_id
|
|
2027
|
-
AND ic.index_id = i.index_id
|
|
2028
|
-
AND c.name = @columnName
|
|
2029
|
-
)
|
|
2030
|
-
-- This index has more than one column (composite)
|
|
2031
|
-
AND (SELECT COUNT(*)
|
|
2032
|
-
FROM sys.index_columns ic2
|
|
2033
|
-
WHERE ic2.object_id = i.object_id
|
|
2034
|
-
AND ic2.index_id = i.index_id) > 1
|
|
2035
|
-
`;
|
|
1406
|
+
const query = this._dbProvider.getCompositeUniqueConstraintCheckSQL(schemaName, tableName, columnName);
|
|
2036
1407
|
try {
|
|
2037
|
-
const result = await pool.
|
|
2038
|
-
.input('schemaName', sql.NVarChar, schemaName)
|
|
2039
|
-
.input('tableName', sql.NVarChar, tableName)
|
|
2040
|
-
.input('columnName', sql.NVarChar, columnName)
|
|
2041
|
-
.query(query);
|
|
1408
|
+
const result = await pool.query(query);
|
|
2042
1409
|
return result.recordset.length > 0;
|
|
2043
1410
|
}
|
|
2044
1411
|
catch (error) {
|
|
@@ -2056,10 +1423,10 @@ GO${permissions}
|
|
|
2056
1423
|
// Find all fields in other entities that are foreign keys to this entity
|
|
2057
1424
|
for (const e of md.Entities) {
|
|
2058
1425
|
for (const ef of e.Fields) {
|
|
2059
|
-
if (ef.RelatedEntityID
|
|
1426
|
+
if (UUIDsEqual(ef.RelatedEntityID, entity.ID) && ef.IsVirtual === false) {
|
|
2060
1427
|
// Skip self-referential foreign keys (e.g., ParentID pointing to same entity)
|
|
2061
1428
|
// These don't create inter-entity dependencies for ordering purposes
|
|
2062
|
-
if (e.ID
|
|
1429
|
+
if (UUIDsEqual(e.ID, entity.ID)) {
|
|
2063
1430
|
continue;
|
|
2064
1431
|
}
|
|
2065
1432
|
// Check if this would generate a cascade operation
|
|
@@ -2114,9 +1481,9 @@ GO${permissions}
|
|
|
2114
1481
|
// Log the dependency map
|
|
2115
1482
|
logStatus(`Cascade delete dependency map built:`);
|
|
2116
1483
|
for (const [dependedOnEntityId, dependentEntityIds] of this.cascadeDeleteDependencies) {
|
|
2117
|
-
const dependedOnEntity = entities.find(e => e.ID
|
|
1484
|
+
const dependedOnEntity = entities.find(e => UUIDsEqual(e.ID, dependedOnEntityId));
|
|
2118
1485
|
const dependentNames = Array.from(dependentEntityIds)
|
|
2119
|
-
.map(id => entities.find(e => e.ID
|
|
1486
|
+
.map(id => entities.find(e => UUIDsEqual(e.ID, id))?.Name || id)
|
|
2120
1487
|
.join(', ');
|
|
2121
1488
|
logStatus(` ${dependedOnEntity?.Name || dependedOnEntityId} is depended on by: ${dependentNames}`);
|
|
2122
1489
|
}
|
|
@@ -2188,7 +1555,7 @@ GO${permissions}
|
|
|
2188
1555
|
logStatus(`Identified ${entitiesNeedingRegeneration.size} entities requiring delete SP regeneration due to cascade dependencies`);
|
|
2189
1556
|
// Store the entity IDs that need regeneration (only if spDeleteGenerated=true)
|
|
2190
1557
|
for (const entityId of entitiesNeedingRegeneration) {
|
|
2191
|
-
const entity = entities.find(e => e.ID
|
|
1558
|
+
const entity = entities.find(e => UUIDsEqual(e.ID, entityId));
|
|
2192
1559
|
if (entity && entity.spDeleteGenerated) {
|
|
2193
1560
|
this.entitiesNeedingDeleteSPRegeneration.add(entityId);
|
|
2194
1561
|
logStatus(` - Marked ${entity.Name} for delete SP regeneration (cascade dependency)`);
|
|
@@ -2202,7 +1569,7 @@ GO${permissions}
|
|
|
2202
1569
|
if (this.orderedEntitiesForDeleteSPRegeneration.length > 0) {
|
|
2203
1570
|
logStatus(`Ordered entities for delete SP regeneration:`);
|
|
2204
1571
|
this.orderedEntitiesForDeleteSPRegeneration.forEach((entityId, index) => {
|
|
2205
|
-
const entity = entities.find(e => e.ID
|
|
1572
|
+
const entity = entities.find(e => UUIDsEqual(e.ID, entityId));
|
|
2206
1573
|
logStatus(` ${index + 1}. ${entity?.Name || entityId}`);
|
|
2207
1574
|
});
|
|
2208
1575
|
}
|
|
@@ -2250,7 +1617,7 @@ GO${permissions}
|
|
|
2250
1617
|
}
|
|
2251
1618
|
if (visiting.has(entityId)) {
|
|
2252
1619
|
// Circular dependency detected - mark it but don't fail
|
|
2253
|
-
const entity = entities.find(e => e.ID
|
|
1620
|
+
const entity = entities.find(e => UUIDsEqual(e.ID, entityId));
|
|
2254
1621
|
logStatus(`Warning: Circular cascade delete dependency detected involving ${entity?.Name || entityId}`);
|
|
2255
1622
|
circularDeps.add(entityId);
|
|
2256
1623
|
return false; // Signal circular dependency but continue processing
|
|
@@ -2277,7 +1644,7 @@ GO${permissions}
|
|
|
2277
1644
|
if (!success && circularDeps.has(entityId)) {
|
|
2278
1645
|
// Entity is part of circular dependency - add it anyway in arbitrary order
|
|
2279
1646
|
// The SQL will still be generated, just not in perfect dependency order
|
|
2280
|
-
logStatus(` - Adding ${entities.find(e => e.ID
|
|
1647
|
+
logStatus(` - Adding ${entities.find(e => UUIDsEqual(e.ID, entityId))?.Name || entityId} despite circular dependency`);
|
|
2281
1648
|
visited.add(entityId);
|
|
2282
1649
|
ordered.push(entityId);
|
|
2283
1650
|
}
|