@memberjunction/codegen-lib 6.1.0-edge.2 → 6.1.0-edge.3

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 (94) hide show
  1. package/LICENSE +180 -4
  2. package/README.md +39 -22
  3. package/dist/Angular/angular-codegen.d.ts +0 -8
  4. package/dist/Angular/angular-codegen.d.ts.map +1 -1
  5. package/dist/Angular/angular-codegen.js +32 -22
  6. package/dist/Angular/angular-codegen.js.map +1 -1
  7. package/dist/Angular/entity-data-grid-related-entity-component.js +2 -2
  8. package/dist/Angular/entity-data-grid-related-entity-component.js.map +1 -1
  9. package/dist/Angular/related-entity-components.d.ts +1 -1
  10. package/dist/Angular/related-entity-components.js +1 -1
  11. package/dist/Database/codeGenDatabaseProvider.d.ts +101 -0
  12. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  13. package/dist/Database/codeGenDatabaseProvider.js +77 -10
  14. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  15. package/dist/Database/manage-metadata.d.ts +292 -2
  16. package/dist/Database/manage-metadata.d.ts.map +1 -1
  17. package/dist/Database/manage-metadata.js +1176 -45
  18. package/dist/Database/manage-metadata.js.map +1 -1
  19. package/dist/Database/materializationAnalysis.d.ts +241 -0
  20. package/dist/Database/materializationAnalysis.d.ts.map +1 -0
  21. package/dist/Database/materializationAnalysis.js +415 -0
  22. package/dist/Database/materializationAnalysis.js.map +1 -0
  23. package/dist/Database/materializationBroadRender.d.ts +67 -0
  24. package/dist/Database/materializationBroadRender.d.ts.map +1 -0
  25. package/dist/Database/materializationBroadRender.js +104 -0
  26. package/dist/Database/materializationBroadRender.js.map +1 -0
  27. package/dist/Database/materializationDrift.d.ts +59 -0
  28. package/dist/Database/materializationDrift.d.ts.map +1 -0
  29. package/dist/Database/materializationDrift.js +85 -0
  30. package/dist/Database/materializationDrift.js.map +1 -0
  31. package/dist/Database/materializationParamClassifier.d.ts +92 -0
  32. package/dist/Database/materializationParamClassifier.d.ts.map +1 -0
  33. package/dist/Database/materializationParamClassifier.js +224 -0
  34. package/dist/Database/materializationParamClassifier.js.map +1 -0
  35. package/dist/Database/materializationParamVerifier.d.ts +71 -0
  36. package/dist/Database/materializationParamVerifier.d.ts.map +1 -0
  37. package/dist/Database/materializationParamVerifier.js +302 -0
  38. package/dist/Database/materializationParamVerifier.js.map +1 -0
  39. package/dist/Database/materializationSqlAst.d.ts +78 -0
  40. package/dist/Database/materializationSqlAst.d.ts.map +1 -0
  41. package/dist/Database/materializationSqlAst.js +142 -0
  42. package/dist/Database/materializationSqlAst.js.map +1 -0
  43. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +68 -21
  44. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  45. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +303 -197
  46. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  47. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts +2 -0
  48. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts.map +1 -0
  49. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js +119 -0
  50. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js.map +1 -0
  51. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts +2 -0
  52. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts.map +1 -0
  53. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js +155 -0
  54. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js.map +1 -0
  55. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +60 -1
  56. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  57. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +294 -0
  58. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  59. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts +2 -0
  60. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts.map +1 -0
  61. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js +547 -0
  62. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js.map +1 -0
  63. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts +2 -0
  64. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts.map +1 -0
  65. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js +721 -0
  66. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js.map +1 -0
  67. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts +2 -0
  68. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts.map +1 -0
  69. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js +78 -0
  70. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js.map +1 -0
  71. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts +2 -0
  72. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts.map +1 -0
  73. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js +155 -0
  74. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js.map +1 -0
  75. package/dist/Database/sql_codegen.d.ts +21 -5
  76. package/dist/Database/sql_codegen.d.ts.map +1 -1
  77. package/dist/Database/sql_codegen.js +84 -26
  78. package/dist/Database/sql_codegen.js.map +1 -1
  79. package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
  80. package/dist/Misc/action_subclasses_codegen.js +4 -4
  81. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  82. package/dist/Misc/entity_subclasses_codegen.d.ts +44 -0
  83. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  84. package/dist/Misc/entity_subclasses_codegen.js +369 -156
  85. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  86. package/dist/Misc/graphql_server_codegen.d.ts +4 -58
  87. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  88. package/dist/Misc/graphql_server_codegen.js +22 -236
  89. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  90. package/dist/Misc/sql_logging.d.ts +25 -0
  91. package/dist/Misc/sql_logging.d.ts.map +1 -1
  92. package/dist/Misc/sql_logging.js +51 -8
  93. package/dist/Misc/sql_logging.js.map +1 -1
  94. package/package.json +30 -30
@@ -1,11 +1,12 @@
1
1
  import { BaseEntity, EntityFieldValueListType, Metadata, TypeScriptTypeFromSQLType } from '@memberjunction/core';
2
+ import { UUIDsEqual } from '@memberjunction/global';
2
3
  import fs from 'fs';
3
4
  import path from 'path';
4
5
  import ts from 'typescript';
5
6
  import { makeDir, sortBySequenceAndCreatedAt } from '../Misc/util.js';
6
- import { logError, logStatus } from './status_logging.js';
7
+ import { logError, logStatus, logWarning } from './status_logging.js';
7
8
  import { ManageMetadataBase } from '../Database/manage-metadata.js';
8
- import { mj_core_schema } from '../Config/config.js';
9
+ import { mj_core_schema, resolveEntityPackageName } from '../Config/config.js';
9
10
  import { SQLLogging } from './sql_logging.js';
