Skip to content

Правила написания кода

Основные принципы

1. Типы функций

  • НИКОГДА не пишем явный return type
  • TypeScript выведет тип автоматически
  • Return types удаляются скриптом pnpm clean:code перед деплоем

2. Early return pattern

  • Вместо:
    typescript
    if (state) {
    	// logic
    }
  • Используем:
    typescript
    if (!state) {
    	return;
    }
    // logic

3. Утилиты

  • Если утилита НЕ зависит от Cloudflare bindings или environment
  • Выносим в отдельный файл: /utils/util_name.util.ts

4. Параметры функций

  • Если функция принимает больше 2 параметров

  • Переделываем на объект с интерфейсом:

    typescript
    // Плохо
    function create(name: string, age: number, email: string) {}
    
    // Хорошо
    function create(data: CreateUserData) {}

5. Типы и интерфейсы

  • Все типы и интерфейсы в одном файле: /types.ts
  • Все интерфейсы с префиксом I:
    typescript
    IUser;
    IChat;
    IMessage;
    ICreateUserData;
  • НИКОГДА не пишем типы напрямую в функциях
  • ВСЕГДА выносим в types.ts и импортируем

6. Именование переменных

  • Используем краткие понятные версии
  • ❌ Плохо: mes, m, messag, messageEntity
  • ✅ Хорошо: message, messageIntegration, callsDatabase, calls

Архитектура

7. Handler-based архитектура

Проект использует handler-based паттерн, а не NestJS DI:

typescript
// handlers/agent-init.ts - defineHandler
import { defineHandler } from "@happ-integ/core";

export const agentInitHandler = defineHandler<IAgentInitPayload, IAgentInitResponse>({
	name: "agent-init",
	retries: 0,
	endpoint: "POST /webhook/agent-init",
	async handler({ payload, step, cache, creds, integrationName }) {
		return { type: "conversation_initiation_client_data", dynamic_variables: { ... } };
	},
});
  • Handlers определяются через defineHandler() из @happ-integ/core
  • Получают DI контекст: payload, step, db, cache, creds, ctx, env
  • Регистрируются через createIntegration() в index.ts

8. Lazy initialization паттерн

Для инициализации клиентов используем lazy init (вместо DI):

typescript
let _db: DB | null = null;

export const getDb = (env: CloudflareBindings) =>
	(_db ??= new DB({
		provider: "d1",
		d1: env.DB,
		project: "sofa",
	}));

// Использование
const db = getDb(env);
await db.insert("calls", { id: "123", status: "pending" });

Преимущества:

  • Нет DI framework overhead
  • Работает в Cloudflare Workers
  • Можно сбросить для тестов: __resetClients()

9. Структура интеграции

integrations/sofa/
├── src/
│   ├── index.ts                        # createIntegration() entry point
│   ├── types.ts                        # Все типы и enums
│   ├── config/
│   │   ├── types.ts                    # Интерфейсы конфигов
│   │   ├── defaults.ts                 # Дефолтные значения
│   │   └── index.ts                    # Barrel export
│   ├── migrations/
│   │   └── index.ts                    # SQL миграции
│   ├── utils/
│   │   ├── create-call-queue.util.ts   # Factory для CallQueue (@happ-integ/call-queue)
│   │   └── call-analysis.util.ts       # AI-анализ звонков
│   └── handlers/                       # defineHandler() функции
│       ├── webinar-call-originate.ts
│       ├── webinar-agent-init.ts
│       ├── webinar-call-events.ts
│       ├── webinar-agent-post-call.ts
│       └── retry.ts
├── package.json
├── wrangler.toml       # Cloudflare конфиг
└── README.md

10. Конфигурация интеграции (defaultConfigs)

Все переменные бизнес-логики обязательно выносятся в KV-based конфигурацию:

  • Названия полей CRM ("Телефон", "Имя", "AI Квалификация")
  • Пороги и лимиты (concurrency_limit, failed_call_duration_threshold)
  • Параметры retry (max_attempts, windows)
  • Настройки LLM (primary_provider, max_tokens)
  • Тексты сообщений и локали (first_message_template, locale)
  • Маппинги статусов (outcome → CRM stage)

