Разработка кастомных хуков (Hooks) Directus

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка кастомных хуков (Hooks) Directus
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка кастомных хуков (Hooks) Directus

Hook Extension в Directus — TypeScript-функция, подписывающаяся на события жизненного цикла: создание, обновление, удаление записей, аутентификация, запросы к API. Используется для side effects, валидации, интеграций с внешними системами.

Типы событий

// Типы хуков:
action('items.create', handler)    // после создания
action('items.update', handler)    // после обновления
action('items.delete', handler)    // после удаления
filter('items.create', handler)    // до создания (можно изменить данные)
filter('items.update', handler)    // до обновления
action('auth.login', handler)      // после успешного входа
action('auth.logout', handler)     // после выхода
action('files.upload', handler)    // после загрузки файла
schedule('0 * * * *', handler)    // cron
init('app.before', handler)        // при старте сервера

Полный пример Hook Extension

// extensions/hooks/business-logic/index.ts
import type { HookExtensionContext } from '@directus/types'
import type { EventContext } from '@directus/types'

export default ({ action, filter, schedule }: HookExtensionContext) => {

  // ===== АВТОМАТИЧЕСКАЯ ГЕНЕРАЦИЯ SLUG =====
  filter('items.create', (payload, meta) => {
    if (meta.collection === 'articles' && payload.title && !payload.slug) {
      payload.slug = generateSlug(payload.title as string)
    }
    if (meta.collection === 'products' && payload.name && !payload.slug) {
      payload.slug = generateSlug(payload.name as string)
    }
    return payload
  })

  // ===== ВАЛИДАЦИЯ =====
  filter('items.create', async (payload, meta, context) => {
    if (meta.collection !== 'orders') return payload

    const { database } = context as EventContext & { database: any }

    // Проверить наличие товара
    if (payload.items && Array.isArray(payload.items)) {
      for (const item of payload.items) {
        const product = await database('products')
          .where({ id: item.product_id })
          .first()

        if (!product) {
          throw new Error(`Product ${item.product_id} not found`)
        }
        if (product.stock < item.quantity) {
          throw new Error(`Insufficient stock for "${product.name}"`)
        }
      }
    }

    return payload
  })

  // ===== ИНВАЛИДАЦИЯ КЭША =====
  action('items.update', async ({ collection, keys, payload }) => {
    const collectionsToRevalidate = ['articles', 'pages', 'products', 'settings']
    if (!collectionsToRevalidate.includes(collection)) return

    try {
      await fetch(`${process.env.NEXTJS_URL}/api/revalidate`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-revalidate-secret': process.env.REVALIDATE_SECRET!,
        },
        body: JSON.stringify({ collection, keys }),
      })
    } catch (err) {
      console.error('Failed to revalidate cache:', err)
    }
  })

  // ===== УВЕДОМЛЕНИЯ =====
  action('items.create', async ({ collection, key, payload }, context) => {
    if (collection !== 'contact_submissions') return

    // Уведомить команду в Slack
    await fetch(process.env.SLACK_WEBHOOK!, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `📬 Новая заявка от ${payload.name} <${payload.email}>\n${payload.message}`,
      }),
    })
  })

  // ===== АУДИТ ЛОГ =====
  action('items.update', async ({ collection, keys, payload, accountability }) => {
    if (!accountability?.user) return

    // Логировать изменения в audit_logs коллекцию
    const { getSchema } = context as any
    const schema = await getSchema()
    // ... записать в audit_logs
  })

  // ===== СИНХРОНИЗАЦИЯ ОСТАТКОВ =====
  action('items.update', async ({ collection, keys, payload }, context) => {
    if (collection !== 'orders') return
    if (payload.status !== 'paid') return

    const { database } = context as EventContext & { database: any }

    const order = await database('orders')
      .where({ id: keys[0] })
      .first()

    if (order?.items) {
      const items = JSON.parse(order.items)
      for (const item of items) {
        await database('products')
          .where({ id: item.product_id })
          .decrement('stock', item.quantity)
      }
    }
  })

  // ===== CRON — ежедневный отчёт =====
  schedule('0 9 * * 1-5', async () => {
    const response = await fetch(`${process.env.API_URL}/custom/reports/sales`)
    const stats = await response.json()

    await fetch(process.env.SLACK_WEBHOOK!, {
      method: 'POST',
      body: JSON.stringify({
        text: `📊 Отчёт за вчера: заказов ${stats.count}, выручка ${stats.revenue.toLocaleString()} ₽`,
      }),
    })
  })
}

function generateSlug(text: string): string {
  const translitMap: Record<string, string> = {
    а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'yo',
    ж: 'zh', з: 'z', и: 'i', й: 'y', к: 'k', л: 'l', м: 'm',
    н: 'n', о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u',
    ф: 'f', х: 'h', ц: 'ts', ч: 'ch', ш: 'sh', щ: 'sch',
    ъ: '', ы: 'y', ь: '', э: 'e', ю: 'yu', я: 'ya',
  }

  return text
    .toLowerCase()
    .replace(/[а-яё]/g, char => translitMap[char] || char)
    .replace(/\s+/g, '-')
    .replace(/[^\w-]/g, '')
    .replace(/-+/g, '-')
    .slice(0, 100)
}

Доступ к данным из хуков

// Через context.database (knex instance)
action('items.create', async (meta, context) => {
  const { database, getSchema } = context as any

  // Прямой SQL через Knex
  const related = await database('categories')
    .where({ id: meta.payload.category_id })
    .first()

  // Через ItemsService
  const schema = await getSchema()
  const { ItemsService } = context as any
  const service = new ItemsService('articles', { schema, accountability: meta.accountability })
  const items = await service.readByQuery({ filter: { status: { _eq: 'published' } } })
})

Сроки

Разработка набора хуков для бизнес-логики (slug, валидация, уведомления, кэш-инвалидация) — 2–3 дня.