@memberjunction/codegen-lib 5.30.1 → 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.
Files changed (57) hide show
  1. package/README.md +2 -0
  2. package/dist/Angular/angular-codegen.js +3 -3
  3. package/dist/Angular/angular-codegen.js.map +1 -1
  4. package/dist/Angular/related-entity-components.js +1 -1
  5. package/dist/Angular/related-entity-components.js.map +1 -1
  6. package/dist/Config/config.js +1 -1
  7. package/dist/Config/config.js.map +1 -1
  8. package/dist/Database/codeGenDatabaseProvider.d.ts +266 -12
  9. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  10. package/dist/Database/codeGenDatabaseProvider.js +352 -0
  11. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  12. package/dist/Database/manage-metadata.d.ts.map +1 -1
  13. package/dist/Database/manage-metadata.js +86 -87
  14. package/dist/Database/manage-metadata.js.map +1 -1
  15. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +105 -17
  16. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  17. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +324 -94
  18. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  19. package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts +99 -0
  20. package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts.map +1 -0
  21. package/dist/Database/providers/postgresql/viewDependencyCapture.js +170 -0
  22. package/dist/Database/providers/postgresql/viewDependencyCapture.js.map +1 -0
  23. package/dist/Database/providers/postgresql/viewFallback.d.ts +75 -0
  24. package/dist/Database/providers/postgresql/viewFallback.d.ts.map +1 -0
  25. package/dist/Database/providers/postgresql/viewFallback.js +172 -0
  26. package/dist/Database/providers/postgresql/viewFallback.js.map +1 -0
  27. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +0 -25
  28. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  29. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +9 -113
  30. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  31. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.d.ts +2 -0
  32. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.d.ts.map +1 -0
  33. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js +207 -0
  34. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js.map +1 -0
  35. package/dist/Database/sql.d.ts +9 -1
  36. package/dist/Database/sql.d.ts.map +1 -1
  37. package/dist/Database/sql.js +41 -7
  38. package/dist/Database/sql.js.map +1 -1
  39. package/dist/Database/sql_codegen.d.ts +30 -0
  40. package/dist/Database/sql_codegen.d.ts.map +1 -1
  41. package/dist/Database/sql_codegen.js +205 -44
  42. package/dist/Database/sql_codegen.js.map +1 -1
  43. package/dist/Misc/advanced_generation.js +1 -1
  44. package/dist/Misc/advanced_generation.js.map +1 -1
  45. package/dist/Misc/createNewUser.js +2 -2
  46. package/dist/Misc/createNewUser.js.map +1 -1
  47. package/dist/Misc/entity_subclasses_codegen.js +5 -5
  48. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  49. package/dist/Misc/graphql_server_codegen.js +6 -6
  50. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  51. package/dist/Misc/sql_logging.d.ts.map +1 -1
  52. package/dist/Misc/sql_logging.js +14 -4
  53. package/dist/Misc/sql_logging.js.map +1 -1
  54. package/dist/runCodeGen.d.ts.map +1 -1
  55. package/dist/runCodeGen.js +66 -2
  56. package/dist/runCodeGen.js.map +1 -1
  57. 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
- skipExecution: true, // skip execution because we execute it all in a giant batch below
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
- failSpinner('Failed to generate entity SQL files');
170
- return false;
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: true,
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
- failSpinner('Failed to regenerate cascade delete SPs');
187
- return false;
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
- failSpinner('Failed to generate permissions for excluded entities');
205
- return false;
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 (TempBatchFile.hasContent()) {
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
- let overallSuccess = true;
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
- let bFail = false;
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
- return this.generateAndExecuteSingleEntitySQLToSeparateFiles({
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(r => {
474
- if (!r.Success)
475
- bFail = true; // keep going, but will return false at the end
476
- files.push(...r.Files); // add the files to the main files array
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
- return { Success: !bFail, Files: files };
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 (!options.skipExecution) {
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
- else
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 viewName = entity.BaseView ? entity.BaseView : `vw${entity.CodeName}`;
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 this._dbProvider.generateBaseView(context) + permissions;
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.Entities.find(e => e.Name === ef.RelatedEntity);
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.Entities.find(e => e.Name === entityName);
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.Entities.find(e => e.Name === entityName);
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) {