@memberjunction/codegen-lib 6.1.0-edge.2 → 6.1.0-edge.4
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 +89 -43
- 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/Config/config.d.ts +118 -1
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +202 -8
- package/dist/Config/config.js.map +1 -1
- package/dist/Database/__tests__/exclude-schema-permissions.test.d.ts +2 -0
- package/dist/Database/__tests__/exclude-schema-permissions.test.d.ts.map +1 -0
- package/dist/Database/__tests__/exclude-schema-permissions.test.js +32 -0
- package/dist/Database/__tests__/exclude-schema-permissions.test.js.map +1 -0
- package/dist/Database/codeGenDatabaseProvider.d.ts +178 -2
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.js +175 -10
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
- package/dist/Database/heal-schema-params.d.ts +33 -0
- package/dist/Database/heal-schema-params.d.ts.map +1 -0
- package/dist/Database/heal-schema-params.js +51 -0
- package/dist/Database/heal-schema-params.js.map +1 -0
- package/dist/Database/manage-metadata.d.ts +301 -2
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +1242 -96
- 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 +93 -56
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +399 -247
- 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/postgresql/__tests__/SoftPrimaryKeyIndex.test.d.ts +2 -0
- package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.js +110 -0
- package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.js.map +1 -0
- package/dist/Database/providers/postgresql/metadataSupportObjects.js +78 -10
- package/dist/Database/providers/postgresql/metadataSupportObjects.js.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +72 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +322 -0
- 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/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.js +195 -0
- package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.js.map +1 -0
- package/dist/Database/schema-filters.d.ts +23 -0
- package/dist/Database/schema-filters.d.ts.map +1 -0
- package/dist/Database/schema-filters.js +42 -0
- package/dist/Database/schema-filters.js.map +1 -0
- package/dist/Database/schema-scope.d.ts.map +1 -1
- package/dist/Database/schema-scope.js +4 -0
- package/dist/Database/schema-scope.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +39 -5
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +176 -67
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/EntityNameScanner/EntityNameScanner.d.ts.map +1 -1
- package/dist/EntityNameScanner/EntityNameScanner.js +33 -2
- package/dist/EntityNameScanner/EntityNameScanner.js.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +8 -4
- package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
- package/dist/Manifest/GenerateClassRegistrationsManifest.js +63 -20
- package/dist/Manifest/GenerateClassRegistrationsManifest.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/entity_subclasses_codegen.d.ts +61 -0
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +437 -158
- 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/dist/Misc/system_integrity.d.ts.map +1 -1
- package/dist/Misc/system_integrity.js +5 -0
- package/dist/Misc/system_integrity.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, resolveEntityImportPackage } 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,22 +119,31 @@ 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.
|
|
121
|
-
const
|
|
122
|
-
|
|
122
|
+
const entitiesWithPK = entities.filter((e) => e.PrimaryKeys.length > 0);
|
|
123
|
+
const localClassNames = new Set(entitiesWithPK.map((e) => `${e.ClassName}Entity`));
|
|
124
|
+
const baseClassImports = [...new Set(entitiesWithPK
|
|
123
125
|
.map((e) => this.resolveEntityBaseClass(e).importStatement)
|
|
124
126
|
.filter((s) => s.length > 0))].join('');
|
|
127
|
+
// Collect every embed + related-record-collection peer across the file, then emit
|
|
128
|
+
// one grouped `import { A, B } from 'pkg'` per npm package. Per-entity import
|
|
129
|
+
// lines would duplicate packages (Address on two owners → two import lines).
|
|
130
|
+
const peerImports = entitiesWithPK.flatMap((e) => EntitySubClassGeneratorBase.CollectPeerClassImports(e, localClassNames));
|
|
131
|
+
const peerImportStatements = EntitySubClassGeneratorBase.FormatPeerImportStatements(peerImports).join('');
|
|
132
|
+
const subclassImports = `${baseClassImports}${peerImportStatements}`;
|
|
125
133
|
const allContent = `${this.generateEntitySubClassFileHeader()} \n ${subclassImports}${zodContent} \n ${sContent}`;
|
|
126
134
|
makeDir(directory);
|
|
127
135
|
fs.writeFileSync(path.join(directory, 'entity_subclasses.ts'), allContent);
|
|
128
136
|
return true;
|
|
129
137
|
}
|
|
130
138
|
catch (err) {
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
140
|
+
logError(message);
|
|
131
141
|
console.error(err);
|
|
132
142
|
return false;
|
|
133
143
|
}
|
|
134
144
|
}
|
|
135
145
|
generateEntitySubClassFileHeader() {
|
|
136
|
-
return `import { BaseEntity, EntitySaveOptions, EntityDeleteOptions, CompositeKey, ValidationResult, ValidationErrorInfo, ValidationErrorType, Metadata, ProviderType, DatabaseProviderBase } from "@memberjunction/core";
|
|
146
|
+
return `import { BaseEntity, EntitySaveOptions, EntityDeleteOptions, CompositeKey, ValidationResult, ValidationErrorInfo, ValidationErrorType, Metadata, ProviderType, DatabaseProviderBase, RunView } from "@memberjunction/core";
|
|
137
147
|
import { RegisterClass } from "@memberjunction/global";
|
|
138
148
|
import { z } from "zod";
|
|
139
149
|
|
|
@@ -167,88 +177,87 @@ export const loadModule = () => {
|
|
|
167
177
|
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
178
|
return '';
|
|
169
179
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
const sClassName = `${entity.ClassName}Entity`;
|
|
181
|
+
// Sort fields by Sequence, then by __mj_CreatedAt for consistent ordering
|
|
182
|
+
const sortedFields = sortBySequenceAndCreatedAt(entity.Fields);
|
|
183
|
+
const fields = sortedFields.map((e) => {
|
|
184
|
+
let values = '';
|
|
185
|
+
let valueList = '';
|
|
186
|
+
if (e.ValueListType && e.ValueListType.length > 0 && e.ValueListType.trim().toLowerCase() !== 'none') {
|
|
187
|
+
// Sort by Sequence to ensure consistent ordering in comments
|
|
188
|
+
const sortedValues = sortBySequenceAndCreatedAt([...e.EntityFieldValues]);
|
|
189
|
+
values = sortedValues.map((v) => `\n * * ${v.Value}${v.Description && v.Description.length > 0 ? ' - ' + EntitySubClassGeneratorBase.SanitizeDescription(v.Description) : ''}`).join('');
|
|
190
|
+
valueList = `\n * * Value List Type: ${e.ValueListType}\n * * Possible Values ` + values;
|
|
191
|
+
}
|
|
192
|
+
// JSONType: when a field has JSONType metadata, we emit the standard string getter/setter
|
|
193
|
+
// unchanged, plus an additional typed "Object" accessor (e.g., DefaultNavItemsObject) that
|
|
194
|
+
// handles JSON.parse/stringify with caching. Interface names are entity-prefixed for safety.
|
|
195
|
+
const hasJSONType = e.JSONType && e.JSONType.trim().length > 0;
|
|
196
|
+
let typeString = TypeScriptTypeFromSQLType(e.Type) + (e.AllowsNull ? ' | null' : '');
|
|
197
|
+
// Build the typed accessor info (used later for the Object suffix property)
|
|
198
|
+
let jsonTypeAccessorInfo = null;
|
|
199
|
+
if (hasJSONType) {
|
|
200
|
+
const originalTypeName = e.JSONType.trim();
|
|
201
|
+
const prefixedTypeName = `${sClassName}_${originalTypeName}`;
|
|
202
|
+
const isArray = e.JSONTypeIsArray === true;
|
|
203
|
+
const fullTypeString = isArray ? `Array<${prefixedTypeName}>` : prefixedTypeName;
|
|
204
|
+
jsonTypeAccessorInfo = { prefixedTypeName, fullTypeString: fullTypeString + (e.AllowsNull ? ' | null' : ''), isArray };
|
|
205
|
+
// typeString stays as the standard SQL-mapped type (string | null) for the base getter
|
|
206
|
+
}
|
|
207
|
+
if (!hasJSONType && e.ValueListTypeEnum !== EntityFieldValueListType.None && e.EntityFieldValues && e.EntityFieldValues.length > 0) {
|
|
208
|
+
// construct a typeString that is a union of the possible values
|
|
209
|
+
const quotes = e.NeedsQuotes ? "'" : '';
|
|
210
|
+
// Sort deterministically by Sequence, CreatedAt, then Value to prevent flip-flopping across runs
|
|
211
|
+
const sortedValues = sortBySequenceAndCreatedAt([...e.EntityFieldValues]);
|
|
212
|
+
typeString = sortedValues.map((v) => `${quotes}${v.Value}${quotes}`).join(' | ');
|
|
213
|
+
if (e.ValueListTypeEnum === EntityFieldValueListType.ListOrUserEntry) {
|
|
214
|
+
// special case becuase a user can enter whatever they want
|
|
215
|
+
typeString += ' | ' + TypeScriptTypeFromSQLType(e.Type);
|
|
182
216
|
}
|
|
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
|
|
217
|
+
// finally, add the null type if it allows null
|
|
218
|
+
if (e.AllowsNull) {
|
|
219
|
+
typeString += ' | null';
|
|
197
220
|
}
|
|
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 = ` /**
|
|
221
|
+
}
|
|
222
|
+
const fieldDeprecatedFlag = e.Status === 'Deprecated' || e.Status === 'Disabled' ?
|
|
223
|
+
`\n * * @deprecated This field is deprecated and will be removed in a future version. Using it will result in console warnings.` : '';
|
|
224
|
+
const fieldDisabledFlag = e.Status === 'Disabled' ?
|
|
225
|
+
`\n * * @disabled This field is disabled and will not be available in the application. Attempting to use it will result in exceptions being thrown` : '';
|
|
226
|
+
// IS-A parent field detection: virtual fields with AllowUpdateAPI on child entities
|
|
227
|
+
const isISAParentField = e.IsVirtual && e.AllowUpdateAPI && entity.IsChildType;
|
|
228
|
+
const isaSourceComment = isISAParentField
|
|
229
|
+
? `\n * * IS-A Source: Inherited from ${this.getISAFieldSourceEntity(entity, e)}`
|
|
230
|
+
: '';
|
|
231
|
+
const jsonTypeComment = hasJSONType && jsonTypeAccessorInfo
|
|
232
|
+
? `\n * * JSON Type: ${jsonTypeAccessorInfo.isArray ? `Array<${jsonTypeAccessorInfo.prefixedTypeName}>` : jsonTypeAccessorInfo.prefixedTypeName}`
|
|
233
|
+
: '';
|
|
234
|
+
const safeName = SafeCodeName(e);
|
|
235
|
+
const conflictNote = safeName !== e.CodeName
|
|
236
|
+
? `\n * * NOTE: Property renamed to \`${safeName}\` to avoid conflict with BaseEntity.${e.CodeName}`
|
|
237
|
+
: '';
|
|
238
|
+
// Standard getter/setter (always uses raw string, even for JSONType fields)
|
|
239
|
+
const getterBody = `return this.Get('${e.Name}');`;
|
|
240
|
+
const setterBody = `this.Set('${e.Name}', value);`;
|
|
241
|
+
let sRet = ` /**
|
|
233
242
|
* * Field Name: ${e.Name}${e.DisplayName && e.DisplayName.length > 0 ? '\n * * Display Name: ' + e.DisplayName : ''}
|
|
234
243
|
* * ${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
244
|
*/
|
|
236
245
|
get ${safeName}(): ${typeString} {
|
|
237
246
|
${getterBody}
|
|
238
247
|
}`;
|
|
239
|
-
|
|
240
|
-
|
|
248
|
+
if (!e.ReadOnly || (e.IsPrimaryKey && !e.AutoIncrement) || isISAParentField) {
|
|
249
|
+
sRet += `
|
|
241
250
|
set ${safeName}(value: ${typeString}) {
|
|
242
251
|
${setterBody}
|
|
243
252
|
}`;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
253
|
+
}
|
|
254
|
+
// JSONType: emit additional typed "Object" accessor with caching
|
|
255
|
+
if (hasJSONType && jsonTypeAccessorInfo) {
|
|
256
|
+
const objName = `${safeName}Object`;
|
|
257
|
+
const cachedField = `_${objName}_cached`;
|
|
258
|
+
const lastRawField = `_${objName}_lastRaw`;
|
|
259
|
+
const ft = jsonTypeAccessorInfo.fullTypeString;
|
|
260
|
+
sRet += `
|
|
252
261
|
|
|
253
262
|
private ${cachedField}: ${ft} | undefined = undefined;
|
|
254
263
|
private ${lastRawField}: string | null = null;
|
|
@@ -270,29 +279,29 @@ export const loadModule = () => {
|
|
|
270
279
|
this.${cachedField} = value;
|
|
271
280
|
this.${lastRawField} = raw;
|
|
272
281
|
}`;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
282
|
+
}
|
|
283
|
+
return sRet;
|
|
284
|
+
}).join('\n\n');
|
|
285
|
+
// Base-class resolution, in precedence order:
|
|
286
|
+
// 1. An explicit custom subclass configured on the entity (EntityObjectSubclassName/Import).
|
|
287
|
+
// 2. ReadOnlyExternalBaseEntity for entities backed by an external data source — they
|
|
288
|
+
// proxy a remote system live and must reject Save/Delete. (External entities only ever
|
|
289
|
+
// appear in downstream generated packages, so the @memberjunction/core-entities import
|
|
290
|
+
// is always a clean cross-package import, never a self-import.)
|
|
291
|
+
// 3. BaseEntity for everything else.
|
|
292
|
+
// Base class + its import come from resolveEntityBaseClass (pure) so the file assembler can hoist
|
|
293
|
+
// and de-duplicate the imports into the header. The per-entity inline import (below) is emitted
|
|
294
|
+
// ONLY in the standalone includeFileHeader path (single entity → no duplication); in the
|
|
295
|
+
// multi-entity file path the assembler owns the (deduped) imports.
|
|
296
|
+
const { baseClass: sBaseClass, importStatement: subClassImportStatement } = this.resolveEntityBaseClass(entity);
|
|
297
|
+
if (entity.ExternalDataSourceID && entity.EntityObjectSubclassName && entity.EntityObjectSubclassImport) {
|
|
298
|
+
// Custom subclass takes precedence over ReadOnlyExternalBaseEntity, so this external entity's
|
|
299
|
+
// read-only guarantee holds ONLY if that subclass itself extends ReadOnlyExternalBaseEntity.
|
|
300
|
+
// Warn so the author doesn't silently lose Save/Delete rejection.
|
|
301
|
+
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.`);
|
|
302
|
+
}
|
|
303
|
+
const loadFieldString = entity.PrimaryKeys.map((f) => `${f.CodeName}: ${f.TSType}`).join(', ');
|
|
304
|
+
const loadFunction = ` /**
|
|
296
305
|
* Loads the ${entity.Name} record from the database
|
|
297
306
|
${entity.PrimaryKeys.map((f) => `* @param ${f.CodeName}: ${f.TSType} - primary key value to load the ${entity.Name} record.`).join('\n ')}
|
|
298
307
|
* @param EntityRelationshipsToLoad - (optional) the relationships to load
|
|
@@ -308,10 +317,10 @@ export const loadModule = () => {
|
|
|
308
317
|
${entity.PrimaryKeys.map((f) => `compositeKey.KeyValuePairs.push({ FieldName: '${f.Name}', Value: ${f.CodeName} });`).join('\n ')}
|
|
309
318
|
return await super.InnerLoad(compositeKey, EntityRelationshipsToLoad);
|
|
310
319
|
}`;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
320
|
+
let deleteFunction = '';
|
|
321
|
+
if (!entity.AllowDeleteAPI) {
|
|
322
|
+
// Entity doesn't allow delete at all
|
|
323
|
+
deleteFunction = ` /**
|
|
315
324
|
* ${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
325
|
* @public
|
|
317
326
|
* @method
|
|
@@ -322,10 +331,10 @@ export const loadModule = () => {
|
|
|
322
331
|
public override async Delete(): Promise<boolean> {
|
|
323
332
|
throw new Error('Delete is not allowed for ${entity.Name}, to enable it set AllowDeleteAPI to 1 in the database.');
|
|
324
333
|
}`;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
334
|
+
}
|
|
335
|
+
else if (entity.CascadeDeletes) {
|
|
336
|
+
// Entity has cascading deletes, wrap in transaction
|
|
337
|
+
deleteFunction = ` /**
|
|
329
338
|
* ${entity.Name} - Delete method override to wrap in transaction since CascadeDeletes is true.
|
|
330
339
|
* Wrapping in a transaction ensures that all cascade delete operations are handled atomically.
|
|
331
340
|
* @public
|
|
@@ -359,10 +368,10 @@ export const loadModule = () => {
|
|
|
359
368
|
return super.Delete(options);
|
|
360
369
|
}
|
|
361
370
|
}`;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
371
|
+
}
|
|
372
|
+
const saveFunction = entity.AllowCreateAPI || entity.AllowUpdateAPI
|
|
373
|
+
? ''
|
|
374
|
+
: ` /**
|
|
366
375
|
* ${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
376
|
* @public
|
|
368
377
|
* @method
|
|
@@ -373,52 +382,54 @@ export const loadModule = () => {
|
|
|
373
382
|
public override async Save(options?: EntitySaveOptions) : Promise<boolean> {
|
|
374
383
|
throw new Error('Save is not allowed for ${entity.Name}, to enable it set AllowCreateAPI and/or AllowUpdateAPI to 1 in the database.');
|
|
375
384
|
}`;
|
|
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;
|
|
385
|
+
const validateFunction = await this.LogAndGenerateValidateFunction(pool, entity, skipDBUpdate);
|
|
386
|
+
const status = entity.Status.trim().toLowerCase();
|
|
387
|
+
const deprecatedFlag = status === 'deprecated' || status === 'disabled' ?
|
|
388
|
+
`\n * @deprecated This entity is deprecated and will be removed in a future version. Using it will result in console warnings.` : '';
|
|
389
|
+
const disabledFlag = status === 'disabled' ?
|
|
390
|
+
`\n * @disabled This entity is disabled and will not be available in the application. Attempting to use it will result in exceptions being thrown` : '';
|
|
391
|
+
// Collect and deduplicate JSONTypeDefinitions for this entity.
|
|
392
|
+
// These are raw TypeScript interface/type definitions (from EntityField.JSONTypeDefinition)
|
|
393
|
+
// that get emitted above the entity class so the typed getters/setters can reference them.
|
|
394
|
+
// A Set is used because multiple fields may share the same definition (e.g., a shared config type).
|
|
395
|
+
// Each definition is validated via the TypeScript compiler API before inclusion.
|
|
396
|
+
const jsonTypeDefinitions = new Set();
|
|
397
|
+
for (const field of sortedFields) {
|
|
398
|
+
if (field.JSONTypeDefinition && field.JSONTypeDefinition.trim().length > 0) {
|
|
399
|
+
const definition = field.JSONTypeDefinition.trim();
|
|
400
|
+
if (field.JSONType && field.JSONType.trim().length > 0) {
|
|
401
|
+
const validation = EntitySubClassGeneratorBase.ValidateJSONTypeDefinition(definition, field.JSONType.trim(), entity.Name, field.Name);
|
|
402
|
+
if (!validation.valid) {
|
|
403
|
+
for (const err of validation.errors) {
|
|
404
|
+
logError(err);
|
|
400
405
|
}
|
|
406
|
+
logError(`[JSONType] Skipping JSONTypeDefinition for ${entity.Name}.${field.Name} due to validation errors. The field will use a plain string getter/setter instead.`);
|
|
407
|
+
field.JSONType = null;
|
|
408
|
+
continue;
|
|
401
409
|
}
|
|
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
410
|
}
|
|
411
|
+
// Prefix all type names defined in this definition block with the entity class name
|
|
412
|
+
// to avoid naming conflicts across entities. Uses AST to find all defined type names.
|
|
413
|
+
let rewrittenDef = definition;
|
|
414
|
+
const sourceFile = ts.createSourceFile('temp.ts', definition, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
415
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
416
|
+
if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) ||
|
|
417
|
+
ts.isEnumDeclaration(node) || ts.isClassDeclaration(node)) && node.name) {
|
|
418
|
+
const originalName = node.name.text;
|
|
419
|
+
const prefixedName = `${sClassName}_${originalName}`;
|
|
420
|
+
rewrittenDef = rewrittenDef.replace(new RegExp('\\b' + originalName + '\\b', 'g'), prefixedName);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
jsonTypeDefinitions.add(rewrittenDef);
|
|
416
424
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
425
|
+
}
|
|
426
|
+
const jsonTypeBlock = jsonTypeDefinitions.size > 0
|
|
427
|
+
? '\n' + Array.from(jsonTypeDefinitions).join('\n\n') + '\n'
|
|
428
|
+
: '';
|
|
429
|
+
const relatedRecordCollections = EntitySubClassGeneratorBase.GenerateRelatedRecordCollections(entity);
|
|
430
|
+
const embeddedRecords = EntitySubClassGeneratorBase.GenerateEmbeddedRecords(entity);
|
|
431
|
+
const hierarchyMethods = EntitySubClassGeneratorBase.GenerateHierarchyMethods(entity, sClassName);
|
|
432
|
+
let sRet = `
|
|
422
433
|
${jsonTypeBlock}
|
|
423
434
|
/**
|
|
424
435
|
* ${entity.Name} - strongly typed entity sub-class
|
|
@@ -431,15 +442,14 @@ ${jsonTypeBlock}
|
|
|
431
442
|
* @public${deprecatedFlag}
|
|
432
443
|
*/
|
|
433
444
|
${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 : ''}
|
|
445
|
+
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
446
|
|
|
436
447
|
${fields}
|
|
437
448
|
}
|
|
438
449
|
`;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
450
|
+
if (includeFileHeader)
|
|
451
|
+
sRet = this.generateEntitySubClassFileHeader() + sRet;
|
|
452
|
+
return sRet;
|
|
443
453
|
}
|
|
444
454
|
/**
|
|
445
455
|
* Emits `DeclareRelatedRecords(...)` declarations for every relationship on this entity whose
|
|
@@ -673,6 +683,275 @@ ${fields}
|
|
|
673
683
|
static EscapeSingleQuotes(value) {
|
|
674
684
|
return String(value ?? '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
675
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Emits `{Field}_Object` / `{Field}_EnsureObject()` for every FK field whose
|
|
688
|
+
* `EmbeddedRecord` column holds a config object.
|
|
689
|
+
*
|
|
690
|
+
* RelatedEntity and the FK field name come from the row (`RelatedEntityID`, `Name`),
|
|
691
|
+
* not the JSON. `AllowsNull` on the same row decides the getter's nullability.
|
|
692
|
+
*
|
|
693
|
+
* @param entity - The entity being generated.
|
|
694
|
+
* @returns The declarations block, or an empty string when none are declared.
|
|
695
|
+
*/
|
|
696
|
+
static GenerateEmbeddedRecords(entity) {
|
|
697
|
+
const declared = (entity.Fields ?? []).filter(f => f.EmbeddedRecord && String(f.EmbeddedRecord).trim().length > 0);
|
|
698
|
+
if (declared.length === 0) {
|
|
699
|
+
return '';
|
|
700
|
+
}
|
|
701
|
+
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
702
|
+
const blocks = [];
|
|
703
|
+
const seen = new Set();
|
|
704
|
+
for (const field of declared) {
|
|
705
|
+
const config = EntitySubClassGeneratorBase.ParseEmbeddedRecordConfig(entity, field);
|
|
706
|
+
if (!config) {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
const nameKey = `${field.Name}_Object`.toLowerCase();
|
|
710
|
+
if (seen.has(nameKey)) {
|
|
711
|
+
logError(`[EmbeddedRecord] ${entity.Name}.${field.Name}: duplicate generated name '${field.Name}_Object'; skipping.`);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
seen.add(nameKey);
|
|
715
|
+
const related = field.RelatedEntityID ? md.EntityByID(field.RelatedEntityID) : undefined;
|
|
716
|
+
if (!related) {
|
|
717
|
+
logError(`[EmbeddedRecord] ${entity.Name}.${field.Name}: RelatedEntityID is missing or unknown; skipping.`);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
blocks.push(EntitySubClassGeneratorBase.RenderEmbeddedRecord(entity, field, related, config));
|
|
721
|
+
}
|
|
722
|
+
return blocks.length > 0 ? '\n' + blocks.join('\n') : '';
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Emits strongly-typed hierarchy traversal helper methods (`GetDescendants`, `GetAncestors`, `GetChildren`)
|
|
726
|
+
* for entities with recursive self-referencing foreign keys.
|
|
727
|
+
*
|
|
728
|
+
* @param entity - The entity being generated.
|
|
729
|
+
* @param sClassName - The generated subclass name.
|
|
730
|
+
* @returns The generated methods block, or an empty string when the entity has no recursive FKs.
|
|
731
|
+
*/
|
|
732
|
+
static GenerateHierarchyMethods(entity, sClassName) {
|
|
733
|
+
const recursiveFKs = (entity.Fields ?? []).filter(f => f.RelatedEntityID != null && (UUIDsEqual(f.RelatedEntityID, entity.ID) || f.RelatedEntity === entity.Name) && !f.IsVirtual && f.IsHierarchy === true);
|
|
734
|
+
if (recursiveFKs.length === 0) {
|
|
735
|
+
return '';
|
|
736
|
+
}
|
|
737
|
+
if (entity.PrimaryKeys.length !== 1) {
|
|
738
|
+
logWarning(`[Hierarchy] Entity '${entity.Name}' has ${recursiveFKs.length} hierarchy foreign key(s) ` +
|
|
739
|
+
`(${recursiveFKs.map(f => f.Name).join(', ')}), but has ${entity.PrimaryKeys.length} primary key fields. ` +
|
|
740
|
+
`MemberJunction hierarchy traversal requires a single-column primary key; skipping subclass hierarchy methods.`);
|
|
741
|
+
return '';
|
|
742
|
+
}
|
|
743
|
+
const methods = [];
|
|
744
|
+
const pkName = entity.FirstPrimaryKey?.Name ?? 'ID';
|
|
745
|
+
for (const field of recursiveFKs) {
|
|
746
|
+
const fieldName = field.Name;
|
|
747
|
+
const rootField = `Root${fieldName}`;
|
|
748
|
+
const depthField = `${fieldName}Depth`;
|
|
749
|
+
const pathField = `${fieldName}Path`;
|
|
750
|
+
const isPrimaryHierarchy = recursiveFKs.length === 1 || fieldName === 'ParentID';
|
|
751
|
+
// For primary hierarchies (e.g. ParentID), BaseEntity already provides strongly-typed
|
|
752
|
+
// GetDescendants<T = this>(), GetAncestors<T = this>(), and GetChildren<T = this>().
|
|
753
|
+
// We only generate named helper methods for non-primary hierarchy fields (e.g. ManagerID).
|
|
754
|
+
if (isPrimaryHierarchy) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
methods.push(`
|
|
758
|
+
/**
|
|
759
|
+
* Retrieves all descendant records in the ${fieldName} hierarchy under this record using a single RunView query.
|
|
760
|
+
* @param maxDepth Optional maximum relative depth to retrieve.
|
|
761
|
+
* @returns Array of descendant entity instances ordered by hierarchy depth.
|
|
762
|
+
*/
|
|
763
|
+
public async Get${fieldName}Descendants<T extends BaseEntity = this>(maxDepth?: number): Promise<T[]> {
|
|
764
|
+
return this.GetDescendants<T>({ parentFieldName: '${fieldName}', maxDepth });
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Retrieves all ancestor records in the ${fieldName} hierarchy from the top-level root down to this record using a single RunView query.
|
|
769
|
+
* @returns Array of ancestor entity instances ordered from root down to parent.
|
|
770
|
+
*/
|
|
771
|
+
public async Get${fieldName}Ancestors<T extends BaseEntity = this>(): Promise<T[]> {
|
|
772
|
+
return this.GetAncestors<T>('${fieldName}');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Retrieves all direct child records in the ${fieldName} hierarchy of this record using a single RunView query.
|
|
777
|
+
* @returns Array of direct child entity instances.
|
|
778
|
+
*/
|
|
779
|
+
public async Get${fieldName}Children<T extends BaseEntity = this>(): Promise<T[]> {
|
|
780
|
+
return this.GetChildren<T>('${fieldName}');
|
|
781
|
+
}`);
|
|
782
|
+
}
|
|
783
|
+
return methods.length > 0 ? '\n' + methods.join('\n') : '';
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* One peer class that must be imported because it is referenced by an embed or
|
|
787
|
+
* related-record collection and is not being emitted in this file.
|
|
788
|
+
*/
|
|
789
|
+
static CollectPeerClassImports(entity, localClassNames, config) {
|
|
790
|
+
const owningSchema = (entity.SchemaName ?? '').trim();
|
|
791
|
+
if (!owningSchema) {
|
|
792
|
+
throw new Error(`[CodeGen] entity import: entity '${entity.Name}' has no SchemaName; cannot resolve peer import packages.`);
|
|
793
|
+
}
|
|
794
|
+
const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
|
|
795
|
+
const byClass = new Map();
|
|
796
|
+
const addPeer = (className, relatedSchema, context) => {
|
|
797
|
+
if (!className || className === 'BaseEntity') {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (localClassNames.has(className)) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const schema = (relatedSchema ?? '').trim();
|
|
804
|
+
if (!schema) {
|
|
805
|
+
throw new Error(`[CodeGen] entity import: ${context} refers to class '${className}' but the related entity has no SchemaName. ` +
|
|
806
|
+
`CodeGen cannot pick an npm package to import it from.`);
|
|
807
|
+
}
|
|
808
|
+
const packageName = resolveEntityImportPackage(schema, owningSchema, config);
|
|
809
|
+
const existing = byClass.get(className);
|
|
810
|
+
if (existing && existing !== packageName) {
|
|
811
|
+
throw new Error(`[CodeGen] entity import: class '${className}' resolved to both '${existing}' and '${packageName}' on entity '${entity.Name}'.`);
|
|
812
|
+
}
|
|
813
|
+
byClass.set(className, packageName);
|
|
814
|
+
};
|
|
815
|
+
for (const field of (entity.Fields ?? []).filter((f) => f.EmbeddedRecord && String(f.EmbeddedRecord).trim().length > 0 && f.RelatedEntityID)) {
|
|
816
|
+
const related = md.EntityByID?.(field.RelatedEntityID);
|
|
817
|
+
if (!related) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
addPeer(`${related.ClassName}Entity`, related.SchemaName, `${entity.Name}.${field.Name} (embedded record)`);
|
|
821
|
+
}
|
|
822
|
+
for (const relationship of (entity.RelatedEntities ?? []).filter((r) => r.RelatedRecordCollection && r.RelatedRecordCollection.trim().length > 0)) {
|
|
823
|
+
const parsed = EntitySubClassGeneratorBase.ParseRelatedRecordCollectionConfig(entity, relationship);
|
|
824
|
+
if (!parsed) {
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const relatedBase = relationship.RelatedEntityClassName?.trim();
|
|
828
|
+
if (!relatedBase) {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
let related;
|
|
832
|
+
if (relationship.RelatedEntityID && typeof md.EntityByID === 'function') {
|
|
833
|
+
related = md.EntityByID(relationship.RelatedEntityID);
|
|
834
|
+
}
|
|
835
|
+
if (!related && relationship.RelatedEntity && typeof md.EntityByName === 'function') {
|
|
836
|
+
related = md.EntityByName(relationship.RelatedEntity);
|
|
837
|
+
}
|
|
838
|
+
addPeer(`${relatedBase}Entity`, related?.SchemaName, `${entity.Name} → ${relationship.RelatedEntity} (related-record collection)`);
|
|
839
|
+
}
|
|
840
|
+
return [...byClass.entries()].map(([className, packageName]) => ({ className, packageName }));
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Groups peer imports into one `import { A, B } from 'pkg'` line per npm package.
|
|
844
|
+
* `@memberjunction/core-entities` is emitted first; remaining packages are alphabetical.
|
|
845
|
+
*/
|
|
846
|
+
static FormatPeerImportStatements(imports) {
|
|
847
|
+
const byPackage = new Map();
|
|
848
|
+
for (const item of imports) {
|
|
849
|
+
if (!item.className || !item.packageName) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
let set = byPackage.get(item.packageName);
|
|
853
|
+
if (!set) {
|
|
854
|
+
set = new Set();
|
|
855
|
+
byPackage.set(item.packageName, set);
|
|
856
|
+
}
|
|
857
|
+
set.add(item.className);
|
|
858
|
+
}
|
|
859
|
+
const packages = [...byPackage.keys()].sort((a, b) => {
|
|
860
|
+
if (a === '@memberjunction/core-entities')
|
|
861
|
+
return -1;
|
|
862
|
+
if (b === '@memberjunction/core-entities')
|
|
863
|
+
return 1;
|
|
864
|
+
return a.localeCompare(b);
|
|
865
|
+
});
|
|
866
|
+
return packages.map((pkg) => {
|
|
867
|
+
const names = [...byPackage.get(pkg)].sort((a, b) => a.localeCompare(b));
|
|
868
|
+
return `import { ${names.join(', ')} } from '${pkg}';\n`;
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* @deprecated Use {@link CollectPeerClassImports} + {@link FormatPeerImportStatements}.
|
|
873
|
+
* Kept so existing call sites still compile; includes related-record collections as well as embeds.
|
|
874
|
+
*/
|
|
875
|
+
static CollectEmbeddedImports(entity, localClassNames, config) {
|
|
876
|
+
return EntitySubClassGeneratorBase.FormatPeerImportStatements(EntitySubClassGeneratorBase.CollectPeerClassImports(entity, localClassNames, config));
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Parses and validates one field's `EmbeddedRecord` JSON.
|
|
880
|
+
* Invalid metadata is skipped with a logged error rather than throwing.
|
|
881
|
+
*/
|
|
882
|
+
static ParseEmbeddedRecordConfig(entity, field) {
|
|
883
|
+
const label = `${entity.Name}.${field.Name}`;
|
|
884
|
+
let parsed;
|
|
885
|
+
try {
|
|
886
|
+
parsed = JSON.parse(String(field.EmbeddedRecord));
|
|
887
|
+
}
|
|
888
|
+
catch (e) {
|
|
889
|
+
logError(`[EmbeddedRecord] ${label}: EmbeddedRecord is not valid JSON ` +
|
|
890
|
+
`(${e instanceof Error ? e.message : String(e)}); skipping.`);
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
894
|
+
logError(`[EmbeddedRecord] ${label}: expected a JSON object; skipping.`);
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
const config = parsed;
|
|
898
|
+
const clearModes = ['delete', 'orphan', 'refuse'];
|
|
899
|
+
if (config.OnClear && !clearModes.includes(config.OnClear)) {
|
|
900
|
+
logError(`[EmbeddedRecord] ${label}: invalid OnClear '${config.OnClear}'; skipping.`);
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
const nestedModes = ['inherit', 'related'];
|
|
904
|
+
if (config.LoadNested && !nestedModes.includes(config.LoadNested)) {
|
|
905
|
+
logError(`[EmbeddedRecord] ${label}: invalid LoadNested '${config.LoadNested}'; skipping.`);
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
const emittedName = field.CodeName ? SafeCodeName(field) : field.Name;
|
|
909
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(emittedName)) {
|
|
910
|
+
logError(`[EmbeddedRecord] ${label}: emitted name '${emittedName}' is not a valid TypeScript identifier; skipping.`);
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
return config;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Renders the private companion plus the public `{Field}_Object` / `{Field}_EnsureObject` API.
|
|
917
|
+
*/
|
|
918
|
+
static RenderEmbeddedRecord(entity, field, related, config) {
|
|
919
|
+
const relatedClass = `${related.ClassName}Entity`;
|
|
920
|
+
const getterType = field.AllowsNull ? `${relatedClass} | null` : relatedClass;
|
|
921
|
+
const code = field.CodeName ? SafeCodeName(field) : field.Name;
|
|
922
|
+
const lines = [
|
|
923
|
+
`ForeignKeyField: '${EntitySubClassGeneratorBase.EscapeSingleQuotes(field.Name)}'`,
|
|
924
|
+
`RelatedEntity: '${EntitySubClassGeneratorBase.EscapeSingleQuotes(related.Name)}'`,
|
|
925
|
+
];
|
|
926
|
+
if (config.OnClear) {
|
|
927
|
+
lines.push(`OnClear: '${config.OnClear}'`);
|
|
928
|
+
}
|
|
929
|
+
if (config.LoadNested) {
|
|
930
|
+
lines.push(`LoadNested: '${config.LoadNested}'`);
|
|
931
|
+
}
|
|
932
|
+
const requiredNote = field.AllowsNull
|
|
933
|
+
? ` Null until ${code}_EnsureObject() or Load() finds a value.`
|
|
934
|
+
: ` Always present after GetEntityObject / NewRecord.`;
|
|
935
|
+
return `
|
|
936
|
+
/**
|
|
937
|
+
* Embedded record: ${related.Name}
|
|
938
|
+
*
|
|
939
|
+
* 1:1 peer joined by this record's ${field.Name}. Loaded and saved with this
|
|
940
|
+
* ${entity.Name} record — see packages/MJCore/docs/embedded-records.md.
|
|
941
|
+
* Declared by EntityField.EmbeddedRecord on '${entity.Name}.${field.Name}'; edit that row, not this file.
|
|
942
|
+
*${requiredNote}
|
|
943
|
+
*/
|
|
944
|
+
private readonly __emb_${code} = this.DeclareEmbeddedRecord<${relatedClass}>({
|
|
945
|
+
${lines.join(',\n ')},
|
|
946
|
+
});
|
|
947
|
+
public get ${code}_Object(): ${getterType} {
|
|
948
|
+
return this.__emb_${code}.Value${field.AllowsNull ? '' : '!'};
|
|
949
|
+
}
|
|
950
|
+
public ${code}_EnsureObject(): ${relatedClass} {
|
|
951
|
+
return this.__emb_${code}.Ensure();
|
|
952
|
+
}
|
|
953
|
+
`;
|
|
954
|
+
}
|
|
676
955
|
/**
|
|
677
956
|
* Finds the source parent entity for an IS-A inherited field by walking the parent chain.
|
|
678
957
|
* Returns the name of the parent entity that originally defines the field (as a non-virtual column).
|