Розробка кастомних хуків (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. Використовується для побічних ефектів, валідації, інтеграцій з зовнішніми системами.

Типи подій

// Типи хуків:
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 дні.