638 lines
18 KiB
Markdown
638 lines
18 KiB
Markdown
# 💻 Практические примеры реализации оптимизаций
|
||
|
||
## ⚠️ Status: Partial Implementation
|
||
|
||
The following optimizations have been **moved to actual code implementation** via the Claude Code prompt and removed from this file:
|
||
- ✅ Graceful Error Handling → `CLAUDE_CODE_PROMPT.md` Phase 1
|
||
- ✅ Performance Monitoring → `CLAUDE_CODE_PROMPT.md` Phase 4
|
||
|
||
This document now focuses on **remaining optimization examples** that are still in design/planning phase and waiting for implementation.
|
||
|
||
---
|
||
|
||
## 1. Incremental Generation с Caching
|
||
|
||
### 1.1 Система кэширования (cache-manager.ts)
|
||
|
||
```typescript
|
||
// tools/cache-manager.ts
|
||
import * as crypto from 'crypto';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
interface CacheEntry {
|
||
hash: string;
|
||
timestamp: number;
|
||
value: any;
|
||
}
|
||
|
||
export class GenerationCache {
|
||
private cacheDir = './.generation-cache';
|
||
|
||
constructor() {
|
||
if (!fs.existsSync(this.cacheDir)) {
|
||
fs.mkdirSync(this.cacheDir, { recursive: true });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Вычислить хеш входных данных
|
||
*/
|
||
computeHash(input: any): string {
|
||
const content = JSON.stringify(input, Object.keys(input).sort());
|
||
return crypto.createHash('sha256').update(content).digest('hex');
|
||
}
|
||
|
||
/**
|
||
* Получить значение из кэша
|
||
*/
|
||
get<T>(key: string, input: any): T | null {
|
||
const hash = this.computeHash(input);
|
||
const cachePath = path.join(this.cacheDir, `${key}.json`);
|
||
|
||
if (!fs.existsSync(cachePath)) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const cached: CacheEntry = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
||
if (cached.hash === hash) {
|
||
console.log(`✅ Cache hit for ${key}`);
|
||
return cached.value;
|
||
}
|
||
} catch (e) {
|
||
// Cache file corrupted, regenerate
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Сохранить значение в кэш
|
||
*/
|
||
set(key: string, input: any, value: any): void {
|
||
const hash = this.computeHash(input);
|
||
const cachePath = path.join(this.cacheDir, `${key}.json`);
|
||
|
||
const entry: CacheEntry = {
|
||
hash,
|
||
timestamp: Date.now(),
|
||
value,
|
||
};
|
||
|
||
fs.writeFileSync(cachePath, JSON.stringify(entry, null, 2));
|
||
}
|
||
|
||
/**
|
||
* Очистить старые кэши (>7 дней)
|
||
*/
|
||
cleanup(maxAgeDays = 7): void {
|
||
const maxAge = maxAgeDays * 24 * 60 * 60 * 1000;
|
||
const now = Date.now();
|
||
|
||
fs.readdirSync(this.cacheDir).forEach(file => {
|
||
const cachePath = path.join(this.cacheDir, file);
|
||
const cached: CacheEntry = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
||
|
||
if (now - cached.timestamp > maxAge) {
|
||
fs.unlinkSync(cachePath);
|
||
console.log(`🗑️ Deleted old cache: ${file}`);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
export const cache = new GenerationCache();
|
||
```
|
||
|
||
### 1.2 Использование в orchestrator.ts
|
||
|
||
```typescript
|
||
// tools/orchestrator.ts (updated)
|
||
import { cache } from './cache-manager';
|
||
|
||
async function orchestrate(task: string): Promise<void> {
|
||
const dslContent = readDSL('domain/toir.api.dsl');
|
||
|
||
// Проверить кэш
|
||
const cachedContract = cache.get('frozen-contract', dslContent);
|
||
let contract = cachedContract || freezeContract(dslContent);
|
||
|
||
if (!cachedContract) {
|
||
cache.set('frozen-contract', dslContent, contract);
|
||
}
|
||
|
||
// Для Prisma: проверить кэш перед генерацией
|
||
const cachedPrismaSchema = cache.get('prisma-schema', contract);
|
||
let prismaOutput;
|
||
|
||
if (cachedPrismaSchema) {
|
||
prismaOutput = cachedPrismaSchema;
|
||
} else {
|
||
prismaOutput = await agent('generator_prisma', {
|
||
contract,
|
||
task: 'Generate Prisma schema'
|
||
});
|
||
cache.set('prisma-schema', contract, prismaOutput);
|
||
}
|
||
|
||
// ... аналогично для других генераторов
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Параллельная генерация
|
||
|
||
### 2.1 Параллельный orchestrator
|
||
|
||
```typescript
|
||
// tools/orchestrator-parallel.ts
|
||
async function orchestrateParallel(task: string): Promise<GenerationResult> {
|
||
const contract = freezeContract(readDSL('domain/toir.api.dsl'));
|
||
|
||
// Независимые генерации - запустить в параллель
|
||
const [prismaOutput, nestOutput, reactOutput] = await Promise.all([
|
||
// Prisma не зависит ни от чего
|
||
agent('generator_prisma', {
|
||
contract,
|
||
task: 'Generate Prisma schema'
|
||
}),
|
||
|
||
// NestJS зависит от Prisma (но не блокирующая зависимость)
|
||
agent('generator_nest_resources', {
|
||
contract,
|
||
// Может получить Prisma результаты позже
|
||
task: 'Generate NestJS resources'
|
||
}),
|
||
|
||
// React независим от NestJS на этапе генерации ресурсов
|
||
agent('generator_react_admin_resources', {
|
||
contract,
|
||
task: 'Generate React Admin resources'
|
||
})
|
||
]);
|
||
|
||
// ПОТОМ: когда все завершены, интегрировать и валидировать
|
||
const dataAccessOutput = await agent('generator_data_access', {
|
||
contract,
|
||
nestModules: nestOutput,
|
||
task: 'Integrate data access'
|
||
});
|
||
|
||
// Финальная валидация
|
||
const reviewed = await agent('reviewer', {
|
||
outputs: {
|
||
prisma: prismaOutput,
|
||
nest: nestOutput,
|
||
react: reactOutput,
|
||
dataAccess: dataAccessOutput
|
||
},
|
||
task: 'Validate all outputs'
|
||
});
|
||
|
||
return reviewed;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Plugin System
|
||
|
||
### 3.1 Plugin Interface
|
||
|
||
```typescript
|
||
// types/generator-plugin.ts
|
||
export interface GeneratorHooks {
|
||
/**
|
||
* Вызывается перед началом всей генерации
|
||
*/
|
||
beforeGeneration?: (contract: FrozenContract) => Promise<void>;
|
||
|
||
/**
|
||
* После фриза контракта, но перед первой генерацией
|
||
*/
|
||
afterContractFreeze?: (contract: FrozenContract) => Promise<FrozenContract>;
|
||
|
||
/**
|
||
* После генерации Prisma schema
|
||
*/
|
||
afterPrismaGeneration?: (schema: string) => Promise<string>;
|
||
|
||
/**
|
||
* После генерации NestJS модулей
|
||
*/
|
||
afterNestResourcesGeneration?: (modules: GeneratedModule[]) => Promise<GeneratedModule[]>;
|
||
|
||
/**
|
||
* После генерации React ресурсов
|
||
*/
|
||
afterReactResourcesGeneration?: (resources: GeneratedResource[]) => Promise<GeneratedResource[]>;
|
||
|
||
/**
|
||
* Финальная интеграция перед валидацией
|
||
*/
|
||
beforeReview?: (outputs: AllOutputs) => Promise<AllOutputs>;
|
||
|
||
/**
|
||
* На ошибку в любом агенте
|
||
*/
|
||
onGenerationError?: (error: Error, stage: string) => Promise<void>;
|
||
}
|
||
|
||
export interface GeneratorPlugin {
|
||
name: string;
|
||
version: string;
|
||
description: string;
|
||
hooks: GeneratorHooks;
|
||
}
|
||
```
|
||
|
||
### 3.2 Plugin Manager
|
||
|
||
```typescript
|
||
// tools/plugin-manager.ts
|
||
export class PluginManager {
|
||
private plugins: GeneratorPlugin[] = [];
|
||
|
||
loadPlugins(pluginDir: string): void {
|
||
const files = fs.readdirSync(pluginDir);
|
||
|
||
files.forEach(file => {
|
||
if (file.endsWith('.plugin.ts') || file.endsWith('.plugin.js')) {
|
||
const pluginPath = path.join(pluginDir, file);
|
||
const plugin = require(pluginPath).default as GeneratorPlugin;
|
||
|
||
this.plugins.push(plugin);
|
||
console.log(`📦 Loaded plugin: ${plugin.name} v${plugin.version}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
async executeHook(
|
||
hookName: keyof GeneratorHooks,
|
||
input: any,
|
||
context: { stage?: string; error?: Error }
|
||
): Promise<any> {
|
||
let result = input;
|
||
|
||
for (const plugin of this.plugins) {
|
||
const hook = plugin.hooks[hookName];
|
||
if (hook) {
|
||
try {
|
||
result = await hook(result);
|
||
console.log(`✅ ${plugin.name}.${hookName}`);
|
||
} catch (e) {
|
||
console.error(`❌ ${plugin.name}.${hookName} failed:`, e);
|
||
if (plugin.hooks.onGenerationError) {
|
||
await plugin.hooks.onGenerationError(e as Error, context.stage || 'unknown');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
}
|
||
|
||
export const pluginManager = new PluginManager();
|
||
```
|
||
|
||
### 3.3 Пример plugin'а
|
||
|
||
```typescript
|
||
// plugins/oauth2-plugin.plugin.ts
|
||
import { GeneratorPlugin } from '../types/generator-plugin';
|
||
|
||
export default {
|
||
name: 'oauth2-auth',
|
||
version: '1.0.0',
|
||
description: 'Add OAuth2 support to generated schema and auth',
|
||
|
||
hooks: {
|
||
afterPrismaGeneration: async (schema: string) => {
|
||
// Добавить OAuth2 таблицы
|
||
const oauthTables = `
|
||
model OAuth2Provider {
|
||
id String @id @default(cuid())
|
||
name String @unique
|
||
clientId String
|
||
clientSecret String
|
||
createdAt DateTime @default(now())
|
||
}
|
||
|
||
model OAuth2Token {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
providerId String
|
||
accessToken String
|
||
refreshToken String?
|
||
expiresAt DateTime
|
||
user User @relation(fields: [userId], references: [id])
|
||
}
|
||
`;
|
||
|
||
return schema + '\n' + oauthTables;
|
||
},
|
||
|
||
afterNestResourcesGeneration: async (modules) => {
|
||
// Добавить OAuth2 контроллер
|
||
const oauth2Module = {
|
||
name: 'oauth2',
|
||
files: {
|
||
'oauth2.controller.ts': `
|
||
import { Controller, Get, Query } from '@nestjs/common';
|
||
|
||
@Controller('auth/oauth2')
|
||
export class OAuth2Controller {
|
||
@Get('callback')
|
||
async handleCallback(@Query('code') code: string) {
|
||
// OAuth2 callback logic
|
||
}
|
||
}
|
||
`
|
||
}
|
||
};
|
||
|
||
return [...modules, oauth2Module];
|
||
}
|
||
}
|
||
} as GeneratorPlugin;
|
||
```
|
||
|
||
---
|
||
|
||
## 4. OpenAPI as Intermediate Representation
|
||
|
||
### 4.1 DSL → OpenAPI Generator
|
||
|
||
```typescript
|
||
// tools/dsl-to-openapi.ts
|
||
import { FrozenContract } from './types';
|
||
import { OpenAPI, Info, Paths } from 'openapi3-ts/oas30';
|
||
|
||
export function dslToOpenAPI(contract: FrozenContract): OpenAPI {
|
||
const openapi: OpenAPI = {
|
||
openapi: '3.1.0',
|
||
info: {
|
||
title: contract.appName || 'Generated API',
|
||
version: contract.version || '1.0.0',
|
||
description: contract.description || ''
|
||
} as Info,
|
||
paths: {} as Paths,
|
||
components: {
|
||
schemas: {}
|
||
}
|
||
};
|
||
|
||
// Для каждого entity - генерировать endpoints
|
||
contract.entities.forEach(entity => {
|
||
const basePath = `/${entity.plural || entity.name}`;
|
||
|
||
// GET /entities
|
||
openapi.paths![`${basePath}`] = {
|
||
get: {
|
||
operationId: `list${entity.name}`,
|
||
tags: [entity.name],
|
||
responses: {
|
||
'200': {
|
||
description: `List of ${entity.plural}`,
|
||
content: {
|
||
'application/json': {
|
||
schema: {
|
||
type: 'array',
|
||
items: { $ref: `#/components/schemas/${entity.name}` }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
post: {
|
||
operationId: `create${entity.name}`,
|
||
tags: [entity.name],
|
||
requestBody: {
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: `#/components/schemas/${entity.name}CreateDTO` }
|
||
}
|
||
}
|
||
},
|
||
responses: {
|
||
'201': {
|
||
description: `Created ${entity.name}`,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: `#/components/schemas/${entity.name}` }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// GET /entities/{id}
|
||
openapi.paths![`${basePath}/{id}`] = {
|
||
get: {
|
||
operationId: `get${entity.name}`,
|
||
parameters: [
|
||
{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }
|
||
],
|
||
tags: [entity.name],
|
||
responses: {
|
||
'200': {
|
||
description: `${entity.name} details`,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: `#/components/schemas/${entity.name}` }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
put: {
|
||
operationId: `update${entity.name}`,
|
||
parameters: [
|
||
{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }
|
||
],
|
||
tags: [entity.name],
|
||
requestBody: {
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: `#/components/schemas/${entity.name}UpdateDTO` }
|
||
}
|
||
}
|
||
},
|
||
responses: {
|
||
'200': {
|
||
description: `Updated ${entity.name}`,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: `#/components/schemas/${entity.name}` }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
delete: {
|
||
operationId: `delete${entity.name}`,
|
||
parameters: [
|
||
{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }
|
||
],
|
||
tags: [entity.name],
|
||
responses: {
|
||
'204': {
|
||
description: 'Deleted successfully'
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// Добавить schema для entity
|
||
openapi.components!.schemas![entity.name] = {
|
||
type: 'object',
|
||
properties: Object.fromEntries(
|
||
entity.fields.map(field => [
|
||
field.name,
|
||
{ type: mapFieldTypeToOpenAPI(field.type) }
|
||
])
|
||
),
|
||
required: entity.fields.filter(f => f.required).map(f => f.name)
|
||
};
|
||
});
|
||
|
||
return openapi;
|
||
}
|
||
|
||
function mapFieldTypeToOpenAPI(fieldType: string): string {
|
||
const mapping: Record<string, string> = {
|
||
'string': 'string',
|
||
'int': 'integer',
|
||
'float': 'number',
|
||
'boolean': 'boolean',
|
||
'date': 'string', // format: date
|
||
'datetime': 'string', // format: date-time
|
||
};
|
||
return mapping[fieldType] || 'string';
|
||
}
|
||
```
|
||
|
||
### 4.2 Usage в package.json
|
||
|
||
```json
|
||
{
|
||
"scripts": {
|
||
"generate:api-summary": "ts-node tools/api-summary-generator.ts",
|
||
"generate:openapi": "ts-node tools/dsl-to-openapi.ts",
|
||
"generate:docs": "npm run generate:openapi && swagger-cli bundle openapi.json --outfile docs/api.html",
|
||
"generate:client-sdk": "openapi-generator-cli generate -i openapi.json -g typescript-fetch -o client/src/generated"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Type Safety с Zod
|
||
|
||
### 5.1 DSL Validation Schema
|
||
|
||
```typescript
|
||
// tools/validation/dsl-schema.ts
|
||
import { z } from 'zod';
|
||
|
||
export const FieldSchema = z.object({
|
||
name: z.string().min(1),
|
||
type: z.enum(['string', 'int', 'float', 'boolean', 'date', 'datetime']),
|
||
required: z.boolean().default(true),
|
||
unique: z.boolean().default(false),
|
||
indexed: z.boolean().default(false),
|
||
});
|
||
|
||
export const EntitySchema = z.object({
|
||
name: z.string().min(1),
|
||
plural: z.string().optional(),
|
||
description: z.string().optional(),
|
||
fields: z.array(FieldSchema).min(1),
|
||
timestamps: z.boolean().default(true),
|
||
});
|
||
|
||
export const RelationshipSchema = z.object({
|
||
from: z.string(),
|
||
to: z.string(),
|
||
type: z.enum(['one-to-one', 'one-to-many', 'many-to-many']),
|
||
cascade: z.boolean().default(false),
|
||
});
|
||
|
||
export const DSLSchema = z.object({
|
||
appName: z.string().min(1),
|
||
version: z.string().default('1.0.0'),
|
||
entities: z.array(EntitySchema),
|
||
relationships: z.array(RelationshipSchema).optional(),
|
||
auth: z.object({
|
||
enabled: z.boolean().default(true),
|
||
providers: z.array(z.string()).optional(),
|
||
}).optional(),
|
||
});
|
||
|
||
export type DSL = z.infer<typeof DSLSchema>;
|
||
export type Entity = z.infer<typeof EntitySchema>;
|
||
export type Field = z.infer<typeof FieldSchema>;
|
||
```
|
||
|
||
### 5.2 Validation в orchestrator
|
||
|
||
```typescript
|
||
// tools/orchestrator.ts (with validation)
|
||
import { DSLSchema, DSL } from './validation/dsl-schema';
|
||
|
||
async function orchestrate(task: string): Promise<void> {
|
||
const rawDSL = readDSL('domain/toir.api.dsl');
|
||
|
||
// Валидировать перед началом
|
||
let validatedDSL: DSL;
|
||
try {
|
||
validatedDSL = DSLSchema.parse(rawDSL);
|
||
console.log('✅ DSL validation passed');
|
||
} catch (error) {
|
||
console.error('❌ DSL validation failed:');
|
||
console.error(error.issues);
|
||
process.exit(1);
|
||
}
|
||
|
||
// Продолжить с validated data
|
||
const contract = freezeContract(validatedDSL);
|
||
// ...
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
> **Removed:** Sections 6 (Nx Caching Configuration) and 8 (placeholder) have
|
||
> been removed. Nx caching is tracked as a future recommendation, not a worked
|
||
> example. Graceful Error Handling and Performance Monitoring are now fully
|
||
> implemented — see `tools/orchestrator.ts` (`generate()`, `GenerationOutput`,
|
||
> `runTask()`) and `tools/performance-monitor.ts` (`PerformanceMonitor` class)
|
||
> for the production code paths instead of illustrative snippets.
|
||
|
||
---
|
||
|
||
## Заключение
|
||
|
||
Эти примеры показывают как:
|
||
1. **Кэшировать** результаты генерации
|
||
2. **Параллелизировать** независимые операции
|
||
3. **Расширять** систему через плагины
|
||
4. **Генерировать** OpenAPI для документации и SDK
|
||
5. **Валидировать** входные данные
|
||
|
||
Реализованные оптимизации (уже в репозитории):
|
||
- **Graceful error handling + parallel generation** →
|
||
`tools/orchestrator.ts` (`generate()`, `Promise.allSettled`, per-stage
|
||
try/catch, discriminated-union `GenerationStatus`).
|
||
- **Performance monitoring** → `tools/performance-monitor.ts`
|
||
(`PerformanceMonitor.mark/measure/summary/export`), отчёт в
|
||
`generation-metrics.json`.
|
||
- **MCP-валидаторы** → `tools/mcp/{prisma,npm,nest}-validator.mjs` +
|
||
`tools/mcp/lib/mcp-server.mjs` (минимальный JSON-RPC stdio сервер без
|
||
внешних зависимостей).
|