Hybrid Database Implementation Plan

Implementation plan for space-per-database + universe-per-collection architecture

🎯 Hybrid Database Implementation Plan

Architecture: ONE Database per Space + Collections per Universe
Status: ✅ APPROVED
Ready to Implement: YES


🏗️ Implementation Phases

Phase 1: Storage System Enhancement (4 hours)

1.1 Add Space/Universe Scoping to GgStorage

File: libs/storage-system/src/lib/services/gg-storage.service.ts

New Methods:

// Space-scoped (shared assets)
async putInSpace(spaceId: string, key: string, value: any)
async getFromSpace(spaceId: string, key: string)
async listInSpace(spaceId: string, prefix?: string)

// Universe-scoped (isolated data)
async putInUniverse(spaceId: string, universeId: string, key: string, value: any)
async getFromUniverse(spaceId: string, universeId: string, key: string)
async listInUniverse(spaceId: string, universeId: string, prefix?: string)

// Cross-universe queries
async queryUniverseData(spaceId, universeId, collection, filter)
async queryAcrossUniverses(spaceId, collection, filter)

1.2 Update Backend Space Service

File: apis/backend-api/src/app/spaces/spaces.service.ts

Enhancement:

async createSpace(spaceData) {
  // 1. Create space record
  const space = await this.spacesModel.create(spaceData);
  
  // 2. Create MongoDB database: space_{spaceId}
  const dbName = `space_${space.id}`;
  await this.createSpaceDatabase(dbName);
  
  // 3. Initialize collections
  await this.initializeSpaceCollections(dbName);
  
  return space;
}

private async createSpaceDatabase(dbName: string) {
  // Create MongoDB database with collections
  const collections = [
    'space_metadata',
    'team_members',
    'billing',
    'space_templates',
    'space_flows',
    'space_templates',
    'audit_logs'
  ];
  
  for (const collection of collections) {
    await this.mongoClient.db(dbName).createCollection(collection);
  }
}

1.3 Update Backend Universe Service

File: apis/backend-api/src/app/universes/universes.service.ts (or create new service)

Enhancement:

async createUniverse(spaceId, universeData) {
  // 1. Create universe record
  const universe = await this.universesModel.create({
    ...universeData,
    spaceId
  });
  
  // 2. Create universe collections in space database
  const dbName = `space_${spaceId}`;
  await this.initializeUniverseCollections(dbName, universe.id);
  
  return universe;
}

private async initializeUniverseCollections(dbName: string, universeId: string) {
  const prefix = `universe_${universeId}_`;
  const collections = [
    `${prefix}metadata`,
    `${prefix}pages`,
    `${prefix}widgets`,
    `${prefix}flows`,
    `${prefix}modules`,
    `${prefix}deployments`
  ];
  
  for (const collection of collections) {
    await this.mongoClient.db(dbName).createCollection(collection);
    // Create indexes for performance
    await this.mongoClient.db(dbName).collection(collection)
      .createIndex({ createdAt: 1 });
  }
}

Deliverable: Storage system supports hybrid model


Phase 2: Space Management App (6 hours)

2.1 Create NX App

npx nx g @nx/angular:app space-management \
  --routing=true \
  --style=scss \
  --standalone=true

2.2 App Structure

apps/space-management/
├── src/
│   ├── app/
│   │   ├── layout/
│   │   │   ├── main-layout.component.ts
│   │   │   └── universe-switcher.component.ts
│   │   │
│   │   ├── dashboard/
│   │   │   ├── universes-dashboard.component.ts
│   │   │   ├── universe-card.component.ts
│   │   │   └── quick-stats.component.ts
│   │   │
│   │   ├── data-browser/
│   │   │   ├── data-browser.component.ts
│   │   │   ├── collections-list.component.ts
│   │   │   ├── data-table.component.ts
│   │   │   ├── record-editor.component.ts
│   │   │   └── query-builder.component.ts
│   │   │
│   │   ├── deployment-manager/
│   │   │   ├── deployment-history.component.ts
│   │   │   └── deployment-details.component.ts
│   │   │
│   │   └── analytics/
│   │       ├── analytics-dashboard.component.ts
│   │       └── charts/
│   │
│   └── ...
└── ...

2.3 Core Services

Files to Create:

apps/space-management/src/app/services/
├── space-data.service.ts       # Query space database
├── universe-data.service.ts    # Query universe collections
├── data-export.service.ts      # Export functionality
└── analytics.service.ts        # Cross-universe analytics

Deliverable: Space management app operational


Phase 3: Data Browser Component (4 hours)

3.1 Create Library

npx nx g @nx/angular:library data-browser \
  --directory=libs/data-browser \
  --standalone=true

3.2 Components