10
11
  /**
11
12
  * Dynamically collects all own property names from BaseEntity's prototype chain
@@ -118,9 +119,13 @@ export class EntitySubClassGeneratorBase {
118
119
  // Only consider entities that actually emit a class: generateEntitySubClass skips PK-less entities
119
120
  // (returns ''), so hoisting their import would leave a dangling/unused import (a build error under
120
121
  // a downstream consumer's noUnusedLocals). Match that skip condition here.
122
+ const localClassNames = new Set(entities.filter((e) => e.PrimaryKeys.length > 0).map((e) => `${e.ClassName}Entity`));
121
123
  const subclassImports = [...new Set(entities
122
124
  .filter((e) => e.PrimaryKeys.length > 0)
123
- .map((e) => this.resolveEntityBaseClass(e).importStatement)
125
+ .flatMap((e) => [
126
+ this.resolveEntityBaseClass(e).importStatement,
127
+ ...EntitySubClassGeneratorBase.CollectEmbeddedImports(e, localClassNames),
128
+ ])
124
129
  .filter((s) => s.length > 0))].join('');
125
130
  const allContent = `${this.generateEntitySubClassFileHeader()} \n ${subclassImports}${zodContent} \n ${sContent}`;
126
131
  makeDir(directory);
@@ -133,7 +138,7 @@ export class EntitySubClassGeneratorBase {
133
138
  }
134
139
  }
135
140
  generateEntitySubClassFileHeader() {
136
- return `import { BaseEntity, EntitySaveOptions, EntityDeleteOptions, CompositeKey, ValidationResult, ValidationErrorInfo, ValidationErrorType, Metadata, ProviderType, DatabaseProviderBase } from "@memberjunction/core";
141
+ return `import { BaseEntity, EntitySaveOptions, EntityDeleteOptions, CompositeKey, ValidationResult, ValidationErrorInfo, ValidationErrorType, Metadata, ProviderType, DatabaseProviderBase, RunView } from "@memberjunction/core";
137
142
  import { RegisterClass } from "@memberjunction/global";
138
143
  import { z } from "zod";
139
144
 
@@ -167,88 +172,87 @@ export const loadModule = () => {
167
172
  console.warn(`SKIPPING TYPESCRIPT GENERATION: Entity ${entity.Name} has no primary keys in metadata. If using soft primary keys, ensure metadata was refreshed after applySoftPKFKConfig().`);
168
173
  return '';
169
174
  }
170
- else {
171
- // Sort fields by Sequence, then by __mj_CreatedAt for consistent ordering
172
- const sortedFields = sortBySequenceAndCreatedAt(entity.Fields);
173
- const sClassName = `${entity.ClassName}Entity`;
174
- const fields = sortedFields.map((e) => {
175
- let values = '';
176
- let valueList = '';
177
- if (e.ValueListType && e.ValueListType.length > 0 && e.ValueListType.trim().toLowerCase() !== 'none') {
178
- // Sort by Sequence to ensure consistent ordering in comments
179
- const sortedValues = sortBySequenceAndCreatedAt([...e.EntityFieldValues]);
180
- values = sortedValues.map((v) => `\n * * ${v.Value}${v.Description && v.Description.length > 0 ? ' - ' + EntitySubClassGeneratorBase.SanitizeDescription(v.Description) : ''}`).join('');
181
- valueList = `\n * * Value List Type: ${e.ValueListType}\n * * Possible Values ` + values;
175
+ const sClassName = `${entity.ClassName}Entity`;
176
+ // Sort fields by Sequence, then by __mj_CreatedAt for consistent ordering
177
+ const sortedFields = sortBySequenceAndCreatedAt(entity.Fields);
178
+ const fields = sortedFields.map((e) => {
179
+ let values = '';
180
+ let valueList = '';
181
+ if (e.ValueListType && e.ValueListType.length > 0 && e.ValueListType.trim().toLowerCase() !== 'none') {
182
+ // Sort by Sequence to ensure consistent ordering in comments
183
+ const sortedValues = sortBySequenceAndCreatedAt([...e.EntityFieldValues]);
184
+ values = sortedValues.map((v) => `\n * * ${v.Value}${v.Description && v.Description.length > 0 ? ' - ' + EntitySubClassGeneratorBase.SanitizeDescription(v.Description) : ''}`).join('');
185
+ valueList = `\n * * Value List Type: ${e.ValueListType}\n * * Possible Values ` + values;
186
+ }
187
+ // JSONType: when a field has JSONType metadata, we emit the standard string getter/setter
188
+ // unchanged, plus an additional typed "Object" accessor (e.g., DefaultNavItemsObject) that
189
+ // handles JSON.parse/stringify with caching. Interface names are entity-prefixed for safety.
190
+ const hasJSONType = e.JSONType && e.JSONType.trim().length > 0;
191
+ let typeString = TypeScriptTypeFromSQLType(e.Type) + (e.AllowsNull ? ' | null' : '');
192
+ // Build the typed accessor info (used later for the Object suffix property)
193
+ let jsonTypeAccessorInfo = null;
194
+ if (hasJSONType) {
195
+ const originalTypeName = e.JSONType.trim();
196
+ const prefixedTypeName = `${sClassName}_${originalTypeName}`;
197
+ const isArray = e.JSONTypeIsArray === true;
198
+ const fullTypeString = isArray ? `Array<${prefixedTypeName}>` : prefixedTypeName;
199
+ jsonTypeAccessorInfo = { prefixedTypeName, fullTypeString: fullTypeString + (e.AllowsNull ? ' | null' : ''), isArray };
200
+ // typeString stays as the standard SQL-mapped type (string | null) for the base getter
201
+ }
202
+ if (!hasJSONType && e.ValueListTypeEnum !== EntityFieldValueListType.None && e.EntityFieldValues && e.EntityFieldValues.length > 0) {
203
+ // construct a typeString that is a union of the possible values
204
+ const quotes = e.NeedsQuotes ? "'" : '';
205
+ // Sort deterministically by Sequence, CreatedAt, then Value to prevent flip-flopping across runs
206
+ const sortedValues = sortBySequenceAndCreatedAt([...e.EntityFieldValues]);
207
+ typeString = sortedValues.map((v) => `${quotes}${v.Value}${quotes}`).join(' | ');
208
+ if (e.ValueListTypeEnum === EntityFieldValueListType.ListOrUserEntry) {
209
+ // special case becuase a user can enter whatever they want
210
+ typeString += ' | ' + TypeScriptTypeFromSQLType(e.Type);
182
211
  }
183
- // JSONType: when a field has JSONType metadata, we emit the standard string getter/setter
184
- // unchanged, plus an additional typed "Object" accessor (e.g., DefaultNavItemsObject) that
185
- // handles JSON.parse/stringify with caching. Interface names are entity-prefixed for safety.
186
- const hasJSONType = e.JSONType && e.JSONType.trim().length > 0;
187
- let typeString = TypeScriptTypeFromSQLType(e.Type) + (e.AllowsNull ? ' | null' : '');
188
- // Build the typed accessor info (used later for the Object suffix property)
189
- let jsonTypeAccessorInfo = null;
190
- if (hasJSONType) {
191
- const originalTypeName = e.JSONType.trim();
192
- const prefixedTypeName = `${sClassName}_${originalTypeName}`;
193
- const isArray = e.JSONTypeIsArray === true;
194
- const fullTypeString = isArray ? `Array<${prefixedTypeName}>` : prefixedTypeName;
195
- jsonTypeAccessorInfo = { prefixedTypeName, fullTypeString: fullTypeString + (e.AllowsNull ? ' | null' : ''), isArray };
196
- // typeString stays as the standard SQL-mapped type (string | null) for the base getter
212
+ // finally, add the null type if it allows null
213
+ if (e.AllowsNull) {
214
+ typeString += ' | null';
197
215
  }
198
- if (!hasJSONType && e.ValueListTypeEnum !== EntityFieldValueListType.None && e.EntityFieldValues && e.EntityFieldValues.length > 0) {
199
- // construct a typeString that is a union of the possible values
200
- const quotes = e.NeedsQuotes ? "'" : '';
201
- // Sort deterministically by Sequence, CreatedAt, then Value to prevent flip-flopping across runs
202
- const sortedValues = sortBySequenceAndCreatedAt([...e.EntityFieldValues]);
203
- typeString = sortedValues.map((v) => `${quotes}${v.Value}${quotes}`).join(' | ');
204
- if (e.ValueListTypeEnum === EntityFieldValueListType.ListOrUserEntry) {
205
- // special case becuase a user can enter whatever they want
206
- typeString += ' | ' + TypeScriptTypeFromSQLType(e.Type);
207
- }
208
- // finally, add the null type if it allows null
209
- if (e.AllowsNull) {
210
- typeString += ' | null';
211
- }
212
- }
213
- const fieldDeprecatedFlag = e.Status === 'Deprecated' || e.Status === 'Disabled' ?
214
- `\n * * @deprecated This field is deprecated and will be removed in a future version. Using it will result in console warnings.` : '';
215
- const fieldDisabledFlag = e.Status === 'Disabled' ?
216
- `\n * * @disabled This field is disabled and will not be available in the application. Attempting to use it will result in exceptions being thrown` : '';
217
- // IS-A parent field detection: virtual fields with AllowUpdateAPI on child entities
218
- const isISAParentField = e.IsVirtual && e.AllowUpdateAPI && entity.IsChildType;
219
- const isaSourceComment = isISAParentField
220
- ? `\n * * IS-A Source: Inherited from ${this.getISAFieldSourceEntity(entity, e)}`
221
- : '';
222
- const jsonTypeComment = hasJSONType && jsonTypeAccessorInfo
223
- ? `\n * * JSON Type: ${jsonTypeAccessorInfo.isArray ? `Array<${jsonTypeAccessorInfo.prefixedTypeName}>` : jsonTypeAccessorInfo.prefixedTypeName}`
224
- : '';
225
- const safeName = SafeCodeName(e);
226
- const conflictNote = safeName !== e.CodeName
227
- ? `\n * * NOTE: Property renamed to \`${safeName}\` to avoid conflict with BaseEntity.${e.CodeName}`
228
- : '';
229
- // Standard getter/setter (always uses raw string, even for JSONType fields)
230
- const getterBody = `return this.Get('${e.Name}');`;
231
- const setterBody = `this.Set('${e.Name}', value);`;
232
- let sRet = ` /**
216
+ }
217
+ const fieldDeprecatedFlag = e.Status === 'Deprecated' || e.Status === 'Disabled' ?
218
+ `\n * * @deprecated This field is deprecated and will be removed in a future version. Using it will result in console warnings.` : '';
219
+ const fieldDisabledFlag = e.Status === 'Disabled' ?
220
+ `\n * * @disabled This field is disabled and will not be available in the application. Attempting to use it will result in exceptions being thrown` : '';
221
+ // IS-A parent field detection: virtual fields with AllowUpdateAPI on child entities
222
+ const isISAParentField = e.IsVirtual && e.AllowUpdateAPI && entity.IsChildType;
223
+ const isaSourceComment = isISAParentField
224
+ ? `\n * * IS-A Source: Inherited from ${this.getISAFieldSourceEntity(entity, e)}`
225
+ : '';
226
+ const jsonTypeComment = hasJSONType && jsonTypeAccessorInfo
227
+ ? `\n * * JSON Type: ${jsonTypeAccessorInfo.isArray ? `Array<${jsonTypeAccessorInfo.prefixedTypeName}>` : jsonTypeAccessorInfo.prefixedTypeName}`
228
+ : '';
229
+ const safeName = SafeCodeName(e);
230
+ const conflictNote = safeName !== e.CodeName
231
+ ? `\n * * NOTE: Property renamed to \`${safeName}\` to avoid conflict with BaseEntity.${e.CodeName}`
232
+ : '';
233
+ // Standard getter/setter (always uses raw string, even for JSONType fields)
234
+ const getterBody = `return this.Get('${e.Name}');`;
235
+ const setterBody = `this.Set('${e.Name}', value);`;
236
+ let sRet = ` /**
233
237
  * * Field Name: ${e.Name}${e.DisplayName && e.DisplayName.length > 0 ? '\n * * Display Name: ' + e.DisplayName : ''}
234
238
  * * ${fieldDeprecatedFlag}${fieldDisabledFlag}SQL Data Type: ${e.SQLFullType}${e.RelatedEntity ? '\n * * Related Entity/Foreign Key: ' + e.RelatedEntity + ' (' + e.RelatedEntityBaseView + '.' + e.RelatedEntityFieldName + ')' : ''}${e.DefaultValue && e.DefaultValue.length > 0 ? '\n * * Default Value: ' + e.DefaultValue : ''}${valueList}${jsonTypeComment}${e.Description && e.Description.length > 0 ? '\n * * Description: ' + EntitySubClassGeneratorBase.SanitizeDescription(e.Description) : ''}${isaSourceComment}${conflictNote}
235
239
  */
