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 (RemoteOperationGeneratorBase → remote_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.
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
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.
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.
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 excludeSchemasfor 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:
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 .env → DB_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
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:
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.
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.RootIDAS [RootParentID],
hier_ParentID.DepthAS [ParentIDDepth],
hier_ParentID.PathAS [ParentIDPath],
hier_ParentID.IsLeafAS [ParentIDIsLeaf],
hier_ParentID.ChildCountAS [ParentIDChildCount]
FROM [sales].[Category] AS c
OUTERAPPLY [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.
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 oneIsNameField 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).
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 --skipfilesonce 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<br/>base table")]
GV["<b>vwFoo</b><br/>owned by CodeGen<br/><i>regenerated every run</i>"]
GT --> GV
end
subgraph CUS["② FULLY CUSTOM — BaseViewGenerated = 0"]
direction TB
CT[("Foo<br/>base table")]
CV["<b>vwFoo</b><br/>owned by the application<br/><i>frozen the day it was copied</i>"]
CT --> CV
end
subgraph LAY["③ LAYERED — GeneratedBaseViewName = vwFooGenerated"]
direction TB
LT[("Foo<br/>base table")]
LI["<b>vwFooGenerated</b><br/>owned by CodeGen<br/><i>regenerated every run</i>"]
LO["<b>vwFoo</b><br/>owned by the application<br/><i>SELECT g.* + your columns</i>"]
LT --> LI
LI -->|"SELECT g.*"| LO
end
GV --> SURF
CV --> SURF
LO --> SURF
SURF["<b>BaseView</b> — the public surface<br/>field discovery · permissions · RunView<br/>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
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.
PostgreSQL — restar the outer view; ship it via pg-migrate
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.
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.
Run CodeGen. It writes the inner view.
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).
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.
⚠️ Adopting layering on an EXISTING entity needs forceRegeneration
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.
All SQL generated during metadata management and object generation is logged to a Flyway-compatible migration file. The SQLOutput configuration controls this behavior:
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.
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)
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.
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.
Create and Update procedures only include the child’s own fields, not parent fields:
-- spCreateEmployee only has Employee-specific parameters
CREATEPROCEDURE [spCreateEmployee]
@ID uniqueidentifier,
@EmployeeNumber nvarchar(50),
@HireDate date,
@Salary decimal(18,2)
-- No FirstName, LastName (those are Person fields)
ASBEGIN
-- 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.
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.
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
INSERTINTOEntityRelationship (...)
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.
4. LLM-Assisted Field Decoration (Advanced Feature)
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.
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.
Stale metadata — If you dropped and recreated tables, metadata may be out of sync. Run a full mj codegen (without --skipdb) to refresh.