libs/data-browser/src/lib/components/
├── data-browser-dashboard/
│   └── data-browser-dashboard.component.ts
│
├── collections-list/
│   └── collections-list.component.ts      # List all collections with counts
│
├── data-table/
│   ├── data-table.component.ts            # Paginated table view
│   ├── data-table-cell.component.ts       # Cell renderer
│   └── data-table-actions.component.ts    # Row actions
│
├── record-editor/
│   ├── record-editor.component.ts         # Edit single record
│   └── field-editor.component.ts          # Type-specific editors
│
├── query-builder/
│   ├── query-builder.component.ts         # Visual MongoDB queries
│   ├── filter-builder.component.ts        # Add filters visually
│   └── query-preview.component.ts         # Show query JSON
│
└── data-export/
    └── data-export.component.ts           # Export to CSV/JSON/Excel

3.3 Key Features

DataBrowserDashboard:
✅ Show all collections in universe
✅ Display record counts
✅ Quick search across collections
✅ Recent queries

DataTable:
✅ Paginated view (50 records per page)
✅ Sortable columns
✅ Inline editing
✅ Bulk actions (delete, export)
✅ Real-time updates

RecordEditor:
✅ Form-based editing
✅ Type-specific inputs (date picker, etc.)
✅ Validation
✅ Save/cancel
✅ Delete confirmation

QueryBuilder:
✅ Visual filter builder
✅ AND/OR logic
✅ Comparison operators (=, !=, >, <, contains, etc.)
✅ Date range selectors
✅ Query preview (MongoDB syntax)
✅ Save queries for reuse

Deliverable: Complete data management UI


Phase 4: Database Management Blocks (3 hours)

4.1 New Practical Blocks

File: libs/singularity-lib/src/lib/templates/database-management-blocks.ts

export const QUERY_COLLECTION_TEMPLATE: BlockTemplate = {
  id: 'query_collection',
  type: 'function',
  name: 'Query Collection',
  title: 'Query Database',
  description: 'Query records from a collection with filters',
  category: 'database',
  
  inputTemplates: [
    { name: 'collection', type: 'input', dataType: 'string', required: true, description: 'Collection name' },
    { name: 'filter', type: 'input', dataType: 'object', required: false, description: 'MongoDB filter' },
    { name: 'limit', type: 'input', dataType: 'number', required: false, description: 'Max results' }
  ],
  outputTemplates: [
    { name: 'records', type: 'output', dataType: 'array', required: true, description: 'Query results' },
    { name: 'count', type: 'output', dataType: 'number', required: false, description: 'Total found' }
  ],
  
  defaultColor: '#8b5cf6',
  icon: 'database',
  defaultSize: { width: 220, height: 120 },
  
  executionFunction: 'executeQueryCollection',
  version: '1.0.0',
  tags: ['database', 'query', 'mongodb'],
  popularity: 90,
  featured: true
};

export const INSERT_RECORD_TEMPLATE: BlockTemplate = {
  id: 'insert_record',
  type: 'action',
  name: 'Insert Record',
  title: 'Create Record',
  description: 'Insert new record into collection',
  category: 'database',
  
  inputTemplates: [
    { name: 'collection', type: 'input', dataType: 'string', required: true, description: 'Collection name' },
    { name: 'data', type: 'input', dataType: 'object', required: true, description: 'Record data' }
  ],
  outputTemplates: [
    { name: 'id', type: 'output', dataType: 'string', required: true, description: 'Inserted record ID' },
    { name: 'record', type: 'output', dataType: 'object', required: false, description: 'Complete record' }
  ],
  
  defaultColor: '#10b981',
  icon: 'plus-circle',
  defaultSize: { width: 220, height: 120 },
  
  executionFunction: 'executeInsertRecord',
  version: '1.0.0',
  tags: ['database', 'create', 'insert'],
  popularity: 95,
  featured: true
};

export const UPDATE_RECORD_TEMPLATE: BlockTemplate = {
  id: 'update_record',
  type: 'action',
  name: 'Update Record',
  title: 'Update Record',
  description: 'Update existing record in collection',
  category: 'database',
  
  inputTemplates: [
    { name: 'collection', type: 'input', dataType: 'string', required: true, description: 'Collection name' },
    { name: 'id', type: 'input', dataType: 'string', required: true, description: 'Record ID' },
    { name: 'data', type: 'input', dataType: 'object', required: true, description: 'Updated data' }
  ],
  outputTemplates: [
    { name: 'success', type: 'output', dataType: 'boolean', required: true, description: 'Update success' },
    { name: 'record', type: 'output', dataType: 'object', required: false, description: 'Updated record' }
  ],
  
  defaultColor: '#f59e0b',
  icon: 'edit',
  defaultSize: { width: 220, height: 120 },
  
  executionFunction: 'executeUpdateRecord',
  version: '1.0.0',
  tags: ['database', 'update', 'edit'],
  popularity: 90,
  featured: true
};

