@qor/chat-layer

Установка

Подключение @qor/chat-layer к Nuxt-приложению.

Требования

  • Node.js >= 20
  • pnpm >= 9
  • Nuxt >= 4.0
  • @qor/auth-layer (подключается автоматически как зависимость)
  • @qor/ui + @nuxt/ui (для UI-компонентов)

Установка пакета

# Если chat-layer в монорепо
pnpm add @qor/chat-layer

Подключение в Nuxt

1. Расширить layer

nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@qor/chat-layer'],

  modules: ['@qor/ui', '@nuxt/ui'],

  // Chat layer — SPA, SSR отключен
  ssr: false,
})
@qor/uiMUST быть указан до@nuxt/ui в массиве modules. Иначе CSS-переменные для семантических цветов не будут сгенерированы.

2. Настроить runtime config

nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@qor/chat-layer'],

  runtimeConfig: {
    // === Server-only ===

    // Prisma database URL (SQLite по умолчанию)
    chatDatabaseUrl: 'file:./chat.db',

    // Директория для загруженных файлов
    chatUploadDir: './uploads',

    // === Public ===
    public: {
      // Base URL для REST API чата
      // Пусто = локальные Nitro-эндпоинты
      // URL = внешний API (серверные роуты исключаются)
      chatApiBase: '',

      // WebSocket URL для Socket.IO
      // Пусто = auto-detect от текущего origin
      chatWsUrl: '',

      // Auth layer config (обязательно)
      authApiBase: '',
    },
  },
})

3. Переменные окружения

.env
# Prisma
CHAT_DATABASE_URL="file:./chat.db"

# Загрузки
NUXT_CHAT_UPLOAD_DIR="./uploads"

# Режим: локальный сервер (по умолчанию)
NUXT_PUBLIC_CHAT_API_BASE=""
NUXT_PUBLIC_CHAT_WS_URL=""

# Или: внешний API
# NUXT_PUBLIC_CHAT_API_BASE="https://chat.api.example.com"
# NUXT_PUBLIC_CHAT_WS_URL="wss://chat.api.example.com"

# Auth layer
NUXT_PUBLIC_AUTH_API_BASE=""

4. CSS (обязательно)

Убедитесь, что в вашем приложении есть main.css:

app/assets/css/main.css
@import "tailwindcss";
@import "@nuxt/ui";
nuxt.config.ts
export default defineNuxtConfig({
  css: ['~/assets/css/main.css'],
})

5. Инициализация Prisma

При первом запуске сгенерируйте Prisma-клиент и создайте базу данных:

# Генерация клиента
npx prisma generate --schema=node_modules/@qor/chat-layer/prisma/schema.prisma

# Создание/миграция базы
npx prisma db push --schema=node_modules/@qor/chat-layer/prisma/schema.prisma
Prisma-клиент генерируется в node_modules/.prisma/chat-client — это кастомный output, чтобы не конфликтовать с другими Prisma-схемами в монорепо.

Конфигурация

Полная таблица параметров

ПараметрТипПо умолчаниюОписание
chatDatabaseUrlstring'file:./chat.db'Prisma database URL (server-only)
chatUploadDirstring'./uploads'Директория для загруженных файлов (server-only)
chatApiBasestring''Base URL для REST API. Пусто = локальный
chatWsUrlstring''WebSocket URL. Пусто = auto-detect

Режимы работы

Локальный сервер (разработка, standalone)

nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@qor/chat-layer'],
  runtimeConfig: {
    chatDatabaseUrl: 'file:./chat.db',
    chatUploadDir: './uploads',
    public: {
      chatApiBase: '',
      chatWsUrl: '',
    },
  },
})