Структура config/:

typescript
// config/types.ts — интерфейсы
export interface ICrmFieldsConfig {
  phone: string;
  name: string;
  // ...
}

// config/defaults.ts — дефолты + общий объект
export const DEFAULT_CRM_FIELDS: ICrmFieldsConfig = { phone: "Phone", name: "Name" };
export const DEFAULT_CONFIGS: Record<string, unknown> = {
  crm_fields: DEFAULT_CRM_FIELDS,
  // ...
};

// config/index.ts — реэкспорт
export * from "./types";
export * from "./defaults";

Использование в handlers:

typescript
import type { ICrmFieldsConfig } from "../config";
import { DEFAULT_CRM_FIELDS } from "../config";

async handler({ config }) {
  const crmFields = (await config.get<ICrmFieldsConfig>("crm_fields")) ?? DEFAULT_CRM_FIELDS;
  // Используй crmFields.phone вместо хардкода "Телефон"
}

Принцип: если значение может отличаться между клиентами/средами — оно должно быть в конфиге. ?? DEFAULT_* гарантирует работу даже если KV пуст.

11. Комментарии

  • НЕ пишем комментарии к коду
  • Код должен быть самодокументируемым
  • Комментарии удаляются скриптом перед деплоем

12. Декомпозиция

  • НЕ используем декомпозицию там, где она не нужна
  • Если функция вызывается один раз - не выносим в отдельную функцию
  • Исключение: утилитарные функции → выносим в utils/

Возвращаемые значения Handler-ов

13. Handler return pattern

Все handlers ВСЕГДА возвращают консистентную структуру:

typescript
// Успех
return { success: true, data: result };

// Ошибка
return { success: false, error: "Error message" };

Используется для обработки результатов.

14. Обработка ошибок в Handlers

typescript
export const myActionHandler = defineHandler<IPayload>({
	name: "my-action",
	endpoint: "POST /webhook/my-action",
	async handler({ payload, step }) {
		const result = await step.run("call-api", () => externalAPI.call());
		return { success: true, data: result };
	},
});

Cloudflare Bindings и Environment

15. Типизация Bindings

Определяем интерфейс для всех Cloudflare bindings:

typescript
interface CloudflareBindings {
	// Environment
	ENVIRONMENT: "dev" | "production";
	DEBUG?: string;

	// D1 Database
	DB: D1Database;

	// KV
	KV: KVNamespace;

	// Секреты
	CRYPTO_KEY: string;
	CRYPTO_SALT: string;
}

Используется в Hono типе:

typescript
const app = new Hono<{ Bindings: CloudflareBindings }>();

16. Доступ к bindings

Handlers определяются через defineHandler() и регистрируются в createIntegration():

typescript
// Каждый handler — отдельный файл, экспортируемый через barrel export
// Webhook routing и обработка происходят автоматически через @happ-integ/core

Packages и зависимости

17. Database пакет (@happ-integ/db)

Вместо TypeORM используем абстракцию @happ-integ/db:

typescript
const db = getDb(env);

// Select
const rows = await db.select("calls", { status: "pending" });

// Insert
await db.insert("calls", { id: "123", status: "pending" });

// Update
await db.update("calls", { status: "completed" }, { id: "123" });

// Delete
await db.delete("calls", { id: "123" });

Поддерживает D1 и Neon.

18. Cache пакет (@happ-integ/cache)

typescript
const cache = getCache(env);

// Get
const value = await cache.get("key");

// Set
await cache.set("key", value, 3600); // TTL в секундах

// Cache-aside pattern
const data = await cache.cacheAside("key", 3600, async () => {
	return await heavyOperation();
});

Обёртка над KV.

19. Credentials пакет (@happ-integ/creds)

typescript
const creds = getCreds(env);

