Member Junction
    Preparing search index...

    Module @memberjunction/codegen-lib - v6.1.0

    @memberjunction/codegen-lib

    The code generation engine for the MemberJunction platform. This library transforms database schema metadata into a complete, type-safe, full-stack application: TypeScript entity classes with Zod validation, Angular form components, SQL stored procedures and views, GraphQL resolvers, Action subclasses, and Remote Operation typed bases (RemoteOperationGeneratorBaseremote_operations.ts, one BaseRemotableOperation subclass per MJ: Remote Operations row — Manual shells or complete AI/Default classes; see the Remote Operations Guide) -- all from a single mj codegen invocation.

    npm install @memberjunction/codegen-lib
    

    CodeGenLib sits at the center of MemberJunction's development workflow. When you change a database schema (add a table, alter a column, define a CHECK constraint), CodeGenLib detects those changes, updates internal metadata, and regenerates synchronized code across every layer of the stack. The result is guaranteed type safety from database to UI with zero manual boilerplate.

    The library is designed for extensibility: every major generator is a base class that can be subclassed and registered via @RegisterClass to override or extend default behavior.

    flowchart TD
        subgraph Input["Input Sources"]
            DB["SQL Server\nDatabase Schema"]
            CFG["mj.config.cjs\nConfiguration"]
            AI["AI Prompts\n(Advanced Generation)"]
        end
    
        subgraph Pipeline["CodeGen Pipeline"]
            META["Metadata\nManagement"]
            SQLGEN["SQL\nGeneration"]
            ENTITY["Entity Class\nGeneration"]
            ANGULAR["Angular\nGeneration"]
            GQL["GraphQL\nGeneration"]
            ACTION["Action\nGeneration"]
        end
    
        subgraph Output["Generated Outputs"]
            VIEWS["Views &\nStored Procedures"]
            TS["TypeScript Entity\nClasses + Zod"]
            NG["Angular Form\nComponents"]
            GQLR["GraphQL\nResolvers"]
            ACTS["Action\nSubclasses"]
            JSON["DB Schema\nJSON"]
        end
    
        DB --> META
        CFG --> META
        AI --> META
    
        META --> SQLGEN
        META --> ENTITY
        META --> ANGULAR
        META --> GQL
        META --> ACTION
    
        SQLGEN --> VIEWS
        ENTITY --> TS
        ANGULAR --> NG
        GQL --> GQLR
        ACTION --> ACTS
        META --> JSON
    
        style Input fill:#2d6a9f,stroke:#1a4971,color:#fff
        style Pipeline fill:#7c5295,stroke:#563a6b,color:#fff
        style Output fill:#2d8659,stroke:#1a5c3a,color:#fff
    

    Authoring constraint: every SQL artifact CodeGen emits ships in a Flyway migration that customer databases will replay forever. The Publish-Then-No-Breaking-Changes Policy governs what schema changes are safe to feed into CodeGen: within a published OpenApp major version, only additive changes are allowed (new tables, new optional columns, widened types, new optional SP parameters). Dropping or renaming columns, narrowing types, and adding required parameters break historical migrations and require a major version bump.

    • Full-Stack Synchronization: A single schema change propagates to TypeScript entities, Angular forms, SQL procedures, and GraphQL resolvers automatically
    • AI-Powered Intelligence: Uses AI prompts to translate CHECK constraints into Zod schemas, generate semantic form layouts, identify name fields, and create entity descriptions
    • Extensible Architecture: Every generator (SQLCodeGenBase, EntitySubClassGeneratorBase, AngularClientGeneratorBase, etc.) can be subclassed and overridden via MJ's class factory
    • Zod Validation Schemas: Generates Zod schemas from SQL CHECK constraints with proper union types and refinements
    • Recursive Foreign Key & Hierarchy Traversal Engine: Automatically detects self-referential foreign keys and generates a 4-routine TVF suite (GetHierarchyMeta, GetDescendants, GetAncestors, GetRootID), 5 computed base-view columns (Root<Field>, <Field>Depth, <Field>Path, <Field>IsLeaf, <Field>ChildCount), and strongly typed TypeScript entity traversal methods (GetDescendants(), GetAncestors(), GetChildren()). See the Recursive Foreign Keys & Hierarchy Traversal Guide.
    • Cascade Delete Generation: Produces cursor-based cascade delete procedures that call child entity stored procedures, respecting business logic at every level. Default is intra-schema only; allowCrossSchemaCascadeDeletes (off) is required to walk FKs in other schemas.
    • Force Regeneration: Surgically regenerate specific SQL objects for specific entities without requiring schema changes
    • Class Registration Manifests: Prevents tree-shaking of @RegisterClass-decorated classes by generating static import manifests
    • SQL Migration Logging: Outputs all generated SQL as Flyway-compatible migration files with schema placeholder support
    • Configurable via Zod-Validated Config: All settings validated at startup through comprehensive Zod schemas with sensible defaults

    The code generation process follows a well-defined pipeline orchestrated by the RunCodeGenBase class:

    flowchart LR
        S0["BEFORE\nCommands"]
        S1["Metadata\nManagement"]
        S2["SQL Object\nGeneration"]
        S3["GraphQL\nResolvers"]
        S4["Entity\nSubclasses"]
        S5["Angular\nComponents"]
        S6["DB Schema\nJSON"]
        S7["Action\nSubclasses"]
        S8["Integrity\nChecks"]
        S9["AFTER\nCommands"]
    
        S0 --> S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7 --> S8 --> S9
    
        style S0 fill:#64748b,stroke:#475569,color:#fff
        style S1 fill:#2d6a9f,stroke:#1a4971,color:#fff
        style S2 fill:#2d6a9f,stroke:#1a4971,color:#fff
        style S3 fill:#7c5295,stroke:#563a6b,color:#fff
        style S4 fill:#7c5295,stroke:#563a6b,color:#fff
        style S5 fill:#7c5295,stroke:#563a6b,color:#fff
        style S6 fill:#7c5295,stroke:#563a6b,color:#fff
        style S7 fill:#7c5295,stroke:#563a6b,color:#fff
        style S8 fill:#b8762f,stroke:#8a5722,color:#fff
        style S9 fill:#64748b,stroke:#475569,color:#fff
    
    Stage Class Description
    BEFORE Commands RunCommandsBase Execute pre-generation shell commands and SQL scripts
    Metadata Management ManageMetadataBase Analyze schema changes, create/update entity metadata, run AI-powered field analysis
    SQL Generation SQLCodeGenBase Generate base views (with recursive CTEs), stored procedures (create/update/delete), foreign key indexes, permissions
    GraphQL Resolvers GraphQLServerGeneratorBase Generate TypeGraphQL resolver and type definitions for all API-enabled entities
    Entity Subclasses EntitySubClassGeneratorBase Generate TypeScript entity classes with Zod validation schemas, typed getters/setters, and value list types
    Angular Components AngularClientGeneratorBase Generate Angular form components with smart field types, category-based layouts, and related entity tabs
    DB Schema JSON DBSchemaGeneratorBase Export database schema as JSON for documentation and AI consumption
    Action Subclasses ActionSubClassGeneratorBase Generate Action implementation classes from metadata-defined business logic
    Integrity Checks SystemIntegrityBase Validate entity field sequences and other system consistency rules
    AFTER Commands RunCommandsBase Execute post-generation commands (typically package builds)

    CodeGen distinguishes between core MemberJunction entities (in the __mj schema) and application-specific entities. Each generator runs twice: once for core entities with output directed to @memberjunction/core-entities, and once for non-core entities with output directed to the application's generated packages. This separation ensures MJ framework code and application code stay independent.

    Every generator base class can be subclassed and registered with a higher priority to customize behavior:

    classDiagram
        class RunCodeGenBase {
            +setupDataSource() SQLServerDataProvider
            +Run(skipDatabaseGeneration) void
        }
    
        class ManageMetadataBase {
            +manageMetadata(pool, user) boolean
            +loadGeneratedCode(pool, user) boolean
        }
    
        class SQLCodeGenBase {
            +manageSQLScriptsAndExecution(pool, entities, dir, user) boolean
            +runCustomSQLScripts(pool, when) boolean
        }
    
        class EntitySubClassGeneratorBase {
            +generateAllEntitySubClasses(pool, entities, dir, skipDB) boolean
        }
    
        class AngularClientGeneratorBase {
            +generateAngularCode(entities, dir, prefix, user) boolean
        }
    
        class GraphQLServerGeneratorBase {
            +generateGraphQLServerCode(entities, dir, lib, exclude) boolean
        }
    
        class ActionSubClassGeneratorBase {
            +generateActions(actions, dir) boolean
        }
    
        RunCodeGenBase --> ManageMetadataBase : creates via ClassFactory
        RunCodeGenBase --> SQLCodeGenBase : creates via ClassFactory
        RunCodeGenBase --> EntitySubClassGeneratorBase : creates via ClassFactory
        RunCodeGenBase --> AngularClientGeneratorBase : creates via ClassFactory
        RunCodeGenBase --> GraphQLServerGeneratorBase : creates via ClassFactory
        RunCodeGenBase --> ActionSubClassGeneratorBase : creates via ClassFactory
    
        style RunCodeGenBase fill:#2d6a9f,stroke:#1a4971,color:#fff
        style ManageMetadataBase fill:#7c5295,stroke:#563a6b,color:#fff
        style SQLCodeGenBase fill:#7c5295,stroke:#563a6b,color:#fff
        style EntitySubClassGeneratorBase fill:#7c5295,stroke:#563a6b,color:#fff
        style AngularClientGeneratorBase fill:#7c5295,stroke:#563a6b,color:#fff
        style GraphQLServerGeneratorBase fill:#7c5295,stroke:#563a6b,color:#fff
        style ActionSubClassGeneratorBase fill:#7c5295,stroke:#563a6b,color:#fff
    

    To override any generator, subclass the base and register it:

    import { RegisterClass } from '@memberjunction/global';
    import { EntitySubClassGeneratorBase } from '@memberjunction/codegen-lib';

    @RegisterClass(EntitySubClassGeneratorBase, undefined, 1) // priority 1 overrides default (0)
    export class CustomEntityGenerator extends EntitySubClassGeneratorBase {
    // Override methods to customize generation
    }
    import { RunCodeGenBase, initializeConfig } from '@memberjunction/codegen-lib';

    // Initialize configuration from working directory
    const config = initializeConfig(process.cwd());

    // Run the complete code generation pipeline
    const codeGen = new RunCodeGenBase();
    await codeGen.Run();

    // Or skip database operations for faster UI-only regeneration
    await codeGen.Run(true);

    The convenience function provides a simpler entry point:

    import { runMemberJunctionCodeGeneration } from '@memberjunction/codegen-lib';

    await runMemberJunctionCodeGeneration();

    Each generator can be used independently:

    import { EntitySubClassGeneratorBase } from '@memberjunction/codegen-lib';
    import { MJGlobal } from '@memberjunction/global';

    const generator = MJGlobal.Instance.ClassFactory.CreateInstance<EntitySubClassGeneratorBase>(
    EntitySubClassGeneratorBase
    );

    await generator.generateAllEntitySubClasses(pool, entities, outputDir, false);

    The manifest generator prevents tree-shaking of @RegisterClass-decorated classes:

    import { generateClassRegistrationsManifest } from '@memberjunction/codegen-lib';

    const result = await generateClassRegistrationsManifest({
    outputPath: './src/generated/class-registrations-manifest.ts',
    appDir: './packages/MJAPI',
    excludePackages: ['@memberjunction'], // Use pre-built manifest for MJ packages
    });

    if (result.success) {
    console.log(`${result.packages.length} packages, ${result.classes.length} classes`);
    }

    See the Class Manifest Guide for comprehensive documentation on the manifest system.

    CodeGenLib uses cosmiconfig to locate configuration. The recommended approach is a mj.config.cjs file at the repository root:

    module.exports = {
    // Database connection
    dbHost: 'localhost',
    dbPort: 1433,
    dbDatabase: 'YourDatabase',
    codeGenLogin: 'codegen_user',
    codeGenPassword: 'your_password',
    mjCoreSchema: '__mj',

    // Output directories for each generator
    output: [
    { type: 'SQL', directory: '../../SQL Scripts/generated' },
    { type: 'Angular', directory: '../MJExplorer/src/app/generated' },
    { type: 'GraphQLServer', directory: '../MJAPI/src/generated' },
    { type: 'CoreEntitySubclasses', directory: '../MJCoreEntities/src/generated' },
    { type: 'EntitySubclasses', directory: '../GeneratedEntities/src/generated' },
    ],

    // AI-powered features
    advancedGeneration: {
    enableAdvancedGeneration: true,
    features: [
    { name: 'SmartFieldIdentification', enabled: true },
    { name: 'FormLayoutGeneration', enabled: true },
    { name: 'ParseCheckConstraints', enabled: true },
    { name: 'TransitiveJoinIntelligence', enabled: true },
    ],
    },

    // SQL output for Flyway migrations
    SQLOutput: {
    enabled: true,
    folderPath: './migrations/v3/',
    convertCoreSchemaToFlywayMigrationFile: true,
    },

    // Force regeneration of specific objects
    forceRegeneration: {
    enabled: false,
    entityWhereClause: "SchemaName = 'dbo'",
    baseViews: true,
    spUpdate: true,
    },
    };

    All configuration is validated at startup using Zod schemas, with clear error messages for invalid settings. Environment variables (DB_HOST, DB_DATABASE, CODEGEN_DB_USERNAME, CODEGEN_DB_PASSWORD) provide fallback values for database connection settings.

    Section Purpose
    output Maps each generator type to its output directory
    advancedGeneration Controls which AI-powered features are enabled
    newEntityDefaults Default settings for newly discovered entities (permissions, tracking, API access)
    forceRegeneration Surgically regenerate specific SQL object types for filtered entities
    SQLOutput Controls Flyway migration file generation from SQL logging
    commands Shell commands to run before/after generation (typically package builds)
    excludeSchemas / excludeTables Filter schemas and tables from metadata discovery
    includeSchemas Opt-in positive scope: generate only these schemas (resolved into excludeSchemas for this run). Heal SQL logged into migrations uses @IncludedSchemaNames from this list plus authored excludeSchemas — never a snapshot of sibling apps on the publisher DB.
    allowCrossSchemaCascadeDeletes Default false. Cascade-delete SQL is intra-schema only. true restores the old walk of every FK pointing at the entity, including other Open Apps. Dangerous; leave off.
    entityPackageName String: npm package this emit writes. Record: install-time host map (those schemas are also skipped for local generation)
    entityImportPackages Schema → npm map for peer classes this emit does not generate (embeds + related-record collections). See below.
    entityNaming Controls ALL CAPS normalization and compound word splitting for entity/field names
    additionalSchemaInfo Path to JSON file with soft PK/FK definitions and schema prefix rules
    dbPlatform Database backend selector. See Database Platform Selection below.

    Embedded records and related-record collections type the generated subclass against the related entity class (DeclareEmbeddedRecord<AddressEntity>, DeclareRelatedRecords<PersonEntity>). When that class is not emitted in the current file, CodeGen must import it from the npm package that owns it.

    Those are three different knobs. Do not overload entityPackageName:

    Knob Meaning
    includeSchemas What this run generates
    string entityPackageName The npm package this run writes those classes into
    Record entityPackageName Install-time host map. Listed schemas are also skipped for local generation (getExternalEntitySchemas). Converting a publisher's string entityPackageName into this Record silently re-routes every unmapped schema.
    entityImportPackages Schema → npm map for peers this run does not generate

    Open App publishers (the repo that develops the app) use the string form plus includeSchemas, and must list sibling apps here:

    entityPackageName: '@mj-biz-apps/orders-entities',
    includeSchemas: ['__mj_BizAppsOrders'],
    entityImportPackages: {
    '__mj_BizAppsCommon': '@mj-biz-apps/common-entities',
    '__mj_BizAppsAccounting': '@mj-biz-apps/accounting-entities',
    },

    Resolution: core schema (__mj) → @memberjunction/core-entities; same schema as the owning entity → this emit's package; then entityImportPackages; then Record entityPackageName (host fallback). An unmapped foreign schema throws — CodeGen will not self-import this emit's package (that was the Orders import { mjBizAppsCommonAddressEntity } from '@mj-biz-apps/orders-entities' bug). Mapping a foreign schema to this emit's own package is also an error. Imports are grouped: one import { A, B } from 'pkg' line per package.

    CodeGenLib accepts a single canonical dbPlatform field with two values:

    // mj.config.cjs
    module.exports = {
    dbPlatform: 'sqlserver', // or 'postgresql'
    // …
    };
    Value Backend
    'sqlserver' (default) Microsoft SQL Server
    'postgresql' PostgreSQL 14+

    The same vocabulary is used by @memberjunction/cli, @memberjunction/server, and every other MJ package that needs to branch on platform. There is one name (dbPlatform) and one pair of values ('sqlserver', 'postgresql') — no aliases ('mssql', 'postgres', 'pg') are recognized in config or env vars.

    If dbPlatform is not set in mj.config.cjs, CodeGen reads DB_PLATFORM from the environment (restricted to the canonical pair) and falls back to 'sqlserver'. An unrecognized non-empty DB_PLATFORM value throws — silent fallback is the bug we don't want, because it routes the wrong provider at the wrong dialect against a real database.

    Migration note (was dbType / DB_TYPE): Earlier dev builds of the PG support exposed both dbType (config key) and DB_TYPE (env var). Both have been replaced by dbPlatform / DB_PLATFORM with strict canonical values. Rename dbType: 'mssql' to dbPlatform: 'sqlserver' (and dbType: 'postgresql' to dbPlatform: 'postgresql') in your mj.config.cjs. Same for DB_TYPE=... in .envDB_PLATFORM=.... Legacy aliases (mssql, postgres, pg) are no longer accepted in either.

    When CodeGen discovers new tables from legacy databases that use ALL CAPS naming conventions (e.g., PAYMENT, INDIVIDUALDESIGNATION), the entityNaming config section controls how those names are converted to human-readable entity and field names.

    // In mj.config.cjs
    module.exports = {
    entityNaming: {
    // Normalize ALL CAPS to Title Case: PAYMENT -> Payment
    normalizeAllCaps: true, // default: true

    // Split compound words: INDIVIDUALDESIGNATION -> Individual Designation
    // Uses dictionary-based matching with backtracking
    splitCompoundWords: true, // default: true

    // Apply same normalization to field/column names
    normalizeFieldNames: true, // default: true

    // Additional domain-specific words for the compound word splitter
    additionalDomainWords: ['AutoCare', 'SKU', 'UPC'],
    },
    };

    Examples of normalization results:

    Database Identifier Normalized Display Name
    PAYMENT Payment
    INDIVIDUALDESIGNATION Individual Designation
    CUSTOMFIELDVALUE Custom Field Value
    CUSTOMERID Customer ID
    LINEITEMEDUCATIONCREDIT Line Item Education Credit
    INVOICEORDERSCHEDULE Invoice Order Schedule
    TERMINATE_REASON Terminate Reason
    AI_COMMERCE_CONTEXT AI Commerce Context

    The compound word splitter uses a built-in dictionary of ~500 common business/technical words, supplemented by known acronyms (ID, API, CRM, SQL, etc.) that stay uppercase. The additionalDomainWords config lets you add industry-specific terms for your database.

    When using additionalSchemaInfo (typically generated by DBAutoDoc), the JSON file can include a Schemas array with entity name prefix/suffix recommendations:

    {
    "Schemas": [
    { "name": "ACCOUNTING", "entityNamePrefix": "Accounting: ", "entityNameSuffix": "" },
    { "name": "CRM", "entityNamePrefix": "CRM: ", "entityNameSuffix": "" }
    ],
    "ACCOUNTING": [
    { "TableName": "PAYMENT", "PrimaryKey": [...], "ForeignKeys": [...] }
    ]
    }

    These prefixes are applied to SchemaInfo records during CodeGen, preventing entity name collisions across schemas (e.g., CRM: Categories vs Award: Categories instead of CATEGORYs__CRM). Config-file rules in newEntityDefaults.NameRulesBySchema still take priority.

    From a SQL table with CHECK constraints, CodeGen produces a complete entity class:

    // Auto-generated from database schema
    export class AIPromptEntity extends BaseEntity {
    // Typed getter/setter for CHECK-constrained field
    get PromptRole(): 'System' | 'User' | 'Assistant' | 'SystemOrUser' {
    return this.Get('PromptRole');
    }
    set PromptRole(value: 'System' | 'User' | 'Assistant' | 'SystemOrUser') {
    this.Set('PromptRole', value);
    }

    // Zod validation from CHECK constraint
    validate(): ValidationResult {
    return this.validateWithZod(AIPromptSchema);
    }
    }

    // Zod schema with union types from CHECK constraint
    export const AIPromptSchema = z.object({
    PromptRole: z.union([
    z.literal('System'),
    z.literal('User'),
    z.literal('Assistant'),
    z.literal('SystemOrUser'),
    ]),
    // ... all other fields
    });

    For tables with self-referential foreign keys configured as hierarchies (EntityField.Configuration setting { "Hierarchy": { "IsHierarchy": true } }) and single-column primary keys, CodeGen automatically generates a 4-routine Table-Valued Function (TVF) suite and projects enriched hierarchy columns into base views via OUTER APPLY (T-SQL) or LEFT JOIN LATERAL (PostgreSQL):

    -- Generated Base View with Hierarchy Columns (T-SQL)
    SELECT 
        c.*,
        hier_ParentID.RootID AS [RootParentID],
        hier_ParentID.Depth AS [ParentIDDepth],
        hier_ParentID.Path AS [ParentIDPath],
        hier_ParentID.IsLeaf AS [ParentIDIsLeaf],
        hier_ParentID.ChildCount AS [ParentIDChildCount]
    FROM [sales].[Category] AS c
    OUTER APPLY [sales].[fnCategoryParentID_GetHierarchyMeta]([c].[ID], [c].[ParentID]) AS hier_ParentID
    
    -- Generated Base View with Hierarchy Columns (PostgreSQL)
    SELECT 
        c.*,
        hier_ParentID."RootID" AS "RootParentID",
        hier_ParentID."Depth" AS "ParentIDDepth",
        hier_ParentID."Path" AS "ParentIDPath",
        hier_ParentID."IsLeaf" AS "ParentIDIsLeaf",
        hier_ParentID."ChildCount" AS "ParentIDChildCount"
    FROM "sales"."Category" AS c
    LEFT JOIN LATERAL "sales"."fn_category_parent_id_get_hierarchy_meta"(c."ID", c."ParentID") AS hier_ParentID ON true
    

    The inline TVF join is zero-overhead: relational query optimizers prune it completely when hierarchy columns are not selected.

    Additionally, generated entity subclasses in TypeScript automatically receive strongly-typed hierarchy traversal helper methods:

    // Auto-generated on entity subclasses with recursive FKs
    const descendants = await category.GetDescendants(maxDepth);
    const ancestors = await category.GetAncestors();
    const children = await category.GetChildren();

    See the Recursive Foreign Keys & Hierarchy Traversal Guide for complete details.

    CodeGen creates production-ready Angular forms with AI-determined field categories and smart field types:

    @Component({
    selector: 'mj-ai-prompt-form',
    template: `
    <mj-form-field [record]="record"
    FieldName="PromptRole"
    Type="dropdownlist"
    [EditMode]="EditMode">
    </mj-form-field>
    `
    })
    export class AIPromptFormComponent extends BaseFormComponent { }

    Delete procedures use cursor-based stored procedure calls to respect business logic at every level of the hierarchy:

    CREATE PROCEDURE [spDeleteOrder] @ID UNIQUEIDENTIFIER AS
    BEGIN
        -- Cascade through child stored procedures
        DECLARE @ItemID UNIQUEIDENTIFIER
        DECLARE cascade_cursor CURSOR FOR
            SELECT [ID] FROM [OrderItems] WHERE [OrderID] = @ID
    
        OPEN cascade_cursor
        FETCH NEXT FROM cascade_cursor INTO @ItemID
        WHILE @@FETCH_STATUS = 0
        BEGIN
            EXEC [spDeleteOrderItem] @ItemID  -- Respects OrderItem's own cascade logic
            FETCH NEXT FROM cascade_cursor INTO @ItemID
        END
        CLOSE cascade_cursor
        DEALLOCATE cascade_cursor
    
        DELETE FROM [Orders] WHERE [ID] = @ID
    END
    

    When advancedGeneration.enableAdvancedGeneration is enabled, CodeGen uses AI prompts (stored in the database as AI Prompt entities) to enhance the generation process:

    flowchart TD
        subgraph Features["AI-Powered Features"]
            SF["Smart Field\nIdentification"]
            FL["Form Layout\nGeneration"]
            CC["CHECK Constraint\nParsing"]
            ED["Entity\nDescriptions"]
            TJ["Transitive Join\nIntelligence"]
            EN["Entity Name\nGeneration"]
        end
    
        subgraph Results["What AI Determines"]
            SF --> R1["Name fields, Default-in-View\nfields, Searchable fields"]
            FL --> R2["Field categories, Icons\nDisplay names, Extended types"]
            CC --> R3["Zod schemas, Validation\nfunctions, Descriptions"]
            ED --> R4["Entity descriptions\nfor new entities"]
            TJ --> R5["Junction table detection\nMany-to-many relationships"]
            EN --> R6["Human-friendly entity\nnames from table names"]
        end
    
        style Features fill:#7c5295,stroke:#563a6b,color:#fff
        style Results fill:#2d8659,stroke:#1a5c3a,color:#fff
    
    Feature Purpose When It Runs
    SmartFieldIdentification Determines which field is the "name" field, which fields show in default views, and which are searchable Entity/field creation, or when AutoUpdate flags allow
    FormLayoutGeneration Groups fields into semantic categories with icons and display names Every run (forms are always regenerated)
    ParseCheckConstraints Translates SQL CHECK constraints into Zod validation schemas and TypeScript union types When CHECK constraints are detected
    EntityDescriptions Generates human-readable descriptions for entities Entity creation only
    TransitiveJoinIntelligence Detects junction tables and many-to-many relationships Entity/relationship creation
    EntityNames Converts technical table names to user-friendly entity names Entity creation only
    VirtualEntityFieldDecoration Analyzes SQL view definitions to identify PKs, FKs, descriptions, and extended types for virtual entities Virtual entity creation (idempotent unless forceRegenerate option is set)

    SmartFieldIdentification may propose several ranked name-field candidates (e.g. FirstName, LastName), but MemberJunction metadata supports exactly one IsNameField per entity — EntityInfo.NameField, the base-view FK-name virtual columns, and RelatedEntityNameFieldMap resolution all assume a single winner. CodeGen enforces this:

    • Eligibility — a name field must be bounded text on the base table. Primary keys, uniqueidentifiers, non-text types, MAX text, and virtual (view-only) fields are rejected.
    • Stability — an entity that already has exactly one valid IsNameField keeps it; AI proposals never move an established winner.
    • Repair — when multiple fields are flagged (historical accumulation), the field literally named Name wins, else the first eligible flagged field in sequence order. All other auto-updatable flags are cleared.
    • Fresh pick — when nothing valid is flagged, the first eligible AI candidate wins.
    • Pinning — fields with AutoUpdateIsNameField = 0 are never set or cleared by CodeGen; EntityInfo.NameField's literal-Name preference arbitrates at runtime if a pinned conflict exists.

    Why this matters: with multiple flags, the FK-name pick silently drifted between CodeGen runs as flags accumulated (observed: a related entity's name column flipping from one field to another, reshaping every view that joins to it).

    The form layout system enforces stability to prevent unnecessary churn:

    • Existing category names and icons are never changed by AI
    • AI can assign fields to existing categories or create categories for genuinely new field groups
    • Existing fields cannot be moved to newly created categories (prevents renaming)
    • Per-field AutoUpdateCategory and AutoUpdateDisplayName flags provide granular control

    Every entity has a BaseView — the object everything reads. Entity field discovery, permissions and the generated CRUD routines all target it, so whatever columns it exposes become first-class EntityField rows.

    There are three arrangements, chosen by two columns on Entity:

    BaseViewGenerated GeneratedBaseViewName Result
    1 NULL Generated. CodeGen writes BaseView. The default, and almost every entity.
    0 NULL Fully custom. CodeGen writes nothing; the application owns BaseView entirely.
    0 vwFooGenerated Layered. CodeGen writes the inner view; the application owns BaseView and wraps it.
    1 vwFooGenerated Refused by a CHECK constraint — contradictory, see below.

    The fourth combination is refused because the two halves of CodeGen read different columns and would disagree: view generation gates on BaseViewGenerated || HasLayeredBaseView and would write the inner view, while the outer view's refresh and its GRANTs gate on !BaseViewGenerated and would be skipped — leaving an entity whose public surface is never granted and never refreshed, with CodeGen reporting success.

    MJ core uses this itself. MJ: Version Installations and MJ: User View Run Details are layered as of v6.1, and the remaining fully-custom core entities are expected to follow.

    One CodeGen pass. Apply the hand-authored overlay (it selects g.* from the inner generated view plus extra columns), mj sync push any Entity pins (e.g. SupportsGeoCoding), then run mj codegen --skipfiles once from the Open App cwd. Pass 1 discovers overlay columns from BaseView (vwSQLColumnsAndEntityFields) and logs EntityField INSERTs; Pass 2 writes only the inner view (GeneratedBaseViewName) and never DROPs the overlay. A second CodeGen run is not required for overlay columns and is how duplicate __mj_Latitude / missing SPs happen.

    EntityField INSERTs are not in SQL Scripts/generated/ (that tree is views/SPs). They go to SQLOutput as CodeGen_Run_<utc>.sql.

    • Run mj codegen from the Open App cwd (mj-app.json). SQLOutput defaults to ./migrations/codegen. Do not run it from the MJ repo with includeSchemas pointing at an app — that used to dump EntityField SQL into MJ/migrations/v5.
    • --sql-output-dir overrides the folder. Pointing it at MJ/migrations/v* from an app fails.
    • If SQLOutput.enabled and no log file is open, CodeGen refuses to apply metadata SQL. Fold the CodeGen_Run file into the app V migration; never transcribe EntityField rows from the live DB. ExtendedType pins stay in metadata/entities (mj sync push).

    The thing to hold onto: BaseView is always the public surface, and the only question is who writes it. Layering splits one view into two so that the mechanical half can keep regenerating.

    flowchart TB
        subgraph GEN["① GENERATED — the default"]
            direction TB
            GT[("Foo
    base table")] GV["vwFoo
    owned by CodeGen
    regenerated every run"] GT --> GV end subgraph CUS["② FULLY CUSTOM — BaseViewGenerated = 0"] direction TB CT[("Foo
    base table")] CV["vwFoo
    owned by the application
    frozen the day it was copied"] CT --> CV end subgraph LAY["③ LAYERED — GeneratedBaseViewName = vwFooGenerated"] direction TB LT[("Foo
    base table")] LI["vwFooGenerated
    owned by CodeGen
    regenerated every run"] LO["vwFoo
    owned by the application
    SELECT g.* + your columns"] LT --> LI LI -->|"SELECT g.*"| LO end GV --> SURF CV --> SURF LO --> SURF SURF["BaseView — the public surface
    field discovery · permissions · RunView
    spCreate / spUpdate / spDelete"] classDef codegen fill:#1f6feb22,stroke:#1f6feb,stroke-width:2px classDef app fill:#d2992222,stroke:#d29922,stroke-width:2px classDef table fill:#8b949e22,stroke:#8b949e,stroke-width:1px classDef surface fill:#23863622,stroke:#238636,stroke-width:2px class GV,LI codegen class CV,LO app class GT,CT,LT table class SURF surface

    Read it as: blue regenerates, amber is hand-written. In ① the whole view is blue and you cannot add a column to it. In ② the whole view is amber — you can add anything, but every display join, geo column and root-ID column is now yours to maintain, and a foreign key added later never appears. ③ puts the boundary in the middle: the ~80 mechanical lines stay blue and keep up with the schema, while your computed columns stay amber and stay reviewable.

    The green node is why the arrangement is invisible to everything downstream — field discovery, permissions, RunView and the CRUD routines all target BaseView in every case, so a column added by the custom layer becomes a first-class virtual EntityField and comes back from a save.

    Fully custom is all-or-nothing. To add one computed column an application inherits the whole generated view — every related-entity display join, the geo join, the recursive root-ID OUTER APPLY, the soft-delete predicate — and must hand-maintain it from then on.

    That is not a one-time cost. Add a foreign key later and its display field simply never appears, because nothing regenerates the join. The failure is silent: the column is absent rather than wrong, so nothing errors and no test notices until somebody asks why a name is blank. It also freezes the entity at whatever CodeGen produced the day the view was copied — geo columns and root-ID columns both arrived after custom views existed in the wild.

    Layering keeps CodeGen generating underneath a thin custom layer:

    -- CodeGen owns this, and keeps it current
    CREATE VIEW [orders].[vwOrderHeadersGenerated] AS
    SELECT o.*, MJCompany_CompanyID.[Name] AS [Company], ... -- 80 lines, regenerated
    
    -- The application owns this, and it stays reviewable
    CREATE VIEW [orders].[vwOrderHeaders] AS
    SELECT g.*,
           CASE WHEN g.Balance > 0 AND g.DueDate < CAST(GETUTCDATE() AS date) THEN 1 ELSE 0 END AS IsOverdue
    FROM   [orders].[vwOrderHeadersGenerated] g;
    

    IsOverdue becomes a virtual EntityField like any other — typed on the entity class, filterable in RunView, visible in Explorer — and is returned by spCreate/spUpdate/spDelete, because those select from BaseView.

    The outer view is still custom SQL. A build engineer ships the PostgreSQL equivalent through pg-migrate (same SELECT g.*, extras FROM inner g shape, with || / LEFT JOIN LATERAL instead of T-SQL). CodeGen never overwrites that outer SQL.

    PostgreSQL expands g.* at CREATE VIEW and freezes the column list, and it has no sp_refreshview. After CodeGen rewrites the inner view it restars the outer definition — rewrites the deparsed SELECT g.col1, g.col2, …, extras back to SELECT g.*, extras — then CREATE OR REPLACE (or, when a new inner column lands in the middle of g.* and PostgreSQL raises 42P16, capture / DROP CASCADE / recreate / replay dependent functions). Open App mj migrate calls spRebindLayeredOuterViewsInSchema for the same rebind.

    Do not leave the outer as a one-time pg-migrate artifact and hope later inner regenerations show up. Without the restar they will not.

    1. Set GeneratedBaseViewName on the entity (and BaseViewGenerated = 0, since the application owns BaseView). For MJ core entities this is declarative metadata, not SQL — see metadata/entities/.layered-base-views.json.
    2. Run CodeGen. It writes the inner view.
    3. Create your BaseView in a migration that runs after CodeGen output, since it selects from the inner view — and may reference generated root-ID functions. On PostgreSQL, ship the same wrapper via pg-migrate (LEFT JOIN LATERAL / || instead of OUTER APPLY / CONVERT).
    4. Run CodeGen again so the new columns are discovered as EntityField rows. On PostgreSQL this pass restars the outer view so g.* includes anything the inner view gained.

    Setting GeneratedBaseViewName is a metadata change, not a schema change. The entity therefore never lands in CodeGen's modified/new list, and logSQLForNewOrModifiedEntity only writes to the migration log for entities in that list.

    The failure is quiet and easy to miss: CodeGen does create the inner view in whatever database you ran it against, and emits nothing. Your dev box looks correct while every other environment never receives the view at all — and the outer view you write in step 3 then selects from an object that does not exist there.

    Scope a forced regeneration to just the entities you are converting, run CodeGen, then remove it:

    // mj.config.cjs — TEMPORARY, delete after capturing the output
    forceRegeneration: {
    enabled: true,
    baseViews: true,
    entityWhereClause: "Name IN ('MJ: Version Installations', 'MJ: User View Run Details')",
    }

    This does not apply to an entity that is layered from the start, or to later schema changes on an already-layered entity — both put the entity in the modified list on their own.

    Step 2 necessarily runs while BaseView does not yet exist — it selects from the inner view that step 2 is creating, so it could not have been created earlier. CodeGen handles this: the sp_refreshview and GRANT it emits against the application-owned view are wrapped in an IF OBJECT_ID(...) IS NOT NULL guard, so the bootstrap pass skips them and every later pass behaves as if the guard were not there. You do not need to order the migrations around it.

    • A view caches its column list. The custom layer does SELECT g.*, so when the schema changes, the inner view must be refreshed before the outer one. CodeGen emits sp_refreshview in that order automatically. Refreshing the outer against a stale inner re-caches the old columns, and the new one stays missing — indistinguishable from never having been added.
    • The names must differ. A view cannot select from itself. A CHECK constraint on Entity refuses equal names, and EntityInfo.HasLayeredBaseView compares case-insensitively so VWFOO and vwFoo are treated as the same object.
    • The custom layer must expose a superset. Whatever BaseView exposed before it was layered, it must still expose afterwards — a column that disappears is a breaking change to the generated entity class. Watch for name collisions in particular: the inner view generates a display column per foreign key (Employee for EmployeeID), so an outer view hand-selecting the same alias produces a duplicate column and fails at CREATE VIEW. Diff the column list before and after.
    • Permissions target BaseView. The inner view needs no separate grants: it is in the same schema with the same owner, so ownership chaining covers it.
    • EntityInfo.GeneratedViewName is the single resolution of "which view does CodeGen write". Use it rather than re-deriving from BaseView; several call sites decide where to write the view, what to name the emitted file, and which object to refresh, and any two disagreeing produce a view under a name nothing reads.

    Regenerate specific SQL objects without schema changes using surgical filtering:

    // In mj.config.cjs
    forceRegeneration: {
    enabled: true,
    // Filter to specific entities
    entityWhereClause: "SchemaName = 'CRM' AND __mj_UpdatedAt >= '2025-06-24'",
    // Control which object types regenerate
    baseViews: true,
    spCreate: false,
    spUpdate: true,
    spDelete: false,
    indexes: true,
    }

    Only the intersection of matched entities and enabled object types gets regenerated.

    All SQL generated during metadata management and object generation is logged to a Flyway-compatible migration file. The SQLOutput configuration controls this behavior:

    SQLOutput: {
    enabled: true,
    folderPath: './migrations/v3/',
    appendToFile: true,
    convertCoreSchemaToFlywayMigrationFile: true,
    schemaPlaceholders: [
    { schema: '__mj', placeholder: '${flyway:defaultSchema}' },
    ],
    }

    The SQLLogging class accumulates all SQL statements during a run and writes them as a single migration file with schema names replaced by Flyway placeholders.

    src/
    index.ts # Public API exports
    runCodeGen.ts # RunCodeGenBase - main pipeline orchestrator

    Config/
    config.ts # Zod-validated configuration schemas and loaders
    db-connection.ts # SQL Server connection pool management

    Database/
    manage-metadata.ts # ManageMetadataBase - schema analysis and metadata sync
    sql_codegen.ts # SQLCodeGenBase - views, procedures, indexes, permissions
    sql.ts # SQLUtilityBase - SQL file management and execution
    dbSchema.ts # DBSchemaGeneratorBase - JSON schema export
    reorder-columns.ts # Table column reordering utilities

    Angular/
    angular-codegen.ts # AngularClientGeneratorBase - form and module generation
    related-entity-components.ts # Base classes for related entity display components
    entity-data-grid-related-entity-component.ts # Data grid component generator
    join-grid-related-entity-component.ts # Join grid component generator
    timeline-related-entity-component.ts # Timeline component generator

    Misc/
    entity_subclasses_codegen.ts # EntitySubClassGeneratorBase - TypeScript entity generation
    action_subclasses_codegen.ts # ActionSubClassGeneratorBase - Action class generation
    graphql_server_codegen.ts # GraphQLServerGeneratorBase - resolver generation
    advanced_generation.ts # AdvancedGeneration - AI-powered enhancement features
    status_logging.ts # Spinner and log utilities (ora-based)
    sql_logging.ts # SQLLogging - migration file accumulator
    system_integrity.ts # SystemIntegrityBase - post-generation validation
    createNewUser.ts # CreateNewUserBase - initial user setup
    runCommand.ts # RunCommandsBase - shell command execution
    util.ts # File system and sorting utilities

    Manifest/
    GenerateClassRegistrationsManifest.ts # Tree-shaking prevention manifest generator

    The main orchestrator class. Creates instances of all generator classes via MJGlobal.ClassFactory and runs the pipeline.

    Method Description
    Run(skipDatabaseGeneration?) Execute the full code generation pipeline. Pass true to skip database operations.
    setupDataSource() Initialize the SQL Server connection pool and data provider.

    Analyzes database schema changes and updates MJ metadata tables.

    Method Description
    manageMetadata(pool, user) Full metadata management: detect schema changes, create entities/fields, run AI features.
    loadGeneratedCode(pool, user) Load previously generated AI code from database (used when skipping DB generation).

    Generates database objects: views, stored procedures, indexes, and permissions. Delegates platform-specific SQL generation to CodeGenDatabaseProvider implementations (see below).

    Method Description
    manageSQLScriptsAndExecution(pool, entities, dir, user) Generate and execute all SQL objects for the given entities.
    runCustomSQLScripts(pool, when) Execute custom SQL scripts configured for the specified timing.

    The CodeGenDatabaseProvider is the abstract base class that encapsulates all database-specific SQL generation for CodeGen. It has 55 abstract methods organized into categories that each platform provider must implement:

    Category Methods Purpose
    DROP Guards generateDropGuard Conditional drop statements (IF EXISTS, IF OBJECT_ID)
    Base Views generateBaseView Entity views with joins and soft-delete filtering
    CRUD Routines generateCRUDCreate, generateCRUDUpdate, generateCRUDDelete Create/Update/Delete stored procedures or functions
    Triggers generateTimestampTrigger Timestamp auto-update triggers
    Indexes generateForeignKeyIndexes Foreign key index generation
    Full-Text Search generateFullTextSearch Platform-specific FTS infrastructure
    Hierarchy TVFs & Views generateHierarchyMetaFunction, generateDescendantsFunction, generateAncestorsFunction, generateRootIDFunction, generateHierarchyFieldSelect, generateHierarchyFieldJoin Recursive hierarchy TVF suite & view join generation
    Permissions generateViewPermissions, generateCRUDPermissions, generateFullTextSearchPermissions GRANT statements per entity role
    Cascade Deletes generateSingleCascadeOperation Cascade delete/update-to-NULL operations
    Timestamp Columns generateTimestampColumns Adding __mj_CreatedAt/__mj_UpdatedAt columns
    Parameter Helpers generateCRUDParamString, generateInsertFieldString, generateUpdateFieldString SQL generation utilities for routines
    DDL Operations addColumnSQL, alterColumnTypeAndNullabilitySQL, dropObjectSQL, etc. Schema modification statements
    Introspection getViewDefinitionSQL, getPrimaryKeyIndexNameSQL, getViewColumnsSQL Catalog queries for schema discovery
    Type System compareDataTypes, get TimestampType Data type comparison and platform constants
    Platform Config getSystemSchemasToExclude, get NeedsViewRefresh, get PlatformKey Platform-specific behavior flags
    SQL Execution executeSQLFileViaShell Shell-based SQL file execution (sqlcmd, psql)
    Default Parsing parseColumnDefaultValue Extracting defaults from catalog metadata
    Provider Package Platform
    SQLServerCodeGenProvider @memberjunction/codegen-lib SQL Server (T-SQL stored procedures, OBJECT_ID checks, sqlcmd)
    PostgreSQLCodeGenProvider @memberjunction/postgresql-dataprovider PostgreSQL (PL/pgSQL functions, DROP IF EXISTS, psql)

    Providers are registered via @RegisterClass(CodeGenDatabaseProvider, 'ProviderName') and selected at runtime based on the configured database platform.

    To add support for a new database (e.g., MySQL, Oracle):

    1. Create a provider class extending CodeGenDatabaseProvider
    2. Implement all 55 abstract methods with platform-native SQL
    3. Create a SQLDialect subclass in @memberjunction/sql-dialect for identifier quoting, type mapping, etc.
    4. Register with @RegisterClass so CodeGen discovers it at runtime
    5. Create a data provider implementing DatabaseProviderBase from @memberjunction/core
    import { RegisterClass } from '@memberjunction/global';
    import { CodeGenDatabaseProvider } from '@memberjunction/codegen-lib';

    @RegisterClass(CodeGenDatabaseProvider, 'MySQLCodeGenProvider')
    export class MySQLCodeGenProvider extends CodeGenDatabaseProvider {
    get PlatformKey(): string { return 'mysql'; }
    get Dialect(): SQLDialect { return new MySQLDialect(); }

    generateDropGuard(objectType, schema, name): string {
    return `DROP ${objectType} IF EXISTS ${schema}.\`${name}\`;`;
    }
    // ... implement remaining 53 abstract methods
    }

    The PostgreSQL provider in @memberjunction/postgresql-dataprovider serves as the reference implementation for adding new backends.

    Generates TypeScript entity classes with Zod validation.

    Method Description
    generateAllEntitySubClasses(pool, entities, dir, skipDB) Generate all entity subclass files including Zod schemas.
    generateEntitySubClass(pool, entity, includeHeader, skipDB) Generate a single entity subclass.
    GenerateSchemaAndType(entity) Generate Zod schema and TypeScript type for an entity.

    Generates Angular form components, section components, and Angular modules.

    Method Description
    generateAngularCode(entities, dir, prefix, user) Generate all Angular components and modules.

    Generates TypeGraphQL resolver and type definitions.

    Method Description
    generateGraphQLServerCode(entities, dir, lib, exclude) Generate GraphQL resolvers for all entities.

    Generates an import manifest that prevents tree-shaking of @RegisterClass decorated classes. See the Class Manifest Guide for full documentation.

    Option Description
    outputPath Path for the generated manifest file
    appDir Directory containing the app's package.json (default: process.cwd())
    filterBaseClasses Only include classes extending specific base classes
    excludePackages Skip packages matching name prefixes (e.g., ['@memberjunction'])
    Function Description
    initializeConfig(cwd) Load and validate configuration from the given directory
    outputDir(type, fallback) Get the configured output directory for a generator type
    getSettingValue(name, default) Get a named setting value from configuration
    mj_core_schema() Get the MJ core schema name (typically __mj)
    resolveEntityPackageName(schema) Package for a schema from entityPackageName (string form returns that string for every schema)
    resolveEntityImportPackage(related, owning) Package to import a peer class from; throws if a foreign schema is unmapped
    thisEmitEntityPackageName(owning) The npm package this CodeGen run writes

    This package depends on:

    MemberJunction supports IS-A (inheritance) relationships between entities, where one entity extends another by adding additional fields while inheriting the parent's schema. CodeGen automatically handles IS-A relationships with specialized generation logic.

    For comprehensive conceptual documentation, see the IS-A Relationships Guide in MJCore.

    When an entity has a ParentEntity relationship (IS-A child):

    Child views automatically JOIN to parent views to provide a complete record with all inherited fields:

    -- CodeGen automatically generates:
    CREATE VIEW [vwEmployee]
    AS
    SELECT
        e.*,               -- All Employee fields
        p.FirstName,       -- Inherited from Person
        p.LastName,        -- Inherited from Person
        p.DateOfBirth      -- Inherited from Person
    FROM
        [__mj].[Employee] AS e
    INNER JOIN
        [__mj].[vwPerson] AS p ON e.[ID] = p.[ID]
    

    This ensures querying the child view returns a complete record including all parent fields.

    Create and Update procedures only include the child's own fields, not parent fields:

    -- spCreateEmployee only has Employee-specific parameters
    CREATE PROCEDURE [spCreateEmployee]
        @ID uniqueidentifier,
        @EmployeeNumber nvarchar(50),
        @HireDate date,
        @Salary decimal(18,2)
        -- No FirstName, LastName (those are Person fields)
    AS BEGIN
        -- Only inserts into Employee table
        INSERT INTO [__mj].[Employee] (ID, EmployeeNumber, HireDate, Salary)
        VALUES (@ID, @EmployeeNumber, @HireDate, @Salary)
    END
    

    Why this design? When creating an Employee, you first create the Person record (which gets an ID), then use that same ID to create the Employee record. The stored procedures reflect this two-step creation pattern.

    GraphQL input types include ALL fields (parent + child) for seamless API usage:

    input CreateEmployeeInput {
      # Parent fields (from Person)
      firstName: String!
      lastName: String!
      dateOfBirth: Date
    
      # Child fields (from Employee)
      employeeNumber: String!
      hireDate: Date!
      salary: Decimal!
    }
    

    This provides a convenient single-operation API while the resolver handles the underlying two-step creation.

    Generated entity classes include JSDoc annotations on getter/setter methods to indicate IS-A relationships:

    export class EmployeeEntity extends BaseEntity {
    /**
    * Inherited from Person entity
    */
    get FirstName(): string {
    return this.Get('FirstName');
    }

    set FirstName(value: string) {
    this.Set('FirstName', value);
    }

    // Own fields have no annotation
    get EmployeeNumber(): string {
    return this.Get('EmployeeNumber');
    }
    }

    manageEntityFields() respects IS-A hierarchy when syncing field metadata:

    • Fields from parent entities are NOT duplicated in child entity metadata
    • Only the child's own fields appear in EntityField for the child entity
    • RelatedEntityID and field relationships are preserved across the hierarchy
    • Prevents metadata pollution from inherited fields

    IS-A relationships are defined in the Entity table:

    -- Person is the base entity
    INSERT INTO Entity (ID, ParentEntity, Name)
    VALUES (NEWID(), NULL, 'Person')
    
    -- Employee IS-A Person
    INSERT INTO Entity (ID, ParentEntity, Name)
    VALUES (NEWID(), 'Person', 'Employee')
    

    CodeGen detects the ParentEntity relationship and applies the specialized generation logic automatically.

    Common scenarios where IS-A relationships improve your schema:

    • Person → Employee, Customer, Vendor - Shared contact information with role-specific fields
    • Document → Invoice, PurchaseOrder, Contract - Common document metadata with type-specific data
    • Product → PhysicalProduct, DigitalProduct - Shared catalog info with delivery-specific fields
    • Communication → Email, SMS, PhoneCall - Common tracking with channel-specific metadata
    1. Keep hierarchies shallow - One or two levels is ideal (Person → Employee, not Person → Worker → Employee)
    2. Parent entities should be meaningful - Don't create artificial base classes just for inheritance
    3. Child fields should be truly specific - If a field applies to all children, put it in the parent
    4. ID management is manual - When creating child records, explicitly use the parent's ID

    MemberJunction supports virtual entities - entities backed by database views instead of tables. Virtual entities enable read-only access to complex queries, external data sources, or denormalized views while maintaining the full MemberJunction metadata and API experience.

    For comprehensive conceptual documentation, see the Virtual Entities Guide in MJCore.

    Virtual entities are defined in database-metadata-config.json under the VirtualEntities array:

    {
    "VirtualEntities": [
    {
    "ViewName": "vwSalesSummary",
    "EntityName": "Sales Summary",
    "SchemaName": "__mj",
    "Description": "Aggregated sales data by region and period",
    "PrimaryKey": ["SummaryID"],
    "ForeignKeys": [
    {
    "FieldName": "RegionID",
    "SchemaName": "__mj",
    "RelatedTable": "Region",
    "RelatedField": "ID",
    "Description": "FK to Region table"
    }
    ]
    }
    ]
    }
    • ViewName: The SQL view name (must already exist in the database)
    • EntityName: The MemberJunction entity name (appears in metadata, UI, APIs)
    • SchemaName: Database schema (typically __mj for core entities)
    • Description: Entity description for metadata and documentation
    • PrimaryKey: Array of column names forming the primary key (supports composite keys)
    • ForeignKeys: Optional array of foreign key relationships to other entities (if omitted, LLM decoration discovers them)

    CodeGen processes virtual entities through several specialized steps:

    Reads the VirtualEntities configuration and calls spCreateVirtualEntity for each entry:

    // CodeGen calls this stored procedure for each virtual entity
    EXEC spCreateVirtualEntity
    @Name = 'Sales Summary',
    @SchemaName = '__mj',
    @BaseView = 'vwSalesSummary',
    @Description = 'Aggregated sales data...',
    @PrimaryKeyColumnName = 'SummaryID'

    This creates the Entity metadata record with VirtualEntity = 1.

    Scans sys.columns on the virtual entity's view and creates EntityField metadata for each column:

    • Automatically detects data types, nullability, and max lengths
    • Creates EntityField records for all view columns
    • Updates existing fields if column definitions change
    • Marks fields as IsVirtual = 1 in metadata
    // CodeGen inspects the view schema
    SELECT
    c.name,
    t.name AS TypeName,
    c.max_length,
    c.is_nullable
    FROM
    sys.columns c
    INNER JOIN
    sys.types t ON c.user_type_id = t.user_type_id
    WHERE
    object_id = OBJECT_ID('__mj.vwSalesSummary')

    Applies the primaryKeyColumnName and foreignKeyDefinitions from the config:

    // Sets the primary key field
    UPDATE EntityField
    SET IsPrimaryKey = 1
    WHERE EntityID = @VirtualEntityID
    AND Name = 'SummaryID'

    // Creates foreign key relationships
    INSERT INTO EntityRelationship (...)
    SELECT ... FROM foreignKeyDefinitions

    Why explicit FK definitions? Views don't have database-level foreign keys, so CodeGen can't detect relationships automatically. The config provides this metadata.

    The decorateVirtualEntitiesWithLLM() pipeline step uses AI to enhance virtual entity field metadata:

    import { AIPromptRunner } from '@memberjunction/ai-prompts';

    // CodeGen calls a database-driven prompt to decorate fields
    const promptParams = new AIPromptParams();
    promptParams.prompt = 'Decorate Virtual Entity Fields';
    promptParams.data = {
    entityName: 'Sales Summary',
    viewDefinition: viewSQL,
    existingFields: fieldsFromMetadata
    };

    const runner = new AIPromptRunner();
    const result = await runner.ExecutePrompt(promptParams);

    The LLM analyzes the view definition and provides:

    • Display names - User-friendly field labels (e.g., TotalRevenue → "Total Revenue")
    • Descriptions - Field-level documentation explaining what each column represents
    • Category assignments - Semantic grouping for form layouts
    • DefaultInView flags - Recommended visibility settings for grid displays

    This is controlled by the VirtualEntityFieldDecoration feature in the Advanced Generation Features configuration.

    After CodeGen processing, virtual entities have complete metadata:

    -- Entity record
    SELECT * FROM Entity WHERE Name = 'Sales Summary'
    -- VirtualEntity = 1, BaseView = 'vwSalesSummary'
    
    -- EntityField records (auto-detected from view)
    SELECT * FROM EntityField WHERE EntityID = @SalesEntityID
    -- Name, Type, Description, IsVirtual = 1
    
    -- EntityRelationship records (from config)
    SELECT * FROM EntityRelationship WHERE EntityID = @SalesEntityID
    -- Foreign keys defined in foreignKeyDefinitions
    

    Virtual entities generate the same TypeScript, GraphQL, and Angular code as table-based entities:

    TypeScript Entity Class:

    export class SalesSummaryEntity extends BaseEntity {
    get SummaryID(): string {
    return this.Get('SummaryID');
    }

    get RegionID(): string {
    return this.Get('RegionID');
    }

    get TotalRevenue(): number {
    return this.Get('TotalRevenue');
    }

    // Save/Delete methods throw errors (read-only entity)
    }

    GraphQL Schema:

    type SalesSummary {
      summaryID: ID!
      regionID: ID!
      region: Region    # Auto-resolved from FK definition
      totalRevenue: Float!
    }
    
    type Query {
      SalesSummaries(filter: String): [SalesSummary!]!
    }
    

    Angular Form:

    <mj-form-field
    [record]="record"
    FieldName="TotalRevenue"
    Type="textbox"
    [ReadOnly]="true" <!-- Virtual entities are read-only -->
    ></mj-form-field>

    Virtual entities are read-only by design:

    • No spCreate, spUpdate, or spDelete procedures generated
    • AllowCreateAPI, AllowUpdateAPI, AllowDeleteAPI set to 0 in metadata
    • Entity class Save() and Delete() methods throw errors
    • GraphQL mutations not generated for virtual entities
    • Angular forms display in read-only mode

    Virtual entity LLM decoration is controlled in the advancedGeneration.features array in mj.config.cjs:

    advancedGeneration: {
    enableAdvancedGeneration: true,
    features: [
    {
    name: 'VirtualEntityFieldDecoration',
    enabled: true,
    // Optional: force re-decoration even if entities already have soft PK/FK annotations
    options: [{ name: 'forceRegenerate', value: true }],
    },
    ],
    },

    By default, VirtualEntityFieldDecoration is enabled and uses an idempotency check — entities that already have IsSoftPrimaryKey or IsSoftForeignKey annotations are skipped. Set the forceRegenerate option to true to override this check and re-run LLM decoration for all virtual entities (useful after prompt improvements or when you want to refresh metadata).

    When active, CodeGen calls decorateVirtualEntitiesWithLLM() after field synchronization.

    Virtual entities excel at:

    • Reporting and Analytics - Pre-aggregated views for dashboards (sales summaries, usage metrics)
    • External Data Sources - Linked server views, API-backed views, federated queries
    • Denormalized Views - Flattened data for grid displays without JOIN overhead
    • Legacy System Integration - Expose legacy tables through normalized MJ entity layer
    • Calculated Fields - Complex computed columns not suitable for table storage
    • Security Views - Row-level security via filtered views with full MJ API access
    1. View must exist first - Create the SQL view before running CodeGen with virtual entity config
    2. Primary key is required - Views must have a unique identifier column
    3. Use meaningful names - entityName appears throughout UI and APIs
    4. Document foreign keys - Explicit FK definitions enable relationship navigation
    5. Enable LLM decoration - Let AI generate field descriptions for better UX
    6. Keep views simple - Complex views with subqueries may have performance issues
    7. Test read operations - Verify grid displays and API queries perform acceptably

    See the MemberJunction Contributing Guide for development setup and guidelines.

    When contributing to CodeGenLib:

    1. All generator base classes use the class factory pattern -- always subclass and register rather than modifying base classes directly
    2. Generated SQL must be valid for the target database platform and produce valid Flyway migration output
    3. Generated TypeScript must compile without errors and follow MJ naming conventions (PascalCase public members)
    4. AI-powered features must enforce stability guarantees (existing categories and icons are never changed)

    CodeGen is invoked through the MemberJunction CLI (mj command). Two subcommands are available:

    Runs the complete pipeline: database schema analysis, metadata sync, and code generation across all layers.

    # Run the full pipeline (most common usage)
    mj codegen

    # Skip database operations, only regenerate code files from existing metadata
    mj codegen --skipdb
    Flag Description
    --skipdb Skip all database operations (metadata sync, SQL object generation). Only regenerates TypeScript entities, Angular components, and GraphQL resolvers from existing metadata.

    Verbose output is controlled via mj.config.cjs (not a CLI flag):

    module.exports = {
    verboseOutput: true, // Enable detailed logging during code generation
    };

    When enabled, you see additional detail about each pipeline stage including per-entity processing, AI prompt calls, and SQL statement execution.

    Generates a TypeScript manifest file that prevents modern bundlers (ESBuild, Vite) from tree-shaking @RegisterClass-decorated classes.

    # Generate manifest with defaults
    mj codegen manifest

    # Generate for a specific app, excluding MJ packages
    mj codegen manifest --appDir ./packages/MJAPI \
    --output ./packages/MJAPI/src/generated/class-registrations-manifest.ts \
    --exclude-packages @memberjunction

    # Only include classes extending specific base classes
    mj codegen manifest --filter BaseEngine --filter BaseAction --verbose
    Flag Short Description
    --output <path> -o Output file path. Default: ./src/generated/class-registrations-manifest.ts
    --appDir <path> -a Root directory whose package.json dependency tree is scanned. Default: cwd
    --filter <class> -f Only include classes extending this base class. Repeatable.
    --exclude-packages <prefix> -e Skip packages whose name starts with this prefix. Repeatable.
    --quiet -q Suppress all output except errors.
    --verbose -v Show detailed per-package scanning progress.
    # 1. Configure database connection in mj.config.cjs
    # 2. Run the full pipeline
    mj codegen

    # 3. Generated output lands in directories specified in mj.config.cjs:
    # - TypeScript entities → packages/GeneratedEntities/src/generated/
    # - Angular forms → packages/MJExplorer/src/app/generated/
    # - GraphQL resolvers → packages/MJAPI/src/generated/
    # - SQL migration file → migrations/v3/
    # CodeGen detects schema changes automatically
    mj codegen

    # Review the generated Flyway migration file
    ls -la migrations/v3/CodeGen_Run_*.sql
    # Skip database operations for a faster run
    mj codegen --skipdb

    Use forceRegeneration in mj.config.cjs:

    module.exports = {
    forceRegeneration: {
    enabled: true,
    entityWhereClause: "SchemaName = 'CRM' AND Name LIKE 'Customer%'",
    baseViews: true,
    spUpdate: true,
    },
    };

    Then run mj codegen. Set enabled: false afterward to avoid unnecessary work on future runs.

    Symptom: CodeGen fails immediately with a connection error or timeout.

    Common fixes:

    1. Wrong credentials — Verify dbHost, dbDatabase, codeGenLogin, and codeGenPassword in mj.config.cjs. Environment variables DB_HOST, DB_DATABASE, CODEGEN_DB_USERNAME, CODEGEN_DB_PASSWORD serve as fallbacks.

    2. Named instance — If using a named instance (e.g., localhost\SQLEXPRESS), set dbInstanceName in config.

    3. Certificate trust — For self-signed certificates, set dbTrustServerCertificate: true.

    Symptom: ENOENT errors when writing generated files.

    Fix: Ensure all directories listed in the output array of mj.config.cjs exist. CodeGen does not create parent directories automatically.

    Common causes:

    1. Missing referenced tables — If a foreign key references a table excluded via excludeSchemas or excludeTables, either include the referenced table or remove the foreign key.

    2. Stale metadata — If you dropped and recreated tables, metadata may be out of sync. Run a full mj codegen (without --skipdb) to refresh.

    Use it when:

    • You only need to regenerate code files from existing metadata
    • The database is temporarily unavailable but you have valid metadata from a previous run

    Don't use it when:

    • You have made schema changes (new tables, altered columns, new constraints)
    • You are running CodeGen for the first time
    • You have changed forceRegeneration settings

    Symptom: Zod validation errors at startup.

    CodeGen validates all configuration using Zod schemas. Common issues:

    • Missing required fields (dbHost, dbDatabase, codeGenLogin, codeGenPassword)
    • Invalid types (dbPort must be a positive integer, verboseOutput must be a boolean)
    • Malformed output array (each entry needs type and directory properties)
    1. Verify advancedGeneration.enableAdvancedGeneration is true in mj.config.cjs
    2. Verify the specific feature is enabled: true in the features array
    3. Ensure the AI Prompts referenced by CodeGen exist in the database
    4. Confirm that at least one AI model is configured and accessible

    Classes

    ActionSubClassGeneratorBase
    AngularClientGeneratorBase
    AngularComponentInfo
    AngularFormSectionInfo
    CodeGenDatabaseProvider
    CodeGenReporter
    ComponentConfigBase
    DBSchemaGeneratorBase
    EmitStats
    EntityDataGridRelatedEntityGenerator
    EntitySubClassGeneratorBase
    GenerationInput
    GenerationResult
    GraphQLServerGeneratorBase
    JoinGridConfigInfo
    JoinGridRelatedEntityGenerator
    LoggerBase
    ManageMetadataBase
    PostgreSQLCodeGenConnection
    PostgreSQLCodeGenProvider
    RelatedEntityDisplayComponentGeneratorBase
    RemoteOperationGeneratorBase
    RunCodeGenBase
    RunCommandsBase
    SQLCodeGenBase
    SQLServerCodeGenConnection
    SQLServerCodeGenProvider
    SQLUtilityBase
    SystemIntegrityBase
    TimelineConfigInfo
    TimelineRelatedEntityGenerator
    ValidatorResult

    Interfaces

    BaseViewGenerationContext
    CascadeDeleteContext
    CatalogPermissionEntry
    CodeGenConnection
    CodeGenQueryResult
    CodeGenTransaction
    CRUDValidationMissing
    DataSourceResult
    EntityConfig
    EntityFieldChange
    EntityFieldSnapshotRow
    EntityNameFinding
    EntityNameScanOptions
    EntityNameScanResult
    EntityRenameEntry
    FieldResolutionGap
    FieldSecurityRunContext
    FullTextSearchResult
    GenerateManifestOptions
    GenerateManifestResult
    HtmlEntityNameFinding
    HtmlEntityNameScanOptions
    HtmlEntityNameScanResult
    ISARelationshipConfig
    LazyChunk
    LazyChunkEntry
    MaterializedBaseViewConfig
    MaterializedColumnSpec
    MetadataFinding
    MetadataNameScanOptions
    MetadataNameScanResult
    MultiWordNameRule
    OrganicKeyConfig
    OrganicKeyRelatedEntityConfig
    OrganicKeyTransitiveViewConfig
    PhasedExecutionResult
    RegexRule
    RegisteredClassInfo
    RemoteOperationEntityDescriptor
    SchemaEmitOptions
    SchemaNamed
    SchemaOutputOverride
    SchemaScopeConfig
    SoftFieldValueListConfig
    SoftFKFieldConfig
    SoftPKFieldConfig
    SoftPKFKTableConfig
    SubclassRenameEntry
    SubpathExportInfo
    ViewRegenEntry
    VirtualEntityConfig

    Type Aliases

    AdvancedGeneration
    AdvancedGenerationFeature
    AdvancedGenerationFeatureOption
    AllowCachingBySchema
    ApplicationRoleDefault
    ApplicationRoleDefaults
    CodeGenQueryRow
    CommandExecutionResult
    CommandInfo
    ConfigInfo
    CRUDType
    CustomSQLScript
    DBSchemaJSONOutput
    DBSchemaJSONOutputBundle
    DirtySchemaSet
    EmbeddedRecordConfig
    EmitStatsSnapshot
    EntityEntry
    EntityNamePatternKind
    EntityNameRulesBySchema
    EntityPermission
    ExcludeTableEntry
    FieldChangeReason
    FileEmitConfig
    ForceRegenerationConfig
    HtmlPatternKind
    IntegrityCheckConfig
    IntegrityCheckResult
    LLMCallEntry
    LogInfo
    MetadataPatternKind
    NewEntityDefaults
    NewEntityPermissionDefaults
    NewEntityRelationshipDefaults
    NewSchemaDefaults
    NewUserSetup
    OutputInfo
    OutputOptionInfo
    PeerClassImport
    PhaseSpan
    RelatedRecordCollectionConfig
    RunCounters
    RunIntegrityCheck
    RunReport
    RunSummary
    SchemaOutputKind
    SchemaOutputOverrideConfig
    SettingInfo
    SPType
    SQLOutputConfig
    SubclassRenameCategory
    TableInfo
    ViewRegenReason

    Variables

    _warnedEnvPrecedencePairs
    configInfo
    CRUDType
    currentWorkingDirectory
    dbDatabase
    DEFAULT_CODEGEN_CONFIG
    DEFAULT_REMOTE_OP_LIBRARY_ITEMS
    DISPLAYNAME_REOPEN_REASONS
    ENTITY_RENAME_MAP
    IN_PROCESS_ADVANCED_GENERATION_ENV
    MAX_INDEX_NAME_LENGTH
    mjCoreSchema
    SPType
    SUBCLASS_RENAME_MAP
    TRACKED_FIELD_COLUMNS
    TYPE_REOPEN_REASONS

    Functions

    applyIncludeSchemaScope
    applyInProcessAdvancedGenerationPolicy
    applyPlatformDependentEnvVars
    assignSubModule
    attemptDeleteFile
    autoIndexForeignKeys
    autoIndexSoftPrimaryKeys
    buildClassRenameRules
    buildEntityNameMap
    buildMultiWordNameRules
    buildSchemaBarrel
    buildSqlConfig
    canonicalJSONStringify
    collectDirtySchemas
    combineFiles
    commands
    computeSchemasToExcludeForIncludeList
    copyDir
    customSqlScripts
    dbPlatform
    deepEqualJSON
    diffEntityFieldSnapshots
    emitSchemaFile
    entitiesNotInExcludedSchemas
    failSpinner
    findSchemaOutputOverride
    fixFile
    fixHtmlFile
    fixMetadataFile
    formatCommandFailureDetail
    generateClassRegistrationsManifest
    getExternalEntitySchemas
    getSetting
    getSettingValue
    getSqlConfig
    groupEntitiesBySchema
    initializeConfig
    loadEmbeddedRenameMap
    logError
    logIf
    logMessage
    logStatus
    logWarning
    makeDir
    makeDirs
    mapLimit
    mj_core_schema
    MSSQLConnection
    outputDir
    outputOptions
    outputOptionValue
    parseExcludeTableEntry
    partitionEntitiesByOutputDirectory
    pruneOrphanedSchemaFiles
    reportCounter
    reportEntityPhase
    reportMark
    reportPhase
    resolveCodeGenDatabaseProvider
    resolveDirtySchemasForEmit
    resolveEntityImportPackage
    resolveEntityNameMap
    resolveEntityPackageName
    resolveLazySubpathExports
    resolveRemoteOperationSchema
    resolveSchemaEmitOptions
    resolveSchemaOutputDirectory
    resolveSubpathExports
    runMemberJunctionCodeGeneration
    runMemberJunctionCodeGenerationWithResult
    sanitizeSchemaFileName
    scanEntityNames
    scanFile
    scanHtmlEntityNames
    scanHtmlFile
    scanMetadataFile
    scanMetadataNames
    schemaKey
    schemaNameMatches
    schemasToEmit
    selectOrphanedSchemaFiles
    setCodeGenSpinnerStream
    sortBySequenceAndCreatedAt
    sortRelatedEntities
    stableHash32
    startSpinner
    stopSpinner
    succeedSpinner
    thisEmitEntityPackageName
    updateSpinner
    warnSpinner
    writeFileIfChanged