@memberjunction/codegen-lib 5.13.0 → 5.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Angular/angular-codegen.d.ts +9 -0
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +76 -2
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +86 -0
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +192 -3
- package/dist/Database/manage-metadata.js.map +1 -1
- package/package.json +18 -17
|
@@ -5,7 +5,7 @@ import { EntityInfo, ExtractActualDefaultValue, LogError, LogStatus, Metadata, S
|
|
|
5
5
|
import { logError, logMessage, logStatus } from "../Misc/status_logging.js";
|
|
6
6
|
import { SQLUtilityBase } from "./sql.js";
|
|
7
7
|
import { AdvancedGeneration } from "../Misc/advanced_generation.js";
|
|
8
|
-
import { SQLParser } from "@memberjunction/
|
|
8
|
+
import { SQLParser } from "@memberjunction/sql-parser";
|
|
9
9
|
import { createDisplayName, generatePluralName, MJGlobal, stripTrailingChars, UUIDsEqual } from "@memberjunction/global";
|
|
10
10
|
import { v4 as uuidv4 } from 'uuid';
|
|
11
11
|
import * as fs from 'fs';
|
|
@@ -313,6 +313,189 @@ export class ManageMetadataBase {
|
|
|
313
313
|
.filter((e) => typeof e.BaseTable === 'string' && typeof e.SchemaName === 'string')
|
|
314
314
|
.map((e) => ({ ...e, BaseTable: e.BaseTable, SchemaName: e.SchemaName }));
|
|
315
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Extracts organic key configurations from the additionalSchemaInfo config file.
|
|
318
|
+
* Walks all schema-keyed table arrays and collects OrganicKeys arrays with their
|
|
319
|
+
* owning schema and table name.
|
|
320
|
+
*/
|
|
321
|
+
extractOrganicKeysFromConfig(config) {
|
|
322
|
+
const results = [];
|
|
323
|
+
for (const [key, value] of Object.entries(config)) {
|
|
324
|
+
// Skip top-level non-schema keys (ISARelationships, VirtualEntities, Entities)
|
|
325
|
+
if (!Array.isArray(value))
|
|
326
|
+
continue;
|
|
327
|
+
if (['ISARelationships', 'VirtualEntities', 'Entities'].includes(key))
|
|
328
|
+
continue;
|
|
329
|
+
const schemaName = key;
|
|
330
|
+
for (const tableConfig of value) {
|
|
331
|
+
if (!tableConfig.TableName || !Array.isArray(tableConfig.OrganicKeys))
|
|
332
|
+
continue;
|
|
333
|
+
const organicKeys = tableConfig.OrganicKeys.map((ok, okIndex) => ({
|
|
334
|
+
Name: ok.Name,
|
|
335
|
+
Description: ok.Description || undefined,
|
|
336
|
+
MatchFieldNames: ok.MatchFieldNames,
|
|
337
|
+
NormalizationStrategy: ok.NormalizationStrategy || 'LowerCaseTrim',
|
|
338
|
+
CustomNormalizationExpression: ok.CustomNormalizationExpression || undefined,
|
|
339
|
+
Sequence: ok.Sequence ?? okIndex,
|
|
340
|
+
RelatedEntities: Array.isArray(ok.RelatedEntities)
|
|
341
|
+
? ok.RelatedEntities.map((re, reIndex) => ({
|
|
342
|
+
SchemaName: re.SchemaName,
|
|
343
|
+
TableName: re.TableName,
|
|
344
|
+
RelatedFieldNames: re.RelatedFieldNames,
|
|
345
|
+
TransitiveObject: re.TransitiveObject,
|
|
346
|
+
TransitiveMatchFieldNames: re.TransitiveMatchFieldNames,
|
|
347
|
+
TransitiveOutputFieldName: re.TransitiveOutputFieldName,
|
|
348
|
+
RelatedEntityJoinFieldName: re.RelatedEntityJoinFieldName,
|
|
349
|
+
TransitiveView: re.TransitiveView,
|
|
350
|
+
DisplayName: re.DisplayName,
|
|
351
|
+
DisplayLocation: re.DisplayLocation || 'After Field Tabs',
|
|
352
|
+
Sequence: re.Sequence ?? reIndex,
|
|
353
|
+
}))
|
|
354
|
+
: [],
|
|
355
|
+
}));
|
|
356
|
+
results.push({ SchemaName: schemaName, TableName: tableConfig.TableName, OrganicKeys: organicKeys });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return results;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Processes organic key configurations from additionalSchemaInfo.
|
|
363
|
+
* For each configured organic key:
|
|
364
|
+
* 1. Creates transitive bridge views if TransitiveView is defined
|
|
365
|
+
* 2. Upserts EntityOrganicKey records (matched on EntityID + Name)
|
|
366
|
+
* 3. Upserts EntityOrganicKeyRelatedEntity records (matched on EntityOrganicKeyID + RelatedEntityID)
|
|
367
|
+
*
|
|
368
|
+
* All SQL is executed AND logged via LogSQLAndExecute for complete CI/CD traceability.
|
|
369
|
+
* Must run AFTER entities are created.
|
|
370
|
+
*/
|
|
371
|
+
async processOrganicKeyConfig(pool) {
|
|
372
|
+
const config = ManageMetadataBase.getSoftPKFKConfig();
|
|
373
|
+
if (!config)
|
|
374
|
+
return { success: true, createdCount: 0, updatedCount: 0 };
|
|
375
|
+
const allOrganicKeys = this.extractOrganicKeysFromConfig(config);
|
|
376
|
+
if (allOrganicKeys.length === 0)
|
|
377
|
+
return { success: true, createdCount: 0, updatedCount: 0 };
|
|
378
|
+
const schema = mj_core_schema();
|
|
379
|
+
let createdCount = 0;
|
|
380
|
+
let updatedCount = 0;
|
|
381
|
+
for (const tableConfig of allOrganicKeys) {
|
|
382
|
+
// Resolve the owning entity
|
|
383
|
+
const ownerResult = await this.runQueryWithParams(pool, `
|
|
384
|
+
${this.selectTop(1, 'ID, Name', `FROM ${this.qs(schema, 'vwEntities')}
|
|
385
|
+
WHERE (BaseTable = @TableName AND SchemaName = @SchemaName)
|
|
386
|
+
OR Name = @TableName`, 'CASE WHEN BaseTable = @TableName AND SchemaName = @SchemaName THEN 0 ELSE 1 END')}
|
|
387
|
+
`, { 'TableName': tableConfig.TableName, 'SchemaName': tableConfig.SchemaName });
|
|
388
|
+
if (ownerResult.recordset.length === 0) {
|
|
389
|
+
logError(` > Organic keys config: entity "${tableConfig.SchemaName}.${tableConfig.TableName}" not found — skipping`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const ownerEntityId = ownerResult.recordset[0].ID;
|
|
393
|
+
const ownerEntityName = ownerResult.recordset[0].Name;
|
|
394
|
+
for (const okConfig of tableConfig.OrganicKeys) {
|
|
395
|
+
try {
|
|
396
|
+
// Step 1: Create transitive views if defined
|
|
397
|
+
for (const re of okConfig.RelatedEntities) {
|
|
398
|
+
if (re.TransitiveView) {
|
|
399
|
+
const viewSchema = re.TransitiveView.SchemaName || re.SchemaName;
|
|
400
|
+
const viewFullName = `${viewSchema}.${re.TransitiveView.Name}`;
|
|
401
|
+
const viewSQL = `CREATE OR ALTER VIEW ${this.qs(viewSchema, re.TransitiveView.Name)} AS\n${re.TransitiveView.SQL}`;
|
|
402
|
+
await this.LogSQLAndExecute(pool, viewSQL, `Create transitive bridge view ${viewFullName} for organic key "${okConfig.Name}" on ${ownerEntityName}`);
|
|
403
|
+
// Auto-populate TransitiveObject from the view definition
|
|
404
|
+
re.TransitiveObject = viewFullName;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Step 2: Upsert EntityOrganicKey (match on EntityID + Name)
|
|
408
|
+
const existingKey = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(schema, 'EntityOrganicKey')} WHERE EntityID = @EntityID AND Name = @Name`, { 'EntityID': ownerEntityId, 'Name': okConfig.Name });
|
|
409
|
+
let organicKeyId;
|
|
410
|
+
const matchFieldNames = okConfig.MatchFieldNames.join(',');
|
|
411
|
+
if (existingKey.recordset.length > 0) {
|
|
412
|
+
// Update existing
|
|
413
|
+
organicKeyId = existingKey.recordset[0].ID;
|
|
414
|
+
const updateSQL = `UPDATE ${this.qs(schema, 'EntityOrganicKey')} SET
|
|
415
|
+
MatchFieldNames = '${matchFieldNames}',
|
|
416
|
+
NormalizationStrategy = '${okConfig.NormalizationStrategy || 'LowerCaseTrim'}',
|
|
417
|
+
${okConfig.CustomNormalizationExpression ? `CustomNormalizationExpression = '${okConfig.CustomNormalizationExpression.replace(/'/g, "''")}',` : ''}
|
|
418
|
+
${okConfig.Description ? `Description = '${okConfig.Description.replace(/'/g, "''")}',` : ''}
|
|
419
|
+
Sequence = ${okConfig.Sequence ?? 0},
|
|
420
|
+
Status = 'Active'
|
|
421
|
+
WHERE ID = '${organicKeyId}'`;
|
|
422
|
+
await this.LogSQLAndExecute(pool, updateSQL, `Update organic key "${okConfig.Name}" on ${ownerEntityName}`);
|
|
423
|
+
updatedCount++;
|
|
424
|
+
logStatus(` > Organic key: Updated "${okConfig.Name}" on ${ownerEntityName}`);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
// Insert new — generate a deterministic UUID based on entity+name for idempotency
|
|
428
|
+
const insertSQL = `INSERT INTO ${this.qs(schema, 'EntityOrganicKey')}
|
|
429
|
+
(EntityID, Name, ${okConfig.Description ? 'Description, ' : ''}MatchFieldNames, NormalizationStrategy, ${okConfig.CustomNormalizationExpression ? 'CustomNormalizationExpression, ' : ''}Sequence, Status)
|
|
430
|
+
VALUES ('${ownerEntityId}', '${okConfig.Name}', ${okConfig.Description ? `'${okConfig.Description.replace(/'/g, "''")}', ` : ''}'${matchFieldNames}', '${okConfig.NormalizationStrategy || 'LowerCaseTrim'}', ${okConfig.CustomNormalizationExpression ? `'${okConfig.CustomNormalizationExpression.replace(/'/g, "''")}', ` : ''}${okConfig.Sequence ?? 0}, 'Active')`;
|
|
431
|
+
await this.LogSQLAndExecute(pool, insertSQL, `Insert organic key "${okConfig.Name}" on ${ownerEntityName}`);
|
|
432
|
+
createdCount++;
|
|
433
|
+
logStatus(` > Organic key: Created "${okConfig.Name}" on ${ownerEntityName}`);
|
|
434
|
+
// Fetch the new ID
|
|
435
|
+
const newKey = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(schema, 'EntityOrganicKey')} WHERE EntityID = @EntityID AND Name = @Name`, { 'EntityID': ownerEntityId, 'Name': okConfig.Name });
|
|
436
|
+
organicKeyId = newKey.recordset[0].ID;
|
|
437
|
+
}
|
|
438
|
+
// Step 3: Upsert EntityOrganicKeyRelatedEntity for each related entity
|
|
439
|
+
for (const reConfig of okConfig.RelatedEntities) {
|
|
440
|
+
const relResult = await this.runQueryWithParams(pool, `
|
|
441
|
+
${this.selectTop(1, 'ID, Name', `FROM ${this.qs(schema, 'vwEntities')}
|
|
442
|
+
WHERE (BaseTable = @TableName AND SchemaName = @SchemaName)
|
|
443
|
+
OR Name = @TableName`, 'CASE WHEN BaseTable = @TableName AND SchemaName = @SchemaName THEN 0 ELSE 1 END')}
|
|
444
|
+
`, { 'TableName': reConfig.TableName, 'SchemaName': reConfig.SchemaName });
|
|
445
|
+
if (relResult.recordset.length === 0) {
|
|
446
|
+
logError(` > Organic key "${okConfig.Name}": related entity "${reConfig.SchemaName}.${reConfig.TableName}" not found — skipping`);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const relEntityId = relResult.recordset[0].ID;
|
|
450
|
+
const relEntityName = relResult.recordset[0].Name;
|
|
451
|
+
// Check if this related entity mapping already exists
|
|
452
|
+
const existingRel = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(schema, 'EntityOrganicKeyRelatedEntity')} WHERE EntityOrganicKeyID = @KeyID AND RelatedEntityID = @RelEntityID`, { 'KeyID': organicKeyId, 'RelEntityID': relEntityId });
|
|
453
|
+
// Build field values
|
|
454
|
+
const isDirect = reConfig.RelatedFieldNames && reConfig.RelatedFieldNames.length > 0;
|
|
455
|
+
const relFieldNames = isDirect ? reConfig.RelatedFieldNames.join(',') : null;
|
|
456
|
+
const transitiveObject = reConfig.TransitiveObject || null;
|
|
457
|
+
const transitiveMatchFields = reConfig.TransitiveMatchFieldNames ? reConfig.TransitiveMatchFieldNames.join(',') : null;
|
|
458
|
+
if (existingRel.recordset.length > 0) {
|
|
459
|
+
const relId = existingRel.recordset[0].ID;
|
|
460
|
+
const updateRelSQL = `UPDATE ${this.qs(schema, 'EntityOrganicKeyRelatedEntity')} SET
|
|
461
|
+
RelatedEntityFieldNames = ${relFieldNames ? `'${relFieldNames}'` : 'NULL'},
|
|
462
|
+
TransitiveObjectName = ${transitiveObject ? `'${transitiveObject}'` : 'NULL'},
|
|
463
|
+
TransitiveObjectMatchFieldNames = ${transitiveMatchFields ? `'${transitiveMatchFields}'` : 'NULL'},
|
|
464
|
+
TransitiveObjectOutputFieldName = ${reConfig.TransitiveOutputFieldName ? `'${reConfig.TransitiveOutputFieldName}'` : 'NULL'},
|
|
465
|
+
RelatedEntityJoinFieldName = ${reConfig.RelatedEntityJoinFieldName ? `'${reConfig.RelatedEntityJoinFieldName}'` : 'NULL'},
|
|
466
|
+
DisplayName = ${reConfig.DisplayName ? `'${reConfig.DisplayName.replace(/'/g, "''")}'` : 'NULL'},
|
|
467
|
+
DisplayLocation = '${reConfig.DisplayLocation || 'After Field Tabs'}',
|
|
468
|
+
Sequence = ${reConfig.Sequence ?? 0}
|
|
469
|
+
WHERE ID = '${relId}'`;
|
|
470
|
+
await this.LogSQLAndExecute(pool, updateRelSQL, `Update organic key related entity: "${okConfig.Name}" → ${relEntityName}`);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
const insertRelSQL = `INSERT INTO ${this.qs(schema, 'EntityOrganicKeyRelatedEntity')}
|
|
474
|
+
(EntityOrganicKeyID, RelatedEntityID, RelatedEntityFieldNames,
|
|
475
|
+
TransitiveObjectName, TransitiveObjectMatchFieldNames, TransitiveObjectOutputFieldName, RelatedEntityJoinFieldName,
|
|
476
|
+
DisplayName, DisplayLocation, Sequence)
|
|
477
|
+
VALUES ('${organicKeyId}', '${relEntityId}',
|
|
478
|
+
${relFieldNames ? `'${relFieldNames}'` : 'NULL'},
|
|
479
|
+
${transitiveObject ? `'${transitiveObject}'` : 'NULL'},
|
|
480
|
+
${transitiveMatchFields ? `'${transitiveMatchFields}'` : 'NULL'},
|
|
481
|
+
${reConfig.TransitiveOutputFieldName ? `'${reConfig.TransitiveOutputFieldName}'` : 'NULL'},
|
|
482
|
+
${reConfig.RelatedEntityJoinFieldName ? `'${reConfig.RelatedEntityJoinFieldName}'` : 'NULL'},
|
|
483
|
+
${reConfig.DisplayName ? `'${reConfig.DisplayName.replace(/'/g, "''")}'` : 'NULL'},
|
|
484
|
+
'${reConfig.DisplayLocation || 'After Field Tabs'}',
|
|
485
|
+
${reConfig.Sequence ?? 0})`;
|
|
486
|
+
await this.LogSQLAndExecute(pool, insertRelSQL, `Insert organic key related entity: "${okConfig.Name}" → ${relEntityName}`);
|
|
487
|
+
}
|
|
488
|
+
logStatus(` > Organic key "${okConfig.Name}": ${existingRel.recordset.length > 0 ? 'updated' : 'created'} → ${relEntityName} (${isDirect ? 'direct' : 'transitive'})`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
493
|
+
logError(` > Organic key config: Failed to process "${okConfig.Name}" on ${ownerEntityName}: ${errMessage}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { success: true, createdCount, updatedCount };
|
|
498
|
+
}
|
|
316
499
|
/**
|
|
317
500
|
* Processes IS-A relationship configurations from the additionalSchemaInfo config.
|
|
318
501
|
* For each configured relationship, looks up both entities by name (or by table name
|
|
@@ -626,6 +809,12 @@ export class ManageMetadataBase {
|
|
|
626
809
|
if (entityConfigResult.updatedCount > 0) {
|
|
627
810
|
logStatus(` > Updated attributes on ${entityConfigResult.updatedCount} entit${entityConfigResult.updatedCount === 1 ? 'y' : 'ies'} from config`);
|
|
628
811
|
}
|
|
812
|
+
// Config-driven organic key setup — create bridge views and upsert organic key metadata
|
|
813
|
+
// Must run AFTER entities exist
|
|
814
|
+
const organicKeyResult = await this.processOrganicKeyConfig(pool);
|
|
815
|
+
if (organicKeyResult.createdCount > 0 || organicKeyResult.updatedCount > 0) {
|
|
816
|
+
logStatus(` > Organic keys: ${organicKeyResult.createdCount} created, ${organicKeyResult.updatedCount} updated from config`);
|
|
817
|
+
}
|
|
629
818
|
start = new Date();
|
|
630
819
|
logStatus(' Syncing schema info from database...');
|
|
631
820
|
if (!await this.updateSchemaInfoFromDatabase(pool, excludeSchemas)) {
|
|
@@ -880,11 +1069,11 @@ export class ManageMetadataBase {
|
|
|
880
1069
|
* for the LLM to use when decorating virtual entity fields.
|
|
881
1070
|
*/
|
|
882
1071
|
buildSourceEntityContext(viewDefinition) {
|
|
883
|
-
const
|
|
1072
|
+
const tableRefs = SQLParser.ExtractTableRefs(viewDefinition);
|
|
884
1073
|
const md = new Metadata();
|
|
885
1074
|
const sourceEntities = [];
|
|
886
1075
|
const seen = new Set();
|
|
887
|
-
for (const tableRef of
|
|
1076
|
+
for (const tableRef of tableRefs) {
|
|
888
1077
|
// Match against MJ entities by BaseTable/BaseView + SchemaName
|
|
889
1078
|
const matchingEntity = md.Entities.find(e => (e.BaseTable.toLowerCase() === tableRef.TableName.toLowerCase() ||
|
|
890
1079
|
e.BaseView.toLowerCase() === tableRef.TableName.toLowerCase()) &&
|