В этом режиме chat layer поднимает:

  • Socket.IO сервер через Engine.IO (привязан к Nitro HTTP серверу)
  • REST API /api/v1/chat/* для истории, загрузок, управления
  • Маршрут /uploads/* для раздачи загруженных файлов
  • SQLite база данных для хранения сообщений

Внешний API (продакшн)

nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@qor/chat-layer'],
  runtimeConfig: {
    public: {
      chatApiBase: 'https://chat.api.example.com',
      chatWsUrl: 'wss://chat.api.example.com',
    },
  },
})

В этом режиме:

  • Серверные роуты /api/v1/chat/* исключаются из бандла через nitro:config хук
  • Socket.IO клиент подключается к chatWsUrl
  • REST-запросы проксируются на chatApiBase
  • Prisma и SQLite не используются

REST API

Эндпоинты (локальный режим)

МетодПутьОписание
GET/api/v1/chat/conversationsСписок диалогов текущего пользователя
POST/api/v1/chat/conversationsСоздать диалог (single/group)
GET/api/v1/chat/conversations/:id/messagesИстория сообщений (cursor-based)
POST/api/v1/chat/uploadЗагрузить файл (multipart/form-data)
POST/api/v1/chat/users/webhookWebhook для создания/обновления ChatUser
GET/api/v1/chat/users/:idПубличный профиль пользователя
GET/uploads/:pathРаздача загруженных файлов

Создание диалога

// POST /api/v1/chat/conversations
const conv = await $fetch('/api/v1/chat/conversations', {
  method: 'POST',
  body: {
    type: 'single',              // 'single' | 'group'
    participantIds: ['user-uuid'], // externalId участников
    name: 'Группа поддержки',     // Только для group
  },
})

Загрузка файла

// POST /api/v1/chat/upload
const formData = new FormData()
formData.append('file', file)

const attachment = await $fetch('/api/v1/chat/upload', {
  method: 'POST',
  body: formData,
})
// → { id, type, url, name, size, mimeType }

Загруженный файл создаётся как "orphan" (messageId = null). При отправке сообщения через Socket.IO передаётся attachmentIds, и сервер привязывает вложения к сообщению.

Пагинация сообщений

// GET /api/v1/chat/conversations/:id/messages?cursor=2025-01-01T12:00:00.000Z
const result = await $fetch(`/api/v1/chat/conversations/${convId}/messages`)
// → { messages: ChatMessage[], cursor: string, hasMore: boolean }

Cursor-based пагинация по createdAt. Курсор — ISO timestamp последнего сообщения.

Socket.IO события

Клиент → Сервер

СобытиеPayloadОписание
message:send{ conversationId, body, type?, replyToId?, attachmentIds? }Отправить сообщение (с ack)
message:edit{ messageId, body }Редактировать сообщение
message:delete{ messageId }Удалить сообщение
message:react{ messageId, emoji }Добавить/убрать реакцию
message:read{ conversationId, messageId }Отметить прочитанным
typing:startconversationIdНачал печатать
typing:stopconversationIdПерестал печатать
conversation:joinconversationIdВойти в комнату
conversation:leaveconversationIdПокинуть группу

Сервер → Клиент

СобытиеPayloadОписание
message:newChatMessageНовое входящее сообщение
message:updated{ id, conversationId, body?, editedAt? }Сообщение отредактировано
message:deleted{ id, conversationId }Сообщение удалено
message:reaction{ messageId, conversationId, reactions[] }Обновление реакций
message:status{ messageId, conversationId, status }Статус доставки изменился
conversation:createdChatConversationСоздан новый диалог
conversation:updatedPartial<ChatConversation> & { id }Обновление диалога
presence:update{ userId, online, lastSeen? }Статус присутствия
typing:start{ conversationId, userId }Пользователь печатает
typing:stop{ conversationId, userId }Пользователь перестал

Регистрация пользователей

Chat layer использует свою таблицу ChatUser с маппингом на внешний externalId. Пользователи создаются автоматически:

  1. При создании диалогаPOST /api/v1/chat/conversations автоматически вызывает upsert для каждого participantId
  2. Через webhookPOST /api/v1/chat/users/webhook для массового создания из внешней системы
// Webhook: создать/обновить ChatUser
await $fetch('/api/v1/chat/users/webhook', {
  method: 'POST',
  body: {
    externalId: 'user-uuid-from-auth',
    displayName: 'Иван Петров',
    avatarUrl: 'https://example.com/avatar.jpg',
  },
})

Быстрый старт

Минимальная конфигурация для запуска:

nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@qor/chat-layer'],
  modules: ['@qor/ui', '@nuxt/ui'],
  ssr: false,
  css: ['~/assets/css/main.css'],
})
Terminal
# 1. Установить зависимости
pnpm install

# 2. Сгенерировать Prisma-клиент
npx prisma generate --schema=node_modules/@qor/chat-layer/prisma/schema.prisma

# 3. Создать базу данных
npx prisma db push --schema=node_modules/@qor/chat-layer/prisma/schema.prisma

# 4. Запустить dev-сервер
pnpm dev

После запуска:

  • Авторизуйтесь через auth layer
  • Перейдите на /chats
  • Chat layer автоматически подключит Socket.IO при появлении useAuth().isAuthenticated