@memberjunction/codegen-lib 6.1.0-edge.4 → 6.1.0-edge.6

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 (136) hide show
  1. package/README.md +20 -0
  2. package/dist/Angular/angular-codegen.d.ts +9 -6
  3. package/dist/Angular/angular-codegen.d.ts.map +1 -1
  4. package/dist/Angular/angular-codegen.js +80 -51
  5. package/dist/Angular/angular-codegen.js.map +1 -1
  6. package/dist/Angular/join-grid-related-entity-component.d.ts.map +1 -1
  7. package/dist/Angular/join-grid-related-entity-component.js +5 -2
  8. package/dist/Angular/join-grid-related-entity-component.js.map +1 -1
  9. package/dist/Angular/related-entity-components.d.ts +11 -0
  10. package/dist/Angular/related-entity-components.d.ts.map +1 -1
  11. package/dist/Angular/related-entity-components.js +23 -0
  12. package/dist/Angular/related-entity-components.js.map +1 -1
  13. package/dist/Angular/timeline-related-entity-component.d.ts.map +1 -1
  14. package/dist/Angular/timeline-related-entity-component.js +3 -2
  15. package/dist/Angular/timeline-related-entity-component.js.map +1 -1
  16. package/dist/Config/config.d.ts +216 -12
  17. package/dist/Config/config.d.ts.map +1 -1
  18. package/dist/Config/config.js +88 -6
  19. package/dist/Config/config.js.map +1 -1
  20. package/dist/Config/schema-output.d.ts +33 -0
  21. package/dist/Config/schema-output.d.ts.map +1 -0
  22. package/dist/Config/schema-output.js +52 -0
  23. package/dist/Config/schema-output.js.map +1 -0
  24. package/dist/Database/codeGenDatabaseProvider.d.ts +68 -1
  25. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  26. package/dist/Database/codeGenDatabaseProvider.js +142 -3
  27. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  28. package/dist/Database/dbSchema.js +2 -1
  29. package/dist/Database/dbSchema.js.map +1 -1
  30. package/dist/Database/decision-metadata-format.d.ts +124 -0
  31. package/dist/Database/decision-metadata-format.d.ts.map +1 -0
  32. package/dist/Database/decision-metadata-format.js +279 -0
  33. package/dist/Database/decision-metadata-format.js.map +1 -0
  34. package/dist/Database/decision-metadata-writer.d.ts +48 -0
  35. package/dist/Database/decision-metadata-writer.d.ts.map +1 -0
  36. package/dist/Database/decision-metadata-writer.js +482 -0
  37. package/dist/Database/decision-metadata-writer.js.map +1 -0
  38. package/dist/Database/entity-field-change-tracking.d.ts +53 -0
  39. package/dist/Database/entity-field-change-tracking.d.ts.map +1 -0
  40. package/dist/Database/entity-field-change-tracking.js +120 -0
  41. package/dist/Database/entity-field-change-tracking.js.map +1 -0
  42. package/dist/Database/entity-level-tree-cycles.d.ts.map +1 -1
  43. package/dist/Database/entity-level-tree-cycles.js +3 -2
  44. package/dist/Database/entity-level-tree-cycles.js.map +1 -1
  45. package/dist/Database/exclude-tables.d.ts +26 -0
  46. package/dist/Database/exclude-tables.d.ts.map +1 -0
  47. package/dist/Database/exclude-tables.js +38 -0
  48. package/dist/Database/exclude-tables.js.map +1 -0
  49. package/dist/Database/field-metadata-lock.d.ts +47 -0
  50. package/dist/Database/field-metadata-lock.d.ts.map +1 -0
  51. package/dist/Database/field-metadata-lock.js +159 -0
  52. package/dist/Database/field-metadata-lock.js.map +1 -0
  53. package/dist/Database/manage-metadata.d.ts +142 -42
  54. package/dist/Database/manage-metadata.d.ts.map +1 -1
  55. package/dist/Database/manage-metadata.js +625 -287
  56. package/dist/Database/manage-metadata.js.map +1 -1
  57. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +2 -2
  58. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  59. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +90 -41
  60. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  61. package/dist/Database/providers/postgresql/metadataSupportObjects.js +31 -8
  62. package/dist/Database/providers/postgresql/metadataSupportObjects.js.map +1 -1
  63. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +3 -1
  64. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  65. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +58 -23
  66. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  67. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js +15 -2
  68. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js.map +1 -1
  69. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js +17 -0
  70. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js.map +1 -1
  71. package/dist/Database/sql_codegen.d.ts +24 -0
  72. package/dist/Database/sql_codegen.d.ts.map +1 -1
  73. package/dist/Database/sql_codegen.js +108 -29
  74. package/dist/Database/sql_codegen.js.map +1 -1
  75. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +22 -0
  76. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
  77. package/dist/Manifest/GenerateClassRegistrationsManifest.js +115 -7
  78. package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
  79. package/dist/Misc/action_subclasses_codegen.js +2 -2
  80. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  81. package/dist/Misc/advanced_generation.d.ts +7 -5
  82. package/dist/Misc/advanced_generation.d.ts.map +1 -1
  83. package/dist/Misc/advanced_generation.js +34 -23
  84. package/dist/Misc/advanced_generation.js.map +1 -1
  85. package/dist/Misc/codegen-reporter.d.ts +5 -0
  86. package/dist/Misc/codegen-reporter.d.ts.map +1 -1
  87. package/dist/Misc/codegen-reporter.js +9 -2
  88. package/dist/Misc/codegen-reporter.js.map +1 -1
  89. package/dist/Misc/emit-stats.d.ts +28 -0
  90. package/dist/Misc/emit-stats.d.ts.map +1 -0
  91. package/dist/Misc/emit-stats.js +43 -0
  92. package/dist/Misc/emit-stats.js.map +1 -0
  93. package/dist/Misc/entity_subclasses_codegen.d.ts +20 -2
  94. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  95. package/dist/Misc/entity_subclasses_codegen.js +120 -51
  96. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  97. package/dist/Misc/file-write.d.ts +15 -0
  98. package/dist/Misc/file-write.d.ts.map +1 -0
  99. package/dist/Misc/file-write.js +33 -0
  100. package/dist/Misc/file-write.js.map +1 -0
  101. package/dist/Misc/graphql_server_codegen.d.ts +12 -2
  102. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  103. package/dist/Misc/graphql_server_codegen.js +72 -16
  104. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  105. package/dist/Misc/remote_operations_codegen.d.ts.map +1 -1
  106. package/dist/Misc/remote_operations_codegen.js +4 -3
  107. package/dist/Misc/remote_operations_codegen.js.map +1 -1
  108. package/dist/Misc/runCommand.d.ts +5 -0
  109. package/dist/Misc/runCommand.d.ts.map +1 -1
  110. package/dist/Misc/runCommand.js +43 -17
  111. package/dist/Misc/runCommand.js.map +1 -1
  112. package/dist/Misc/schema-emit.d.ts +123 -0
  113. package/dist/Misc/schema-emit.d.ts.map +1 -0
  114. package/dist/Misc/schema-emit.js +221 -0
  115. package/dist/Misc/schema-emit.js.map +1 -0
  116. package/dist/Misc/sql_logging.d.ts +63 -8
  117. package/dist/Misc/sql_logging.d.ts.map +1 -1
  118. package/dist/Misc/sql_logging.js +169 -42
  119. package/dist/Misc/sql_logging.js.map +1 -1
  120. package/dist/Misc/util.d.ts +12 -0
  121. package/dist/Misc/util.d.ts.map +1 -1
  122. package/dist/Misc/util.js +71 -8
  123. package/dist/Misc/util.js.map +1 -1
  124. package/dist/index.d.ts +8 -0
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +8 -0
  127. package/dist/index.js.map +1 -1
  128. package/dist/plugins/index.d.ts +3 -0
  129. package/dist/plugins/index.d.ts.map +1 -1
  130. package/dist/plugins/index.js +28 -1
  131. package/dist/plugins/index.js.map +1 -1
  132. package/dist/runCodeGen.d.ts +20 -2
  133. package/dist/runCodeGen.d.ts.map +1 -1
  134. package/dist/runCodeGen.js +186 -33
  135. package/dist/runCodeGen.js.map +1 -1
  136. package/package.json +29 -29