236
240
  get ${safeName}(): ${typeString} {
237
241
  ${getterBody}
238
242
  }`;
239
- if (!e.ReadOnly || (e.IsPrimaryKey && !e.AutoIncrement) || isISAParentField) {
240
- sRet += `
243
+ if (!e.ReadOnly || (e.IsPrimaryKey && !e.AutoIncrement) || isISAParentField) {
244
+ sRet += `
241
245
  set ${safeName}(value: ${typeString}) {
242
246
  ${setterBody}
243
247
  }`;
244
- }
245
- // JSONType: emit additional typed "Object" accessor with caching
246
- if (hasJSONType && jsonTypeAccessorInfo) {
247
- const objName = `${safeName}Object`;
248
- const cachedField = `_${objName}_cached`;
249
- const lastRawField = `_${objName}_lastRaw`;
250
- const ft = jsonTypeAccessorInfo.fullTypeString;
251
- sRet += `
248
+ }
249
+ // JSONType: emit additional typed "Object" accessor with caching
250
+ if (hasJSONType && jsonTypeAccessorInfo) {
251
+ const objName = `${safeName}Object`;
252
+ const cachedField = `_${objName}_cached`;
253
+ const lastRawField = `_${objName}_lastRaw`;
254
+ const ft = jsonTypeAccessorInfo.fullTypeString;
255
+ sRet += `
252
256
 
253
257
  private ${cachedField}: ${ft} | undefined = undefined;
254
258
  private ${lastRawField}: string | null = null;
@@ -270,29 +274,29 @@ export const loadModule = () => {
270
274
  this.${cachedField} = value;
271
275
  this.${lastRawField} = raw;
272
276
  }`;
273
- }
274
- return sRet;
275
- }).join('\n\n');
276
- // Base-class resolution, in precedence order:
277
- // 1. An explicit custom subclass configured on the entity (EntityObjectSubclassName/Import).
278
- // 2. ReadOnlyExternalBaseEntity for entities backed by an external data source — they
279
- // proxy a remote system live and must reject Save/Delete. (External entities only ever
280
- // appear in downstream generated packages, so the @memberjunction/core-entities import
281
- // is always a clean cross-package import, never a self-import.)
282
- // 3. BaseEntity for everything else.
283
- // Base class + its import come from resolveEntityBaseClass (pure) so the file assembler can hoist
284
- // and de-duplicate the imports into the header. The per-entity inline import (below) is emitted
285
- // ONLY in the standalone includeFileHeader path (single entity → no duplication); in the
286
- // multi-entity file path the assembler owns the (deduped) imports.
287
- const { baseClass: sBaseClass, importStatement: subClassImportStatement } = this.resolveEntityBaseClass(entity);
288
- if (entity.ExternalDataSourceID && entity.EntityObjectSubclassName && entity.EntityObjectSubclassImport) {
289
- // Custom subclass takes precedence over ReadOnlyExternalBaseEntity, so this external entity's
290
- // read-only guarantee holds ONLY if that subclass itself extends ReadOnlyExternalBaseEntity.
291
- // Warn so the author doesn't silently lose Save/Delete rejection.
292
- logStatus(` ⚠️ External entity '${entity.Name}' has a custom subclass ('${entity.EntityObjectSubclassName}') that overrides ReadOnlyExternalBaseEntity — Save/Delete will be rejected only if '${entity.EntityObjectSubclassName}' extends ReadOnlyExternalBaseEntity.`);
293
277
  }
294
- const loadFieldString = entity.PrimaryKeys.map((f) => `${f.CodeName}: ${f.TSType}`).join(', ');
295
- const loadFunction = ` /**
278
+ return sRet;
279
+ }).join('\n\n');
280
+ // Base-class resolution, in precedence order:
281
+ // 1. An explicit custom subclass configured on the entity (EntityObjectSubclassName/Import).
282
+ // 2. ReadOnlyExternalBaseEntity for entities backed by an external data source — they
283
+ // proxy a remote system live and must reject Save/Delete. (External entities only ever
284
+ // appear in downstream generated packages, so the @memberjunction/core-entities import
285
+ // is always a clean cross-package import, never a self-import.)
286
+ // 3. BaseEntity for everything else.
287
+ // Base class + its import come from resolveEntityBaseClass (pure) so the file assembler can hoist
288
+ // and de-duplicate the imports into the header. The per-entity inline import (below) is emitted
289
+ // ONLY in the standalone includeFileHeader path (single entity → no duplication); in the
290
+ // multi-entity file path the assembler owns the (deduped) imports.
291
+ const { baseClass: sBaseClass, importStatement: subClassImportStatement } = this.resolveEntityBaseClass(entity);
292
+ if (entity.ExternalDataSourceID && entity.EntityObjectSubclassName && entity.EntityObjectSubclassImport) {
293
+ // Custom subclass takes precedence over ReadOnlyExternalBaseEntity, so this external entity's
294
+ // read-only guarantee holds ONLY if that subclass itself extends ReadOnlyExternalBaseEntity.
295
+ // Warn so the author doesn't silently lose Save/Delete rejection.
296
+ logStatus(` ⚠️ External entity '${entity.Name}' has a custom subclass ('${entity.EntityObjectSubclassName}') that overrides ReadOnlyExternalBaseEntity — Save/Delete will be rejected only if '${entity.EntityObjectSubclassName}' extends ReadOnlyExternalBaseEntity.`);
297
+ }
298
+ const loadFieldString = entity.PrimaryKeys.map((f) => `${f.CodeName}: ${f.TSType}`).join(', ');
299
+ const loadFunction = ` /**
296
300
  * Loads the ${entity.Name} record from the database
297
301
  ${entity.PrimaryKeys.map((f) => `* @param ${f.CodeName}: ${f.TSType} - primary key value to load the ${entity.Name} record.`).join('\n ')}
298
302
  * @param EntityRelationshipsToLoad - (optional) the relationships to load
@@ -308,10 +312,10 @@ export const loadModule = () => {
308
312
  ${entity.PrimaryKeys.map((f) => `compositeKey.KeyValuePairs.push({ FieldName: '${f.Name}', Value: ${f.CodeName} });`).join('\n ')}
309
313
  return await super.InnerLoad(compositeKey, EntityRelationshipsToLoad);
310
314
  }`;
311
- let deleteFunction = '';
312
- if (!entity.AllowDeleteAPI) {
313
- // Entity doesn't allow delete at all
314
- deleteFunction = ` /**
315
+ let deleteFunction = '';
316
+ if (!entity.AllowDeleteAPI) {
317
+ // Entity doesn't allow delete at all
318
+ deleteFunction = ` /**
315
319
  * ${entity.Name} - AllowDeleteAPI is set to 0 in the database. Delete is not allowed, so this method is generated to override the base class method and throw an error. To enable delete for this entity, set AllowDeleteAPI to 1 in the database.
316
320
  * @public
317
321
  * @method
@@ -322,10 +326,10 @@ export const loadModule = () => {
322
326
  public override async Delete(): Promise<boolean> {
323
327
  throw new Error('Delete is not allowed for ${entity.Name}, to enable it set AllowDeleteAPI to 1 in the database.');
324
328
  }`;
325
- }
326
- else if (entity.CascadeDeletes) {
327
- // Entity has cascading deletes, wrap in transaction
328
- deleteFunction = ` /**
329
+ }
330
+ else if (entity.CascadeDeletes) {
331
+ // Entity has cascading deletes, wrap in transaction
332
+ deleteFunction = ` /**
329
333
  * ${entity.Name} - Delete method override to wrap in transaction since CascadeDeletes is true.
330
334
  * Wrapping in a transaction ensures that all cascade delete operations are handled atomically.
331
335
  * @public
@@ -359,10 +363,10 @@ export const loadModule = () => {
359
363
  return super.Delete(options);
360
364
  }
361
365
  }`;
362
- }
363
- const saveFunction = entity.AllowCreateAPI || entity.AllowUpdateAPI
364
- ? ''
365
- : ` /**
366
+ }
367
+ const saveFunction = entity.AllowCreateAPI || entity.AllowUpdateAPI
368
+ ? ''
369
+ : ` /**
366
370
  * ${entity.Name} - AllowCreateAPI and AllowUpdateAPI are both set to 0 in the database. Save is not allowed, so this method is generated to override the base class method and throw an error. To enable save for this entity, set AllowCreateAPI and/or AllowUpdateAPI to 1 in the database.
367
371
  * @public
368
372
  * @method
@@ -373,52 +377,54 @@ export const loadModule = () => {
373
377
  public override async Save(options?: EntitySaveOptions) : Promise<boolean> {
374
378
  throw new Error('Save is not allowed for ${entity.Name}, to enable it set AllowCreateAPI and/or AllowUpdateAPI to 1 in the database.');
375
379
  }`;
376
- const validateFunction = await this.LogAndGenerateValidateFunction(pool, entity, skipDBUpdate);
377
- const status = entity.Status.trim().toLowerCase();
378
- const deprecatedFlag = status === 'deprecated' || status === 'disabled' ?
379
- `\n * @deprecated This entity is deprecated and will be removed in a future version. Using it will result in console warnings.` : '';
380
- const disabledFlag = status === 'disabled' ?
381
- `\n * @disabled This entity is disabled and will not be available in the application. Attempting to use it will result in exceptions being thrown` : '';
382
- // Collect and deduplicate JSONTypeDefinitions for this entity.
383
- // These are raw TypeScript interface/type definitions (from EntityField.JSONTypeDefinition)
384
- // that get emitted above the entity class so the typed getters/setters can reference them.
385
- // A Set is used because multiple fields may share the same definition (e.g., a shared config type).
386
- // Each definition is validated via the TypeScript compiler API before inclusion.
387
- const jsonTypeDefinitions = new Set();
388
- for (const field of sortedFields) {
389
- if (field.JSONTypeDefinition && field.JSONTypeDefinition.trim().length > 0) {
390
- const definition = field.JSONTypeDefinition.trim();
391
- if (field.JSONType && field.JSONType.trim().length > 0) {
392
- const validation = EntitySubClassGeneratorBase.ValidateJSONTypeDefinition(definition, field.JSONType.trim(), entity.Name, field.Name);
393
- if (!validation.valid) {
394
- for (const err of validation.errors) {
395
- logError(err);
396
- }
397
- logError(`[JSONType] Skipping JSONTypeDefinition for ${entity.Name}.${field.Name} due to validation errors. The field will use a plain string getter/setter instead.`);
398
- field.JSONType = null;
399
- continue;
380
+ const validateFunction = await this.LogAndGenerateValidateFunction(pool, entity, skipDBUpdate);
381
+ const status = entity.Status.trim().toLowerCase();
382
+ const deprecatedFlag = status === 'deprecated' || status === 'disabled' ?
383
+ `\n * @deprecated This entity is deprecated and will be removed in a future version. Using it will result in console warnings.` : '';
384
+ const disabledFlag = status === 'disabled' ?
385
+ `\n * @disabled This entity is disabled and will not be available in the application. Attempting to use it will result in exceptions being thrown` : '';
386
+ // Collect and deduplicate JSONTypeDefinitions for this entity.
387
+ // These are raw TypeScript interface/type definitions (from EntityField.JSONTypeDefinition)
388
+ // that get emitted above the entity class so the typed getters/setters can reference them.
389
+ // A Set is used because multiple fields may share the same definition (e.g., a shared config type).
390
+ // Each definition is validated via the TypeScript compiler API before inclusion.
391
+ const jsonTypeDefinitions = new Set();
392
+ for (const field of sortedFields) {
393
+ if (field.JSONTypeDefinition && field.JSONTypeDefinition.trim().length > 0) {
394
+ const definition = field.JSONTypeDefinition.trim();
395
+ if (field.JSONType && field.JSONType.trim().length > 0) {
396
+ const validation = EntitySubClassGeneratorBase.ValidateJSONTypeDefinition(definition, field.JSONType.trim(), entity.Name, field.Name);
397
+ if (!validation.valid) {
398
+ for (const err of validation.errors) {
399
+ logError(err);
400
400
  }
401
+ logError(`[JSONType] Skipping JSONTypeDefinition for ${entity.Name}.${field.Name} due to validation errors. The field will use a plain string getter/setter instead.`);
402
+ field.JSONType = null;
403
+ continue;
401
404
  }
402
- // Prefix all type names defined in this definition block with the entity class name
403
- // to avoid naming conflicts across entities. Uses AST to find all defined type names.
404
- let rewrittenDef = definition;
405
- const sourceFile = ts.createSourceFile('temp.ts', definition, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
406
- ts.forEachChild(sourceFile, (node) => {
407
- if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) ||
408
- ts.isEnumDeclaration(node) || ts.isClassDeclaration(node)) && node.name) {
409
- const originalName = node.name.text;
410
- const prefixedName = `${sClassName}_${originalName}`;
411
- rewrittenDef = rewrittenDef.replace(new RegExp('\\b' + originalName + '\\b', 'g'), prefixedName);
412
- }
413
- });
414
- jsonTypeDefinitions.add(rewrittenDef);
415
405
  }
406
+ // Prefix all type names defined in this definition block with the entity class name
407
+ // to avoid naming conflicts across entities. Uses AST to find all defined type names.
408
+ let rewrittenDef = definition;
409
+ const sourceFile = ts.createSourceFile('temp.ts', definition, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
410
+ ts.forEachChild(sourceFile, (node) => {
411
+ if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) ||
412
+ ts.isEnumDeclaration(node) || ts.isClassDeclaration(node)) && node.name) {
413
+ const originalName = node.name.text;
414
+ const prefixedName = `${sClassName}_${originalName}`;
415
+ rewrittenDef = rewrittenDef.replace(new RegExp('\\b' + originalName + '\\b', 'g'), prefixedName);
416
+ }
417
+ });
418
+ jsonTypeDefinitions.add(rewrittenDef);
416
419
  }
417
- const jsonTypeBlock = jsonTypeDefinitions.size > 0
418
- ? '\n' + Array.from(jsonTypeDefinitions).join('\n\n') + '\n'
419
- : '';
420
- const relatedRecordCollections = EntitySubClassGeneratorBase.GenerateRelatedRecordCollections(entity);
421
- let sRet = `
420
+ }
421
+ const jsonTypeBlock = jsonTypeDefinitions.size > 0
422
+ ? '\n' + Array.from(jsonTypeDefinitions).join('\n\n') + '\n'
423
+ : '';
424
+ const relatedRecordCollections = EntitySubClassGeneratorBase.GenerateRelatedRecordCollections(entity);
425
+ const embeddedRecords = EntitySubClassGeneratorBase.GenerateEmbeddedRecords(entity);
426
+ const hierarchyMethods = EntitySubClassGeneratorBase.GenerateHierarchyMethods(entity, sClassName);
427
+ let sRet = `
422
428
  ${jsonTypeBlock}
423
429
  /**
424
430
  * ${entity.Name} - strongly typed entity sub-class
@@ -431,15 +437,14 @@ ${jsonTypeBlock}
431
437
  * @public${deprecatedFlag}
432
438
  */
433
439
  ${includeFileHeader ? subClassImportStatement : ''}@RegisterClass(BaseEntity, '${entity.Name}')
434
- export class ${sClassName} extends ${sBaseClass}<${sClassName}Type> {${relatedRecordCollections}${loadFunction ? '\n' + loadFunction : ''}${saveFunction ? '\n\n' + saveFunction : ''}${deleteFunction ? '\n\n' + deleteFunction : ''}${validateFunction ? '\n\n' + validateFunction : ''}
440
+ export class ${sClassName} extends ${sBaseClass}<${sClassName}Type> {${relatedRecordCollections}${embeddedRecords}${hierarchyMethods}${loadFunction ? '\n' + loadFunction : ''}${saveFunction ? '\n\n' + saveFunction : ''}${deleteFunction ? '\n\n' + deleteFunction : ''}${validateFunction ? '\n\n' + validateFunction : ''}
435
441
 
436
442
  ${fields}
437
443
  }
438
444
  `;
439
- if (includeFileHeader)
440
- sRet = this.generateEntitySubClassFileHeader() + sRet;
441
- return sRet;
442
- }
445
+ if (includeFileHeader)
446
+ sRet = this.generateEntitySubClassFileHeader() + sRet;
447
+ return sRet;
443
448
  }
444
449
  /**
445
450
  * Emits `DeclareRelatedRecords(...)` declarations for every relationship on this entity whose
@@ -673,6 +678,214 @@ ${fields}
673
678
  static EscapeSingleQuotes(value) {
674
679
  return String(value ?? '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
675
680
  }
681
+ /**
682
+ * Emits `{Field}_Object` / `{Field}_EnsureObject()` for every FK field whose
683
+ * `EmbeddedRecord` column holds a config object.
684
+ *
685
+ * RelatedEntity and the FK field name come from the row (`RelatedEntityID`, `Name`),
686
+ * not the JSON. `AllowsNull` on the same row decides the getter's nullability.
687
+ *
688
+ * @param entity - The entity being generated.
689
+ * @returns The declarations block, or an empty string when none are declared.
690
+ */
691
+ static GenerateEmbeddedRecords(entity) {
692
+ const declared = (entity.Fields ?? []).filter(f => f.EmbeddedRecord && String(f.EmbeddedRecord).trim().length > 0);
693
+ if (declared.length === 0) {
694
+ return '';
695
+ }
696
+ const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
697
+ const blocks = [];
698
+ const seen = new Set();
699
+ for (const field of declared) {
700
+ const config = EntitySubClassGeneratorBase.ParseEmbeddedRecordConfig(entity, field);
701
+ if (!config) {
702
+ continue;
703
+ }
704
+ const nameKey = `${field.Name}_Object`.toLowerCase();
705
+ if (seen.has(nameKey)) {
706
+ logError(`[EmbeddedRecord] ${entity.Name}.${field.Name}: duplicate generated name '${field.Name}_Object'; skipping.`);
707
+ continue;
708
+ }
709
+ seen.add(nameKey);
710
+ const related = field.RelatedEntityID ? md.EntityByID(field.RelatedEntityID) : undefined;
711
+ if (!related) {
712
+ logError(`[EmbeddedRecord] ${entity.Name}.${field.Name}: RelatedEntityID is missing or unknown; skipping.`);
713
+ continue;
714
+ }
715
+ blocks.push(EntitySubClassGeneratorBase.RenderEmbeddedRecord(entity, field, related, config));
716
+ }
717
+ return blocks.length > 0 ? '\n' + blocks.join('\n') : '';
718
+ }
719
+ /**
720
+ * Emits strongly-typed hierarchy traversal helper methods (`GetDescendants`, `GetAncestors`, `GetChildren`)
721
+ * for entities with recursive self-referencing foreign keys.
722
+ *
723
+ * @param entity - The entity being generated.
724
+ * @param sClassName - The generated subclass name.
725
+ * @returns The generated methods block, or an empty string when the entity has no recursive FKs.
726
+ */
727
+ static GenerateHierarchyMethods(entity, sClassName) {
728
+ const recursiveFKs = (entity.Fields ?? []).filter(f => f.RelatedEntityID != null && (UUIDsEqual(f.RelatedEntityID, entity.ID) || f.RelatedEntity === entity.Name) && !f.IsVirtual && f.IsHierarchy === true);
729
+ if (recursiveFKs.length === 0) {
730
+ return '';
731
+ }
732
+ if (entity.PrimaryKeys.length !== 1) {
733
+ logWarning(`[Hierarchy] Entity '${entity.Name}' has ${recursiveFKs.length} hierarchy foreign key(s) ` +
734
+ `(${recursiveFKs.map(f => f.Name).join(', ')}), but has ${entity.PrimaryKeys.length} primary key fields. ` +
735
+ `MemberJunction hierarchy traversal requires a single-column primary key; skipping subclass hierarchy methods.`);
736
+ return '';
737
+ }
738
+ const methods = [];
739
+ const pkName = entity.FirstPrimaryKey?.Name ?? 'ID';
740
+ for (const field of recursiveFKs) {
741
+ const fieldName = field.Name;
742
+ const rootField = `Root${fieldName}`;
743
+ const depthField = `${fieldName}Depth`;
744
+ const pathField = `${fieldName}Path`;
745
+ const isPrimaryHierarchy = recursiveFKs.length === 1 || fieldName === 'ParentID';
746
+ // For primary hierarchies (e.g. ParentID), BaseEntity already provides strongly-typed
747
+ // GetDescendants<T = this>(), GetAncestors<T = this>(), and GetChildren<T = this>().
748
+ // We only generate named helper methods for non-primary hierarchy fields (e.g. ManagerID).
749
+ if (isPrimaryHierarchy) {
750
+ continue;
751
+ }
752
+ methods.push(`
753
+ /**
754
+ * Retrieves all descendant records in the ${fieldName} hierarchy under this record using a single RunView query.
755
+ * @param maxDepth Optional maximum relative depth to retrieve.
756
+ * @returns Array of descendant entity instances ordered by hierarchy depth.
757
+ */
758
+ public async Get${fieldName}Descendants<T extends BaseEntity = this>(maxDepth?: number): Promise<T[]> {
759
+ return this.GetDescendants<T>({ parentFieldName: '${fieldName}', maxDepth });
760
+ }
761
+
762
+ /**
763
+ * Retrieves all ancestor records in the ${fieldName} hierarchy from the top-level root down to this record using a single RunView query.
764
+ * @returns Array of ancestor entity instances ordered from root down to parent.
765
+ */
766
+ public async Get${fieldName}Ancestors<T extends BaseEntity = this>(): Promise<T[]> {
767
+ return this.GetAncestors<T>('${fieldName}');
768
+ }
769
+
770
+ /**
771
+ * Retrieves all direct child records in the ${fieldName} hierarchy of this record using a single RunView query.
772
+ * @returns Array of direct child entity instances.
773
+ */
774
+ public async Get${fieldName}Children<T extends BaseEntity = this>(): Promise<T[]> {
775
+ return this.GetChildren<T>('${fieldName}');
776
+ }`);
777
+ }
778
+ return methods.length > 0 ? '\n' + methods.join('\n') : '';
779
+ }
780
+ /**
781
+ * Import statements for embedded peers that live in another generated file.
782
+ *
783
+ * @param entity - The owning entity.
784
+ * @param localClassNames - Class names already emitted in this file.
785
+ */
786
+ static CollectEmbeddedImports(entity, localClassNames) {
787
+ const declared = (entity.Fields ?? []).filter(f => f.EmbeddedRecord && String(f.EmbeddedRecord).trim().length > 0 && f.RelatedEntityID);
788
+ if (declared.length === 0) {
789
+ return [];
790
+ }
791
+ const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
792
+ const imports = [];
793
+ const seen = new Set();
794
+ for (const field of declared) {
795
+ const related = md.EntityByID(field.RelatedEntityID);
796
+ if (!related) {
797
+ continue;
798
+ }
799
+ const className = `${related.ClassName}Entity`;
800
+ if (localClassNames.has(className) || seen.has(className)) {
801
+ continue;
802
+ }
803
+ seen.add(className);
804
+ const coreSchema = typeof mj_core_schema === 'function' ? mj_core_schema() : String(mj_core_schema);
805
+ const pkg = related.SchemaName && related.SchemaName.toLowerCase() === String(coreSchema).toLowerCase()
806
+ ? '@memberjunction/core-entities'
807
+ : resolveEntityPackageName(related.SchemaName);
808
+ imports.push(`import { ${className} } from '${pkg}';\n`);
809
+ }
810
+ return imports;
811
+ }
812
+ /**
813
+ * Parses and validates one field's `EmbeddedRecord` JSON.
814
+ * Invalid metadata is skipped with a logged error rather than throwing.
815
+ */
816
+ static ParseEmbeddedRecordConfig(entity, field) {
817
+ const label = `${entity.Name}.${field.Name}`;
818
+ let parsed;
819
+ try {
820
+ parsed = JSON.parse(String(field.EmbeddedRecord));
821
+ }
822
+ catch (e) {
823
+ logError(`[EmbeddedRecord] ${label}: EmbeddedRecord is not valid JSON ` +
824
+ `(${e instanceof Error ? e.message : String(e)}); skipping.`);
825
+ return null;
826
+ }
827
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
828
+ logError(`[EmbeddedRecord] ${label}: expected a JSON object; skipping.`);
829
+ return null;
830
+ }
831
+ const config = parsed;
832
+ const clearModes = ['delete', 'orphan', 'refuse'];
833
+ if (config.OnClear && !clearModes.includes(config.OnClear)) {
834
+ logError(`[EmbeddedRecord] ${label}: invalid OnClear '${config.OnClear}'; skipping.`);
835
+ return null;
836
+ }
837
+ const nestedModes = ['inherit', 'related'];
838
+ if (config.LoadNested && !nestedModes.includes(config.LoadNested)) {
839
+ logError(`[EmbeddedRecord] ${label}: invalid LoadNested '${config.LoadNested}'; skipping.`);
840
+ return null;
841
+ }
842
+ const emittedName = field.CodeName ? SafeCodeName(field) : field.Name;
843
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(emittedName)) {
844
+ logError(`[EmbeddedRecord] ${label}: emitted name '${emittedName}' is not a valid TypeScript identifier; skipping.`);
845
+ return null;
846
+ }
847
+ return config;
848
+ }
849
+ /**
850
+ * Renders the private companion plus the public `{Field}_Object` / `{Field}_EnsureObject` API.
851
+ */
852
+ static RenderEmbeddedRecord(entity, field, related, config) {
853
+ const relatedClass = `${related.ClassName}Entity`;
854
+ const getterType = field.AllowsNull ? `${relatedClass} | null` : relatedClass;
855
+ const code = field.CodeName ? SafeCodeName(field) : field.Name;
856
+ const lines = [
857
+ `ForeignKeyField: '${EntitySubClassGeneratorBase.EscapeSingleQuotes(field.Name)}'`,
858
+ `RelatedEntity: '${EntitySubClassGeneratorBase.EscapeSingleQuotes(related.Name)}'`,
859
+ ];
860
+ if (config.OnClear) {
861
+ lines.push(`OnClear: '${config.OnClear}'`);
862
+ }
863
+ if (config.LoadNested) {
864
+ lines.push(`LoadNested: '${config.LoadNested}'`);
865
+ }
866
+ const requiredNote = field.AllowsNull
867
+ ? ` Null until ${code}_EnsureObject() or Load() finds a value.`
868
+ : ` Always present after GetEntityObject / NewRecord.`;
869
+ return `
870
+ /**
871
+ * Embedded record: ${related.Name}
872
+ *
873
+ * 1:1 peer joined by this record's ${field.Name}. Loaded and saved with this
874
+ * ${entity.Name} record — see packages/MJCore/docs/embedded-records.md.
875
+ * Declared by EntityField.EmbeddedRecord on '${entity.Name}.${field.Name}'; edit that row, not this file.
876
+ *${requiredNote}
877
+ */
878
+ private readonly __emb_${code} = this.DeclareEmbeddedRecord<${relatedClass}>({
879
+ ${lines.join(',\n ')},
880
+ });
881
+ public get ${code}_Object(): ${getterType} {
882
+ return this.__emb_${code}.Value${field.AllowsNull ? '' : '!'};
883
+ }
884
+ public ${code}_EnsureObject(): ${relatedClass} {
885
+ return this.__emb_${code}.Ensure();
886
+ }
887
+ `;
888
+ }
676
889
  /**
677
890
  * Finds the source parent entity for an IS-A inherited field by walking the parent chain.
678
891
  * Returns the name of the parent entity that originally defines the field (as a non-virtual column).