Code Generation (CodeGen)
@memberjunction/codegen
The @memberjunction/codegen
package is a powerful code generation system that automates the creation of TypeScript code based on metadata definitions in the MemberJunction platform.
Overview
The CodeGen system transforms entity and relationship metadata into executable TypeScript code. This approach reduces boilerplate code, ensures consistency between database schema and application code, and accelerates development.
Key Features
- Database-driven: Generate code directly from database schema
- Template-based: Customizable code templates
- Type safety: Generate strongly-typed TypeScript classes
- Extensible: Plugin architecture for custom code generation
- Automated: Integrated with build processes and CI/CD pipelines
Components
CodeGenerator
The CodeGenerator
is the main entry point for the code generation process:
class CodeGenerator {
// Configuration
constructor(config: CodeGenConfig);
// Core generation methods
public generateAll(): Promise<GenerationResult>;
public generateForEntity(entityName: string): Promise<GenerationResult>;
// Helper methods
public loadMetadata(): Promise<EntityMetadata[]>;
public validateConfig(): boolean;
}
CodeGenConfig
CodeGenConfig
defines the configuration for the code generation process:
interface CodeGenConfig {
// Database connection
connectionString: string;
databaseProvider: string;
// Output settings
outputPath: string;
// Entity filters
entities?: {
include?: string[];
exclude?: string[];
overrides?: EntityOverride[];
};
// Templates
templates?: {
entity?: string;
repository?: string;
interface?: string;
// Other template types...
};
// Database options
database?: {
generateViews?: boolean;
viewPrefix?: string;
fullTextIndexing?: boolean;
};
// TypeScript options
typescript?: {
strictNullChecks?: boolean;
useEnums?: boolean;
};
}
Template Engine
The template engine processes code templates to generate TypeScript files:
class TemplateEngine {
// Template loading and compilation
public loadTemplate(templatePath: string): Template;
public compileTemplate(templateContent: string): Template;
// Template rendering
public renderTemplate(template: Template, data: any): string;
}
Metadata Provider
The metadata provider retrieves entity metadata from various sources:
interface MetadataProvider {
getMetadata(): Promise<EntityMetadata[]>;
}
// Database metadata provider
class DatabaseMetadataProvider implements MetadataProvider {
constructor(connectionString: string, provider: string);
public getMetadata(): Promise<EntityMetadata[]>;
}
// JSON metadata provider
class JsonMetadataProvider implements MetadataProvider {
constructor(filePath: string);
public getMetadata(): Promise<EntityMetadata[]>;
}
// Code metadata provider
class CodeMetadataProvider implements MetadataProvider {
constructor(sourcePath: string);
public getMetadata(): Promise<EntityMetadata[]>;
}
Code Writers
Code writers handle the creation of specific code artifacts:
// Entity writer
class EntityWriter {
public writeEntity(entityMetadata: EntityMetadata, outputPath: string): Promise<string>;
}
// Repository writer
class RepositoryWriter {
public writeRepository(entityMetadata: EntityMetadata, outputPath: string): Promise<string>;
}
// Interface writer
class InterfaceWriter {
public writeInterface(entityMetadata: EntityMetadata, outputPath: string): Promise<string>;
}
// GraphQL schema writer
class GraphQLSchemaWriter {
public writeSchema(metadata: EntityMetadata[], outputPath: string): Promise<string>;
}
Plugin System
The plugin system allows for extending the code generation process:
interface CodeGenPlugin {
name: string;
// Lifecycle hooks
beforeGeneration(entities: EntityMetadata[]): Promise<EntityMetadata[]>;
afterEntityGeneration(entity: EntityMetadata, generatedCode: string): Promise<string>;
afterGeneration(): Promise<void>;
}
Templates
Templates define how code is generated from metadata. Here's an example entity template:
// entity.template.ts
export default function(entity) {
return `
// Generated by MemberJunction CodeGen
// Do not modify this file directly
import { Entity, EntityBase, EntityProperty } from '@memberjunction/core';
${entity.relationships.map(rel => `import { ${rel.relatedEntityName} } from './${rel.relatedEntityName.toLowerCase()}.entity';`).join('\n')}
@Entity('${entity.name}', '${entity.schema}', '${entity.tableName}')
export class ${entity.name} extends EntityBase {
// Properties
${entity.properties.map(prop => `
@EntityProperty(${JSON.stringify(prop.metadata)})
${prop.name}: ${prop.typescriptType};`).join('')}
// Navigation properties
${entity.relationships.map(rel => `
${rel.propertyName}?: ${rel.relatedEntityName};`).join('')}
// Constructor
constructor() {
super();
${entity.properties.map(prop => `
this.${prop.name} = ${prop.defaultValue};`).join('')}
}
}
`;
}
Usage
Command Line Interface
The CodeGen system can be used via a command-line interface:
# Generate all code
npx mj-codegen --config ./codegen-config.json
# Generate code for specific entities
npx mj-codegen --entities Member,Donation --config ./codegen-config.json
# Generate and execute database objects
npx mj-codegen --database --config ./codegen-config.json
Programmatic API
The CodeGen system can also be used programmatically:
import { CodeGenerator } from '@memberjunction/codegen';
// Create a code generator with configuration
const generator = new CodeGenerator({
connectionString: 'Server=localhost;Database=MemberJunction;User Id=sa;Password=yourpassword;',
databaseProvider: 'mssql',
outputPath: './src/generated'
});
// Generate all code
async function generateCode() {
try {
const result = await generator.generateAll();
console.log(`Generated ${result.fileCount} files`);
} catch (error) {
console.error('Code generation failed:', error);
}
}
generateCode();
Configuration Examples
Basic Configuration
{
"connectionString": "Server=localhost;Database=MemberJunction;User Id=sa;Password=yourpassword;",
"databaseProvider": "mssql",
"outputPath": "./src/generated"
}
Advanced Configuration
{
"connectionString": "Server=localhost;Database=MemberJunction;User Id=sa;Password=yourpassword;",
"databaseProvider": "mssql",
"outputPath": "./src/generated",
"entities": {
"include": ["Member*", "Donation*", "Event*"],
"exclude": ["*Audit", "*Log"],
"overrides": [
{
"name": "Member",
"properties": [
{
"name": "Password",
"exclude": true
},
{
"name": "FirstName",
"displayName": "Given Name"
}
]
}
]
},
"templates": {
"entity": "./templates/custom-entity.template.ts",
"repository": "./templates/custom-repository.template.ts"
},
"database": {
"generateViews": true,
"viewPrefix": "vw",
"fullTextIndexing": true
},
"typescript": {
"strictNullChecks": true,
"useEnums": true
},
"plugins": [
{
"path": "./plugins/documentation-plugin.js",
"options": {
"outputPath": "./docs/generated"
}
}
]
}
Generated Code Examples
Entity Class
// Generated by MemberJunction CodeGen
// Do not modify this file directly
import { Entity, EntityBase, EntityProperty } from '@memberjunction/core';
import { MembershipType } from './membershiptype.entity';
@Entity('Member', 'dbo', 'Members')
export class Member extends EntityBase {
// Properties
@EntityProperty({ isPrimaryKey: true, isAutoIncrement: true })
MemberID: number;
@EntityProperty({ isRequired: true, maxLength: 100 })
FirstName: string;
@EntityProperty({ isRequired: true, maxLength: 100 })
LastName: string;
@EntityProperty({ isRequired: true, maxLength: 255 })
Email: string;
@EntityProperty({ foreignKey: 'FK_Members_MembershipTypes' })
MembershipTypeID: number;
// Navigation properties
MembershipType?: MembershipType;
// Constructor
constructor() {
super();
this.MemberID = 0;
this.FirstName = '';
this.LastName = '';
this.Email = '';
this.MembershipTypeID = 0;
}
}
Interface Definition
// Generated by MemberJunction CodeGen
// Do not modify this file directly
export interface IMember {
MemberID: number;
FirstName: string;
LastName: string;
Email: string;
MembershipTypeID: number;
}
Repository Class
// Generated by MemberJunction CodeGen
// Do not modify this file directly
import { Repository, RepositoryBase } from '@memberjunction/core';
import { Member } from '../entities/member.entity';
@Repository('Member')
export class MemberRepository extends RepositoryBase<Member> {
// Constructor
constructor() {
super('Member');
}
// Custom query methods
public async findByEmail(email: string): Promise<Member | null> {
const filter = [
{ fieldName: 'Email', operator: 'eq', value: email }
];
const members = await this.getWithFilter(filter);
return members.length > 0 ? members[0] : null;
}
}
Database Features
Base Views
The CodeGen system can generate SQL views for optimized data access:
-- Generated by MemberJunction CodeGen
CREATE OR ALTER VIEW vwMembers AS
SELECT
m.MemberID,
m.FirstName,
m.LastName,
m.Email,
m.MembershipTypeID,
mt.Name AS MembershipTypeName,
mt.Duration AS MembershipTypeDuration
FROM
Members m
LEFT JOIN
MembershipTypes mt ON m.MembershipTypeID = mt.MembershipTypeID;
Full Text Indexing
CodeGen can generate full-text indexes for improved search:
-- Generated by MemberJunction CodeGen
IF NOT EXISTS (SELECT * FROM sys.fulltext_catalogs WHERE name = 'MJFullTextCatalog')
BEGIN
CREATE FULLTEXT CATALOG MJFullTextCatalog AS DEFAULT;
END
IF NOT EXISTS (SELECT *
FROM sys.fulltext_indexes
WHERE object_id = OBJECT_ID('dbo.Members'))
BEGIN
CREATE FULLTEXT INDEX ON dbo.Members
(
FirstName LANGUAGE 1033,
LastName LANGUAGE 1033,
Email LANGUAGE 1033
)
KEY INDEX PK_Members
ON MJFullTextCatalog
WITH CHANGE_TRACKING AUTO;
END
Stored Procedures
CodeGen can generate stored procedures for common data operations:
-- Generated by MemberJunction CodeGen
CREATE OR ALTER PROCEDURE dbo.GetMemberByEmail
@Email nvarchar(255)
AS
BEGIN
SELECT *
FROM dbo.Members
WHERE Email = @Email;
END
Extending the CodeGen System
Custom Templates
You can customize the code generation by providing custom templates:
- Create a template file:
// custom-entity.template.ts
export default function(entity) {
return `
// Custom entity template for ${entity.name}
import { Entity, EntityBase, EntityProperty } from '@memberjunction/core';
@Entity('${entity.name}', '${entity.schema}', '${entity.tableName}')
export class ${entity.name} extends EntityBase {
// Properties
${entity.properties.map(prop => `
@EntityProperty(${JSON.stringify(prop.metadata)})
${prop.name}: ${prop.typescriptType};`).join('')}
// Custom constructor
constructor() {
super();
${entity.properties.map(prop => `
this.${prop.name} = ${prop.defaultValue};`).join('')}
console.log('${entity.name} instance created');
}
}
`;
}
- Configure CodeGen to use your template:
{
"templates": {
"entity": "./templates/custom-entity.template.ts"
}
}
Custom Plugins
Create plugins to extend the CodeGen functionality:
// documentation-plugin.ts
import { CodeGenPlugin, EntityMetadata } from '@memberjunction/codegen';
import * as fs from 'fs';
import * as path from 'path';
export class DocumentationPlugin implements CodeGenPlugin {
name = 'DocumentationPlugin';
options: any;
constructor(options: any) {
this.options = options || {};
}
async beforeGeneration(entities: EntityMetadata[]): Promise<EntityMetadata[]> {
// Ensure output directory exists
const outputPath = this.options.outputPath || './docs/generated';
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
return entities;
}
async afterEntityGeneration(entity: EntityMetadata, generatedCode: string): Promise<string> {
// Generate documentation for this entity
const docContent = this.generateEntityDocumentation(entity);
// Write to file
const outputPath = this.options.outputPath || './docs/generated';
const filePath = path.join(outputPath, `${entity.name}.md`);
fs.writeFileSync(filePath, docContent);
return generatedCode;
}
async afterGeneration(): Promise<void> {
// Generate index documentation
const outputPath = this.options.outputPath || './docs/generated';
const indexPath = path.join(outputPath, 'index.md');
const indexContent = '# Generated Entity Documentation\n\n' +
'This documentation is automatically generated by the DocumentationPlugin.\n';
fs.writeFileSync(indexPath, indexContent);
}
private generateEntityDocumentation(entity: EntityMetadata): string {
return `# ${entity.name}
## Overview
${entity.description || `The ${entity.name} entity represents data in the ${entity.tableName} table.`}
## Properties
${entity.properties.map(prop => `
### ${prop.name}
- **Type**: ${prop.dataType}
- **Required**: ${prop.isRequired ? 'Yes' : 'No'}
- **Primary Key**: ${prop.isPrimaryKey ? 'Yes' : 'No'}
${prop.description ? `- **Description**: ${prop.description}` : ''}
`).join('')}
## Relationships
${entity.relationships.map(rel => `
### ${rel.name}
- **Related Entity**: ${rel.relatedEntityName}
- **Relationship Type**: ${rel.relationType}
- **Foreign Key**: ${rel.foreignKeyName}
`).join('')}
`;
}
}
Register your plugin in the config:
{
"plugins": [
{
"path": "./plugins/documentation-plugin.ts",
"options": {
"outputPath": "./docs/generated"
}
}
]
}
Multi-Database Support
Configure CodeGen for multiple databases:
{
"databases": [
{
"name": "membership",
"connectionString": "Server=localhost;Database=Membership;User Id=sa;Password=yourpassword;",
"databaseProvider": "mssql",
"outputPath": "./src/generated/membership"
},
{
"name": "financials",
"connectionString": "Server=localhost;Database=Financials;User Id=sa;Password=yourpassword;",
"databaseProvider": "mssql",
"outputPath": "./src/generated/financials"
}
]
}
CI/CD Integration
Integrate CodeGen into your CI/CD pipeline:
# GitHub Actions workflow example
name: CodeGen
on:
push:
branches: [ main ]
paths:
- 'database/**'
- 'schema/**'
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run CodeGen
run: npx mj-codegen --config ./codegen-config.json
- name: Commit generated code
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "chore: update generated code"
file_pattern: src/generated/**
# Enable verbose logging
npx mj-codegen --verbose --config ./codegen-config.json
# Output logs to file
npx mj-codegen --log ./codegen.log --config ./codegen-config.json
Related Packages
The CodeGen system interacts with several other MemberJunction packages:
- @memberjunction/core: Provides base classes that generated code extends
- @memberjunction/data-access: Uses generated entity classes for data access
- @memberjunction/graphql-server: Uses generated schema for GraphQL API
- @memberjunction/global-types: Shares type definitions across packages
Future Enhancements
The MemberJunction CodeGen system continues to evolve with planned enhancements:
- Schema Evolution: Better tracking and migration of schema changes
- AI-Enhanced Generation: Using AI to suggest entity relationships and validations
- Visual Modeling: Graphical interface for metadata management
- Cross-Platform Support: Enhanced support for various database platforms
- Integration Connectors: Automatic generation of integration code for external systems
Updated 1 day ago