@@ -1,4 +1,4 @@
1
- import { EntityInfo, Metadata } from '@memberjunction/core';
1
+ import { EntityInfo, Metadata, ShouldJoinRecordGeoCodes, HasNativeLatLngFields, NativeLatitudeField, NativeLongitudeField, ListEmbeddedGeoSpecs } from '@memberjunction/core';
2
2
  import { logError, logStatus, logWarning, startSpinner, updateSpinner, succeedSpinner, failSpinner } from '../Misc/status_logging.js';
3
3
  import * as fs from 'fs';
4
4
  import path from 'path';
@@ -12,6 +12,7 @@ import { combineFiles, logIf } from '../Misc/util.js';
12
12
  import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
13
13
  import { SQLLogging } from '../Misc/sql_logging.js';
14
14
  import { TempBatchFile } from '../Misc/temp_batch_file.js';
15
+ import { writeFileIfChanged as writeFileIfChangedShared } from '../Misc/file-write.js';
15
16
  export const SPType = {
16
17
  Create: 'Create',
17
18
  Update: 'Update',
@@ -67,6 +68,13 @@ export class SQLCodeGenBase {
67
68
  * Ordered list of entity IDs for delete SP regeneration (dependency order)
68
69
  */
69
70
  this.orderedEntitiesForDeleteSPRegeneration = [];
71
+ /**
72
+ * `schema.routine` keys (lowercased) present in the database at the start of
73
+ * this SQL-generation pass. Used to force-log CREATE PROC for routines that
74
+ * were dropped out-of-band (BigSchemaDemo recreate, a failed prior batch)
75
+ * while the entity itself is not in newEntityList/modifiedEntityList.
76
+ */
77
+ this.existingRoutines = null;
70
78
  }
71
79
  get SQLUtilityObject() {
72
80
  return this._sqlUtilityObject;
@@ -79,6 +87,7 @@ export class SQLCodeGenBase {
79
87
  }
80
88
  async manageSQLScriptsAndExecution(pool, entities, directory, currentUser) {
81
89
  try {
90
+ this.existingRoutines = null;
82
91
  // Build list of entities qualified for forced regeneration if entityWhereClause is provided
83
92
  if (configInfo.forceRegeneration?.enabled && configInfo.forceRegeneration?.entityWhereClause) {
84
93
  this.filterEntitiesQualifiedForRegeneration = true; // Enable filtering
@@ -114,6 +123,7 @@ export class SQLCodeGenBase {
114
123
  return false;
115
124
  }
116
125
  succeedSpinner(`Custom SQL scripts completed (${(new Date().getTime() - startTime.getTime()) / 1000}s)`);
126
+ await this.loadExistingRoutines(pool, entities);
117
127
  // ALWAYS use the first filter where we only include entities that have IncludeInAPI = 1
118
128
  // Entities are already sorted by name in PostProcessEntityMetadata (see providerBase.ts)
119
129
  const baselineEntities = entities.filter(e => e.IncludeInAPI);
@@ -187,7 +197,8 @@ export class SQLCodeGenBase {
187
197
  // AI Engine init because vwAIModels is gone. SQL Server doesn't
188
198
  // hit this because its execution path is bulk-monolithic, not
189
199
  // phased per-entity.
190
- const perEntityBatchSize = configInfo.dbPlatform === 'postgresql' ? 1 : 5;
200
+ const configuredBatch = configInfo.fileEmit?.sqlEntityBatchSize ?? 8;
201
+ const perEntityBatchSize = configInfo.dbPlatform === 'postgresql' ? 1 : configuredBatch;
191
202
  // Generate SQL for entities that don't need cascade delete regeneration
192
203
  const genResult = await this.generateAndExecuteEntitySQLToSeparateFiles({
193
204
  pool,
@@ -669,6 +680,8 @@ export class SQLCodeGenBase {
669
680
  // BaseView while CodeGen keeps writing the inner view underneath it. Gating on
670
681
  // BaseViewGenerated alone would skip the inner view and leave the custom layer selecting
671
682
  // from an object that does not exist.
683
+ // One pass: overlay already applied → Pass 1 sees extra BaseView columns and logs
684
+ // EntityField INSERTs; Pass 2 DROPs/creates only GeneratedViewName, never the overlay.
672
685
  const generatesView = (entity.BaseViewGenerated || entity.HasLayeredBaseView) && !entity.VirtualEntity;
673
686
  const tvfSQL = generatesView ? this.generateRecursiveFKTVFs(entity) : '';
674
687
  const viewPieces = generatesView
@@ -719,14 +732,7 @@ export class SQLCodeGenBase {
719
732
  * This avoids false timestamp updates and unnecessary I/O.
720
733
  */
721
734
  writeFileIfChanged(filePath, newContent) {
722
- if (fs.existsSync(filePath)) {
723
- const existing = fs.readFileSync(filePath, 'utf-8');
724
- if (existing === newContent) {
725
- return false;
726
- }
727
- }
728
- fs.writeFileSync(filePath, newContent);
729
- return true;
735
+ return writeFileIfChangedShared(filePath, newContent);
730
736
  }
731
737
  /**
732
738
  * Fetches the current base view definition from SQL Server and compares it against newly generated SQL.
@@ -1106,7 +1112,7 @@ export class SQLCodeGenBase {
1106
1112
  if (options.writeFiles) {
1107
1113
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, false, true));
1108
1114
  this.writeFileIfChanged(filePath, s);
1109
- this.logSQLForNewOrModifiedEntity(options.entity, s, `spCreate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
1115
+ this.logSQLForNewOrModifiedEntity(options.entity, s, `spCreate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, this.forceLogForRoutine(options.entity, spName, baseViewChanged));
1110
1116
  files.push(filePath);
1111
1117
  }
1112
1118
  sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
@@ -1117,7 +1123,7 @@ export class SQLCodeGenBase {
1117
1123
  if (options.writeFiles) {
1118
1124
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
1119
1125
  this.writeFileIfChanged(filePath, s);
1120
- this.logSQLForNewOrModifiedEntity(options.entity, s, `spCreate Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
1126
+ this.logSQLForNewOrModifiedEntity(options.entity, s, `spCreate Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, this.forceLogForRoutine(options.entity, spName, baseViewChanged));
1121
1127
  files.push(filePath);
1122
1128
  }
1123
1129
  // now, append the permissions to the return string IF we did NOT generate the proc - because if we generated the proc, that
@@ -1136,7 +1142,7 @@ export class SQLCodeGenBase {
1136
1142
  if (options.writeFiles) {
1137
1143
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, false, true));
1138
1144
  this.writeFileIfChanged(filePath, s);
1139
- this.logSQLForNewOrModifiedEntity(options.entity, s, `spUpdate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
1145
+ this.logSQLForNewOrModifiedEntity(options.entity, s, `spUpdate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, this.forceLogForRoutine(options.entity, spName, baseViewChanged));
1140
1146
  files.push(filePath);
1141
1147
  }
1142
1148
  sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
@@ -1147,7 +1153,7 @@ export class SQLCodeGenBase {
1147
1153
  if (options.writeFiles) {
1148
1154
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
1149
1155
  this.writeFileIfChanged(filePath, s);
1150
- this.logSQLForNewOrModifiedEntity(options.entity, s, `spUpdate Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
1156
+ this.logSQLForNewOrModifiedEntity(options.entity, s, `spUpdate Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, this.forceLogForRoutine(options.entity, spName, baseViewChanged));
1151
1157
  files.push(filePath);
1152
1158
  }
1153
1159
  // now, append the permissions to the return string IF we did NOT generate the proc - because if we generated the proc, that
@@ -1172,7 +1178,7 @@ export class SQLCodeGenBase {
1172
1178
  if (options.writeFiles) {
1173
1179
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, false, true));
1174
1180
  this.writeFileIfChanged(filePath, s);
1175
- this.logSQLForNewOrModifiedEntity(options.entity, s, `spDelete SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
1181
+ this.logSQLForNewOrModifiedEntity(options.entity, s, `spDelete SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, this.forceLogForRoutine(options.entity, spName, baseViewChanged));
1176
1182
  files.push(filePath);
1177
1183
  }
1178
1184
  sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
@@ -1183,7 +1189,7 @@ export class SQLCodeGenBase {
1183
1189
  if (options.writeFiles) {
1184
1190
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
1185
1191
  this.writeFileIfChanged(filePath, s);
1186
- this.logSQLForNewOrModifiedEntity(options.entity, s, `spDelete Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
1192
+ this.logSQLForNewOrModifiedEntity(options.entity, s, `spDelete Permissions for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, this.forceLogForRoutine(options.entity, spName, baseViewChanged));
1187
1193
  files.push(filePath);
1188
1194
  }
1189
1195
  // now, append the permissions to the return string IF we did NOT generate the proc - because if we generated the proc, that
@@ -1229,6 +1235,42 @@ export class SQLCodeGenBase {
1229
1235
  getSPName(entity, type) {
1230
1236
  return this._dbProvider.getCRUDRoutineName(entity, type);
1231
1237
  }
1238
+ /**
1239
+ * Snapshot every CRUD routine currently in the database (one query) so
1240
+ * generate-and-log can force-emit CREATE PROC for objects that vanished
1241
+ * without the entity landing on newEntityList/modifiedEntityList.
1242
+ */
1243
+ async loadExistingRoutines(pool, entities) {
1244
+ this.existingRoutines = new Set();
1245
+ const schemas = [...new Set(entities.map((e) => e.SchemaName).filter((s) => !!s))];
1246
+ const sql = this._dbProvider.getRoutineNamesBySchemaSQL(schemas);
1247
+ if (!sql || !sql.trim()) {
1248
+ return;
1249
+ }
1250
+ try {
1251
+ const result = await pool.query(sql);
1252
+ for (const row of result.recordset ?? []) {
1253
+ const schemaName = String(row.schema_name ?? '').toLowerCase();
1254
+ const routineName = String(row.routine_name ?? '').toLowerCase();
1255
+ if (schemaName && routineName) {
1256
+ this.existingRoutines.add(`${schemaName}.${routineName}`);
1257
+ }
1258
+ }
1259
+ }
1260
+ catch (e) {
1261
+ logWarning(`Could not load existing CRUD routines (missing-proc self-heal disabled this run): ${e instanceof Error ? e.message : String(e)}`);
1262
+ this.existingRoutines = null;
1263
+ }
1264
+ }
1265
+ isRoutineMissing(schema, routineName) {
1266
+ if (!this.existingRoutines) {
1267
+ return false;
1268
+ }
1269
+ return !this.existingRoutines.has(`${schema}.${routineName}`.toLowerCase());
1270
+ }
1271
+ forceLogForRoutine(entity, routineName, baseViewChanged) {
1272
+ return baseViewChanged || this.isRoutineMissing(entity.SchemaName, routineName);
1273
+ }
1232
1274
  getEntityPermissionFileNames(entity) {
1233
1275
  const files = [];
1234
1276
  // all entities have a base view - and we always generate permissions for the base view even if not generated base view
@@ -1556,17 +1598,22 @@ export class SQLCodeGenBase {
1556
1598
  const classNameFirstChar = entity.BaseTableCodeName.charAt(0).toLowerCase();
1557
1599
  let relatedFieldsString = await this.generateBaseViewRelatedFieldsString(pool, entity.Fields);
1558
1600
  const relatedFieldsJoinString = this.generateBaseViewJoins(entity, entity.Fields);
1559
- // Geo support: add __mj_Latitude and __mj_Longitude virtual fields for geo-enabled entities
1560
- // Skip for Record Geo Codes itself to avoid circular self-reference in the view
1601
+ // Geo WRITE source: native lat/lng aliases, or RecordGeoCode JOIN when the entity
1602
+ // has writable Geo* fields and no native coords. Display-only entities (Person/Org
1603
+ // virtual PrimaryAddress*) do not get a RecordGeoCode join on their own ID.
1561
1604
  if (entity.SupportsGeoCoding && entity.Name.trim().toLowerCase() !== 'mj: record geo codes') {
1562
1605
  const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
1563
- const geoFieldsSelect = this.hasNativeGeoFields(entity.Fields)
1606
+ const geoFieldsSelect = HasNativeLatLngFields(entity.Fields)
1564
1607
  ? this.generateNativeGeoFields(entity.Fields, classNameFirstChar, qi)
1565
- : this.generateRecordGeoCodeFields(qi);
1608
+ : (ShouldJoinRecordGeoCodes(entity) ? this.generateRecordGeoCodeFields(qi) : '');
1566
1609
  if (geoFieldsSelect) {
1567
1610
  relatedFieldsString += (relatedFieldsString ? ',\n' : '') + geoFieldsSelect;
1568
1611
  }
1569
1612
  }
1613
+ const embeddedGeo = this.generateEmbeddedGeoSelect(entity, classNameFirstChar);
1614
+ if (embeddedGeo.select) {
1615
+ relatedFieldsString += (relatedFieldsString ? ',\n' : '') + embeddedGeo.select;
1616
+ }
1570
1617
  // GRANTs target the PUBLIC view (BaseView), not the one this method generates. For a
1571
1618
  // LAYERED entity those are different objects, and the outer one may not exist yet: the
1572
1619
  // documented setup is "name the inner view -> run CodeGen -> then create BaseView", so the
@@ -1680,10 +1727,9 @@ export class SQLCodeGenBase {
1680
1727
  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)}`;
1681
1728
  }
1682
1729
  }
1683
- // Geo support: add LEFT JOIN to vwRecordGeoCodes for entities with SupportsGeoCoding = 1
1684
- // that don't have native GeoLatitude/GeoLongitude fields (those get aliased directly).
1685
- // Skip for Record Geo Codes itself to avoid circular self-reference in the view.
1686
- if (entity.SupportsGeoCoding && !this.hasNativeGeoFields(entityFields) && entity.Name.trim().toLowerCase() !== 'mj: record geo codes') {
1730
+ // Geo WRITE source: RecordGeoCode JOIN only when there are writable Geo* fields
1731
+ // and no native lat/lng. Display-only entities skip this join.
1732
+ if (ShouldJoinRecordGeoCodes(entity)) {
1687
1733
  const dialect = this._dbProvider.Dialect;
1688
1734
  const qi = dialect.QuoteIdentifier.bind(dialect);
1689
1735
  const qs = dialect.QuoteSchema.bind(dialect);
@@ -1707,6 +1753,7 @@ export class SQLCodeGenBase {
1707
1753
  }
1708
1754
  sOutput += `LEFT OUTER JOIN\n ${qs(mjCoreSchema, 'vwRecordGeoCodes')} AS __mj_rgc\n ON\n __mj_rgc.${qi('EntityID')} = '${entity.ID}'\n AND __mj_rgc.${qi('RecordID')} = ${recordIdExpr}\n AND __mj_rgc.${qi('LocationType')} = 'Primary'`;
1709
1755
  }
1756
+ sOutput += this.generateEmbeddedGeoJoins(entity, classNameFirstChar);
1710
1757
  return sOutput;
1711
1758
  }
1712
1759
  /**
@@ -1723,21 +1770,53 @@ export class SQLCodeGenBase {
1723
1770
  * on any geo-eligible entity whose RecordGeoCode-based view ran once.
1724
1771
  */
1725
1772
  hasNativeGeoFields(entityFields) {
1726
- const hasLat = entityFields.some(f => f.ExtendedType === 'GeoLatitude' && !f.IsVirtual);
1727
- const hasLng = entityFields.some(f => f.ExtendedType === 'GeoLongitude' && !f.IsVirtual);
1728
- return hasLat && hasLng;
1773
+ return HasNativeLatLngFields(entityFields);
1729
1774
  }
1730
1775
  /**
1731
1776
  * Generate SELECT aliases for native lat/lng fields → __mj_Latitude / __mj_Longitude.
1732
1777
  * Virtual fields are excluded — see hasNativeGeoFields for the rationale.
1733
1778
  */
1734
1779
  generateNativeGeoFields(entityFields, classNameFirstChar, qi) {
1735
- const latField = entityFields.find(f => f.ExtendedType === 'GeoLatitude' && !f.IsVirtual);
1736
- const lngField = entityFields.find(f => f.ExtendedType === 'GeoLongitude' && !f.IsVirtual);
1780
+ const latField = NativeLatitudeField(entityFields);
1781
+ const lngField = NativeLongitudeField(entityFields);
1737
1782
  if (!latField || !lngField)
1738
1783
  return '';
1739
1784
  return ` ${qi(classNameFirstChar)}.${qi(latField.Name)} AS ${qi('__mj_Latitude')},\n ${qi(classNameFirstChar)}.${qi(lngField.Name)} AS ${qi('__mj_Longitude')}`;
1740
1785
  }
1786
+ /**
1787
+ * Bubble lat/lng from EmbeddedRecord peers as `__mj_Latitude_{FK}`.
1788
+ * Geocode lives on the Address (or other source); the parent only displays it.
1789
+ */
1790
+ generateEmbeddedGeoSelect(entity, classNameFirstChar) {
1791
+ const specs = ListEmbeddedGeoSpecs(entity.Fields);
1792
+ if (specs.length === 0)
1793
+ return { select: '', joins: '' };
1794
+ const md = new Metadata(); // global-provider-ok: CodeGen is CLI tool
1795
+ const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
1796
+ const qs = this._dbProvider.Dialect.QuoteSchema.bind(this._dbProvider.Dialect);
1797
+ const selects = [];
1798
+ const joins = [];
1799
+ for (const spec of specs) {
1800
+ const related = md.Entities.find(e => e.ID.toLowerCase() === spec.relatedEntityID.toLowerCase());
1801
+ if (!related)
1802
+ continue;
1803
+ const latF = NativeLatitudeField(related.Fields) ?? related.Fields.find(f => f.Name === '__mj_Latitude' || f.Name === 'PrimaryAddressLatitude');
1804
+ const lngF = NativeLongitudeField(related.Fields) ?? related.Fields.find(f => f.Name === '__mj_Longitude' || f.Name === 'PrimaryAddressLongitude');
1805
+ if (!latF || !lngF)
1806
+ continue;
1807
+ const alias = `__mj_emb_${spec.foreignKeyField}`;
1808
+ const schema = spec.relatedSchemaName || related.SchemaName;
1809
+ const table = spec.relatedBaseTable || related.BaseTable;
1810
+ const joinKind = spec.allowsNull ? 'LEFT OUTER' : 'INNER';
1811
+ joins.push(`${joinKind} JOIN\n ${qs(schema, table)} AS ${alias}\n ON\n ${qi(classNameFirstChar)}.${qi(spec.foreignKeyField)} = ${alias}.${qi(related.FirstPrimaryKey.Name)}`); // first-pk-ok: FK target — a single FK column joins to the related entity's single key
1812
+ selects.push(` ${alias}.${qi(latF.Name)} AS ${qi(spec.lat)},\n ${alias}.${qi(lngF.Name)} AS ${qi(spec.lng)}`);
1813
+ }
1814
+ return { select: selects.join(',\n'), joins: joins.join('\n') };
1815
+ }
1816
+ generateEmbeddedGeoJoins(entity, classNameFirstChar) {
1817
+ const { joins } = this.generateEmbeddedGeoSelect(entity, classNameFirstChar);
1818
+ return joins ? ((entity.Fields.length ? '\n' : '') + joins) : '';
1819
+ }
1741
1820
  /**
1742
1821
  * Generate SELECT aliases from the RecordGeoCode LEFT JOIN → __mj_Latitude / __mj_Longitude.
1743
1822
  */