// Get credentials for integration
const secrets = await creds.get<{ API_KEY: string }>("sofa");
console.log(secrets.API_KEY);

// Set credentials
await creds.set("sofa", { API_KEY: "sk-..." });

Автоматически шифрует значения в D1.

20. LLM пакет (@happ-integ/llm)

typescript
const llm = new LLMService({
	primary: "groq",
	secondary: "claude",
	env,
});

const response = await llm.chat({
	messages: [{ role: "user", content: "Hello" }],
});

Автоматический fallback при ошибке провайдера.

21. CRM пакеты (NetHunt, KeyCRM, Bitrix и т.д.)

typescript
import { NethuntClient } from "@happ-integ/nethunt";

const nethunt = new NethuntClient(email, apiKey);
const record = await nethunt.getRecord(recordId);
await nethunt.updateRecord(recordId, { fields: { status: "done" } });

Именование

27. Сущности и переменные

  • Переменные в единственном числе:
    typescript
    message, call, user;
  • Сервисы/клиенты в множественном числе (если множество операций):
    typescript
    ctx.db, ctx.cache, ctx.creds;
  • Приватные переменные с префиксом _:
    typescript
    let _db: DB | null = null;
    const _logger = console;

28. Именование файлов

  • Файлы называем в snake_case:
    handle-init.ts
    voice-init.ts
    types.ts

29. Enums vs Constants

  • Enums в приоритете над константами для типобезопасных значений:

    typescript
    // ПРАВИЛЬНО
    export enum CallStatusEnum {
    	INITIATED = "initiated",
    	IN_PROGRESS = "in_progress",
    	COMPLETED = "completed",
    }
    
    // НЕПРАВИЛЬНО
    export const CALL_STATUS = {
    	INITIATED: "initiated",
    	IN_PROGRESS: "in_progress",
    };

30. Константы для endpoints (Hono маршруты)

Константы для endpoints должны содержать точно те пути, которые используются в декораторах:

typescript
export const INSTAGRAM_ENDPOINTS = {
	AUTH: ":companyId/auth",
	CALLBACK: "callback",
	CHATS: ":companyId/chats",
};

// Использование в Hono
app.get(INSTAGRAM_ENDPOINTS.AUTH);
app.post(INSTAGRAM_ENDPOINTS.CALLBACK);

31. Префиксы для констант

  • НИКОГДА не именуем константы просто ENDPOINTS или FIELD_NAMES

  • ВСЕГДА пишем с префиксом модуля/интеграции:

    typescript
    // НЕПРАВИЛЬНО
    export const ENDPOINTS = { ... };
    export const FIELD_NAMES = { ... };
    
    // ПРАВИЛЬНО
    export const INSTAGRAM_ENDPOINTS = { ... };
    export const INSTAGRAM_FIELD_NAMES = { ... };

Главное правило - Минимализм

32. Чем меньше кода - тем лучше

  • Идеальный handler должен умещаться на один экран монитора
  • Бизнес-логика должна быть минималистичной
  • Только главная логика, без лишнего кода
  • Объемный утилитарный код выносим в /utils/
  • Handlers должны быть крайне чистыми

Тестирование

33. Структура тестов

src/handlers/
├── init.ts
└── init.test.ts    # Тест рядом с файлом

Используем Vitest с mocking:

typescript
import { describe, it, expect, vi } from "vitest";
import { agentInitHandler } from "./agent-init";

describe("agentInitHandler", () => {
	it("should return default response for empty phone", async () => {
		const result = await agentInitHandler.handler({
			payload: { phone_number: "" },
			// ... mock context
		});
		expect(result.type).toBe("conversation_initiation_client_data");
	});
});

Pre-commit и Linting

34. Husky + lint-staged Pipeline

Перед каждым коммитом запускается:

1. remove-comments.script.ts  - удаляет все комментарии
2. remove-return-types.script.ts - удаляет явные return types
3. eslint --fix  - исправляет lint ошибки
4. prettier --write  - форматирует код

Конфигурация в package.json:

