Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Enterprise-Scale Metadata Organization

English Version | 中文版本

This example demonstrates best practices for organizing metadata in large-scale ObjectQL projects, suitable for enterprise applications with hundreds of objects and complex business domains.

📋 Problem Statement

When building large applications, poor metadata organization leads to:

  • Difficulty finding objects - scattered files with no clear structure
  • Merge conflicts - multiple teams editing the same files
  • Unclear ownership - no way to know which team owns which domain
  • Deployment risks - can't deploy modules independently
  • Maintenance burden - hard to understand relationships between objects

🎯 Solution: Domain-Driven Structure

This example shows a modular, domain-based organization pattern that scales to enterprise needs.

src/
├── core/                      # Shared/Foundation Layer
│   ├── objects/              # Base objects used across domains
│   │   ├── user.object.yml
│   │   ├── organization.object.yml
│   │   └── attachment.object.yml
│   ├── i18n/
│   │   ├── en/
│   │   └── zh-CN/
│   └── index.ts
│
├── modules/                   # Business Domain Modules
│   ├── crm/                  # Customer Relationship Module
│   │   ├── objects/
│   │   │   ├── account.object.yml
│   │   │   ├── contact.object.yml
│   │   │   ├── opportunity.object.yml
│   │   │   └── lead.object.yml
│   │   ├── actions/
│   │   │   └── convert-lead.action.ts
│   │   ├── hooks/
│   │   │   └── opportunity.hooks.ts
│   │   ├── i18n/
│   │   │   ├── en/
│   │   │   └── zh-CN/
│   │   ├── README.md
│   │   └── index.ts
│   │
│   ├── hr/                   # Human Resources Module
│   │   ├── objects/
│   │   │   ├── employee.object.yml
│   │   │   ├── department.object.yml
│   │   │   ├── position.object.yml
│   │   │   └── timesheet.object.yml
│   │   ├── actions/
│   │   ├── hooks/
│   │   ├── i18n/
│   │   ├── README.md
│   │   └── index.ts
│   │
│   ├── finance/              # Finance & Accounting Module
│   │   ├── objects/
│   │   │   ├── invoice.object.yml
│   │   │   ├── payment.object.yml
│   │   │   ├── expense.object.yml
│   │   │   └── budget.object.yml
│   │   ├── actions/
│   │   ├── hooks/
│   │   ├── i18n/
│   │   ├── README.md
│   │   └── index.ts
│   │
│   └── project/              # Project Management Module
│       ├── objects/
│       │   ├── project.object.yml
│       │   ├── task.object.yml
│       │   ├── milestone.object.yml
│       │   └── timesheet-entry.object.yml
│       ├── actions/
│       ├── hooks/
│       ├── i18n/
│       ├── README.md
│       └── index.ts
│
├── extensions/               # Custom Extensions/Overrides
│   ├── user.extension.object.yml
│   └── README.md
│
├── shared/                   # Shared Utilities
│   ├── constants.ts
│   ├── validators.ts
│   └── utils.ts
│
└── index.ts                  # Application Entry Point

🏗️ Architecture Principles

1. Separation of Concerns

Each module is self-contained with its own:

  • Object definitions (.object.yml)
  • Business logic (actions & hooks)
  • Translations (i18n)
  • Documentation

2. Clear Dependencies

