Capacitor Generator Implementation Guide
This document describes how each Capacitor generator implements features for specific databases and ORMs. It provides transparency about what happens under the hood when you target different platforms.
Generator Architecture and SPI
Capacitor uses a pluggable generator architecture that separates the universal data model from database and framework-specific implementations. Generators follow a formal Service Provider Interface (SPI) that enables third-party generator development and composition.
Generator Lifecycle
Every generator follows a defined lifecycle with distinct phases:
discover → configure → validate → resolve → generate → post-process → package → publish
1. Discover: Build system finds generators via plugin declarations in capacitor-build.json
{
"plugins": {
"generators": {
"com.acme.capacitor:generator-django": "1.0.0"
}
}
}
2. Configure: Generator receives its configuration block from capacitor-build.json
{
"generators": {
"java-jpa-sdk": {
"generator": "capacitor:java-jpa",
"target": "production-sql",
"options": { /* generator-specific options */ }
}
}
}
3. Validate: Generator confirms compatibility (framework + database target)
- Checks if the framework supports the target database
- Validates that required options are provided
- Reports errors for unsupported feature combinations
4. Resolve: Generator resolves the semantic model into its internal representation
- Parses all entities, shapes, enums, repositories
- Resolves namespace imports and dependencies
- Builds the complete type graph
5. Generate: Generator emits files (source, config, migration, docs, tests)
- Produces entity classes, repository interfaces, DTOs
- Generates database migrations or schemas
- Creates documentation and test helpers
6. Post-process: Decorators and plugins modify generated output
- Applies formatting (Prettier, Black, gofmt)
- Adds cross-cutting concerns (logging, metrics)
- Injects framework-specific annotations
7. Package: Output is assembled into a publishable artifact
- Creates JAR, npm package, Python wheel, etc.
- Includes metadata (pom.xml, package.json, setup.py)
- Bundles documentation and resources
8. Publish: Artifact is pushed to configured repository
- Maven Central, npm registry, PyPI, etc.
- Validates credentials and permissions
- Creates release tags and changelog
Generator Types
Built-in Generators
Shipped with Capacitor and referenced by name:
capacitor:java-jpa- JPA/Hibernate for Javacapacitor:typescript-prisma- Prisma for TypeScriptcapacitor:typescript-typeorm- TypeORM for TypeScriptcapacitor:python-sqlalchemy- SQLAlchemy for Pythoncapacitor:go-gorm- GORM for Gocapacitor:csharp-entity-framework- Entity Framework for C#capacitor:swift-core-data- Core Data for Swiftcapacitor:typescript-dynamodb- AWS SDK for DynamoDBcapacitor:typescript-mongoose- Mongoose for MongoDB
External Generators
Third-party generators installed from repositories:
{
"plugins": {
"repositories": [
"https://plugins.capacitor-lang.dev",
"https://repo.acme.com/capacitor-plugins"
],
"generators": {
"com.acme.capacitor:generator-django": "1.0.0",
"org.capacitor:generator-graphql": "2.1.0",
"io.micronaut:generator-micronaut-data": "1.5.0"
}
}
}
Local Generators
Proprietary or in-development generators:
{
"plugins": {
"local": [
"./generators/custom-oracle-generator",
"./generators/company-standards"
]
}
}
Generator Decorators
Decorators wrap generators to add cross-cutting functionality without modifying the base generator. Multiple decorators can be chained using the decorator pattern.
Common Decorators
Lombok Decorator (Java):
{
"generators": {
"java-jpa-sdk": {
"generator": "capacitor:java-jpa",
"decorators": [
{
"decorator": "org.capacitor:decorator-lombok",
"config": {
"builder": true,
"data": true,
"slf4j": true
}
}
]
}
}
}
Adds Lombok annotations to generated entities:
@Data
@Builder
@Slf4j
@Entity
public class User {
// Generated fields
}
MapStruct Decorator (Java):
{
"decorator": "org.capacitor:decorator-mapstruct",
"config": {
"componentModel": "spring"
}
}
Generates MapStruct mappers between entities and DTOs:
@Mapper(componentModel = "spring")
public interface UserMapper {
UserProfile toProfile(User user);
User fromCreateInput(CreateUserInput input);
}
OpenTelemetry Decorator:
{
"decorator": "org.capacitor:decorator-opentelemetry",
"config": {
"traceRepositoryMethods": true,
"metricsEnabled": true
}
}
Adds distributed tracing and metrics to repository methods:
@Repository
public class UserRepository {
@Traced(operation = "user.findById")
@Metered(name = "user.repository.findById")
public Optional<User> findById(UUID id) { ... }
}
Available Decorators
| Decorator | Target Languages | Purpose |
|---|---|---|
decorator-lombok | Java | Lombok annotations (@Data, @Builder, etc.) |
decorator-mapstruct | Java | Entity-DTO mapping code generation |
decorator-swagger | Java, TypeScript, C# | OpenAPI/Swagger annotations |
decorator-spring-security | Java | @PreAuthorize annotations on repositories |
decorator-opentelemetry | All | Distributed tracing and metrics |
decorator-logging | All | Structured logging injection |
decorator-validation | All | Enhanced validation annotations |
decorator-i18n | All | Internationalization support |
Feature Support Negotiation
When a generator encounters a feature, it classifies support into one of four categories:
Support Levels
NATIVE (✅): Database natively supports this feature
Example: PostgreSQL enums
CREATE TYPE user_status AS ENUM ('ACTIVE', 'INACTIVE', 'SUSPENDED');
SHIMMED (🔧): Feature requires application-level shim code
Example: MongoDB enums (implemented via validation)
{
validator: {
$jsonSchema: {
properties: {
status: {
enum: ["ACTIVE", "INACTIVE", "SUSPENDED"]
}
}
}
}
}
+ TypeScript enum type + SDK validation
PARTIAL (⚠️): Feature is partially supported with limitations
Example: Full-text search on MySQL (native but limited features vs PostgreSQL)
UNSUPPORTED (❌): Feature cannot be implemented (generation error)
Example: Views on DynamoDB → Generation error with migration suggestions
Compatibility Reports
The build system generates compatibility reports showing how each feature is implemented:
$ capacitor build --generator java-sdk
Compatibility Report for java-sdk (JPA + PostgreSQL 15):
────────────────────────────────────────────────────────
✅ NATIVE: entities, shapes, enums, relationships, unique constraints,
check constraints, sequences, views, materialized views,
repositories, mixins, invariants, documentation
⚡ SHIMMED: soft deletes (JPA interceptor @SQLDelete),
encryption (@pii - application-level encryption),
audit trails (JPA @EntityListeners),
multi-tenancy (Hibernate @Filter),
fixtures (test helper classes)
⚠️ PARTIAL: full-text search (native tsvector, limited ranking options)
❌ UNSUPPORTED: none
Generated 47 files in ./src/main/java/com/acme/dal
Generator Configuration Options
Each generator accepts standard and custom options:
Standard Options (All Generators)
| Option | Type | Description |
|---|---|---|
target | String | Database target to generate for |
output | String | Output directory path |
service | String | Service name to generate (filters entities) |
includeQueries | Boolean | Generate query methods |
includeRepositories | Boolean | Generate repository interfaces |
includeDTOs | Boolean | Generate input/output DTOs |
includeTests | Boolean | Generate test helpers |
includeFixtures | Boolean | Generate test fixtures |
includeDocumentation | Boolean | Generate inline documentation |
Language-Specific Options
Java/JPA:
{
"options": {
"package": "com.acme.data.users",
"jakartaEE": true,
"lombokAnnotations": true,
"springBoot": {
"enabled": true,
"version": "3.2",
"autoConfiguration": true
}
}
}
TypeScript/Prisma:
{
"options": {
"esm": true,
"strictNullChecks": true,
"zodValidation": true
}
}
Python/SQLAlchemy:
{
"options": {
"package": "acme_data_users",
"asyncSupport": true,
"typeHints": true,
"pydanticModels": true
}
}
Best Practices for Generator Selection
- Match Framework to Use Case: Choose JPA for enterprise Java, Prisma for TypeScript backends, SQLAlchemy for Python
- Validate Compatibility: Check Framework Compatibility Matrix before selecting
- Use Decorators Sparingly: Only add decorators for truly cross-cutting concerns
- Test with Multiple Targets: Validate generated code compiles and tests pass
- Version Dependencies: Pin generator versions for reproducible builds
- Review Compatibility Reports: Understand which features are shimmed vs native
- Custom Generators: Only build custom generators when built-in options are insufficient
Creating Custom Generators
Custom generators can be built by implementing the generator SPI. While the reference implementation is in Java, generators can be written in any language that can be invoked by the Capacitor CLI.
Minimal generator interface (conceptual):
interface CapacitorGenerator {
String id(); // Generator ID
Set<Language> targetLanguages(); // Supported languages
Set<DatabaseTarget> supportedDatabases(); // Supported databases
void initialize(GeneratorConfig config); // Configure generator
ValidationResult validate(Model, Target); // Validate compatibility
GenerationResult generate(Model, Context); // Generate code
}
Generator plugin structure:
my-generator/
├── generator.yaml # Generator metadata
├── templates/ # Code generation templates
│ ├── entity.mustache
│ ├── repository.mustache
│ └── dto.mustache
├── src/ # Generator implementation
│ └── generator.js # Main generator logic
└── tests/ # Generator tests
└── generator.test.js
For detailed documentation on creating custom generators, see the Capacitor Generator SDK Documentation.
Generator Support Matrix
| Feature | PostgreSQL | MySQL | MongoDB | Cassandra | DynamoDB | Prisma | TypeORM |
|---|---|---|---|---|---|---|---|
| Core Features | |||||||
| Entities | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native |
| Shapes | ✅ Composite | ✅ JSON | 🔧 Embedded | ❌ Flatten | 🔧 Nested | ✅ Native | ✅ Embedded |
| Enums | ✅ Native | ✅ Native | 🔧 Validation | ❌ String | 🔧 Validation | ✅ Native | ✅ Native |
| Nullable Types | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native |
| Constraints | |||||||
| Unique | ✅ Native | ✅ Native | ✅ Native | ⚠️ Limited | 🔧 SDK | ✅ Native | ✅ Native |
| Default Values | ✅ Native | ✅ Native | ✅ Native | ✅ Native | 🔧 SDK | ✅ Native | ✅ Native |
| Check Constraints | ✅ Native | ✅ Native | 🔧 Validation | ❌ N/A | 🔧 SDK | ⚠️ Limited | 🔧 Shim |
| Length/Range | ✅ Native | ✅ Native | 🔧 Validation | ❌ N/A | 🔧 SDK | ⚠️ Limited | 🔧 Decorator |
| Relationships | |||||||
| One-to-One | ✅ FK | ✅ FK | 🔧 Reference | 🔧 Denormalize | 🔧 Reference | ✅ Native | ✅ Native |
| One-to-Many | ✅ FK | ✅ FK | 🔧 Reference | 🔧 Denormalize | 🔧 Reference | ✅ Native | ✅ Native |
| Many-to-Many | ✅ Join Table | ✅ Join Table | 🔧 Array | 🔧 Multi-Table | 🔧 GSI | ✅ Native | ✅ Native |
| Cascade Delete | ✅ Native | ✅ Native | 🔧 SDK | ❌ Manual | 🔧 Lambda | ✅ Native | ✅ Native |
| Access Patterns | |||||||
| Primary Key | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native | ✅ Native |
| Global Index | ✅ B-Tree | ✅ B-Tree | ✅ Index | ✅ Index | ✅ GSI | ✅ Native | ✅ Native |
| Composite Index | ✅ Native | ✅ Native | ✅ Compound | ✅ Composite | ✅ Composite Key | ✅ Native | ✅ Native |
| Partial/Sparse Index | ✅ Native | ⚠️ Limited | ✅ Sparse | ❌ N/A | ❌ N/A | ⚠️ Limited | ⚠️ Limited |
| Full-Text Search | ✅ Native | ✅ Native | ✅ Text Index | ❌ N/A | ❌ External | ⚠️ DB-dependent | ⚠️ DB-dependent |
| Advanced Features | |||||||
| Views | ✅ Native | ✅ Native | ✅ Pipeline | ❌ N/A | ❌ N/A | ❌ N/A | ⚠️ Query |
| Materialized Views | ✅ Native | 🔧 Trigger | 🔧 Collection | ⚠️ Auto | 🔧 Stream+Lambda | ❌ N/A | 🔧 Shim |
| Computed Fields | ✅ Generated | ✅ Generated | 🔧 Virtual | ❌ N/A | 🔧 SDK | ⚠️ Limited | ✅ Virtual |
| Sequences | ✅ Native | ✅ Auto-Inc | 🔧 Counter | ❌ UUID | 🔧 Atomic | ✅ Native | ✅ Native |
| Soft Delete | 🔧 Column | 🔧 Column | 🔧 Field | 🔧 Flag | 🔧 Attribute | ✅ Native | ✅ Decorator |
| Audit Trails | 🔧 Trigger | 🔧 Trigger | 🔧 Change Stream | ❌ N/A | ✅ Streams | 🔧 Middleware | 🔧 Subscriber |
| Multi-Tenancy | 🔧 RLS | 🔧 Column | 🔧 Field | 🔧 Partition | 🔧 GSI | 🔧 Middleware | 🔧 Filter |
| Streams/CDC | ✅ Logical Rep | ✅ Binlog | ✅ Change Stream | ❌ N/A | ✅ Streams | ❌ N/A | 🔧 Subscriber |
| v0.2 Platform Features | |||||||
| Namespaces | ✅ Schema | ✅ Schema | 🔧 DB/Prefix | 🔧 Keyspace | 🔧 Prefix | ✅ Module | ✅ Module |
| Repositories | ✅ Spring Data | ✅ Spring Data | ✅ SDK Class | 🔧 SDK Class | ✅ SDK Class | ✅ Native | ✅ Native |
| Input DTOs | ✅ Records | ✅ Records | ✅ Interfaces | ✅ Interfaces | ✅ Interfaces | ✅ Native | ✅ Native |
| Output DTOs | ✅ Records | ✅ Records | ✅ Interfaces | ✅ Interfaces | ✅ Projection | ✅ Native | ✅ Native |
| Mixins | ✅ @MappedSuperclass | ✅ @MappedSuperclass | 🔧 Plugin | 🔧 Interface | 🔧 Interface | ✅ Native | ✅ Native |
| Invariants | ✅ Bean Validation + CHECK | ✅ Bean Validation + CHECK | 🔧 Zod + Schema | 🔧 SDK Only | 🔧 SDK Only | ✅ Validation | ✅ Validation |
| Fixtures | ✅ @Sql Scripts | ✅ @Sql Scripts | 🔧 JSON + Helpers | 🔧 CQL + Helpers | 🔧 JSON + BatchWrite | ✅ Seed Scripts | ✅ Seed Scripts |
| Documentation | ✅ Javadoc | ✅ Javadoc | ✅ JSDoc/TSDoc | ✅ JSDoc/TSDoc | ✅ TSDoc | ✅ TSDoc | ✅ TSDoc |
Legend:
- ✅ Native: Implemented using database/ORM native features
- 🔧 Shim: Implemented via generated application code or additional infrastructure
- ⚠️ Limited: Partial support with limitations
- ❌ N/A: Cannot be implemented (generation error)
Documentation Generation
Capacitor generates comprehensive documentation from your schema definitions, including inline code documentation, standalone documentation sites, entity-relationship diagrams, and access pattern analysis.
Documentation Sources
Documentation is extracted from three sources in Capacitor schemas:
1. Triple-Slash Comments (///)
/// Represents a registered user in the system.
///
/// Users are created via the registration API and must have
/// a verified email before they can access premium features.
entity User {
@identity
/// System-generated unique identifier. Immutable after creation.
userId: UUID,
/// Unique username for the user. Must be 3-50 characters.
@length(min: 3, max: 50)
username: String
}
2. @documentation Trait
@documentation(
title: "User Management System",
description: "Core entities for user authentication and profile management",
version: "2.1.0",
author: "Platform Team",
tags: ["authentication", "users", "core"]
)
entity User {
// ...
}
3. @example Trait
@example(
name: "Create Active User",
code: """
{
"username": "alice",
"email": "alice@example.com",
"status": "ACTIVE"
}
"""
)
input CreateUserInput for User {
include: [username, email, status]
}
Output Formats
Configure documentation outputs in capacitor-build.json:
{
"documentation": {
"enabled": true,
"outputs": [
{
"format": "html",
"output": "./docs/html",
"theme": "modern",
"includeDiagrams": true
},
{
"format": "markdown",
"output": "./docs/markdown"
},
{
"format": "inline",
"styles": ["javadoc", "jsdoc", "tsdoc"]
}
]
}
}
Inline Code Documentation
Generates language-specific documentation comments in all generated code files.
Java (Javadoc)
/**
* Represents a registered user in the system.
*
* <p>Users are created via the registration API and must have
* a verified email before they can access premium features.</p>
*
* <h3>Database Mapping</h3>
* <ul>
* <li>Table: users</li>
* <li>Primary Key: user_id (UUID)</li>
* <li>Indexes: username (unique), email (unique)</li>
* </ul>
*
* <h3>Access Patterns</h3>
* <ul>
* <li>Find by ID: O(1)</li>
* <li>Find by username: O(1)</li>
* <li>Find by email: O(1)</li>
* </ul>
*
* @author Platform Team
* @version 2.1.0
* @see UserRepository
* @see CreateUserInput
* @since 1.0.0
*/
@Entity
@Table(name = "users")
public class User {
/**
* System-generated unique identifier. Immutable after creation.
*
* <p>This field serves as the primary key and cannot be changed
* after the user is created.</p>
*/
@Id
@Column(name = "user_id", nullable = false, updatable = false)
private UUID userId;
}
TypeScript/JavaScript (TSDoc/JSDoc)
/**
* Represents a registered user in the system.
*
* Users are created via the registration API and must have
* a verified email before they can access premium features.
*
* **Collection**: `users`
* **Indexes**:
* - Primary key: userId (unique)
* - Unique index: username
* - Unique index: email
*
* **Access Patterns**:
* - Find by ID: O(1)
* - Find by username: O(1) via UsernameIndex GSI
* - Find by email: O(1) via EmailIndex GSI
*
* @author Platform Team
* @version 2.1.0
* @since 1.0.0
*/
export interface User {
/**
* System-generated unique identifier. Immutable after creation.
*
* @readonly
*/
userId: string;
/**
* Unique username for the user. Must be 3-50 characters.
*
* @minLength 3
* @maxLength 50
*/
username: string;
}
Python (Docstrings)
class User(Base):
"""Represents a registered user in the system.
Users are created via the registration API and must have
a verified email before they can access premium features.
Database Mapping:
Table: users
Primary Key: user_id (UUID)
Indexes: username (unique), email (unique)
Access Patterns:
- Find by ID: O(1)
- Find by username: O(1)
- Find by email: O(1)
Attributes:
user_id (UUID): System-generated unique identifier. Immutable after creation.
username (str): Unique username for the user. Must be 3-50 characters.
email (str): User's email address. Must be valid and verified.
status (UserStatus): Current status of the user account.
Author:
Platform Team
Version:
2.1.0
"""
user_id: Mapped[UUID] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(50), unique=True)
C# (XML Documentation)
/// <summary>
/// Represents a registered user in the system.
///
/// Users are created via the registration API and must have
/// a verified email before they can access premium features.
/// </summary>
/// <remarks>
/// <para><strong>Database Mapping:</strong></para>
/// <list type="bullet">
/// <item>Table: users</item>
/// <item>Primary Key: UserId (Guid)</item>
/// <item>Indexes: Username (unique), Email (unique)</item>
/// </list>
/// <para><strong>Access Patterns:</strong></para>
/// <list type="bullet">
/// <item>Find by ID: O(1)</item>
/// <item>Find by username: O(1)</item>
/// <item>Find by email: O(1)</item>
/// </list>
/// </remarks>
/// <author>Platform Team</author>
/// <version>2.1.0</version>
[Table("users")]
public class User
{
/// <summary>
/// System-generated unique identifier. Immutable after creation.
/// </summary>
/// <value>A unique identifier that cannot be changed after creation.</value>
[Key]
[Column("user_id")]
public Guid UserId { get; set; }
}
HTML Documentation Site
Generates a static documentation website with navigation, search, and diagrams.
Features:
- Entity catalog with full details
- Repository method documentation
- DTO schemas with examples
- Interactive ER diagrams
- Access pattern analysis
- Full-text search
- Responsive design
- Dark/light theme toggle
Example structure:
docs/html/
├── index.html
├── entities/
│ ├── User.html
│ ├── Order.html
│ └── Product.html
├── repositories/
│ ├── UserRepository.html
│ └── OrderRepository.html
├── dtos/
│ ├── CreateUserInput.html
│ └── UserSummary.html
├── diagrams/
│ ├── er-diagram.html
│ └── access-patterns.html
├── assets/
│ ├── styles.css
│ └── search.js
└── api/
└── search-index.json
Themes: modern, minimal, corporate, github
Markdown Documentation
Generates markdown files suitable for GitHub/GitLab wikis or static site generators.
Example output (User.md):
# User
Represents a registered user in the system.
Users are created via the registration API and must have a verified email before they can access premium features.
## Database Schema
**Table**: `users`
| Column | Type | Constraints | Description |
|--------|------|-------------|-------------|
| user_id | UUID | PRIMARY KEY | System-generated unique identifier |
| username | VARCHAR(50) | UNIQUE, NOT NULL | Unique username for the user |
| email | VARCHAR(255) | UNIQUE, NOT NULL | User's email address |
| status | user_status | NOT NULL | Current status of the user account |
| created_at | TIMESTAMP | NOT NULL | Record creation timestamp |
| updated_at | TIMESTAMP | NOT NULL | Last update timestamp |
## Indexes
- **Primary Key**: `user_id`
- **Unique Index**: `username`
- **Unique Index**: `email`
## Access Patterns
- **Find by ID**: O(1) via primary key
- **Find by username**: O(1) via unique index
- **Find by email**: O(1) via unique index
- **List active users**: O(n) with status filter
## Relationships
- **One-to-Many**: `User` → `Order` (via `user_id`)
- **One-to-Many**: `User` → `Session` (via `user_id`)
## Repository Methods
### CRUD Operations
- `User findById(UUID userId)`
- `List<User> findAll()`
- `User save(User user)`
- `void deleteById(UUID userId)`
### Custom Queries
- `Optional<User> findByUsername(String username)`
- `Optional<User> findByEmail(String email)`
- `List<User> findByStatus(UserStatus status)`
### Mutations
- `User deactivate(UUID userId)` - Set user status to INACTIVE
### Aggregations
- `Map<UserStatus, Long> countByStatus()` - Count users by status
## DTOs
### CreateUserInput
Input DTO for creating a new user.
```json
{
"username": "alice",
"email": "alice@example.com",
"phone": "+14155551234"
}
```
### UserSummary
Output DTO for user list views.
```json
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"username": "alice",
"email": "alice@example.com",
"status": "ACTIVE",
"createdAt": "2024-01-15T10:00:00Z"
}
```
## Version History
- **v2.1.0** - Added phone field and email verification
- **v2.0.0** - Migrated to UUID primary keys
- **v1.0.0** - Initial release
Entity-Relationship Diagrams
Generates ER diagrams in multiple formats.
Mermaid
erDiagram
USER ||--o{ ORDER : places
USER ||--o{ SESSION : has
ORDER ||--|{ ORDER_ITEM : contains
PRODUCT ||--o{ ORDER_ITEM : "included in"
USER {
uuid user_id PK
string username UK
string email UK
enum status
timestamp created_at
timestamp updated_at
}
ORDER {
uuid order_id PK
uuid user_id FK
enum status
decimal total
timestamp created_at
}
ORDER_ITEM {
uuid item_id PK
uuid order_id FK
string product_sku FK
int quantity
decimal price
}
PRODUCT {
string sku PK
string name
decimal price
int inventory
}
SESSION {
uuid session_id PK
uuid user_id FK
timestamp expires_at
}
PlantUML
@startuml
entity "User" as user {
* user_id : UUID <<PK>>
--
* username : String <<UK>>
* email : String <<UK>>
* status : UserStatus
* created_at : Timestamp
* updated_at : Timestamp
}
entity "Order" as order {
* order_id : UUID <<PK>>
--
* user_id : UUID <<FK>>
* status : OrderStatus
* total : Decimal
* created_at : Timestamp
}
entity "OrderItem" as order_item {
* item_id : UUID <<PK>>
--
* order_id : UUID <<FK>>
* product_sku : String <<FK>>
* quantity : Integer
* price : Decimal
}
entity "Product" as product {
* sku : String <<PK>>
--
* name : String
* price : Decimal
* inventory : Integer
}
user ||--o{ order : "places"
order ||--|{ order_item : "contains"
product ||--o{ order_item : "included in"
@enduml
Configuration:
{
"documentation": {
"diagrams": {
"formats": ["mermaid", "plantuml", "svg", "png"],
"layout": "top-to-bottom",
"includeAttributes": true,
"includeCardinality": true,
"groupByService": true
}
}
}
Access Pattern Documentation
Generates documentation analyzing how data is accessed across different query patterns.
Example (access-patterns.md):
# Access Patterns Analysis
## User Entity
### Primary Access Patterns
| Pattern | Frequency | Complexity | Index | Notes |
|---------|-----------|------------|-------|-------|
| Find by ID | High | O(1) | Primary Key | Most common lookup |
| Find by username | High | O(1) | username_idx | Login flow |
| Find by email | Medium | O(1) | email_idx | Password reset |
| List by status | Low | O(n) | Sequential Scan | Consider materialized view |
### Performance Recommendations
- ✅ **Optimal**: Primary key and unique index queries
- ⚠️ **Review**: Status filtering causes full table scan on large datasets
- 💡 **Suggestion**: Add composite index on (status, created_at) for common queries
## Order Entity
### Primary Access Patterns
| Pattern | Frequency | Complexity | Index | Notes |
|---------|-----------|------------|-------|-------|
| Find by ID | High | O(1) | Primary Key | Order detail page |
| Find by user | High | O(log n) | user_id_idx | User order history |
| Find by date range | Medium | O(n) | created_at_idx | Reports and analytics |
| Count by status | Low | O(n) | Aggregate query | Dashboard |
### Query Optimization
```sql
-- Optimized query using covering index
CREATE INDEX order_user_status_idx ON orders(user_id, status, created_at)
INCLUDE (order_id, total);
-- Enables efficient user order history without table lookup
SELECT order_id, status, total, created_at
FROM orders
WHERE user_id = ? AND status = 'ACTIVE'
ORDER BY created_at DESC;
---
### Compatibility Matrices
Documents feature support across different database targets.
**Example** (`compatibility.md`):
```markdown
# Feature Compatibility Matrix
## User Entity - PostgreSQL vs MongoDB
| Feature | PostgreSQL | MongoDB | Migration Notes |
|---------|-----------|---------|-----------------|
| Enums | ✅ Native `user_status` type | 🔧 Validation + SDK | Add runtime validation |
| Unique constraints | ✅ Native UNIQUE | ✅ Unique index | Direct mapping |
| Check constraints | ✅ Native CHECK | 🔧 Schema validation | Some constraints SDK-only |
| Foreign keys | ✅ Native FK | 🔧 Reference + SDK | Add cascade delete logic |
| Transactions | ✅ ACID | ⚠️ Limited (replica sets only) | Review transaction boundaries |
## Cost Implications - DynamoDB
| Feature | Implementation | Cost Impact |
|---------|----------------|-------------|
| Unique username | Separate index table | +100% write cost |
| Find by email | GSI | +100% write cost, +storage |
| Count by status | Full table scan | Very high RCU usage |
| Aggregations | Scan or Stream+Lambda | High ongoing cost |
**Recommendation**: Consider PostgreSQL for complex query patterns. DynamoDB better suited for simple key-value access at massive scale.
Build Commands
Generate documentation using CLI:
# Generate all configured documentation
capacitor build --docs
# Generate specific format only
capacitor build --docs --format html
capacitor build --docs --format markdown
# Generate docs without building code
capacitor docs
# Serve HTML docs locally
capacitor docs --serve --port 8080
Output:
Generating documentation...
✅ Inline documentation (Javadoc, TSDoc, Docstrings)
- 47 entities
- 23 repositories
- 65 DTOs
✅ HTML documentation site
- Output: ./docs/html
- Theme: modern
- 3 ER diagrams generated
✅ Markdown documentation
- Output: ./docs/markdown
- 47 entity pages
- 23 repository pages
✅ Access pattern analysis
- 12 entities analyzed
- 3 optimization recommendations
Documentation complete. View at: file://./docs/html/index.html
PostgreSQL Generator
Overview
PostgreSQL is Capacitor's reference implementation. The generator produces pure SQL DDL and leverages PostgreSQL's rich feature set to implement nearly all Capacitor features natively.
Core Features
Entities
Implementation: Standard CREATE TABLE
-- From: entity User { @identity userId: UUID, username: String }
CREATE TABLE users (
user_id UUID PRIMARY KEY,
username VARCHAR(255) NOT NULL
);
Shapes (Composite Types)
Implementation: PostgreSQL composite types or JSONB
-- From: shape Address { street: String, city: String }
CREATE TYPE address AS (
street VARCHAR(255),
city VARCHAR(255),
state VARCHAR(2),
zip_code VARCHAR(10)
);
CREATE TABLE warehouses (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
location address NOT NULL
);
Alternative (JSONB for flexibility):
CREATE TABLE warehouses (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
location JSONB NOT NULL
);
Enums
Implementation: Native CREATE TYPE ... AS ENUM
-- From: enum OrderStatus { PENDING, CONFIRMED, SHIPPED }
CREATE TYPE order_status AS ENUM ('PENDING', 'CONFIRMED', 'SHIPPED', 'DELIVERED');
CREATE TABLE orders (
order_id UUID PRIMARY KEY,
status order_status NOT NULL DEFAULT 'PENDING'
);
Linter: ✅ No warnings
Constraints
Unique Constraints
CREATE TABLE users (
user_id UUID PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE
);
Check Constraints
CREATE TABLE products (
sku VARCHAR(50) PRIMARY KEY,
price NUMERIC(10,2) NOT NULL CHECK (price > 0),
inventory INTEGER NOT NULL CHECK (inventory >= 0)
);
Default Values
CREATE TABLE products (
sku VARCHAR(50) PRIMARY KEY,
inventory INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
Linter: ✅ No warnings
Relationships
One-to-Many with Cascade
CREATE TABLE authors (
author_id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
CREATE TABLE books (
isbn VARCHAR(13) PRIMARY KEY,
title VARCHAR(500) NOT NULL,
author_id UUID NOT NULL,
FOREIGN KEY (author_id)
REFERENCES authors(author_id)
ON DELETE CASCADE
);
Many-to-Many
-- Generated junction table
CREATE TABLE student_course_enrollments (
student_id UUID NOT NULL,
course_id VARCHAR(50) NOT NULL,
enrolled_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES courses(course_id) ON DELETE CASCADE
);
Linter: ✅ No warnings
Views and Materialized Views
Views
-- From: view ActiveUsers
CREATE VIEW active_users AS
SELECT user_id, username, email, last_login
FROM users
WHERE status = 'active'
AND last_login > NOW() - INTERVAL '30 days';
Materialized Views
-- From: @materialized(refresh: "incremental")
CREATE MATERIALIZED VIEW order_summary AS
SELECT
customer_id,
COUNT(order_id) as total_orders,
SUM(amount) as revenue,
AVG(amount) as avg_order_value
FROM orders
GROUP BY customer_id;
-- Create unique index for concurrent refresh
CREATE UNIQUE INDEX order_summary_customer_idx
ON order_summary(customer_id);
-- Generated refresh function
CREATE OR REPLACE FUNCTION refresh_order_summary()
RETURNS void AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY order_summary;
END;
$$ LANGUAGE plpgsql;
-- Scheduled via pg_cron or external scheduler
SELECT cron.schedule('refresh-order-summary', '0 * * * *',
'CALL refresh_order_summary()');
Linter: ✅ No warnings
Computed Fields
CREATE TABLE orders (
order_id UUID PRIMARY KEY,
subtotal NUMERIC(10,2) NOT NULL,
tax_rate NUMERIC(4,3) NOT NULL,
-- Generated column (computed on read)
total_with_tax NUMERIC(10,2) GENERATED ALWAYS AS
(subtotal * (1 + tax_rate)) STORED
);
Linter: ✅ No warnings
Sequences
CREATE SEQUENCE invoice_number_seq
START WITH 1000
INCREMENT BY 1
CACHE 20;
CREATE TABLE invoices (
invoice_number INTEGER PRIMARY KEY
DEFAULT nextval('invoice_number_seq'),
customer_id UUID NOT NULL,
amount NUMERIC(10,2) NOT NULL
);
Linter: ✅ No warnings
Advanced Features
Full-Text Search
CREATE TABLE articles (
id UUID PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
search_vector tsvector GENERATED ALWAYS AS
(to_tsvector('english', title || ' ' || content)) STORED
);
CREATE INDEX articles_search_idx ON articles USING GIN(search_vector);
Soft Delete
CREATE TABLE documents (
doc_id UUID PRIMARY KEY,
title VARCHAR(255) NOT NULL,
deleted_at TIMESTAMP DEFAULT NULL
);
-- Generated view excludes soft-deleted
CREATE VIEW active_documents AS
SELECT * FROM documents WHERE deleted_at IS NULL;
Audit Trail via Trigger
CREATE TABLE audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
table_name VARCHAR(50) NOT NULL,
record_id UUID NOT NULL,
action VARCHAR(10) NOT NULL,
old_data JSONB,
new_data JSONB,
changed_at TIMESTAMP NOT NULL DEFAULT NOW(),
changed_by VARCHAR(255)
);
CREATE OR REPLACE FUNCTION audit_trigger_func()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_log (table_name, record_id, action, old_data, new_data)
VALUES (TG_TABLE_NAME, NEW.id, TG_OP, row_to_json(OLD), row_to_json(NEW));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER users_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();
Linter: ✅ No warnings
Namespace Support
Implementation: Maps to PostgreSQL schema names
-- From: namespace com.acme.data.users
CREATE SCHEMA IF NOT EXISTS com_acme_data_users;
-- Entities created within the schema
CREATE TABLE com_acme_data_users.users (
user_id UUID PRIMARY KEY,
username VARCHAR(255) NOT NULL
);
-- Types created within the schema
CREATE TYPE com_acme_data_users.user_status AS ENUM ('ACTIVE', 'INACTIVE', 'SUSPENDED');
Alternative mapping (when schema isolation not needed):
-- Use namespace as table prefix
CREATE TABLE com_acme_data_users__users (
user_id UUID PRIMARY KEY,
username VARCHAR(255) NOT NULL
);
Generator Note: Configure via namespaceStrategy option:
schema(default): Each namespace becomes a PostgreSQL schemaprefix: Namespace becomes table name prefix (useful for shared hosting)
Repository Pattern
Implementation: Generates Spring Data JPA interfaces or similar ORM repository classes
// From: repository UserRepository for User { ... }
package com.acme.data.users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.Modifying;
import java.util.UUID;
import java.util.Optional;
import java.util.Map;
@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
// Auto-generated CRUD methods from JpaRepository:
// - save(User entity)
// - findById(UUID id)
// - findAll()
// - deleteById(UUID id)
// - count()
// From: @query findByUsername(username: String): User?
Optional<User> findByUsername(String username);
// From: @mutation deactivate(userId: UUID): User
@Modifying
@Query("UPDATE User u SET u.status = 'INACTIVE' WHERE u.userId = :userId")
User deactivate(UUID userId);
// From: @aggregate countByStatus(): Map<UserStatus, Long>
@Query("SELECT u.status as status, COUNT(u) as count FROM User u GROUP BY u.status")
Map<UserStatus, Long> countByStatus();
}
Traits Support:
// From: repository UserRepository for User with @cached, @audited { ... }
@Repository
@Cacheable // Generated from @cached trait
public interface UserRepository extends JpaRepository<User, UUID> {
@Cacheable("users")
Optional<User> findByUsername(String username);
@CacheEvict(value = "users", allEntries = true)
@Audited // Generated from @audited trait - logs all mutations
User save(User user);
}
Linter: ✅ No warnings
Input/Output DTOs
Implementation: Generates Java record classes or POJOs with MapStruct mappers
// From: input CreateUserInput for User { include: [username, email, phone] }
package com.acme.data.users.dto;
import jakarta.validation.constraints.*;
public record CreateUserInput(
@NotBlank @Size(min = 3, max = 50) String username,
@NotBlank @Email String email,
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phone
) {}
// From: output UserSummary from User { include: [userId, username, email, status, createdAt] }
public record UserSummary(
UUID userId,
String username,
String email,
UserStatus status,
Instant createdAt
) {}
// MapStruct mapper for conversion
@Mapper(componentModel = "spring")
public interface UserMapper {
User toEntity(CreateUserInput input);
UserSummary toSummary(User entity);
List<UserSummary> toSummaryList(List<User> entities);
}
With allOptional: true:
// From: input UpdateUserInput for User { include: [email, phone], allOptional: true }
public record UpdateUserInput(
@Email Optional<String> email,
@Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") Optional<String> phone
) {}
// Mapper handles optional fields
@Mapper(componentModel = "spring")
public interface UserMapper {
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntityFromInput(UpdateUserInput input, @MappingTarget User entity);
}
Linter: ✅ No warnings
Mixins
Implementation: JPA @MappedSuperclass or entity listeners for behavioral composition
// From: mixin Auditable { createdAt: Timestamp, createdBy: UUID?, ... }
package com.acme.data.common;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.UUID;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {
@Column(name = "created_at", nullable = false, updatable = false)
@CreatedDate
private Instant createdAt;
@Column(name = "created_by", updatable = false)
private UUID createdBy;
@Column(name = "updated_at", nullable = false)
@LastModifiedDate
private Instant updatedAt;
@Column(name = "updated_by")
private UUID updatedBy;
// Getters and setters...
}
// From: entity User with Auditable, SoftDeletable { ... }
@Entity
@Table(name = "users")
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE user_id = ?")
@Where(clause = "deleted_at IS NULL")
public class User extends Auditable implements SoftDeletable {
@Id
@Column(name = "user_id")
private UUID userId;
@Column(name = "username", nullable = false, unique = true)
private String username;
// Inherited from Auditable:
// - createdAt, createdBy, updatedAt, updatedBy
@Column(name = "deleted_at")
private Instant deletedAt; // From SoftDeletable mixin
// Getters and setters...
}
Common generated mixins:
Auditable→@MappedSuperclasswith@CreatedDate,@LastModifiedDateSoftDeletable→@SQLDeleteannotation +deletedAtfieldVersioned→@Versionfield for optimistic lockingTenantScoped→@TenantIdfield with@Filter
Linter: ✅ No warnings
Invariants (Cross-Field Validation)
Implementation: Bean Validation custom validators + database CHECK constraints
// From: entity DateRange with @invariant(condition: "endDate > startDate", ...)
package com.acme.data.scheduling;
import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
// Custom validation annotation
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateRangeValidator.class)
public @interface ValidDateRange {
String message() default "End date must be after start date";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// Validator implementation
public class DateRangeValidator implements ConstraintValidator<ValidDateRange, DateRange> {
@Override
public boolean isValid(DateRange range, ConstraintValidatorContext context) {
if (range.getStartDate() == null || range.getEndDate() == null) {
return true; // Null checks handled by @NotNull
}
return range.getEndDate().isAfter(range.getStartDate());
}
}
// Entity with invariant
@Entity
@Table(name = "date_ranges")
@ValidDateRange
public class DateRange {
@Id
private UUID id;
@NotNull
@Column(name = "start_date", nullable = false)
private LocalDate startDate;
@NotNull
@Column(name = "end_date", nullable = false)
private LocalDate endDate;
// Getters and setters...
}
Database-level enforcement:
-- Generated migration includes CHECK constraint
CREATE TABLE date_ranges (
id UUID PRIMARY KEY,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
CONSTRAINT valid_date_range CHECK (end_date > start_date)
);
Linter: ✅ No warnings
Fixtures (Test Data)
Implementation: @Sql scripts or test helper classes
// From: fixture UserFixtures for User { activeUser: { ... }, inactiveUser: { ... } }
package com.acme.data.users;
import org.springframework.test.context.jdbc.Sql;
// SQL seed script approach
@Sql(scripts = "/test-data/user-fixtures.sql")
public class UserRepositoryTest {
@Autowired
private UserRepository repository;
@Test
void testFindActiveUser() {
// activeUser fixture is loaded from SQL
User user = repository.findByUsername("alice").orElseThrow();
assertEquals(UserStatus.ACTIVE, user.getStatus());
}
}
Generated SQL fixture (test-data/user-fixtures.sql):
-- From: fixture UserFixtures for User
INSERT INTO users (user_id, username, email, status, created_at, updated_at)
VALUES
('550e8400-e29b-41d4-a716-446655440000', 'alice', 'alice@example.com', 'ACTIVE', NOW(), NOW()),
('550e8400-e29b-41d4-a716-446655440001', 'bob', 'bob@example.com', 'INACTIVE', NOW(), NOW());
Alternative: Test data builder approach:
// Generated test helper class
public class UserFixtures {
public static User activeUser() {
return User.builder()
.userId(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"))
.username("alice")
.email("alice@example.com")
.status(UserStatus.ACTIVE)
.createdAt(Instant.now())
.updatedAt(Instant.now())
.build();
}
public static User inactiveUser() {
return User.builder()
.userId(UUID.fromString("550e8400-e29b-41d4-a716-446655440001"))
.username("bob")
.email("bob@example.com")
.status(UserStatus.INACTIVE)
.createdAt(Instant.now())
.updatedAt(Instant.now())
.build();
}
}
Linter: ✅ No warnings
Documentation Generation
Implementation: Javadoc comments on all generated classes
// From: /// Represents a registered user in the system.
// /// Users are created via the registration API...
// entity User { ... }
/**
* Represents a registered user in the system.
*
* <p>Users are created via the registration API and must have
* a verified email before they can access premium features.</p>
*
* <p><strong>Access Patterns:</strong></p>
* <ul>
* <li>Primary key: userId (UUID)</li>
* <li>Unique index: username</li>
* <li>Unique index: email</li>
* </ul>
*
* @see UserRepository
* @since 1.0.0
*/
@Entity
@Table(name = "users")
public class User extends Auditable {
/**
* System-generated unique identifier. Immutable after creation.
*
* <p>This field serves as the primary key and cannot be changed
* after the user is created.</p>
*/
@Id
@Column(name = "user_id", nullable = false, updatable = false)
private UUID userId;
/**
* Unique username for the user. Must be 3-50 characters.
*
* <p>Usernames are case-sensitive and must be unique across
* the entire system.</p>
*/
@Column(name = "username", nullable = false, unique = true, length = 50)
@Size(min = 3, max = 50)
private String username;
// Additional fields with documentation...
}
Repository documentation:
/**
* Repository interface for {@link User} entity operations.
*
* <p>Provides CRUD operations and custom queries for user management.
* All methods are automatically audited and cached where appropriate.</p>
*
* @see User
* @see UserMapper
*/
@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
/**
* Find a user by their username.
*
* @param username the username to search for (case-sensitive)
* @return an Optional containing the user if found, or empty if not found
*/
Optional<User> findByUsername(String username);
}
Linter: ✅ No warnings
MongoDB Generator
Overview
MongoDB generator produces schema validation rules and generates application-layer shims for features not natively supported.
Core Features
Entities
Implementation: Collection with JSON schema validation
// From: entity User
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["userId", "username", "email"],
properties: {
userId: { bsonType: "string" },
username: { bsonType: "string" },
email: { bsonType: "string" }
}
}
}
});
// Unique indexes
db.users.createIndex({ userId: 1 }, { unique: true });
db.users.createIndex({ username: 1 }, { unique: true });
db.users.createIndex({ email: 1 }, { unique: true });
Enums
Implementation: Schema validation + SDK types
// MongoDB schema validation
db.createCollection("orders", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["orderId", "status"],
properties: {
orderId: { bsonType: "string" },
status: {
enum: ["PENDING", "CONFIRMED", "SHIPPED", "DELIVERED"],
description: "Must be a valid order status"
}
}
}
}
});
// Generated TypeScript SDK
export enum OrderStatus {
PENDING = "PENDING",
CONFIRMED = "CONFIRMED",
SHIPPED = "SHIPPED",
DELIVERED = "DELIVERED"
}
// Runtime validation in SDK
class OrderRepository {
async create(order: CreateOrderInput) {
// Validate enum
if (!Object.values(OrderStatus).includes(order.status)) {
throw new ValidationError(`Invalid status: ${order.status}`);
}
return await db.collection("orders").insertOne(order);
}
}
Linter: ℹ️ Info
[INFO] Enums enforced via schema validation and SDK types.
Direct database writes bypass SDK validation.
Constraints
Check Constraints (Shimmed)
// Schema validation
db.createCollection("products", {
validator: {
$jsonSchema: {
bsonType: "object",
properties: {
price: {
bsonType: "double",
minimum: 0,
description: "Price must be positive"
},
inventory: {
bsonType: "int",
minimum: 0
}
}
}
}
});
// SDK enforces checks
class ProductRepository {
async create(product: CreateProductInput) {
if (product.price <= 0) {
throw new ValidationError("Price must be positive");
}
if (product.inventory < 0) {
throw new ValidationError("Inventory cannot be negative");
}
return await db.collection("products").insertOne(product);
}
}
Linter: ⚠️ Warning
[WARN] Check constraints enforced via schema validation and SDK.
Direct database access may bypass these checks.
Recommendation: Use generated SDK exclusively.
Relationships
One-to-Many (Reference Pattern)
// Authors collection
db.authors.insertOne({
_id: new ObjectId("..."),
authorId: "uuid-123",
name: "Author Name"
});
// Books collection with reference
db.books.insertOne({
_id: new ObjectId("..."),
isbn: "123-456",
title: "Book Title",
authorId: "uuid-123" // Reference to author
});
// SDK provides join helper
class BookRepository {
async findWithAuthor(isbn: string) {
const book = await db.collection("books").findOne({ isbn });
if (!book) return null;
const author = await db.collection("authors")
.findOne({ authorId: book.authorId });
return { ...book, author };
}
}
Cascade Delete (Shimmed)
// Generated cascade delete logic
class UserRepository {
async delete(userId: string) {
const session = client.startSession();
try {
await session.withTransaction(async () => {
// Delete related orders
await db.collection("orders").deleteMany(
{ userId },
{ session }
);
// Delete user
await db.collection("users").deleteOne(
{ userId },
{ session }
);
});
} finally {
await session.endSession();
}
}
}
Linter: ⚠️ Warning
[WARN] Referential integrity enforced in application code.
Direct database access bypasses these checks.
Use transactions to ensure consistency.
Views
Regular Views
Implementation: Aggregation pipeline views
// From: view ActiveUsers
db.createView(
"activeUsers",
"users",
[
{
$match: {
status: "active",
lastLogin: { $gt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }
}
},
{
$project: {
userId: 1,
username: 1,
email: 1,
lastLogin: 1
}
}
]
);
Linter: ⚠️ Warning
[WARN] MongoDB views are read-only aggregation pipelines.
Writes must go to the source collection.
Views are computed on query (not cached).
Materialized Views (Shimmed)
Implementation: Separate collection + scheduled aggregation
// Create collection for materialized data
db.createCollection("orderSummary");
db.orderSummary.createIndex({ customerId: 1 }, { unique: true });
// Generated refresh function
async function refreshOrderSummary() {
const pipeline = [
{
$group: {
_id: "$customerId",
totalOrders: { $sum: 1 },
revenue: { $sum: "$amount" },
avgOrderValue: { $avg: "$amount" }
}
},
{
$project: {
customerId: "$_id",
totalOrders: 1,
revenue: 1,
avgOrderValue: 1
}
},
{ $out: "orderSummary" }
];
await db.collection("orders").aggregate(pipeline).toArray();
console.log("Order summary refreshed");
}
// Scheduler (using node-cron or similar)
import cron from 'node-cron';
cron.schedule('0 * * * *', refreshOrderSummary); // Hourly
// For on_change refresh, use Change Streams
const changeStream = db.collection("orders").watch();
changeStream.on("change", (change) => {
if (["insert", "update", "delete"].includes(change.operationType)) {
refreshOrderSummary(); // Or use debouncing
}
});
Linter: ⚠️ Warning
[WARN] Materialized views require manual refresh orchestration.
Generated code includes:
- Separate collection for materialized data
- Aggregation pipeline for refresh
- Scheduler integration (requires Node.js runtime)
Performance: Full refresh can be expensive for large datasets.
Consider: Incremental updates using Change Streams for real-time needs.
Advanced Features
Full-Text Search
// Create text index
db.articles.createIndex(
{ title: "text", content: "text" },
{
weights: { title: 10, content: 1 },
name: "article_text_search"
}
);
// Generated search method
class ArticleRepository {
async search(query: string, options?: SearchOptions) {
return await db.collection("articles").find(
{ $text: { $search: query } },
{
score: { $meta: "textScore" },
...options
}
).sort({ score: { $meta: "textScore" } }).toArray();
}
}
Linter: ✅ No warnings
Change Data Capture
// From: stream UserChanges
const changeStream = db.collection("users").watch([
{
$match: {
operationType: { $in: ["insert", "update", "delete"] }
}
}
]);
changeStream.on("change", async (change) => {
// Send to external system (Kafka, EventBridge, etc.)
await kafka.send({
topic: "user-events",
messages: [{
key: change.documentKey._id.toString(),
value: JSON.stringify(change)
}]
});
});
Linter: ✅ No warnings
Namespace Support
Implementation: Maps to database name or collection prefix
// From: namespace com.acme.data.users
// Option 1: Separate database per namespace
use com_acme_data_users;
db.createCollection("users", {
validator: { /* ... */ }
});
// Option 2: Collection prefix (single database)
use shared_database;
db.createCollection("com_acme_data_users__users", {
validator: { /* ... */ }
});
Generator Note: Configure via namespaceStrategy option:
database(default): Each namespace becomes a separate MongoDB databaseprefix: Namespace becomes collection name prefix (useful for shared hosting/MongoDB Atlas free tier)
Linter: ℹ️ Info
[INFO] Namespace strategy affects connection strings and access control.
Separate databases provide better isolation but require more connection management.
Repository Pattern
Implementation: Generates TypeScript/JavaScript classes with Mongoose or native driver
// From: repository UserRepository for User { ... }
import { Collection, Db, Filter, UpdateFilter } from 'mongodb';
import { User, UserStatus, CreateUserInput, UserSummary } from './types';
export class UserRepository {
private collection: Collection<User>;
constructor(db: Db) {
this.collection = db.collection<User>('users');
}
// Auto-generated CRUD methods
async create(input: CreateUserInput): Promise<User> {
const user: User = {
userId: crypto.randomUUID(),
...input,
createdAt: new Date(),
updatedAt: new Date()
};
await this.collection.insertOne(user);
return user;
}
async findById(userId: string): Promise<User | null> {
return await this.collection.findOne({ userId });
}
async findAll(): Promise<User[]> {
return await this.collection.find({}).toArray();
}
async deleteById(userId: string): Promise<boolean> {
const result = await this.collection.deleteOne({ userId });
return result.deletedCount > 0;
}
// From: @query findByUsername(username: String): User?
async findByUsername(username: string): Promise<User | null> {
return await this.collection.findOne({ username });
}
// From: @mutation deactivate(userId: UUID): User
async deactivate(userId: string): Promise<User> {
const result = await this.collection.findOneAndUpdate(
{ userId },
{ $set: { status: UserStatus.INACTIVE, updatedAt: new Date() } },
{ returnDocument: 'after' }
);
if (!result.value) {
throw new Error(`User not found: ${userId}`);
}
return result.value;
}
// From: @aggregate countByStatus(): Map<UserStatus, Long>
async countByStatus(): Promise<Map<UserStatus, number>> {
const pipeline = [
{
$group: {
_id: '$status',
count: { $sum: 1 }
}
}
];
const results = await this.collection.aggregate(pipeline).toArray();
return new Map(results.map(r => [r._id as UserStatus, r.count]));
}
}
With Mongoose:
// Alternative: Mongoose-based repository
import mongoose, { Schema, Model } from 'mongoose';
const userSchema = new Schema<User>({
userId: { type: String, required: true, unique: true },
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
status: { type: String, enum: Object.values(UserStatus), required: true }
}, { timestamps: true });
const UserModel: Model<User> = mongoose.model('User', userSchema);
export class UserRepository {
async findByUsername(username: string): Promise<User | null> {
return await UserModel.findOne({ username }).exec();
}
async deactivate(userId: string): Promise<User> {
const user = await UserModel.findOneAndUpdate(
{ userId },
{ status: UserStatus.INACTIVE },
{ new: true }
).exec();
if (!user) {
throw new Error(`User not found: ${userId}`);
}
return user;
}
}
Linter: ℹ️ Info
[INFO] Repository pattern generated as application-layer SDK.
Choose native driver for lightweight apps, Mongoose for complex schemas.
Input/Output DTOs
Implementation: TypeScript interfaces or Zod schemas
// From: input CreateUserInput for User { include: [username, email, phone] }
import { z } from 'zod';
export const CreateUserInputSchema = z.object({
username: z.string().min(3).max(50),
email: z.string().email(),
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/)
});
export type CreateUserInput = z.infer<typeof CreateUserInputSchema>;
// From: output UserSummary from User { include: [userId, username, email, status, createdAt] }
export interface UserSummary {
userId: string;
username: string;
email: string;
status: UserStatus;
createdAt: Date;
}
// Mapper functions
export class UserMapper {
static toSummary(user: User): UserSummary {
return {
userId: user.userId,
username: user.username,
email: user.email,
status: user.status,
createdAt: user.createdAt
};
}
static toSummaryList(users: User[]): UserSummary[] {
return users.map(this.toSummary);
}
}
With allOptional: true:
// From: input UpdateUserInput for User { include: [email, phone], allOptional: true }
export const UpdateUserInputSchema = z.object({
email: z.string().email().optional(),
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/).optional()
});
export type UpdateUserInput = z.infer<typeof UpdateUserInputSchema>;
// Repository method uses partial update
async update(userId: string, input: UpdateUserInput): Promise<User> {
const update: any = {};
if (input.email !== undefined) update.email = input.email;
if (input.phone !== undefined) update.phone = input.phone;
const result = await this.collection.findOneAndUpdate(
{ userId },
{ $set: { ...update, updatedAt: new Date() } },
{ returnDocument: 'after' }
);
if (!result.value) {
throw new Error(`User not found: ${userId}`);
}
return result.value;
}
Linter: ✅ No warnings
Mixins
Implementation: TypeScript mixins or Mongoose plugins for behavioral composition
// From: mixin Auditable { createdAt: Timestamp, createdBy: UUID?, ... }
// TypeScript mixin interface
export interface Auditable {
createdAt: Date;
createdBy?: string;
updatedAt: Date;
updatedBy?: string;
}
// Mongoose plugin approach
import { Schema, Document } from 'mongoose';
export function auditablePlugin(schema: Schema) {
schema.add({
createdAt: { type: Date, required: true, default: Date.now },
createdBy: { type: String },
updatedAt: { type: Date, required: true, default: Date.now },
updatedBy: { type: String }
});
schema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
}
// From: entity User with Auditable, SoftDeletable { ... }
export interface User extends Auditable, SoftDeletable {
userId: string;
username: string;
email: string;
status: UserStatus;
}
const userSchema = new Schema<User>({
userId: { type: String, required: true, unique: true },
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
status: { type: String, enum: Object.values(UserStatus), required: true }
});
// Apply mixin plugins
userSchema.plugin(auditablePlugin);
userSchema.plugin(softDeletablePlugin);
SoftDeletable mixin:
export interface SoftDeletable {
deletedAt?: Date;
}
export function softDeletablePlugin(schema: Schema) {
schema.add({
deletedAt: { type: Date }
});
// Override find queries to exclude soft-deleted
schema.pre(/^find/, function(next) {
if (this.getOptions().includeSoftDeleted !== true) {
this.where({ deletedAt: null });
}
next();
});
// Soft delete method
schema.methods.softDelete = async function() {
this.deletedAt = new Date();
return await this.save();
};
}
Linter: ✅ No warnings
Invariants (Cross-Field Validation)
Implementation: Schema validation + Zod runtime validation
// From: entity DateRange with @invariant(condition: "endDate > startDate", ...)
// Zod schema with custom refinement
export const DateRangeSchema = z.object({
id: z.string().uuid(),
startDate: z.date(),
endDate: z.date()
}).refine(
(data) => data.endDate > data.startDate,
{
message: "End date must be after start date",
path: ["endDate"]
}
);
export type DateRange = z.infer<typeof DateRangeSchema>;
// Repository validates before insertion
export class DateRangeRepository {
async create(input: Omit<DateRange, 'id'>): Promise<DateRange> {
const dateRange: DateRange = {
id: crypto.randomUUID(),
...input
};
// Validate invariant
const validated = DateRangeSchema.parse(dateRange);
await this.collection.insertOne(validated);
return validated;
}
}
MongoDB schema validation:
// Also enforced at database level
db.createCollection("date_ranges", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["id", "startDate", "endDate"],
properties: {
id: { bsonType: "string" },
startDate: { bsonType: "date" },
endDate: { bsonType: "date" }
}
},
$expr: {
$gt: ["$endDate", "$startDate"]
}
}
});
Linter: ⚠️ Warning
[WARN] Invariants enforced via schema validation and SDK.
Direct database writes may bypass validation.
Use generated SDK for all write operations.
Fixtures (Test Data)
Implementation: JSON seed data files or test helper functions
// From: fixture UserFixtures for User { activeUser: { ... }, inactiveUser: { ... } }
// Generated seed data (fixtures/users.json)
export const UserFixtures = {
activeUser: {
userId: "550e8400-e29b-41d4-a716-446655440000",
username: "alice",
email: "alice@example.com",
status: "ACTIVE",
createdAt: new Date("2024-01-15T10:00:00Z"),
updatedAt: new Date("2024-01-15T10:00:00Z")
},
inactiveUser: {
userId: "550e8400-e29b-41d4-a716-446655440001",
username: "bob",
email: "bob@example.com",
status: "INACTIVE",
createdAt: new Date("2024-01-10T08:00:00Z"),
updatedAt: new Date("2024-01-10T08:00:00Z")
}
};
// Test helper to load fixtures
export async function loadUserFixtures(db: Db) {
const collection = db.collection<User>('users');
// Clear existing test data
await collection.deleteMany({ userId: { $in: [
UserFixtures.activeUser.userId,
UserFixtures.inactiveUser.userId
]}});
// Insert fixtures
await collection.insertMany([
UserFixtures.activeUser,
UserFixtures.inactiveUser
]);
}
Usage in tests:
import { MongoClient } from 'mongodb';
import { loadUserFixtures, UserFixtures } from './fixtures';
describe('UserRepository', () => {
let client: MongoClient;
let repository: UserRepository;
beforeEach(async () => {
client = await MongoClient.connect(process.env.MONGO_URL!);
await loadUserFixtures(client.db('test'));
repository = new UserRepository(client.db('test'));
});
test('should find active user', async () => {
const user = await repository.findByUsername('alice');
expect(user).toBeDefined();
expect(user!.status).toBe(UserStatus.ACTIVE);
});
afterEach(async () => {
await client.close();
});
});
Linter: ✅ No warnings
Documentation Generation
Implementation: JSDoc comments on all generated classes
// From: /// Represents a registered user in the system.
// /// Users are created via the registration API...
// entity User { ... }
/**
* Represents a registered user in the system.
*
* Users are created via the registration API and must have
* a verified email before they can access premium features.
*
* **Collection**: `users`
* **Indexes**:
* - Primary key: userId (unique)
* - Unique index: username
* - Unique index: email
*
* @since 1.0.0
*/
export interface User extends Auditable, SoftDeletable {
/**
* System-generated unique identifier. Immutable after creation.
*
* This field serves as the primary key and cannot be changed
* after the user is created.
*/
userId: string;
/**
* Unique username for the user. Must be 3-50 characters.
*
* Usernames are case-sensitive and must be unique across
* the entire system.
*/
username: string;
/**
* User's email address. Must be valid and verified.
*/
email: string;
/**
* Current status of the user account.
*/
status: UserStatus;
}
/**
* Repository interface for User entity operations.
*
* Provides CRUD operations and custom queries for user management.
* All write operations validate data against the schema and business rules.
*
* @see User
* @see CreateUserInput
* @see UserSummary
*/
export class UserRepository {
/**
* Find a user by their username.
*
* @param username - The username to search for (case-sensitive)
* @returns The user if found, or null if not found
*/
async findByUsername(username: string): Promise<User | null> {
return await this.collection.findOne({ username });
}
}
Linter: ✅ No warnings
DynamoDB Generator
Overview
DynamoDB generator produces CloudFormation/Terraform for tables and extensive application shims. Many relational concepts require complex workarounds.
Core Features
Entities
Implementation: DynamoDB table
# CloudFormation
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
Enums (Shimmed)
Implementation: SDK validation only
// No database-level enum support
// Generated TypeScript SDK with validation
export enum OrderStatus {
PENDING = "PENDING",
CONFIRMED = "CONFIRMED",
SHIPPED = "SHIPPED"
}
class OrderRepository {
async create(order: CreateOrderInput) {
// Validate enum before write
if (!Object.values(OrderStatus).includes(order.status)) {
throw new ValidationError(`Invalid status: ${order.status}`);
}
return await dynamodb.put({
TableName: "orders",
Item: marshall(order)
});
}
}
Linter: ⚠️ Warning
[WARN] Enums enforced in SDK only. No database-level validation.
Direct AWS SDK/Console access bypasses validation.
Recommendation: Use generated SDK exclusively.
Constraints
Unique Constraints (Complex)
Implementation: Conditional writes + separate index table
# Main table
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: users
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
# Separate table for username uniqueness
UsernameIndexTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: users_username_index
AttributeDefinitions:
- AttributeName: username
AttributeType: S
KeySchema:
- AttributeName: username
KeyType: HASH
// Generated SDK enforces uniqueness
class UserRepository {
async create(user: CreateUserInput) {
const client = new DynamoDBClient({});
try {
// Atomic: Insert into uniqueness table first
await client.send(new PutItemCommand({
TableName: "users_username_index",
Item: marshall({ username: user.username, userId: user.userId }),
ConditionExpression: "attribute_not_exists(username)"
}));
// Insert main record
await client.send(new PutItemCommand({
TableName: "users",
Item: marshall(user)
}));
return user;
} catch (error) {
if (error.name === "ConditionalCheckFailedException") {
throw new UniqueConstraintViolation("Username already exists");
}
throw error;
}
}
}
Linter: ⚠️ Warning
[WARN] Unique constraints require separate index tables.
Cost: Additional table + write capacity.
Performance: Requires two writes per operation.
Generated: Conditional write logic in SDK.
Relationships
One-to-Many (Reference with GSI)
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: orders
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: orderId
AttributeType: S
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: orderId
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: UserOrdersIndex
KeySchema:
- AttributeName: userId
KeyType: HASH
- AttributeName: orderId
KeyType: RANGE
Projection:
ProjectionType: ALL
// SDK provides query helper
class OrderRepository {
async findByUser(userId: string) {
return await dynamodb.query({
TableName: "orders",
IndexName: "UserOrdersIndex",
KeyConditionExpression: "userId = :userId",
ExpressionAttributeValues: {
":userId": { S: userId }
}
});
}
}
Cascade Delete (Lambda Shim)
# DynamoDB Stream
UsersTable:
Properties:
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
# Lambda function for cascade
CascadeDeleteFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Runtime: nodejs18.x
Code:
ZipFile: |
exports.handler = async (event) => {
for (const record of event.Records) {
if (record.eventName === 'REMOVE') {
const userId = record.dynamodb.Keys.userId.S;
// Delete related orders
await deleteOrdersByUser(userId);
}
}
};
EventSourceMapping:
Type: AWS::Lambda::EventSourceMapping
Properties:
EventSourceArn: !GetAtt UsersTable.StreamArn
FunctionName: !Ref CascadeDeleteFunction
Linter: ⚠️ Warning
[WARN] Cascade deletes require DynamoDB Streams + Lambda.
Generated infrastructure:
- DynamoDB Stream on parent table
- Lambda function for delete logic
- IAM roles and permissions
Cost: Lambda invocations + Stream reads.
Eventual consistency: Deletes are asynchronous.
Views (Not Supported)
Implementation: ❌ Cannot implement
Linter: ❌ Error
[ERROR] Views are not supported for DynamoDB targets.
Alternatives:
1. Create denormalized entity and use Streams to keep updated
2. Query and filter in application code
3. Use separate analytics database (e.g., Athena, Redshift)
Example denormalization pattern:
entity ActiveUserSnapshot {
@identity userId: UUID,
username: String,
email: Email,
@system snapshotTimestamp: Timestamp
}
stream SyncActiveUsers {
source: User,
destination: "dynamodb/table/active_user_snapshot",
filter: "status == 'active'"
}
Materialized Views (Complex Shim)
Implementation: Separate table + DynamoDB Streams + Lambda
# Materialized view table
OrderSummaryTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: order_summary
AttributeDefinitions:
- AttributeName: customerId
AttributeType: S
KeySchema:
- AttributeName: customerId
KeyType: HASH
# Lambda to update summary
UpdateSummaryFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Runtime: nodejs18.x
Code:
ZipFile: |
exports.handler = async (event) => {
for (const record of event.Records) {
if (record.eventName === 'INSERT' || record.eventName === 'MODIFY') {
const order = unmarshall(record.dynamodb.NewImage);
// Atomic counter update
await dynamodb.update({
TableName: 'order_summary',
Key: { customerId: order.customerId },
UpdateExpression:
'ADD totalOrders :one, revenue :amount SET lastUpdated = :now',
ExpressionAttributeValues: {
':one': 1,
':amount': order.amount,
':now': Date.now()
}
});
}
}
};
Linter: ⚠️ Warning
[WARN] Materialized views on DynamoDB require extensive infrastructure:
Generated resources:
- Separate DynamoDB table for aggregated data
- DynamoDB Streams on source table(s)
- Lambda function for aggregation logic
- IAM roles and permissions
- CloudWatch alarms for monitoring
Cost implications:
- Additional table storage
- Stream read capacity
- Lambda invocations (can be high for active tables)
Performance:
- Eventually consistent (stream processing delay)
- Atomic counters prevent race conditions
Alternative: Use DynamoDB + S3 + Athena for complex analytics.
Namespace Support
Implementation: Maps to table name prefix or CloudFormation stack names
# From: namespace com.acme.data.users
# Option 1: Table prefix
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: com-acme-data-users--users
# ... rest of configuration
# Option 2: Separate CloudFormation stack per namespace
# Stack name: com-acme-data-users
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: users
Generator Note: Configure via namespaceStrategy option:
prefix(default): Namespace becomes table name prefixstack: Each namespace deployed as separate CloudFormation stack (better isolation but more complex management)
Linter: ℹ️ Info
[INFO] Namespace strategy affects resource naming and IAM policies.
Table prefixes work for most cases; separate stacks useful for team isolation.
Repository Pattern
Implementation: Generates TypeScript SDK with DynamoDB DocumentClient wrappers
// From: repository UserRepository for User { ... }
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
DynamoDBDocumentClient,
GetCommand,
PutCommand,
UpdateCommand,
DeleteCommand,
QueryCommand,
ScanCommand
} from '@aws-sdk/lib-dynamodb';
import { User, UserStatus, CreateUserInput, UserSummary } from './types';
export class UserRepository {
private readonly docClient: DynamoDBDocumentClient;
private readonly tableName = 'users';
constructor(client: DynamoDBClient) {
this.docClient = DynamoDBDocumentClient.from(client);
}
// Auto-generated CRUD methods
async create(input: CreateUserInput): Promise<User> {
const user: User = {
userId: crypto.randomUUID(),
...input,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
await this.docClient.send(new PutCommand({
TableName: this.tableName,
Item: user,
ConditionExpression: 'attribute_not_exists(userId)'
}));
return user;
}
async findById(userId: string): Promise<User | null> {
const result = await this.docClient.send(new GetCommand({
TableName: this.tableName,
Key: { userId }
}));
return result.Item as User || null;
}
async deleteById(userId: string): Promise<boolean> {
await this.docClient.send(new DeleteCommand({
TableName: this.tableName,
Key: { userId }
}));
return true;
}
// From: @query findByUsername(username: String): User?
// Requires GSI on username
async findByUsername(username: string): Promise<User | null> {
const result = await this.docClient.send(new QueryCommand({
TableName: this.tableName,
IndexName: 'UsernameIndex',
KeyConditionExpression: 'username = :username',
ExpressionAttributeValues: {
':username': username
},
Limit: 1
}));
return result.Items?.[0] as User || null;
}
// From: @mutation deactivate(userId: UUID): User
async deactivate(userId: string): Promise<User> {
const result = await this.docClient.send(new UpdateCommand({
TableName: this.tableName,
Key: { userId },
UpdateExpression: 'SET #status = :inactive, updatedAt = :now',
ExpressionAttributeNames: {
'#status': 'status'
},
ExpressionAttributeValues: {
':inactive': UserStatus.INACTIVE,
':now': new Date().toISOString()
},
ReturnValues: 'ALL_NEW'
}));
if (!result.Attributes) {
throw new Error(`User not found: ${userId}`);
}
return result.Attributes as User;
}
// From: @aggregate countByStatus(): Map<UserStatus, Long>
// Requires full table scan - expensive!
async countByStatus(): Promise<Map<UserStatus, number>> {
const counts = new Map<UserStatus, number>();
let lastKey: any = undefined;
do {
const result = await this.docClient.send(new ScanCommand({
TableName: this.tableName,
ProjectionExpression: '#status',
ExpressionAttributeNames: {
'#status': 'status'
},
ExclusiveStartKey: lastKey
}));
for (const item of result.Items || []) {
const status = item.status as UserStatus;
counts.set(status, (counts.get(status) || 0) + 1);
}
lastKey = result.LastEvaluatedKey;
} while (lastKey);
return counts;
}
}
Required GSI for username query:
GlobalSecondaryIndexes:
- IndexName: UsernameIndex
KeySchema:
- AttributeName: username
KeyType: HASH
Projection:
ProjectionType: ALL
Linter: ⚠️ Warning
[WARN] Custom queries require GSIs or full table scans.
Generated: GSI configurations for all @query methods with non-PK fields.
Cost: Each GSI doubles write costs and adds storage costs.
Aggregations use Scan operations - extremely expensive for large tables!
Recommendation: Use DynamoDB Streams + Lambda + aggregate table.
Input/Output DTOs
Implementation: TypeScript interfaces with Zod validation
// From: input CreateUserInput for User { include: [username, email, phone] }
import { z } from 'zod';
export const CreateUserInputSchema = z.object({
username: z.string().min(3).max(50),
email: z.string().email(),
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/)
});
export type CreateUserInput = z.infer<typeof CreateUserInputSchema>;
// From: output UserSummary from User { include: [userId, username, email, status, createdAt] }
export interface UserSummary {
userId: string;
username: string;
email: string;
status: UserStatus;
createdAt: string; // ISO string
}
// Projection expression helper
export class UserMapper {
static readonly SUMMARY_PROJECTION = 'userId, username, email, #status, createdAt';
static readonly SUMMARY_ATTRIBUTE_NAMES = {
'#status': 'status' // 'status' is a reserved word in DynamoDB
};
static toSummary(user: User): UserSummary {
return {
userId: user.userId,
username: user.username,
email: user.email,
status: user.status,
createdAt: user.createdAt
};
}
}
// Repository uses projection for efficient queries
async findAllSummaries(): Promise<UserSummary[]> {
const result = await this.docClient.send(new ScanCommand({
TableName: this.tableName,
ProjectionExpression: UserMapper.SUMMARY_PROJECTION,
ExpressionAttributeNames: UserMapper.SUMMARY_ATTRIBUTE_NAMES
}));
return (result.Items || []).map(item => item as UserSummary);
}
Linter: ℹ️ Info
[INFO] DTOs generate ProjectionExpression helpers to minimize read costs.
DynamoDB charges by data transferred, so projections reduce costs significantly.
Mixins
Implementation: TypeScript interfaces composed in entity types
// From: mixin Auditable { createdAt: Timestamp, createdBy: UUID?, ... }
export interface Auditable {
createdAt: string; // ISO 8601 timestamp
createdBy?: string;
updatedAt: string;
updatedBy?: string;
}
// From: mixin SoftDeletable { deletedAt: Timestamp? }
export interface SoftDeletable {
deletedAt?: string;
}
// From: entity User with Auditable, SoftDeletable { ... }
export interface User extends Auditable, SoftDeletable {
userId: string;
username: string;
email: string;
status: UserStatus;
}
// Repository helper for soft delete
export class UserRepository {
async softDelete(userId: string): Promise<User> {
const result = await this.docClient.send(new UpdateCommand({
TableName: this.tableName,
Key: { userId },
UpdateExpression: 'SET deletedAt = :now',
ExpressionAttributeValues: {
':now': new Date().toISOString()
},
ReturnValues: 'ALL_NEW'
}));
if (!result.Attributes) {
throw new Error(`User not found: ${userId}`);
}
return result.Attributes as User;
}
// Exclude soft-deleted items from queries
async findAllActive(): Promise<User[]> {
const result = await this.docClient.send(new ScanCommand({
TableName: this.tableName,
FilterExpression: 'attribute_not_exists(deletedAt)'
}));
return result.Items as User[] || [];
}
}
With Auditable auto-update:
// Intercept all updates to set updatedAt
async update(userId: string, updates: Partial<User>): Promise<User> {
const updateExpression: string[] = [];
const attributeValues: any = {};
Object.entries(updates).forEach(([key, value], index) => {
updateExpression.push(`#attr${index} = :val${index}`);
attributeValues[`:val${index}`] = value;
});
// Always update timestamp
updateExpression.push('updatedAt = :now');
attributeValues[':now'] = new Date().toISOString();
const result = await this.docClient.send(new UpdateCommand({
TableName: this.tableName,
Key: { userId },
UpdateExpression: 'SET ' + updateExpression.join(', '),
ExpressionAttributeValues: attributeValues,
ReturnValues: 'ALL_NEW'
}));
return result.Attributes as User;
}
Linter: ✅ No warnings
Invariants (Cross-Field Validation)
Implementation: SDK-level validation only (no database enforcement)
// From: entity DateRange with @invariant(condition: "endDate > startDate", ...)
// Zod schema with custom refinement
export const DateRangeSchema = z.object({
id: z.string().uuid(),
startDate: z.string().datetime(), // ISO 8601
endDate: z.string().datetime()
}).refine(
(data) => new Date(data.endDate) > new Date(data.startDate),
{
message: "End date must be after start date",
path: ["endDate"]
}
);
export type DateRange = z.infer<typeof DateRangeSchema>;
// Repository validates before write
export class DateRangeRepository {
async create(input: Omit<DateRange, 'id'>): Promise<DateRange> {
const dateRange: DateRange = {
id: crypto.randomUUID(),
...input
};
// Validate invariant
const validated = DateRangeSchema.parse(dateRange);
await this.docClient.send(new PutCommand({
TableName: this.tableName,
Item: validated
}));
return validated;
}
}
Linter: ⚠️ Warning
[WARN] DynamoDB has no database-level constraint support.
Invariants enforced in SDK only.
Direct AWS Console/SDK access bypasses ALL validation!
Mitigation strategies:
- IAM policies to restrict direct table access
- DynamoDB Streams + Lambda for validation monitoring
- Regular data validation jobs
Recommendation: Use generated SDK exclusively.
Fixtures (Test Data)
Implementation: JSON seed data with batch write helpers
// From: fixture UserFixtures for User { activeUser: { ... }, inactiveUser: { ... } }
// Generated seed data
export const UserFixtures = {
activeUser: {
userId: "550e8400-e29b-41d4-a716-446655440000",
username: "alice",
email: "alice@example.com",
status: UserStatus.ACTIVE,
createdAt: "2024-01-15T10:00:00.000Z",
updatedAt: "2024-01-15T10:00:00.000Z"
},
inactiveUser: {
userId: "550e8400-e29b-41d4-a716-446655440001",
username: "bob",
email: "bob@example.com",
status: UserStatus.INACTIVE,
createdAt: "2024-01-10T08:00:00.000Z",
updatedAt: "2024-01-10T08:00:00.000Z"
}
};
// Test helper to load fixtures
export async function loadUserFixtures(docClient: DynamoDBDocumentClient) {
const items = Object.values(UserFixtures);
// Batch write (handles chunking for >25 items)
for (let i = 0; i < items.length; i += 25) {
const batch = items.slice(i, i + 25);
await docClient.send(new BatchWriteCommand({
RequestItems: {
'users': batch.map(item => ({
PutRequest: { Item: item }
}))
}
}));
}
}
// Cleanup helper
export async function clearUserFixtures(docClient: DynamoDBDocumentClient) {
const keys = Object.values(UserFixtures).map(user => user.userId);
for (let i = 0; i < keys.length; i += 25) {
const batch = keys.slice(i, i + 25);
await docClient.send(new BatchWriteCommand({
RequestItems: {
'users': batch.map(userId => ({
DeleteRequest: { Key: { userId } }
}))
}
}));
}
}
Usage in tests:
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { loadUserFixtures, clearUserFixtures, UserFixtures } from './fixtures';
describe('UserRepository', () => {
let docClient: DynamoDBDocumentClient;
let repository: UserRepository;
beforeEach(async () => {
const client = new DynamoDBClient({
endpoint: 'http://localhost:8000', // DynamoDB Local
region: 'us-east-1'
});
docClient = DynamoDBDocumentClient.from(client);
await loadUserFixtures(docClient);
repository = new UserRepository(client);
});
test('should find active user', async () => {
const user = await repository.findById(UserFixtures.activeUser.userId);
expect(user).toBeDefined();
expect(user!.status).toBe(UserStatus.ACTIVE);
});
afterEach(async () => {
await clearUserFixtures(docClient);
});
});
Linter: ℹ️ Info
[INFO] Fixtures generate BatchWrite helpers for efficient test data loading.
DynamoDB Local recommended for local testing (https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html).
Documentation Generation
Implementation: TSDoc comments on all generated classes
// From: /// Represents a registered user in the system.
// /// Users are created via the registration API...
// entity User { ... }
/**
* Represents a registered user in the system.
*
* Users are created via the registration API and must have
* a verified email before they can access premium features.
*
* **DynamoDB Table**: `users`
* **Primary Key**: `userId` (HASH)
* **Global Secondary Indexes**:
* - `UsernameIndex`: username (HASH) - for username lookups
* - `EmailIndex`: email (HASH) - for email lookups
*
* **Access Patterns**:
* - Get user by ID: O(1) via primary key
* - Find by username: O(1) via UsernameIndex GSI
* - Find by email: O(1) via EmailIndex GSI
* - List all users: O(n) Scan operation (expensive!)
*
* @since 1.0.0
*/
export interface User extends Auditable, SoftDeletable {
/**
* System-generated unique identifier. Immutable after creation.
*
* **Partition Key**: This field is the hash key for the DynamoDB table.
*/
userId: string;
/**
* Unique username for the user. Must be 3-50 characters.
*
* **GSI**: Indexed via `UsernameIndex` for efficient lookups.
*/
username: string;
/**
* User's email address. Must be valid and verified.
*
* **GSI**: Indexed via `EmailIndex` for efficient lookups.
*/
email: string;
/**
* Current status of the user account.
*
* **Note**: DynamoDB has no enum enforcement - validation in SDK only.
*/
status: UserStatus;
}
/**
* Repository interface for User entity operations.
*
* Provides CRUD operations and custom queries for user management.
* All write operations validate data against business rules.
*
* **Cost Considerations**:
* - Primary key access: 1 RCU per 4KB
* - GSI queries: Same as primary key access
* - Scans: Very expensive, avoid for production queries
*
* **Best Practices**:
* - Use primary key or GSI queries whenever possible
* - Avoid Scan operations on large tables
* - Use ProjectionExpression to minimize data transfer
* - Enable DynamoDB Auto Scaling for production
*
* @see User
* @see CreateUserInput
* @see UserSummary
*/
export class UserRepository {
/**
* Find a user by their username.
*
* Uses the `UsernameIndex` GSI for efficient O(1) lookup.
*
* @param username - The username to search for (case-sensitive)
* @returns The user if found, or null if not found
* @throws {Error} If the query fails
*/
async findByUsername(username: string): Promise<User | null> {
// Implementation...
}
/**
* Count users by status.
*
* **WARNING**: This operation performs a full table Scan and is
* extremely expensive for large tables. Use sparingly or consider
* maintaining counts in a separate aggregate table.
*
* @returns A map of status to count
*/
async countByStatus(): Promise<Map<UserStatus, number>> {
// Implementation...
}
}
Linter: ✅ No warnings
Cassandra Generator
Overview
Cassandra generator targets Apache Cassandra and ScyllaDB. It focuses on partition-based data modeling and has limited support for relational features.
Core Features
Entities with Partition Keys
-- From: entity TimeSeriesData with partition and sort
CREATE TABLE sensor_data (
device_id UUID,
timestamp TIMESTAMP,
value DOUBLE,
PRIMARY KEY ((device_id), timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC);
Materialized Views (Native but Limited)
-- Cassandra has native materialized views
CREATE MATERIALIZED VIEW tasks_by_priority AS
SELECT * FROM tasks
WHERE project_id IS NOT NULL
AND priority IS NOT NULL
AND task_id IS NOT NULL
PRIMARY KEY ((project_id), priority, task_id)
WITH CLUSTERING ORDER BY (priority DESC);
Linter: ⚠️ Warning
[WARN] Cassandra materialized views have restrictions:
- Can only add one non-PK column to the materialized view PK
- All base table PK columns must be in materialized view PK
- Automatic maintenance (cannot control refresh)
- Performance impact on writes
Recommendation: Carefully plan materialized view strategy.
Alternative: Manual denormalization for complex cases.
Enums (Not Supported)
Implementation: Use strings with application validation
CREATE TABLE orders (
order_id UUID PRIMARY KEY,
status TEXT -- Would be enum in Capacitor
);
// SDK enforces enum
export enum OrderStatus {
PENDING = "PENDING",
CONFIRMED = "CONFIRMED"
}
class OrderRepository {
async create(order: CreateOrderInput) {
if (!Object.values(OrderStatus).includes(order.status)) {
throw new ValidationError(`Invalid status`);
}
// Insert with validated string
}
}
Linter: ℹ️ Info
[INFO] Cassandra does not support enums.
Implementation: TEXT column + SDK validation.
Prisma Generator
Overview
Prisma generator produces .prisma schema files that work with Prisma's own code generators. Support varies based on the underlying database.
Example Output
// Generated from Capacitor
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
}
model User {
id String @id @default(uuid())
username String @unique
email String @unique
orders Order[]
createdAt DateTime @default(now())
}
model Order {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
status OrderStatus @default(PENDING)
amount Decimal
createdAt DateTime @default(now())
@@index([userId])
}
Limitations
Views (Not Supported)
Linter: ⚠️ Warning
[WARN] Prisma does not support views in schema.
Workarounds:
1. Use raw SQL queries: prisma.$queryRaw
2. Create separate model and manually sync
3. Use PostgreSQL generator if views are critical
Example raw query:
const activeUsers = await prisma.$queryRaw`
SELECT * FROM active_users_view
`;
Materialized Views (Not Supported)
Linter: ❌ Error
[ERROR] Materialized views not supported in Prisma.
Use PostgreSQL generator for native materialized view support.
TypeORM Generator
Overview
TypeORM generator produces TypeScript entity decorators and works with multiple databases.
Example Output
// Generated from Capacitor
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
ManyToOne,
Index,
CreateDateColumn
} from 'typeorm';
export enum OrderStatus {
PENDING = "PENDING",
CONFIRMED = "CONFIRMED",
SHIPPED = "SHIPPED",
DELIVERED = "DELIVERED"
}
@Entity('users')
@Index(['username'], { unique: true })
@Index(['email'], { unique: true })
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
username: string;
@Column({ unique: true })
email: string;
@OneToMany(() => Order, order => order.user)
orders: Order[];
@CreateDateColumn()
createdAt: Date;
}
@Entity('orders')
@Index(['userId'])
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
userId: string;
@ManyToOne(() => User, user => user.orders, { onDelete: 'CASCADE' })
user: User;
@Column({
type: 'enum',
enum: OrderStatus,
default: OrderStatus.PENDING
})
status: OrderStatus;
@Column('decimal', { precision: 10, scale: 2 })
amount: number;
@CreateDateColumn()
createdAt: Date;
}
Computed Fields
@Entity('orders')
export class Order {
@Column('decimal')
subtotal: number;
@Column('decimal')
taxRate: number;
// Virtual property (not stored)
@Column({ select: false, insert: false, update: false })
get totalWithTax(): number {
return this.subtotal * (1 + this.taxRate);
}
}
Artifact Packaging and Publishing
Capacitor generates language-specific artifacts ready for publishing to package repositories. Each generator produces a complete, distributable package with proper dependency management, versioning, and metadata.
Semantic Versioning
All generated artifacts follow semantic versioning (SemVer):
service UserDataService {
version: "2.1.0",
entities: [User, UserPreferences, Session],
repositories: [UserRepository, SessionRepository]
}
Version format: MAJOR.MINOR.PATCH
- MAJOR: Breaking changes (incompatible API changes)
- MINOR: New features (backward-compatible additions)
- PATCH: Bug fixes (backward-compatible fixes)
Java (Maven/Gradle)
Directory Structure
target/
└── generated/
└── java/
├── pom.xml # Maven build configuration
├── build.gradle # Gradle build configuration (alternative)
└── src/
└── main/
├── java/
│ └── com/
│ └── acme/
│ └── data/
│ └── users/
│ ├── entities/
│ │ ├── User.java
│ │ └── UserPreferences.java
│ ├── repositories/
│ │ ├── UserRepository.java
│ │ └── SessionRepository.java
│ ├── dto/
│ │ ├── CreateUserInput.java
│ │ ├── UpdateUserInput.java
│ │ └── UserSummary.java
│ ├── mappers/
│ │ └── UserMapper.java
│ └── config/
│ └── UserDataConfiguration.java
└── resources/
├── META-INF/
│ └── spring.factories # Spring Boot auto-configuration
└── db/
└── migration/
├── V1__initial_schema.sql
└── V2__add_user_preferences.sql
Maven Configuration (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme.data</groupId>
<artifactId>user-data-service</artifactId>
<version>2.1.0</version>
<packaging>jar</packaging>
<name>User Data Service</name>
<description>Generated data access layer for user management</description>
<properties>
<java.version>17</java.version>
<spring.boot.version>3.2.0</spring.boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
Publishing to Maven Central
# Build artifact
capacitor build --generator java-jpa --package
# Generate POM with signing
mvn clean deploy -P release
# Or using Capacitor CLI
capacitor build --publish --repository maven-central
Generated artifact: user-data-service-2.1.0.jar
TypeScript/JavaScript (npm)
Directory Structure
target/
└── generated/
└── typescript/
├── package.json # npm package configuration
├── tsconfig.json # TypeScript configuration
├── README.md # Generated documentation
├── src/
│ ├── entities/
│ │ ├── User.ts
│ │ └── UserPreferences.ts
│ ├── repositories/
│ │ ├── UserRepository.ts
│ │ └── SessionRepository.ts
│ ├── dto/
│ │ ├── CreateUserInput.ts
│ │ ├── UpdateUserInput.ts
│ │ └── UserSummary.ts
│ ├── mappers/
│ │ └── UserMapper.ts
│ ├── types/
│ │ └── index.ts # Re-exports all types
│ └── index.ts # Main entry point
├── dist/ # Compiled JavaScript (after build)
│ ├── index.js
│ ├── index.d.ts
│ └── ...
└── tests/
└── fixtures/
└── users.fixtures.ts
Package Configuration (package.json)
{
"name": "@acme/user-data-service",
"version": "2.1.0",
"description": "Generated data access layer for user management",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "tsc",
"test": "jest",
"prepublishOnly": "npm run build"
},
"keywords": [
"capacitor",
"data-layer",
"orm",
"user-management"
],
"author": "Platform Team",
"license": "MIT",
"dependencies": {
"mongodb": "^6.3.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.3",
"jest": "^29.7.0"
},
"engines": {
"node": ">=18.0.0"
}
}
Publishing to npm
# Build artifact
capacitor build --generator typescript-sdk --package
# Publish to npm
npm publish --access public
# Or using Capacitor CLI
capacitor build --publish --repository npm
Generated artifact: @acme/user-data-service-2.1.0.tgz
Python (PyPI)
Directory Structure
target/
└── generated/
└── python/
├── setup.py # Package setup
├── pyproject.toml # Modern Python packaging
├── README.md
├── MANIFEST.in
├── acme_user_data/
│ ├── __init__.py
│ ├── entities/
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── user_preferences.py
│ ├── repositories/
│ │ ├── __init__.py
│ │ ├── user_repository.py
│ │ └── session_repository.py
│ ├── dto/
│ │ ├── __init__.py
│ │ ├── create_user_input.py
│ │ └── user_summary.py
│ └── mappers/
│ ├── __init__.py
│ └── user_mapper.py
└── tests/
└── fixtures/
└── users.py
Package Configuration (pyproject.toml)
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "acme-user-data-service"
version = "2.1.0"
description = "Generated data access layer for user management"
readme = "README.md"
authors = [
{ name = "Platform Team", email = "platform@acme.com" }
]
keywords = ["capacitor", "data-layer", "orm", "user-management"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
requires-python = ">=3.10"
dependencies = [
"sqlalchemy>=2.0.0",
"pydantic>=2.5.0",
"alembic>=1.13.0"
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"black>=23.12.0",
"mypy>=1.7.0"
]
[project.urls]
Homepage = "https://github.com/acme/user-data-service"
Documentation = "https://acme.github.io/user-data-service"
Publishing to PyPI
# Build artifact
capacitor build --generator python-sqlalchemy --package
# Build distribution
python -m build
# Publish to PyPI
twine upload dist/*
# Or using Capacitor CLI
capacitor build --publish --repository pypi
Generated artifacts:
acme_user_data_service-2.1.0-py3-none-any.whlacme_user_data_service-2.1.0.tar.gz
Go (Go Modules)
Directory Structure
target/
└── generated/
└── go/
├── go.mod # Go module definition
├── go.sum # Dependency checksums
├── README.md
├── entities/
│ ├── user.go
│ └── user_preferences.go
├── repositories/
│ ├── user_repository.go
│ └── session_repository.go
├── dto/
│ ├── create_user_input.go
│ └── user_summary.go
└── fixtures/
└── users.go
Module Configuration (go.mod)
module github.com/acme/user-data-service
go 1.21
require (
github.com/google/uuid v1.5.0
gorm.io/gorm v1.25.5
gorm.io/driver/postgres v1.5.4
)
require (
github.com/jackc/pgx/v5 v5.5.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
)
Publishing as Go Module
# Build artifact
capacitor build --generator go-gorm --package
# Tag version
git tag v2.1.0
git push origin v2.1.0
# Module is now available via:
# go get github.com/acme/user-data-service@v2.1.0
Module import: import "github.com/acme/user-data-service/repositories"
C# (NuGet)
Directory Structure
target/
└── generated/
└── csharp/
├── Acme.UserDataService.csproj # Project file
├── Acme.UserDataService.nuspec # NuGet package spec
├── README.md
├── Entities/
│ ├── User.cs
│ └── UserPreferences.cs
├── Repositories/
│ ├── IUserRepository.cs
│ ├── UserRepository.cs
│ └── SessionRepository.cs
├── Dto/
│ ├── CreateUserInput.cs
│ └── UserSummary.cs
└── Mappers/
└── UserMapper.cs
Project File (.csproj)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2.1.0</Version>
<PackageId>Acme.UserDataService</PackageId>
<Authors>Platform Team</Authors>
<Description>Generated data access layer for user management</Description>
<PackageTags>capacitor;data-layer;orm;user-management</PackageTags>
<RepositoryUrl>https://github.com/acme/user-data-service</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="AutoMapper" Version="12.0.1" />
</ItemGroup>
</Project>
Publishing to NuGet
# Build artifact
capacitor build --generator csharp-ef --package
# Pack NuGet package
dotnet pack --configuration Release
# Publish to NuGet.org
dotnet nuget push bin/Release/Acme.UserDataService.2.1.0.nupkg \
--api-key YOUR_API_KEY \
--source https://api.nuget.org/v3/index.json
# Or using Capacitor CLI
capacitor build --publish --repository nuget
Generated artifact: Acme.UserDataService.2.1.0.nupkg
Dependency Management
Generated artifacts declare their dependencies based on the generator and target database.
Example: Java JPA + PostgreSQL
Dependencies automatically included:
<dependencies>
<!-- ORM Framework -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Database Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Validation -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
Example: TypeScript + MongoDB
Dependencies automatically included:
{
"dependencies": {
"mongodb": "^6.3.0", // Database driver
"zod": "^3.22.4" // Runtime validation
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.3"
}
}
Build Configuration
Configure artifact generation in capacitor-build.json:
{
"metadata": {
"name": "UserDataService",
"version": "2.1.0",
"organization": "com.acme",
"author": "Platform Team",
"license": "MIT"
},
"generators": [
{
"id": "java-jpa",
"enabled": true,
"output": "./target/generated/java",
"options": {
"package": "com.acme.data.users",
"springBoot": {
"enabled": true,
"version": "3.2"
}
}
}
],
"publishing": {
"enabled": true,
"repositories": [
{
"type": "maven-central",
"credentials": "${env:MAVEN_CENTRAL_TOKEN}"
}
],
"releaseNotes": "auto" // Extract from git commits
}
}
Publishing Commands
# Package artifacts (no publish)
capacitor build --package
# Package and publish to all configured repositories
capacitor build --publish
# Publish to specific repository
capacitor build --publish --repository maven-central
capacitor build --publish --repository npm
# Dry run (verify without publishing)
capacitor build --publish --dry-run
# Publish with custom version (overrides capacitor-build.json)
capacitor build --publish --version 2.2.0-beta.1
Output:
Building artifacts...
✅ Java (Maven)
- Artifact: user-data-service-2.1.0.jar
- Location: target/generated/java/target/
✅ TypeScript (npm)
- Artifact: @acme/user-data-service-2.1.0.tgz
- Location: target/generated/typescript/
Publishing artifacts...
✅ Published to Maven Central
- Group: com.acme.data
- Artifact: user-data-service
- Version: 2.1.0
- URL: https://central.sonatype.com/artifact/com.acme.data/user-data-service/2.1.0
✅ Published to npm
- Package: @acme/user-data-service
- Version: 2.1.0
- URL: https://www.npmjs.com/package/@acme/user-data-service/v/2.1.0
Complete! Artifacts published successfully.
Migration Between Targets
Capacitor can analyze migration paths between databases.
Example: PostgreSQL → MongoDB
capacitor migrate-analysis --from postgres --to mongodb
# Output:
Migration Analysis: PostgreSQL → MongoDB
✓ Compatible (no changes needed):
- Entities: User, Order, Product (3)
- Basic relationships (3)
- Enums (2) - will use validation
⚠ Requires Shims:
- Materialized view: OrderSummary
Implementation: Collection + scheduled aggregation
Performance: Full refresh every hour
- Foreign key cascades: User → Orders
Implementation: Application-level transaction
Requires: MongoDB sessions
- Check constraints: Product.price > 0
Implementation: Schema validation + SDK
❌ Manual Changes Required:
- View: ActiveUsers
Action: Convert to denormalized collection or query in app
- Full-text search on Article.content
Action: Create text index (supported)
Estimated migration complexity: Medium
Recommended approach: Blue-green deployment with dual-write period
Example: DynamoDB → PostgreSQL
capacitor migrate-analysis --from dynamodb --to postgres
# Output:
Migration Analysis: DynamoDB → PostgreSQL
✓ Improvements (simpler in target):
- Unique constraints: username, email
Current: Separate index tables
Target: Native UNIQUE constraint
Benefit: Reduced complexity and cost
- Referential integrity: User → Orders
Current: Application-level with Streams + Lambda
Target: Native FOREIGN KEY
Benefit: Database-enforced, no Lambda costs
- Materialized view: OrderSummary
Current: Separate table + Stream + Lambda
Target: Native MATERIALIZED VIEW
Benefit: Simpler, no Lambda infrastructure
⚠ Data modeling changes:
- Denormalized data in DynamoDB may need normalization
- Review GSI usage - may become standard indexes
Estimated migration complexity: Low-Medium
Recommended approach: Export to S3, transform, import via COPY
Performance Considerations
PostgreSQL
- Best for: ACID transactions, complex queries, referential integrity
- Watch out for: Index bloat, vacuum performance on large tables
MongoDB
- Best for: Flexible schemas, horizontal scaling, document-oriented data
- Watch out for: Join performance (use denormalization), lack of transactions across collections in older versions
DynamoDB
- Best for: Massive scale, predictable performance, serverless workloads
- Watch out for: Cost at scale, complex query patterns, eventual consistency
Cassandra
- Best for: Time-series data, write-heavy workloads, multi-datacenter
- Watch out for: Limited query flexibility, no joins, eventual consistency