json
"lint-staged": {
  "*.{ts,tsx}": [
    "tsx scripts/remove-comments.script.ts --no-backup",
    "tsx scripts/remove-return-types.script.ts --no-backup",
    "eslint --fix",
    "prettier --write"
  ]
}

35. ESLint правила

Ключевые правила в eslint.config.js:

  • @typescript-eslint/explicit-function-return-type: "off" - нет явных return types
  • @typescript-eslint/no-unused-vars: ["error", { argsIgnorePattern: "^_" }] - игнорируем _ параметры
  • no-multiple-empty-lines - макс 1 пустая строка
  • semi: "always" - всегда точка с запятой

Index.ts

36. Использование index.ts

В пакетах index.ts используется для реэкспорта публичного API:

typescript
// packages/llm/src/index.ts
export * from "./enums";
export * from "./types";
export * from "./services";

Позволяет импортировать напрямую:

typescript
import { LLMService, CallStatusEnum } from "@happ-integ/llm";

Вместо:

typescript
import { LLMService } from "@happ-integ/llm/services";
import { CallStatusEnum } from "@happ-integ/llm/enums";

Логирование

37. Правила логирования

Проект использует централизованный Logger из @happ-integ/logger. Прямые вызовы console.error/console.warn запрещены в packages и workers — используйте Logger.

Два уровня логирования:

УровеньГдеИнструмент
Handlerssrc/handlers/ctx.log, step.run(), ctx (trace-система @happ-integ/trace)
Packages и Gatewaypackages/*/src/, workers/gateway/Logger из @happ-integ/logger

LOG_LEVEL:

КомандаLOG_LEVELЧто видно
pnpm start sofainfoinfo, warn, error
pnpm debug sofadebugdebug, info, warn, error (включая step :input/:output)
CF prodinfoinfo, warn, error → CF Observability

Логируем ТОЛЬКО:

  • ❌ ОШИБКИ (в packages, handlers, services)
  • ⚠️ ВАРНИНГИ (редко, только fallback-ситуации)

НЕ логируем:

  • DEBUG сообщения (они видны только при pnpm debug)
  • INFO сообщения в packages
  • SUCCESS сообщения
  • Progress сообщения
  • Промежуточные операции

В Packages (src/client.ts, src/services/):

Используем Logger из @happ-integ/logger. Создаём один экземпляр на модуль:

typescript
import { Logger } from "@happ-integ/logger";

const logger = new Logger("redis");

// В catch блоках — logger.error(message, error)
catch (error) {
	logger.error("set: Failed to set key", error);
	throw error;
}

// Для fallback — logger.warn(message)
logger.warn(`chat: Primary (${primary}) failed, falling back to ${secondary}`);

// Плохо ❌
console.error("[ERROR] redis-set: Failed");
console.log("Connecting to Redis...");

В Handlers (src/handlers/):

Используем step.run() и ctx.log из trace-системы (НЕ Logger):

typescript
// Хорошо — step.run() для структурированных операций
const record = await step.run("fetch-record", () => nethunt.getRecord(id));

// Хорошо — ctx.log для ошибок
catch (error) {
	ctx.log.error("Handler failed", error);
	return { success: false, error: error.message };
}

// Плохо ❌
console.log("Starting handler");
console.error("Failed:", error);

В Scripts (scripts/*):

Единственное место где допустимы прямые console.error/console.log — CLI скрипты:

typescript
if (!target) {
	console.error("❌ Error: Target is required");
	process.exit(1);
}
console.log("✅ Setup completed");

// Плохо ❌
console.log("Starting environment generation...");
console.log("Processing file...");

В Workers (workers/gateway/src/index.ts):

Используем Logger:

typescript
import { Logger } from "@happ-integ/logger";
const logger = new Logger("gateway");

catch (error) {
	logger.error(`${name}: Service call failed`, error);
	return c.json({ error: "Service unavailable" }, 503);
}

// Плохо ❌
console.error(`[ERROR] gateway-${name}: Service call failed`, error);