@memberjunction/codegen-lib 6.1.0-edge.1 → 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.
- package/LICENSE +180 -4
- package/README.md +39 -22
- package/dist/Angular/angular-codegen.d.ts +0 -8
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +32 -22
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.js +2 -2
- package/dist/Angular/entity-data-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/related-entity-components.d.ts +1 -1
- package/dist/Angular/related-entity-components.js +1 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts +107 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.js +77 -10
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +354 -14
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +1308 -65
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/materializationAnalysis.d.ts +241 -0
- package/dist/Database/materializationAnalysis.d.ts.map +1 -0
- package/dist/Database/materializationAnalysis.js +415 -0
- package/dist/Database/materializationAnalysis.js.map +1 -0
- package/dist/Database/materializationBroadRender.d.ts +67 -0
- package/dist/Database/materializationBroadRender.d.ts.map +1 -0
- package/dist/Database/materializationBroadRender.js +104 -0
- package/dist/Database/materializationBroadRender.js.map +1 -0
- package/dist/Database/materializationDrift.d.ts +59 -0
- package/dist/Database/materializationDrift.d.ts.map +1 -0
- package/dist/Database/materializationDrift.js +85 -0
- package/dist/Database/materializationDrift.js.map +1 -0
- package/dist/Database/materializationParamClassifier.d.ts +92 -0
- package/dist/Database/materializationParamClassifier.d.ts.map +1 -0
- package/dist/Database/materializationParamClassifier.js +224 -0
- package/dist/Database/materializationParamClassifier.js.map +1 -0
- package/dist/Database/materializationParamVerifier.d.ts +71 -0
- package/dist/Database/materializationParamVerifier.d.ts.map +1 -0
- package/dist/Database/materializationParamVerifier.js +302 -0
- package/dist/Database/materializationParamVerifier.js.map +1 -0
- package/dist/Database/materializationSqlAst.d.ts +78 -0
- package/dist/Database/materializationSqlAst.d.ts.map +1 -0
- package/dist/Database/materializationSqlAst.js +142 -0
- package/dist/Database/materializationSqlAst.js.map +1 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +69 -30
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +325 -216
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts +2 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js +119 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js.map +1 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts +2 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js +155 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +61 -2
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +306 -3
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js +547 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js +721 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js +78 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js +155 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js.map +1 -0
- package/dist/Database/sql_codegen.d.ts +21 -5
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +85 -27
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +4 -4
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/createNewUser.js +1 -1
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts +44 -0
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +369 -156
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts +4 -58
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +22 -236
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +25 -0
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +51 -8
- package/dist/Misc/sql_logging.js.map +1 -1
- 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
|
-
.
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
240
|
-
|
|
243
|
+
if (!e.ReadOnly || (e.IsPrimaryKey && !e.AutoIncrement) || isISAParentField) {
|
|
244
|
+
sRet += `
|
|
241
245
|
set ${safeName}(value: ${typeString}) {
|
|
242
246
|
${setterBody}
|
|
243
247
|
}`;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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).
|