Generation Patterns

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

Code Generation Patterns - @gograb-space/mock-api

This document provides specific patterns and templates optimized for AI code generators creating mock APIs.

🎯 Quick Generation Templates

1. Basic Entity Mock Template

// Template: {{EntityName}}Mock
import { MockDefinition, MockRequest, MockResponse, DataGenerator } from '@gograb-space/mock-api';

const generator = DataGenerator.getInstance();
let {{entityName}}sDB = generator.generateArray(() => generator.generate{{EntityName}}(), 50);
let next{{EntityName}}Id = {{entityName}}sDB.length + 1;

export const {{EntityName}}Mock: MockDefinition = {
  // List with pagination and search
  'GET /api/{{entityPath}}': (req: MockRequest): MockResponse => {
    const page = parseInt(req.query.page || '1');
    const limit = parseInt(req.query.limit || '10');
    const search = req.query.search || '';

    let filtered = {{entityName}}sDB;
    if (search) {
      filtered = {{entityName}}sDB.filter(item =>
        item.name.toLowerCase().includes(search.toLowerCase()) ||
        item.description?.toLowerCase().includes(search.toLowerCase())
      );
    }

    const start = (page - 1) * limit;
    const items = filtered.slice(start, start + limit);

    return {
      status: 200,
      data: {
        {{entityName}}s: items,
        pagination: {
          page,
          limit,
          total: filtered.length,
          totalPages: Math.ceil(filtered.length / limit)
        }
      }
    };
  },

  // Get single item
  'GET /api/{{entityPath}}/:id': (req: MockRequest): MockResponse => {
    const {{entityName}} = {{entityName}}sDB.find(item => item.id === req.params.id);
    return {{entityName}}
      ? { status: 200, data: { {{entityName}} } }
      : { status: 404, data: { error: '{{EntityName}} not found', code: '{{ENTITY_NAME}}_NOT_FOUND' } };
  },

  // Create new item
  'POST /api/{{entityPath}}': (req: MockRequest): MockResponse => {
    const requiredFields = [{{#each requiredFields}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}];
    const missing = requiredFields.filter(field => !req.body[field]);

    if (missing.length > 0) {
      return {
        status: 400,
        data: {
          error: 'Validation Error',
          message: 'Missing required fields',
          code: 'VALIDATION_ERROR',
          fields: missing.reduce((acc, field) => ({ ...acc, [field]: 'Required' }), {})
        }
      };
    }

    const new{{EntityName}} = {
      ...req.body,
      id: next{{EntityName}}Id++,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };

    {{entityName}}sDB.push(new{{EntityName}});

    return {
      status: 201,
      data: { {{entityName}}: new{{EntityName}} },
      headers: { Location: `/api/{{entityPath}}/${new{{EntityName}}.id}` }
    };
  },

  // Update item
  'PUT /api/{{entityPath}}/:id': (req: MockRequest): MockResponse => {
    const index = {{entityName}}sDB.findIndex(item => item.id === req.params.id);

    if (index === -1) {
      return { status: 404, data: { error: '{{EntityName}} not found', code: '{{ENTITY_NAME}}_NOT_FOUND' } };
    }

    {{entityName}}sDB[index] = {
      ...{{entityName}}sDB[index],
      ...req.body,
      updatedAt: new Date().toISOString()
    };

    return { status: 200, data: { {{entityName}}: {{entityName}}sDB[index] } };
  },

  // Delete item
  'DELETE /api/{{entityPath}}/:id': (req: MockRequest): MockResponse => {
    const index = {{entityName}}sDB.findIndex(item => item.id === req.params.id);

    if (index === -1) {
      return { status: 404, data: { error: '{{EntityName}} not found', code: '{{ENTITY_NAME}}_NOT_FOUND' } };
    }

    const deleted = {{entityName}}sDB.splice(index, 1)[0];
    return {
      status: 200,
      data: {
        message: '{{EntityName}} deleted successfully',
        deleted{{EntityName}}: { id: deleted.id, name: deleted.name }
      }
    };
  }
};

2. Service Integration Template

// Template: {{EntityName}}Service
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface {{EntityName}} {
  id: string | number;
  {{#each fields}}
  {{name}}: {{type}};
  {{/each}}
  createdAt: string;
  updatedAt: string;
}

export interface {{EntityName}}ListResponse {
  {{entityName}}s: {{EntityName}}[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

export interface Create{{EntityName}}Request {
  {{#each requiredFields}}
  {{name}}: {{type}};
  {{/each}}
  {{#each optionalFields}}
  {{name}}?: {{type}};
  {{/each}}
}

@Injectable({ providedIn: 'root' })
export class {{EntityName}}Service {
  private http = inject(HttpClient);
  private baseUrl = '/api/{{entityPath}}';

  get{{EntityName}}s(params?: {
    page?: number;
    limit?: number;
    search?: string;
    [key: string]: any;
  }): Observable<{{EntityName}}ListResponse> {
    return this.http.get<{{EntityName}}ListResponse>(this.baseUrl, { params });
  }

  get{{EntityName}}(id: string | number): Observable<{ {{entityName}}: {{EntityName}} }> {
    return this.http.get<{ {{entityName}}: {{EntityName}} }>(`${this.baseUrl}/${id}`);
  }

  create{{EntityName}}({{entityName}}: Create{{EntityName}}Request): Observable<{ {{entityName}}: {{EntityName}} }> {
    return this.http.post<{ {{entityName}}: {{EntityName}} }>(this.baseUrl, {{entityName}});
  }

  update{{EntityName}}(id: string | number, {{entityName}}: Partial<{{EntityName}}>): Observable<{ {{entityName}}: {{EntityName}} }> {
    return this.http.put<{ {{entityName}}: {{EntityName}} }>(`${this.baseUrl}/${id}`, {{entityName}});
  }

  delete{{EntityName}}(id: string | number): Observable<{ message: string; deleted{{EntityName}}: Partial<{{EntityName}}> }> {
    return this.http.delete<{ message: string; deleted{{EntityName}}: Partial<{{EntityName}}> }>(`${this.baseUrl}/${id}`);
  }
}

3. Component Integration Template

// Template: {{EntityName}}ListComponent
import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged, switchMap, startWith } from 'rxjs/operators';
import { {{EntityName}}Service, {{EntityName}} } from './{{entityName}}.service';

@Component({
  selector: 'app-{{entityName}}-list',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <div class="{{entityName}}-list">
      <div class="header">
        <h2>{{EntityName}}s</h2>
        <button (click)="create{{EntityName}}()">Add {{EntityName}}</button>
      </div>

      <div class="search">
        <input
          type="text"
          placeholder="Search {{entityName}}s..."
          [formControl]="searchControl"
        />
      </div>

      <div class="list">
        @for ({{entityName}} of {{entityName}}s(); track {{entityName}}.id) {
          <div class="{{entityName}}-item">
            <h3>{{ {{entityName}}.name }}</h3>
            <p>{{ {{entityName}}.description }}</p>
            <div class="actions">
              <button (click)="edit{{EntityName}}({{entityName}})">Edit</button>
              <button (click)="delete{{EntityName}}({{entityName}}.id)" class="danger">Delete</button>
            </div>
          </div>
        } @empty {
          <div class="empty">No {{entityName}}s found</div>
        }
      </div>

      <div class="pagination">
        <button
          (click)="loadPage(currentPage() - 1)"
          [disabled]="currentPage() <= 1"
        >
          Previous
        </button>
        <span>Page {{ currentPage() }} of {{ totalPages() }}</span>
        <button
          (click)="loadPage(currentPage() + 1)"
          [disabled]="currentPage() >= totalPages()"
        >
          Next
        </button>
      </div>
    </div>
  `,
  styles: [`
    .{{entityName}}-list {
      padding: 1rem;
    }
    .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 1rem;
    }
    .search input {
      width: 100%;
      padding: 0.5rem;
      margin-bottom: 1rem;
    }
    .{{entityName}}-item {
      border: 1px solid #ddd;
      padding: 1rem;
      margin-bottom: 0.5rem;
      border-radius: 4px;
    }
    .actions {
      margin-top: 0.5rem;
    }
    .actions button {
      margin-right: 0.5rem;
      padding: 0.25rem 0.5rem;
    }
    .danger {
      background-color: #dc3545;
      color: white;
    }
    .pagination {
      display: flex;
      justify-content: center;
      align-items: center;
      margin-top: 1rem;
    }
    .pagination button {
      margin: 0 0.5rem;
    }
    .empty {
      text-align: center;
      padding: 2rem;
      color: #666;
    }
  `]
})
export class {{EntityName}}ListComponent {
  private {{entityName}}Service = inject({{EntityName}}Service);

  // Signals
  {{entityName}}s = signal<{{EntityName}}[]>([]);
  currentPage = signal(1);
  totalPages = signal(1);
  loading = signal(false);

  // Form controls
  searchControl = new FormControl('');

  constructor() {
    // Setup search
    this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      startWith(''),
      switchMap(search => {
        this.loading.set(true);
        return this.{{entityName}}Service.get{{EntityName}}s({
          page: 1,
          limit: 10,
          search: search || undefined
        });
      })
    ).subscribe({
      next: (response) => {
        this.{{entityName}}s.set(response.{{entityName}}s);
        this.currentPage.set(response.pagination.page);
        this.totalPages.set(response.pagination.totalPages);
        this.loading.set(false);
      },
      error: (error) => {
        console.error('Error loading {{entityName}}s:', error);
        this.loading.set(false);
      }
    });
  }

  loadPage(page: number): void {
    if (page < 1 || page > this.totalPages()) return;

    this.loading.set(true);
    this.{{entityName}}Service.get{{EntityName}}s({
      page,
      limit: 10,
      search: this.searchControl.value || undefined
    }).subscribe({
      next: (response) => {
        this.{{entityName}}s.set(response.{{entityName}}s);
        this.currentPage.set(response.pagination.page);
        this.loading.set(false);
      },
      error: (error) => {
        console.error('Error loading page:', error);
        this.loading.set(false);
      }
    });
  }

  create{{EntityName}}(): void {
    // Implementation for creating new {{entityName}}
    console.log('Create {{entityName}}');
  }

  edit{{EntityName}}({{entityName}}: {{EntityName}}): void {
    // Implementation for editing {{entityName}}
    console.log('Edit {{entityName}}:', {{entityName}});
  }

  delete{{EntityName}}(id: string | number): void {
    if (!confirm('Are you sure you want to delete this {{entityName}}?')) return;

    this.{{entityName}}Service.delete{{EntityName}}(id).subscribe({
      next: () => {
        // Remove from list
        this.{{entityName}}s.update(items => items.filter(item => item.id !== id));
      },
      error: (error) => {
        console.error('Error deleting {{entityName}}:', error);
      }
    });
  }
}

🚀 Advanced Generation Patterns

4. Relationship-Aware Mock Template

// Template for entities with relationships
export const {{EntityName}}MockWithRelations: MockDefinition = {
  'GET /api/{{entityPath}}/:id/{{relationName}}': (req: MockRequest): MockResponse => {
    const {{entityName}}Id = req.params.id;
    const {{entityName}} = {{entityName}}sDB.find(item => item.id === {{entityName}}Id);

    if (!{{entityName}}) {
      return { status: 404, data: { error: '{{EntityName}} not found' } };
    }

    // Get related entities
    const related{{RelationName}} = {{relationName}}sDB.filter(item =>
      item.{{entityName}}Id === {{entityName}}Id
    );

    return {
      status: 200,
      data: {
        {{entityName}}Id,
        {{relationName}}: related{{RelationName}},
        total: related{{RelationName}}.length
      }
    };
  },

  'POST /api/{{entityPath}}/:id/{{relationName}}': (req: MockRequest): MockResponse => {
    const {{entityName}}Id = req.params.id;
    const {{entityName}} = {{entityName}}sDB.find(item => item.id === {{entityName}}Id);

    if (!{{entityName}}) {
      return { status: 404, data: { error: '{{EntityName}} not found' } };
    }

    const new{{RelationName}} = {
      ...req.body,
      id: next{{RelationName}}Id++,
      {{entityName}}Id,
      createdAt: new Date().toISOString()
    };

    {{relationName}}sDB.push(new{{RelationName}});

    return {
      status: 201,
      data: { {{relationName}}: new{{RelationName}} }
    };
  }
};

5. Filtered Search Mock Template

// Template for advanced filtering
export const {{EntityName}}SearchMock: MockDefinition = {
  'POST /api/{{entityPath}}/search': (req: MockRequest): MockResponse => {
    const filters = req.body;
    let results = {{entityName}}sDB;

    // Apply filters
    {{#each filterFields}}
    if (filters.{{name}}) {
      {{#if isString}}
      results = results.filter(item =>
        item.{{name}}.toLowerCase().includes(filters.{{name}}.toLowerCase())
      );
      {{else if isNumber}}
      results = results.filter(item => {
        if (filters.{{name}}.min !== undefined && item.{{name}} < filters.{{name}}.min) return false;
        if (filters.{{name}}.max !== undefined && item.{{name}} > filters.{{name}}.max) return false;
        return true;
      });
      {{else if isBoolean}}
      results = results.filter(item => item.{{name}} === filters.{{name}});
      {{else if isEnum}}
      results = results.filter(item =>
        filters.{{name}}.includes ? filters.{{name}}.includes(item.{{name}}) : item.{{name}} === filters.{{name}}
      );
      {{/if}}
    }
    {{/each}}

    // Apply sorting
    if (filters.sortBy) {
      results.sort((a, b) => {
        const direction = filters.sortOrder === 'desc' ? -1 : 1;
        if (a[filters.sortBy] < b[filters.sortBy]) return -1 * direction;
        if (a[filters.sortBy] > b[filters.sortBy]) return 1 * direction;
        return 0;
      });
    }

    // Apply pagination
    const page = filters.page || 1;
    const limit = filters.limit || 10;
    const start = (page - 1) * limit;
    const paginatedResults = results.slice(start, start + limit);

    return {
      status: 200,
      data: {
        results: paginatedResults,
        pagination: {
          page,
          limit,
          total: results.length,
          totalPages: Math.ceil(results.length / limit)
        },
        filters: filters,
        appliedAt: new Date().toISOString()
      }
    };
  }
};

6. Authentication-Aware Mock Template

// Template with authentication
export const {{EntityName}}AuthMock: MockDefinition = {
  'GET /api/{{entityPath}}/my': (req: MockRequest): MockResponse => {
    const authHeader = req.headers['Authorization'] || req.headers['authorization'];

    if (!authHeader?.startsWith('Bearer ')) {
      return {
        status: 401,
        data: { error: 'Unauthorized', code: 'MISSING_AUTH_TOKEN' }
      };
    }

    // Extract user from token (mock implementation)
    const token = authHeader.replace('Bearer ', '');
    const userId = extractUserIdFromToken(token); // Mock function

    if (!userId) {
      return {
        status: 401,
        data: { error: 'Invalid token', code: 'INVALID_AUTH_TOKEN' }
      };
    }

    const user{{EntityName}}s = {{entityName}}sDB.filter(item => item.userId === userId);

    return {
      status: 200,
      data: {
        {{entityName}}s: user{{EntityName}}s,
        userId,
        total: user{{EntityName}}s.length
      }
    };
  }
};

// Mock token extraction function
function extractUserIdFromToken(token: string): string | null {
  // In real app, this would decode JWT
  if (token === 'mock-token') return 'user-1';
  if (token === 'admin-token') return 'admin-1';
  return null;
}

🎨 Style and Convention Patterns

Naming Conventions

// Entity naming patterns
const entityPatterns = {
  // Singular form for types and interfaces
  entityType: '{{EntityName}}',

  // Plural form for collections
  collectionName: '{{entityName}}s',

  // Service naming
  serviceName: '{{EntityName}}Service',

  // Mock definition naming
  mockName: '{{EntityName}}Mock',

  // Database variable naming
  dbVariable: '{{entityName}}sDB',

  // ID variable naming
  idVariable: 'next{{EntityName}}Id',

  // API path naming (kebab-case)
  apiPath: '/api/{{entity-path}}',

  // Component naming
  componentName: '{{EntityName}}ListComponent',

  // Error code naming
  errorCode: '{{ENTITY_NAME}}_NOT_FOUND'
};

Response Structure Patterns

// Standard response patterns
const responsePatterns = {
  // Single item response
  singleItem: {
    status: 200,
    data: { {{entityName}}: item }
  },

  // List response
  listItems: {
    status: 200,
    data: {
      {{entityName}}s: items,
      pagination: {
        page: number,
        limit: number,
        total: number,
        totalPages: number
      }
    }
  },

  // Created response
  created: {
    status: 201,
    data: { {{entityName}}: newItem },
    headers: { Location: '/api/{{entityPath}}/{{id}}' }
  },

  // Deleted response
  deleted: {
    status: 200,
    data: {
      message: '{{EntityName}} deleted successfully',
      deleted{{EntityName}}: { id: item.id }
    }
  },

  // Error responses
  notFound: {
    status: 404,
    data: {
      error: '{{EntityName}} not found',
      code: '{{ENTITY_NAME}}_NOT_FOUND',
      message: '{{EntityName}} with ID {{id}} not found'
    }
  },

  validationError: {
    status: 400,
    data: {
      error: 'Validation Error',
      code: 'VALIDATION_ERROR',
      fields: { fieldName: 'Error message' }
    }
  }
};

File Organization Pattern

src/mocks/
├── index.ts                    # Export all mocks
├── {{entityName}}/
│   ├── {{entityName}}.mock.ts  # Main mock definition
│   ├── {{entityName}}.types.ts # Type definitions
│   └── {{entityName}}.utils.ts # Helper utilities
└── shared/
    ├── error-responses.ts      # Common error patterns
    ├── pagination.utils.ts     # Pagination helpers
    └── validation.utils.ts     # Validation helpers

🔧 Integration Helpers

Mock Registration Helper

// Helper for registering multiple mocks
export function registerEntityMocks(
  mockUtils: MockInterceptorUtilsService,
  entityMocks: Record<string, MockDefinition>
): void {
  const allMocks = Object.values(entityMocks).reduce(
    (acc, mock) => ({ ...acc, ...mock }),
    {}
  );

  mockUtils.updateMocks(allMocks);
}

// Usage
registerEntityMocks(mockUtils, {
  users: UsersMock,
  products: ProductsMock,
  orders: OrdersMock
});

Environment-Based Mock Loading

// Helper for conditional mock loading
export function loadMocksForEnvironment(
  environment: string,
  mockUtils: MockInterceptorUtilsService
): void {
  const mockConfigs = {
    development: {
      enabled: true,
      delay: [100, 300],
      logging: { enabled: true, level: 'info' as const },
      mocks: { ...DevelopmentMocks }
    },
    testing: {
      enabled: true,
      delay: 0,
      logging: { enabled: false, level: 'error' as const },
      mocks: { ...TestingMocks }
    },
    demo: {
      enabled: true,
      delay: [200, 500],
      logging: { enabled: true, level: 'info' as const },
      mocks: { ...DemoMocks }
    }
  };

  const config = mockConfigs[environment];
  if (config) {
    mockUtils.configureMockInterceptor(config, config.mocks);
  }
}

This documentation provides comprehensive patterns optimized for AI code generators, enabling consistent and efficient mock API development.