export const DELETE_RECORD_TEMPLATE: BlockTemplate = {
  id: 'delete_record',
  type: 'action',
  name: 'Delete Record',
  title: 'Delete Record',
  description: 'Remove record from collection',
  category: 'database',
  
  inputTemplates: [
    { name: 'collection', type: 'input', dataType: 'string', required: true, description: 'Collection name' },
    { name: 'id', type: 'input', dataType: 'string', required: true, description: 'Record ID' }
  ],
  outputTemplates: [
    { name: 'success', type: 'output', dataType: 'boolean', required: true, description: 'Delete success' }
  ],
  
  defaultColor: '#ef4444',
  icon: 'trash',
  defaultSize: { width: 220, height: 120 },
  
  executionFunction: 'executeDeleteRecord',
  version: '1.0.0',
  tags: ['database', 'delete', 'remove'],
  popularity: 75,
  featured: false
};

export const AGGREGATE_DATA_TEMPLATE: BlockTemplate = {
  id: 'aggregate_data',
  type: 'function',
  name: 'Aggregate Data',
  title: 'Aggregate',
  description: 'Run aggregation pipeline on collection',
  category: 'database',
  
  inputTemplates: [
    { name: 'collection', type: 'input', dataType: 'string', required: true, description: 'Collection name' },
    { name: 'pipeline', type: 'input', dataType: 'array', required: true, description: 'Aggregation pipeline' }
  ],
  outputTemplates: [
    { name: 'results', type: 'output', dataType: 'array', required: true, description: 'Aggregated results' }
  ],
  
  defaultColor: '#8b5cf6',
  icon: 'chart-bar',
  defaultSize: { width: 220, height: 120 },
  
  executionFunction: 'executeAggregateData',
  version: '1.0.0',
  tags: ['database', 'aggregate', 'analytics'],
  popularity: 80,
  featured: false
};

Backend Endpoints:

POST /api/spaces/:spaceId/universes/:universeId/data/query
POST /api/spaces/:spaceId/universes/:universeId/data/insert
PUT /api/spaces/:spaceId/universes/:universeId/data/update
DELETE /api/spaces/:spaceId/universes/:universeId/data/delete
POST /api/spaces/:spaceId/universes/:universeId/data/aggregate
POST /api/spaces/:spaceId/universes/:universeId/data/export

// Cross-universe analytics
POST /api/spaces/:spaceId/analytics/query

Phase 2: Backend Space Database Service (3 hours)

Create: apis/backend-api/src/app/spaces/space-database.service.ts

@Injectable()
export class SpaceDatabaseService {
  constructor(
    @InjectConnection() private connection: Connection
  ) {}

  async createSpaceDatabase(spaceId: string): Promise<void> {
    const dbName = `space_${spaceId}`;
    
    // MongoDB automatically creates database on first write
    // Create initial collections
    const collections = [
      'space_metadata',
      'team_members',
      'billing',
      'space_templates',
      'space_flows',
      'space_templates',
      'audit_logs'
    ];

    for (const collectionName of collections) {
      await this.connection.db(dbName).createCollection(collectionName);
      
      // Create indexes
      await this.connection.db(dbName)
        .collection(collectionName)
        .createIndex({ createdAt: 1 });
      
      await this.connection.db(dbName)
        .collection(collectionName)
        .createIndex({ updatedAt: 1 });
    }

    // Initialize space metadata
    await this.connection.db(dbName)
      .collection('space_metadata')
      .insertOne({
        _id: spaceId,
        created: new Date(),
        status: 'active',
        universes: []
      });
  }

  async createUniverseCollections(spaceId: string, universeId: string): Promise<void> {
    const dbName = `space_${spaceId}`;
    const prefix = `universe_${universeId}_`;
    
    const collections = [
      `${prefix}metadata`,
      `${prefix}pages`,
      `${prefix}widgets`,
      `${prefix}flows`,
      `${prefix}modules`,
      `${prefix}deployments`
    ];

    for (const collectionName of collections) {
      await this.connection.db(dbName).createCollection(collectionName);
      
      // Universe-specific indexes
      await this.connection.db(dbName)
        .collection(collectionName)
        .createIndex({ universeId: 1 });
    }

    // Initialize universe metadata
    await this.connection.db(dbName)
      .collection(`${prefix}metadata`)
      .insertOne({
        _id: universeId,
        spaceId,
        created: new Date(),
        deployed: false,
        deploymentUrl: null
      });
  }
}

Phase 3: Data Browser Library (5 hours)

Create: libs/data-browser

Key Component: data-browser-dashboard.component.ts