Application Layer (modules/*)
        ↓
  Foundation Layer (core/*)
        ↓
   External Plugins

3. Team Ownership

Each module can be owned by a different team:

  • modules/crm/ → Sales Team
  • modules/hr/ → HR Team
  • modules/finance/ → Finance Team

4. Independent Deployment

Modules can be:

  • Developed in parallel
  • Tested independently
  • Deployed as feature flags
  • Extracted to separate packages

📦 Module Structure

Each module follows this pattern:

modules/[domain]/
├── objects/              # Domain object definitions
├── actions/              # Custom actions (*.action.ts)
├── hooks/                # Lifecycle hooks (*.hooks.ts)
├── i18n/                 # Module-specific translations
│   ├── en/
│   └── zh-CN/
├── README.md             # Module documentation
└── index.ts              # Module exports

🔗 Object Naming Conventions

Prefixing Strategy

For large projects, consider prefixing object names:

# ❌ Bad: Name collision risk
name: task

# ✅ Good: Clear module ownership
name: project_task

When to prefix:

  • ✅ When multiple modules might have similar concepts
  • ✅ In multi-tenant or plugin architectures
  • ❌ When it's clearly a core shared object (e.g., user, organization)

File Naming

[object_name].object.yml     # Object definition
[object_name].action.ts      # Actions for this object
[object_name].hooks.ts       # Hooks for this object
[object_name].data.yml       # Seed data (optional)

🌐 Internationalization at Scale

Three-Layer Strategy

  1. Core Layer (core/i18n/)

    • Shared objects (user, organization)
    • Common UI labels
  2. Module Layer (modules/[domain]/i18n/)

    • Domain-specific objects
    • Business terminology
  3. Extension Layer (extensions/i18n/)

    • Customer-specific customizations
    • Regional variants

Example Structure

core/i18n/
  en/
    user.json
    organization.json
  zh-CN/
    user.json
    organization.json

modules/crm/i18n/
  en/
    account.json
    opportunity.json
  zh-CN/
    account.json
    opportunity.json

🔐 Index & Performance Strategy

Field-Level Indexes (Simple)

For single-column lookups:

fields:
  email:
    type: text
    unique: true    # Creates unique index
  status:
    type: select
    index: true     # Creates regular index

Composite Indexes (Advanced)

Define at object root for multi-column queries:

indexes:
  # For query: WHERE status = 'open' ORDER BY created_at DESC
  status_created_idx:
    fields: [status, created_at]
  
  # For unique constraint: UNIQUE(company_id, email)
  company_email_unique:
    fields: [company_id, email]
    unique: true

Index Strategy by Module

High-Traffic Modules (CRM, Finance):

  • Add indexes on every filter field
  • Use composite indexes for common query patterns
  • Monitor query performance regularly

Low-Traffic Modules (HR, Admin):

  • Basic indexes on primary lookup fields
  • Add more as needed based on usage

🧩 Extension Pattern

Use extensions to customize objects without modifying core definitions:

Core Definition (core/objects/user.object.yml):

name: user
fields:
  name: { type: text }
  email: { type: text }

Extension (extensions/user.extension.object.yml):

name: user  # Same name triggers merge
fields:
  # Add custom field
  employee_id:
    type: text
    label: Employee ID
  
  # Override existing field
  email:
    required: true
    unique: true

🧪 Testing Strategy

Unit Tests

Test individual object schemas:

// modules/crm/objects/__tests__/account.test.ts
describe('Account Object', () => {
  it('should have required fields', () => {
    const account = loadObject('account');
    expect(account.fields.name.required).toBe(true);
  });
});

Integration Tests

Test module interactions:

// modules/crm/__tests__/integration.test.ts
describe('CRM Module', () => {
  it('should convert lead to opportunity', async () => {
    // Test cross-object logic
  });
});

📊 Real-World Size Reference

Project Size Objects Modules Teams Structure
Small (Startup) 10-30 1-2 1 Flat /objects/
Medium (Scale-up) 30-100 3-5 2-3 /modules/ by domain
Large (Enterprise) 100-500 8-15 5-10 /modules/ + /plugins/
Very Large (Platform) 500+ 15+ 10+ Monorepo with packages

🚀 Migration Path

From Flat to Modular

  1. Analyze - Group objects by business domain
  2. Create - Create module directories
  3. Move - Relocate objects to appropriate modules
  4. Test - Verify imports and references still work
  5. Document - Update README files

Gradual Approach

You don't have to reorganize everything at once:

src/
├── objects/          # Legacy flat structure (deprecated)
├── modules/          # New modular structure
│   └── crm/          # Start with one module
└── index.ts          # Loads from both

💡 Pro Tips

  1. Start Simple - Don't over-engineer for 10 objects. Use modules when you hit 30-50 objects.

  2. Document Boundaries - Each module README should explain:

    • What business domain it covers
    • Key objects and relationships
    • Team ownership
    • Dependencies on other modules
  3. Avoid Circular Dependencies - Use shared objects in core/ to break cycles.

  4. Version Control - Use .gitignore to exclude generated files:

    dist/
    *.generated.ts
    node_modules/
    
  5. Code Generation - Run objectql generate to create TypeScript types for each module separately.

📚 See Also

🤝 Contributing

This is a living example. If you have suggestions for enterprise-scale patterns, please open an issue or PR!