@memberjunction/codegen-lib 5.30.0 → 5.31.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 +2 -0
- package/dist/Angular/angular-codegen.js +3 -3
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/related-entity-components.js +1 -1
- package/dist/Angular/related-entity-components.js.map +1 -1
- package/dist/Config/config.js +1 -1
- package/dist/Config/config.js.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts +266 -12
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.js +352 -0
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +86 -87
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +105 -17
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +324 -94
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts +99 -0
- package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/viewDependencyCapture.js +170 -0
- package/dist/Database/providers/postgresql/viewDependencyCapture.js.map +1 -0
- package/dist/Database/providers/postgresql/viewFallback.d.ts +75 -0
- package/dist/Database/providers/postgresql/viewFallback.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/viewFallback.js +172 -0
- package/dist/Database/providers/postgresql/viewFallback.js.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +0 -25
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +9 -113
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js +207 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js.map +1 -0
- package/dist/Database/sql.d.ts +9 -1
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +41 -7
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +30 -0
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +205 -44
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.js +1 -1
- package/dist/Misc/advanced_generation.js.map +1 -1
- package/dist/Misc/createNewUser.js +2 -2
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +5 -5
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +6 -6
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +14 -4
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +66 -2
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +18 -18
|
@@ -149,25 +149,43 @@ export class SQLCodeGenBase {
|
|
|
149
149
|
// STEP 2(b) - generate all the SQL files and execute them
|
|
150
150
|
startSpinner(`Generating SQL for ${includedEntities.length} entities...`);
|
|
151
151
|
const step2StartTime = new Date();
|
|
152
|
+
// When the provider offers a phased executor (PG today), run per-entity
|
|
153
|
+
// execution DURING file generation. This gives us view → CRUD fns →
|
|
154
|
+
// grants phasing with 42P16 recovery, and lets us skip the bulk
|
|
155
|
+
// file-execution pass at step 2(e) — re-running the same SQL there
|
|
156
|
+
// could re-trigger 42P16 without the phased recovery wrapper.
|
|
157
|
+
const useProviderPhasedExecution = !!this._dbProvider.executeEntityPhased;
|
|
158
|
+
const perEntitySkipExecution = !useProviderPhasedExecution;
|
|
152
159
|
// First, separate entities that need cascade delete regeneration from others
|
|
153
160
|
const entitiesWithoutCascadeRegeneration = includedEntities.filter(e => !this.entitiesNeedingDeleteSPRegeneration.has(e.ID));
|
|
154
161
|
const entitiesForCascadeRegeneration = this.orderedEntitiesForDeleteSPRegeneration
|
|
155
162
|
.map(id => includedEntities.find(e => UUIDsEqual(e.ID, id)))
|
|
156
163
|
.filter(e => e !== undefined);
|
|
164
|
+
// Track per-batch success without short-circuiting. Historically,
|
|
165
|
+
// any single batch failure here returned `false` immediately, which
|
|
166
|
+
// skipped the cascade-regen and excluded-perms batches and left
|
|
167
|
+
// their entities with no SQL emitted at all. The post-run CRUD
|
|
168
|
+
// validator (in runCodeGen.ts) is the authoritative ship-gate now,
|
|
169
|
+
// so we want every batch to attempt its work and surface the
|
|
170
|
+
// complete picture of what's missing — partial generation is
|
|
171
|
+
// strictly more useful than nothing.
|
|
172
|
+
let entityGenSuccess = true;
|
|
157
173
|
// Generate SQL for entities that don't need cascade delete regeneration
|
|
158
174
|
const genResult = await this.generateAndExecuteEntitySQLToSeparateFiles({
|
|
159
175
|
pool,
|
|
160
176
|
entities: entitiesWithoutCascadeRegeneration,
|
|
161
177
|
directory,
|
|
162
178
|
onlyPermissions: false,
|
|
163
|
-
|
|
179
|
+
// PG uses phased per-entity execution (with 42P16 fallback);
|
|
180
|
+
// other dialects defer to the bulk step 2(e) below.
|
|
181
|
+
skipExecution: perEntitySkipExecution,
|
|
164
182
|
writeFiles: true,
|
|
165
183
|
batchSize: 5,
|
|
166
184
|
enableSQLLoggingForNewOrModifiedEntities: true
|
|
167
185
|
}); // enable sql logging for NEW entities....
|
|
168
186
|
if (!genResult.Success) {
|
|
169
|
-
|
|
170
|
-
|
|
187
|
+
logError('Main entity SQL generation batch had failures — continuing with cascade-regen and excluded-perms batches; validator will report any missing routines.');
|
|
188
|
+
entityGenSuccess = false;
|
|
171
189
|
}
|
|
172
190
|
// Generate SQL for cascade delete regenerations in dependency order (sequentially)
|
|
173
191
|
if (entitiesForCascadeRegeneration.length > 0) {
|
|
@@ -177,14 +195,14 @@ export class SQLCodeGenBase {
|
|
|
177
195
|
entities: entitiesForCascadeRegeneration,
|
|
178
196
|
directory,
|
|
179
197
|
onlyPermissions: false,
|
|
180
|
-
skipExecution:
|
|
198
|
+
skipExecution: perEntitySkipExecution,
|
|
181
199
|
writeFiles: true,
|
|
182
200
|
batchSize: 1, // Process sequentially to maintain dependency order
|
|
183
201
|
enableSQLLoggingForNewOrModifiedEntities: true
|
|
184
202
|
});
|
|
185
203
|
if (!cascadeGenResult.Success) {
|
|
186
|
-
|
|
187
|
-
|
|
204
|
+
logError('Cascade-regen batch had failures — continuing; validator will report any missing routines.');
|
|
205
|
+
entityGenSuccess = false;
|
|
188
206
|
}
|
|
189
207
|
genResult.Files.push(...cascadeGenResult.Files);
|
|
190
208
|
}
|
|
@@ -201,10 +219,15 @@ export class SQLCodeGenBase {
|
|
|
201
219
|
enableSQLLoggingForNewOrModifiedEntities: false /*don't log this stuff, it is just permissions for excluded entities*/
|
|
202
220
|
});
|
|
203
221
|
if (!genResult2.Success) {
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
logError('Excluded-entities permissions batch had failures — continuing; validator will report any missing routines.');
|
|
223
|
+
entityGenSuccess = false;
|
|
224
|
+
}
|
|
225
|
+
if (entityGenSuccess) {
|
|
226
|
+
succeedSpinner(`Entity generation completed (${(new Date().getTime() - step2StartTime.getTime()) / 1000}s)`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
failSpinner(`Entity generation completed with batch failures (${(new Date().getTime() - step2StartTime.getTime()) / 1000}s) — see post-run CRUD validator for the authoritative gap report`);
|
|
206
230
|
}
|
|
207
|
-
succeedSpinner(`Entity generation completed (${(new Date().getTime() - step2StartTime.getTime()) / 1000}s)`);
|
|
208
231
|
// 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
|
|
209
232
|
startSpinner('Creating combined SQL files...');
|
|
210
233
|
const allEntityFiles = this.createCombinedEntitySQLFiles(directory, baselineEntities);
|
|
@@ -214,7 +237,16 @@ export class SQLCodeGenBase {
|
|
|
214
237
|
startSpinner('Executing entity SQL files...');
|
|
215
238
|
const step2eStartTime = new Date();
|
|
216
239
|
let executionSuccess = false;
|
|
217
|
-
if (
|
|
240
|
+
if (useProviderPhasedExecution) {
|
|
241
|
+
// Per-entity phased execution already ran during steps 2(b)/(c).
|
|
242
|
+
// Skip the bulk file re-execution — replaying the same SQL here
|
|
243
|
+
// would re-trigger 42P16 without the phased recovery wrapper and
|
|
244
|
+
// undo the work the phased executor just did.
|
|
245
|
+
TempBatchFile.cleanup();
|
|
246
|
+
executionSuccess = true;
|
|
247
|
+
logIf(configInfo?.verboseOutput ?? false, 'Skipping bulk SQL file execution — per-entity phased execution already ran');
|
|
248
|
+
}
|
|
249
|
+
else if (TempBatchFile.hasContent()) {
|
|
218
250
|
// Execute temp batch files in dependency order (matches CodeGen run log)
|
|
219
251
|
const tempFiles = TempBatchFile.getTempFilePaths();
|
|
220
252
|
logIf(configInfo?.verboseOutput ?? false, `Executing ${tempFiles.length} temp batch file(s) in dependency order`);
|
|
@@ -227,7 +259,9 @@ export class SQLCodeGenBase {
|
|
|
227
259
|
logIf(configInfo?.verboseOutput ?? false, `Executing ${allEntityFiles.length} combined file(s)`);
|
|
228
260
|
executionSuccess = await this.SQLUtilityObject.executeSQLFiles(allEntityFiles, configInfo?.verboseOutput ?? false);
|
|
229
261
|
}
|
|
230
|
-
|
|
262
|
+
// Initialize from entityGenSuccess so any per-batch failures from
|
|
263
|
+
// STEP 2(b)/(c) carry forward instead of being lost. Bug 2 fix.
|
|
264
|
+
let overallSuccess = entityGenSuccess;
|
|
231
265
|
if (!executionSuccess) {
|
|
232
266
|
failSpinner('Failed to execute entity SQL files');
|
|
233
267
|
TempBatchFile.cleanup(); // Cleanup on error
|
|
@@ -273,7 +307,7 @@ export class SQLCodeGenBase {
|
|
|
273
307
|
const uniqueEntityNames = [...new Set(regenList.map(e => e.EntityName))];
|
|
274
308
|
startSpinner(`Regenerating ${uniqueEntityNames.length} entities with late-phase view changes...`);
|
|
275
309
|
// Refresh metadata to pick up DB changes from Step 3 (e.g., SupportsGeoCoding = 1)
|
|
276
|
-
const regenMd = new Metadata();
|
|
310
|
+
const regenMd = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
277
311
|
await regenMd.Refresh();
|
|
278
312
|
// Resolve entity names to EntityInfo objects from the refreshed metadata
|
|
279
313
|
const entitiesToRegen = uniqueEntityNames
|
|
@@ -342,7 +376,7 @@ export class SQLCodeGenBase {
|
|
|
342
376
|
succeedSpinner(`SQL CodeGen completed successfully (${((new Date().getTime() - startTime.getTime()) / 1000)}s total)`);
|
|
343
377
|
}
|
|
344
378
|
// now - we need to tell our metadata object to refresh itself
|
|
345
|
-
const md = new Metadata();
|
|
379
|
+
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
346
380
|
await md.Refresh();
|
|
347
381
|
return overallSuccess;
|
|
348
382
|
}
|
|
@@ -449,34 +483,51 @@ export class SQLCodeGenBase {
|
|
|
449
483
|
options.batchSize = 5; // default to 5 if not specified
|
|
450
484
|
const files = [];
|
|
451
485
|
try {
|
|
452
|
-
|
|
486
|
+
const failedEntities = [];
|
|
453
487
|
const totalEntities = options.entities.length;
|
|
488
|
+
// Build the will-regenerate set once per batch. A dialect phased
|
|
489
|
+
// executor (e.g. PG) uses this to skip restoring dependents that
|
|
490
|
+
// are about to be rebuilt with new definitions — avoids stale
|
|
491
|
+
// captured definitions conflicting with newly-regenerated targets.
|
|
492
|
+
const willRegenerate = new Set(options.entities.map(e => `${e.SchemaName}.${e.BaseView}`));
|
|
454
493
|
for (let i = 0; i < totalEntities; i += options.batchSize) {
|
|
455
494
|
const batch = options.entities.slice(i, i + options.batchSize);
|
|
456
495
|
const promises = batch.map(async (e) => {
|
|
457
496
|
const pkeyField = e.Fields.find(f => f.IsPrimaryKey);
|
|
458
497
|
if (!pkeyField) {
|
|
459
498
|
logError(`SKIPPING SQL GENERATION: Entity ${e.Name} has no primary key field in metadata. If using soft primary keys, ensure metadata was refreshed after applySoftPKFKConfig().`);
|
|
460
|
-
return { Success: false, Files: [] };
|
|
499
|
+
return { entity: e, result: { Success: false, Files: [] } };
|
|
461
500
|
}
|
|
462
|
-
|
|
501
|
+
const result = await this.generateAndExecuteSingleEntitySQLToSeparateFiles({
|
|
463
502
|
pool: options.pool,
|
|
464
503
|
entity: e,
|
|
465
504
|
directory: options.directory,
|
|
466
505
|
onlyPermissions: options.onlyPermissions,
|
|
467
506
|
writeFiles: options.writeFiles,
|
|
468
507
|
skipExecution: options.skipExecution,
|
|
469
|
-
enableSQLLoggingForNewOrModifiedEntities: options.enableSQLLoggingForNewOrModifiedEntities
|
|
508
|
+
enableSQLLoggingForNewOrModifiedEntities: options.enableSQLLoggingForNewOrModifiedEntities,
|
|
509
|
+
willRegenerate,
|
|
470
510
|
});
|
|
511
|
+
return { entity: e, result };
|
|
471
512
|
});
|
|
472
513
|
const results = await Promise.all(promises);
|
|
473
|
-
results.forEach(
|
|
474
|
-
if (!
|
|
475
|
-
|
|
476
|
-
files.push(...
|
|
514
|
+
results.forEach(({ entity, result }) => {
|
|
515
|
+
if (!result.Success)
|
|
516
|
+
failedEntities.push(entity);
|
|
517
|
+
files.push(...result.Files); // add the files to the main files array
|
|
477
518
|
});
|
|
478
519
|
}
|
|
479
|
-
|
|
520
|
+
// Batch summary — makes the full failure scope visible in one pass instead of
|
|
521
|
+
// forcing the operator to bisect across individually-logged per-entity errors.
|
|
522
|
+
// Per-entity error detail is already emitted inside generateAndExecuteSingleEntitySQLToSeparateFiles;
|
|
523
|
+
// this just recaps WHO failed so it's easy to spot.
|
|
524
|
+
if (failedEntities.length > 0) {
|
|
525
|
+
const list = failedEntities
|
|
526
|
+
.map(e => ` - ${e.SchemaName}.${e.Name}`)
|
|
527
|
+
.join('\n');
|
|
528
|
+
logError(`SQL generation/execution failed for ${failedEntities.length} of ${totalEntities} entity(ies):\n${list}`);
|
|
529
|
+
}
|
|
530
|
+
return { Success: failedEntities.length === 0, Files: files };
|
|
480
531
|
}
|
|
481
532
|
catch (err) {
|
|
482
533
|
logError(err);
|
|
@@ -531,20 +582,96 @@ export class SQLCodeGenBase {
|
|
|
531
582
|
async generateAndExecuteSingleEntitySQLToSeparateFiles(options) {
|
|
532
583
|
try {
|
|
533
584
|
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
|
|
534
|
-
if (
|
|
535
|
-
return {
|
|
536
|
-
Success: await this.SQLUtilityObject.executeSQLScript(options.pool, sql + "\n\nGO\n\n" + permissionsSQL, true), // combine the SQL and permissions and execute it,
|
|
537
|
-
Files: files
|
|
538
|
-
};
|
|
585
|
+
if (options.skipExecution) {
|
|
586
|
+
return { Success: true, Files: files };
|
|
539
587
|
}
|
|
540
|
-
|
|
588
|
+
// Dialect-specific phased execution path — providers that implement
|
|
589
|
+
// executeEntityPhased run view → CRUD functions → permissions in
|
|
590
|
+
// distinct phases so a view failure doesn't silently skip the
|
|
591
|
+
// CREATE FUNCTIONs that follow (pg simple-query-protocol). Phase 2
|
|
592
|
+
// is explicitly gated on phase 1 success so we never create fn_*
|
|
593
|
+
// against a missing or stale view rowtype.
|
|
594
|
+
if (!options.onlyPermissions && this._dbProvider.executeEntityPhased) {
|
|
595
|
+
const phaseResult = await this.executeEntityInPhases(options.pool, options.entity, options.willRegenerate);
|
|
596
|
+
if (!phaseResult.success) {
|
|
597
|
+
const where = phaseResult.phase ?? 'unknown';
|
|
598
|
+
logError(`Phased execution for ${options.entity.SchemaName}.${options.entity.Name} failed in phase "${where}": ${phaseResult.error?.message ?? '(no message)'}`);
|
|
599
|
+
return { Success: false, Files: files };
|
|
600
|
+
}
|
|
541
601
|
return { Success: true, Files: files };
|
|
602
|
+
}
|
|
603
|
+
// Fallback: original monolithic path (SQL Server; also the path when
|
|
604
|
+
// `onlyPermissions` is set — then we just want the permissions SQL).
|
|
605
|
+
return {
|
|
606
|
+
Success: await this.SQLUtilityObject.executeSQLScript(options.pool, sql + "\n\nGO\n\n" + permissionsSQL, true),
|
|
607
|
+
Files: files
|
|
608
|
+
};
|
|
542
609
|
}
|
|
543
610
|
catch (err) {
|
|
544
611
|
logError(err);
|
|
545
612
|
return { Success: false, Files: [] };
|
|
546
613
|
}
|
|
547
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Generates the entity's per-phase SQL pieces (view, CRUD functions, view
|
|
617
|
+
* permissions) and dispatches them to the provider's phased executor. Lives
|
|
618
|
+
* here because it needs access to the internal generators; the provider
|
|
619
|
+
* doesn't own context building for the view.
|
|
620
|
+
*/
|
|
621
|
+
async executeEntityInPhases(pool, entity, willRegenerate) {
|
|
622
|
+
// Pieces needed for the phased executor. Empty string when the entity
|
|
623
|
+
// doesn't warrant that phase's DDL (e.g. virtual entities don't have
|
|
624
|
+
// views; entities with AllowCreateAPI=false don't get fn_create_*).
|
|
625
|
+
// Root-ID TVFs — entities with self-referencing ParentID FKs need a
|
|
626
|
+
// recursive helper function (fn_<table>_<field>_get_root_id) that the
|
|
627
|
+
// base view references. These MUST exist before phase 1 (view) runs;
|
|
628
|
+
// otherwise PG raises `function does not exist` and phase 2 (CRUD)
|
|
629
|
+
// gets gated off.
|
|
630
|
+
const tvfSQL = entity.BaseViewGenerated && !entity.VirtualEntity
|
|
631
|
+
? this.generateRecursiveFKTVFs(entity)
|
|
632
|
+
: '';
|
|
633
|
+
const viewPieces = entity.BaseViewGenerated && !entity.VirtualEntity
|
|
634
|
+
? await this.generateBaseViewPieces(pool, entity)
|
|
635
|
+
: { viewSQL: '', viewPermSQL: '' };
|
|
636
|
+
const crudCreateSQL = entity.AllowCreateAPI && entity.spCreateGenerated && !entity.VirtualEntity
|
|
637
|
+
? this.generateSPCreate(entity)
|
|
638
|
+
: '';
|
|
639
|
+
const crudUpdateSQL = entity.AllowUpdateAPI && entity.spUpdateGenerated && !entity.VirtualEntity
|
|
640
|
+
? this.generateSPUpdate(entity)
|
|
641
|
+
: '';
|
|
642
|
+
const crudDeleteSQL = entity.AllowDeleteAPI && entity.spDeleteGenerated && !entity.VirtualEntity
|
|
643
|
+
? await this.generateSPDelete(entity, pool)
|
|
644
|
+
: '';
|
|
645
|
+
return this._dbProvider.executeEntityPhased({
|
|
646
|
+
entity,
|
|
647
|
+
tvfSQL,
|
|
648
|
+
viewSQL: viewPieces.viewSQL,
|
|
649
|
+
crudCreateSQL,
|
|
650
|
+
crudUpdateSQL,
|
|
651
|
+
crudDeleteSQL,
|
|
652
|
+
viewPermSQL: viewPieces.viewPermSQL,
|
|
653
|
+
willRegenerate,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Concatenates the root-ID TVF SQL for every recursive ParentID-style FK
|
|
658
|
+
* on this entity. Mirrors the inline TVF generation in
|
|
659
|
+
* `generateSingleEntitySQLToSeparateFiles` so the phased executor can run
|
|
660
|
+
* the same DDL ahead of the base view.
|
|
661
|
+
*/
|
|
662
|
+
generateRecursiveFKTVFs(entity) {
|
|
663
|
+
const recursiveFKs = this.detectRecursiveForeignKeys(entity);
|
|
664
|
+
if (recursiveFKs.length === 0)
|
|
665
|
+
return '';
|
|
666
|
+
let combined = '';
|
|
667
|
+
for (const field of recursiveFKs) {
|
|
668
|
+
const functionName = `fn${entity.BaseTable}${field.Name}_GetRootID`;
|
|
669
|
+
combined += this.generateSingleEntitySQLFileHeader(entity, functionName)
|
|
670
|
+
+ this.generateRootIDFunction(entity, field)
|
|
671
|
+
+ '\n' + this._dbProvider.BatchSeparator + '\n';
|
|
672
|
+
}
|
|
673
|
+
return combined;
|
|
674
|
+
}
|
|
548
675
|
/**
|
|
549
676
|
* Writes a generated SQL file to disk only if the content has changed from the existing file.
|
|
550
677
|
* Returns true if the file was written (content changed or file is new), false if unchanged.
|
|
@@ -705,7 +832,7 @@ export class SQLCodeGenBase {
|
|
|
705
832
|
* (BaseViewGenerated === false), are included in the API, and are not virtual.
|
|
706
833
|
*/
|
|
707
834
|
getModifiedCustomBaseViewEntities() {
|
|
708
|
-
const md = new Metadata();
|
|
835
|
+
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
709
836
|
const modifiedOrNewNames = [
|
|
710
837
|
...ManageMetadataBase.modifiedEntityList,
|
|
711
838
|
...ManageMetadataBase.newEntityList,
|
|
@@ -1036,7 +1163,7 @@ export class SQLCodeGenBase {
|
|
|
1036
1163
|
? entity.FullTextSearchFunction
|
|
1037
1164
|
: `fnSearch${entity.CodeName}`;
|
|
1038
1165
|
if (entity.FullTextSearchFunctionGenerated && (!entity.FullTextSearchFunction || entity.FullTextSearchFunction.length === 0)) {
|
|
1039
|
-
const md = new Metadata();
|
|
1166
|
+
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
1040
1167
|
const u = UserCache.Instance.Users[0];
|
|
1041
1168
|
if (!u)
|
|
1042
1169
|
throw new Error('Could not find the first user in the cache, cant generate the full text search function without a user');
|
|
@@ -1197,7 +1324,18 @@ export class SQLCodeGenBase {
|
|
|
1197
1324
|
return joins.join('\n');
|
|
1198
1325
|
}
|
|
1199
1326
|
async generateBaseView(pool, entity) {
|
|
1200
|
-
const
|
|
1327
|
+
const { viewSQL, viewPermSQL } = await this.generateBaseViewPieces(pool, entity);
|
|
1328
|
+
return viewSQL + viewPermSQL;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Same as `generateBaseView`, but returns the view DDL and the GRANT/permissions
|
|
1332
|
+
* DDL as separate strings rather than concatenating. Used by phased per-entity
|
|
1333
|
+
* execution paths (e.g. PG's phased executor) where the view must run under
|
|
1334
|
+
* 42P16 fallback recovery but the permissions must run afterwards in a later
|
|
1335
|
+
* phase — concatenating them would prevent the executor from detecting where
|
|
1336
|
+
* a failure actually occurred.
|
|
1337
|
+
*/
|
|
1338
|
+
async generateBaseViewPieces(pool, entity) {
|
|
1201
1339
|
const classNameFirstChar = entity.BaseTableCodeName.charAt(0).toLowerCase();
|
|
1202
1340
|
let relatedFieldsString = await this.generateBaseViewRelatedFieldsString(pool, entity.Fields);
|
|
1203
1341
|
const relatedFieldsJoinString = this.generateBaseViewJoins(entity, entity.Fields);
|
|
@@ -1213,10 +1351,6 @@ export class SQLCodeGenBase {
|
|
|
1213
1351
|
}
|
|
1214
1352
|
}
|
|
1215
1353
|
const permissions = this.generateViewPermissions(entity);
|
|
1216
|
-
const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
|
|
1217
|
-
const whereClause = entity.DeleteType === 'Soft' ? `WHERE
|
|
1218
|
-
${qi(classNameFirstChar)}.${qi(EntityInfo.DeletedAtFieldName)} IS NULL
|
|
1219
|
-
` : '';
|
|
1220
1354
|
// Detect recursive foreign keys and generate TVF joins and root field selects
|
|
1221
1355
|
const recursiveFKs = this.detectRecursiveForeignKeys(entity);
|
|
1222
1356
|
const rootFields = recursiveFKs.length > 0 ? this.generateRootFieldSelects(entity, recursiveFKs, classNameFirstChar) : '';
|
|
@@ -1233,7 +1367,10 @@ export class SQLCodeGenBase {
|
|
|
1233
1367
|
rootFieldsSelect: rootFields,
|
|
1234
1368
|
rootJoins: rootJoins,
|
|
1235
1369
|
};
|
|
1236
|
-
return
|
|
1370
|
+
return {
|
|
1371
|
+
viewSQL: this._dbProvider.generateBaseView(context),
|
|
1372
|
+
viewPermSQL: permissions,
|
|
1373
|
+
};
|
|
1237
1374
|
}
|
|
1238
1375
|
generateViewPermissions(entity) {
|
|
1239
1376
|
return this._dbProvider.generateViewPermissions(entity);
|
|
@@ -1320,7 +1457,7 @@ export class SQLCodeGenBase {
|
|
|
1320
1457
|
let sOutput = '';
|
|
1321
1458
|
let fieldCount = 0;
|
|
1322
1459
|
const manageMD = MJGlobal.Instance.ClassFactory.CreateInstance(ManageMetadataBase);
|
|
1323
|
-
const md = new Metadata();
|
|
1460
|
+
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
1324
1461
|
const allGeneratedAliases = [];
|
|
1325
1462
|
// Get fields that are related entities with join field configuration.
|
|
1326
1463
|
//
|
|
@@ -1334,6 +1471,30 @@ export class SQLCodeGenBase {
|
|
|
1334
1471
|
// Result: _RelatedEntityJoinFieldMappings is populated with all fields to be joined from the related entity.
|
|
1335
1472
|
// If both old and new configs are set, they work together (new fields extend or replace the NameField).
|
|
1336
1473
|
const qualifyingFields = entityFields.filter(f => f.RelatedEntityID && (f.IncludeRelatedEntityNameFieldInBaseView || f.RelatedEntityJoinFieldsConfig));
|
|
1474
|
+
// PostgreSQL bootstrap fix: on a fresh PG install, vwEntityFields may be a simple
|
|
1475
|
+
// SELECT * without JOINs, so related entity properties (RelatedEntity, RelatedEntityClassName,
|
|
1476
|
+
// RelatedEntitySchemaName, RelatedEntityBaseTable, RelatedEntityBaseView, RelatedEntityCodeName)
|
|
1477
|
+
// are NULL even though ef.RelatedEntityID is populated. Resolve all of them from the loaded
|
|
1478
|
+
// entities list so the first CodeGen run can generate proper JOIN-based views.
|
|
1479
|
+
for (const ef of qualifyingFields) {
|
|
1480
|
+
if (ef.RelatedEntityID && (!ef.RelatedEntity || !ef.RelatedEntityClassName || !ef.RelatedEntitySchemaName || !ef.RelatedEntityBaseTable)) {
|
|
1481
|
+
const relatedEntity = md.Entities.find(e => e.ID.toLowerCase() === ef.RelatedEntityID.toLowerCase());
|
|
1482
|
+
if (relatedEntity) {
|
|
1483
|
+
if (!ef.RelatedEntity)
|
|
1484
|
+
ef.RelatedEntity = relatedEntity.Name;
|
|
1485
|
+
if (!ef.RelatedEntityClassName)
|
|
1486
|
+
ef.RelatedEntityClassName = relatedEntity.ClassName || relatedEntity.BaseTable;
|
|
1487
|
+
if (!ef.RelatedEntitySchemaName)
|
|
1488
|
+
ef.RelatedEntitySchemaName = relatedEntity.SchemaName;
|
|
1489
|
+
if (!ef.RelatedEntityBaseTable)
|
|
1490
|
+
ef.RelatedEntityBaseTable = relatedEntity.BaseTable;
|
|
1491
|
+
if (!ef.RelatedEntityBaseView)
|
|
1492
|
+
ef.RelatedEntityBaseView = relatedEntity.BaseView;
|
|
1493
|
+
if (!ef.RelatedEntityCodeName)
|
|
1494
|
+
ef.RelatedEntityCodeName = relatedEntity.CodeName;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1337
1498
|
for (const ef of qualifyingFields) {
|
|
1338
1499
|
const config = ef.RelatedEntityJoinFieldsConfig || { mode: 'extend' };
|
|
1339
1500
|
if (config.mode === 'disable') {
|
|
@@ -1403,7 +1564,7 @@ export class SQLCodeGenBase {
|
|
|
1403
1564
|
continue;
|
|
1404
1565
|
}
|
|
1405
1566
|
// Get field metadata from related entity to check if virtual
|
|
1406
|
-
const relatedEntity = md.
|
|
1567
|
+
const relatedEntity = md.EntityByName(ef.RelatedEntity);
|
|
1407
1568
|
const relatedField = relatedEntity?.Fields.find(f => f.Name.toLowerCase() === fieldName.toLowerCase());
|
|
1408
1569
|
const isVirtual = relatedField?.IsVirtual || false;
|
|
1409
1570
|
ef._RelatedEntityJoinFieldMappings.push({
|
|
@@ -1430,8 +1591,8 @@ export class SQLCodeGenBase {
|
|
|
1430
1591
|
return sOutput;
|
|
1431
1592
|
}
|
|
1432
1593
|
getIsNameFieldForSingleEntity(entityName) {
|
|
1433
|
-
const md = new Metadata(); // use the full metadata entity list, not the filtered version that we receive
|
|
1434
|
-
const e = md.
|
|
1594
|
+
const md = new Metadata(); // use the full metadata entity list, not the filtered version that we receive // global-provider-ok: codegen runs offline against a single provider
|
|
1595
|
+
const e = md.EntityByName(entityName);
|
|
1435
1596
|
if (e) {
|
|
1436
1597
|
const ef = e.NameField;
|
|
1437
1598
|
if (e.NameField)
|
|
@@ -1455,8 +1616,8 @@ export class SQLCodeGenBase {
|
|
|
1455
1616
|
return baseName + relatedFieldName;
|
|
1456
1617
|
}
|
|
1457
1618
|
validateFieldExists(entityName, fieldName) {
|
|
1458
|
-
const md = new Metadata();
|
|
1459
|
-
const entity = md.
|
|
1619
|
+
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
1620
|
+
const entity = md.EntityByName(entityName);
|
|
1460
1621
|
if (!entity)
|
|
1461
1622
|
return false;
|
|
1462
1623
|
return entity.Fields.some(f => f.Name.toLowerCase() === fieldName.toLowerCase());
|
|
@@ -1514,7 +1675,7 @@ export class SQLCodeGenBase {
|
|
|
1514
1675
|
async generateCascadeDeletes(entity, pool) {
|
|
1515
1676
|
let sOutput = '';
|
|
1516
1677
|
if (entity.CascadeDeletes) {
|
|
1517
|
-
const md = new Metadata();
|
|
1678
|
+
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
1518
1679
|
// Find all fields in other entities that are foreign keys to this entity
|
|
1519
1680
|
for (const e of md.Entities) {
|
|
1520
1681
|
for (const ef of e.Fields) {
|
|
@@ -1624,7 +1785,7 @@ export class SQLCodeGenBase {
|
|
|
1624
1785
|
*/
|
|
1625
1786
|
analyzeCascadeDeleteDependencies(entity) {
|
|
1626
1787
|
if (entity.CascadeDeletes) {
|
|
1627
|
-
const md = new Metadata();
|
|
1788
|
+
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
1628
1789
|
// Find all fields in other entities that are foreign keys to this entity
|
|
1629
1790
|
for (const e of md.Entities) {
|
|
1630
1791
|
for (const ef of e.Fields) {
|