@Component({
  selector: 'lib-data-browser',
  standalone: true,
  template: `
    <div class="data-browser">
      <!-- Universe Selector -->
      <div class="universe-selector">
        <select [(ngModel)]="selectedUniverse">
          @for (universe of universes(); track universe.id) {
            <option [value]="universe.id">{{ universe.name }}</option>
          }
        </select>
      </div>

      <!-- Collections List -->
      <div class="collections-panel">
        <h3>Collections</h3>
        @for (collection of collections(); track collection.name) {
          <div class="collection-item" 
               (click)="selectCollection(collection.name)"
               [class.active]="selectedCollection() === collection.name">
            <i class="fas fa-database"></i>
            <span>{{ collection.name }}</span>
            <span class="count">{{ collection.count }}</span>
          </div>
        }
      </div>

      <!-- Data Table -->
      <div class="data-panel">
        @if (selectedCollection()) {
          <lib-data-table
            [spaceId]="spaceId()"
            [universeId]="selectedUniverse()"
            [collection]="selectedCollection()"
            (recordEdit)="editRecord($event)"
            (recordDelete)="deleteRecord($event)">
          </lib-data-table>
        }
      </div>

      <!-- Record Editor Modal -->
      @if (editingRecord()) {
        <lib-record-editor
          [record]="editingRecord()"
          [collection]="selectedCollection()"
          (save)="saveRecord($event)"
          (cancel)="cancelEdit()">
        </lib-record-editor>
      }
    </div>
  `
})
export class DataBrowserComponent {
  readonly spaceId = input.required<string>();
  readonly universeId = input.required<string>();

  private readonly spaceData = inject(SpaceDataService);

  readonly selectedUniverse = signal<string>('');
  readonly selectedCollection = signal<string | null>(null);
  readonly editingRecord = signal<any>(null);

  readonly universes = signal<Array<{ id: string; name: string }>>([]);
  readonly collections = signal<Array<{ name: string; count: number }>>([]);

  async ngOnInit() {
    await this.loadUniverses();
    await this.loadCollections();
  }

  private async loadUniverses() {
    // Load all universes for this space
    const universes = await this.spaceData.getUniverses(this.spaceId());
    this.universes.set(universes);
    
    if (universes.length > 0) {
      this.selectedUniverse.set(this.universeId() || universes[0].id);
    }
  }

  private async loadCollections() {
    const universeId = this.selectedUniverse();
    const collections = await this.spaceData.getCollections(
      this.spaceId(),
      universeId
    );
    this.collections.set(collections);
  }

  async selectCollection(name: string) {
    this.selectedCollection.set(name);
  }

  async editRecord(record: any) {
    this.editingRecord.set(record);
  }

  async saveRecord(record: any) {
    await this.spaceData.updateRecord(
      this.spaceId(),
      this.selectedUniverse(),
      this.selectedCollection()!,
      record
    );
    this.editingRecord.set(null);
    await this.loadCollections(); // Refresh
  }
}

Deliverable: Full-featured data browser


Phase 4: Backend Data API (2 hours)

Create: apis/backend-api/src/app/data/data.controller.ts

@Controller('api/spaces/:spaceId/universes/:universeId/data')
@UseGuards(BackendAuthGuard)
export class DataController {
  constructor(
    private readonly spaceDatabaseService: SpaceDatabaseService
  ) {}

  @Post('query')
  async queryData(
    @Param('spaceId') spaceId: string,
    @Param('universeId') universeId: string,
    @Body() query: { collection: string; filter?: any; limit?: number; sort?: any }
  ) {
    const dbName = `space_${spaceId}`;
    const collectionName = `universe_${universeId}_${query.collection}`;
    
    const results = await this.connection.db(dbName)
      .collection(collectionName)
      .find(query.filter || {})
      .limit(query.limit || 50)
      .sort(query.sort || { createdAt: -1 })
      .toArray();

    return {
      success: true,
      data: results,
      count: results.length
    };
  }

  @Post('insert')
  async insertRecord(
    @Param('spaceId') spaceId: string,
    @Param('universeId') universeId: string,
    @Body() body: { collection: string; data: any }
  ) {
    const dbName = `space_${spaceId}`;
    const collectionName = `universe_${universeId}_${body.collection}`;
    
    const result = await this.connection.db(dbName)
      .collection(collectionName)
      .insertOne({
        ...body.data,
        createdAt: new Date(),
        updatedAt: new Date()
      });

    return {
      success: true,
      id: result.insertedId,
      data: body.data
    };
  }

  // Similar for update, delete, aggregate, export
}

Deliverable: Backend API for data operations


📊 Implementation Timeline

Week 1

Week 2

Estimated: 10-12 working days


✅ Success Criteria

v1.1 Complete When:


🎯 Ready to Implement

Architecture: ✅ Approved (Hybrid)
Documentation: ✅ Updated
Plan: ✅ Detailed

Next: Begin Phase 1 implementation! 🚀