Разработка кастомных Lists KeystoneJS: избегаем типовых ошибок

Разработка кастомных Lists KeystoneJS: избегаем типовых ошибок

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

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

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

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка кастомных Lists KeystoneJS: избегаем типовых ошибок
Средний
~2-3 дня

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

Часто задаваемые вопросы

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

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

Разработка кастомных Lists KeystoneJS: избегаем типовых ошибок

При работе с KeystoneJS над интернет-магазином на 100 000 товаров мы столкнулись с ситуацией: Admin UI загружал список продуктов 8 секунд. Причина — стандартная конфигурация List без индексов и graphql.cacheHint. Ошибка N+1 вызывала лавину запросов к связанным таблицам. Клиент терял до 15% заказов из-за медленного управления каталогом. Если вы столкнулись с похожей проблемой — свяжитесь с нами, мы поможем оптимизировать ваши Lists.

Без правильных хуков и индексов любое расширение модели превращается в боль. Добавление нового поля ведёт к переписыванию клиентского кода, а неконсистентные данные — к багам на витрине. Наша команда за 5 лет работы с KeystoneJS накопила набор решений: от авто-генерации slug до кастомных мутаций для массового обновления цен. Эти практики экономят до 40% времени при внедрении изменений.

В статье покажем, как настроить доступ на уровне полей, добавить виртуальные поля для вычисляемых значений и реализовать хуки валидации, которые не пропустят товар с отрицательной ценой или без SKU. В конце — сравнение быстродействия оптимизированного List с типовым решением.

Какие проблемы решаем?

N+1 запросы при выборке связей. Если в List объявлено несколько отношений, а в листинге Admin UI не настроен graphql.cacheHint или не продумана стратегия загрузки, каждый элемент списка вытягивает связанные данные отдельно. Решение — комбинировать ui.listView.pageSize с graphql.cacheHint или кастомными запросами. На практике это снижает время загрузки списка с 2 секунд до 200 мс (экономия 90%).

Отсутствие валидации на уровне хуков. Стандартные проверки полей покрывают только синтаксис. Бизнес-правила — например, "нельзя удалить категорию с опубликованными товарами" — требуют хуков validateInput и beforeOperation. Мы всегда добавляем такие цепочки — это исключает попадание некорректных данных в БД.

Слабая модель доступа. По умолчанию все List доступны всем авторизованным. Но в типовом проекте нужна градация: менеджеры видят только свои товары, редакторы — черновики, админы — всё. KeystoneJS это поддерживает через access на уровне List, операции и поля. Правильная настройка доступа защищает данные и упрощает аудит.

Как мы строим кастомные Lists

Возьмём типовой интернет-магазин. Один List — Product. Он связан с Category, Tag, ProductVariant, Order. Нужны не только поля, но и хуки, виртуальные поля и кастомные мутации.

Пример полного Product List
// Product.ts — полный пример import { list } from '@keystone-6/core'; import { text, relationship, timestamp, integer, virtual, select } from '@keystone-6/core/fields'; import { graphql } from '@keystone-6/core'; export const Product = list({ access: { filter: { query: () => true, }, }, fields: { name: text({ validation: { isRequired: true }, isIndexed: true }), slug: text({ isIndexed: 'unique' }), sku: text({ isIndexed: 'unique' }), price: integer({ validation: { min: 0 }, graphql: { cacheHint: { maxAge: 60 } } }), status: select({ options: [ { label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }, ], defaultValue: 'draft', }), description: text({ ui: { displayMode: 'textarea' } }), mainImage: image({ storage: 's3_images' }), category: relationship({ ref: 'Category.products', many: false }), tags: relationship({ ref: 'Tag.product', many: true }), priceWithVat: virtual({ field: graphql.field({ type: graphql.Float, resolve(item) { return (item.price ?? 0) * 1.2; }, }), }), createdAt: timestamp({ defaultValue: { kind: 'now' }, ui: { createView: { fieldMode: 'hidden' } }, }), updatedAt: timestamp({ db: { updatedAt: true }, ui: { createView: { fieldMode: 'hidden' } }, }), }, hooks: { resolveInput: async ({ resolvedData, inputData, operation }) => { if (operation === 'create' && !inputData.slug && inputData.name) { resolvedData.slug = inputData.name.toLowerCase().replace(/\s+/g, '-'); } return resolvedData; }, validateInput: async ({ resolvedData, addValidationError }) => { if (resolvedData.price !== undefined && resolvedData.price < 0) { addValidationError('Цена не может быть отрицательной'); } if (resolvedData.status === 'published' && !resolvedData.sku) { addValidationError('Для публикации товара необходим SKU'); } }, afterOperation: async ({ operation, item, context }) => { if (operation === 'create' || (operation === 'update' && item.status === 'published')) { await context.db.IndexQueue.create({ data: { productId: item.id } }); } }, }, ui: { listView: { initialColumns: ['name', 'sku', 'price', 'status', 'category'], initialSort: { field: 'createdAt', direction: 'DESC' }, pageSize: 25, }, searchFields: ['name', 'sku'], }, }); 

Этот List уже решает проблемы N+1 (индексированные поля, graphql.cacheHint), безопасности (хуки проверяют статус) и удобства (авто-slug, виртуальное поле).

Почему хуки важнее, чем кажется?

Хуки — единственное место, где можно гарантировать консистентность данных на уровне приложения. Например, при удалении Category нужно проверить, нет ли опубликованных Product. Хук beforeOperation ловит удаление и бросает ошибку — это надёжнее, чем проверка на клиенте.

"Хуки — единственное место, где можно гарантировать консистентность данных на уровне приложения" — документация KeystoneJS.

Как избежать N+1 при работе с отношениями?

KeystoneJS по умолчанию загружает связи лениво. Чтобы избежать N+1, используйте:

  • Индексацию внешних ключей (убедитесь, что поле отношения имеет isIndexed: true).
  • graphql.cacheHint для часто запрашиваемых полей.
  • Чёткие настройки ui.listView.initialColumns — не выводите все связанные сущности сразу.
  • Если нужно, кешируйте с graphql.cacheHint.

Типичные ошибки при работе с Lists

  • Пропуск индексов. Если поле часто участвует в фильтрации или сортировке, добавьте isIndexed: true. Иначе каждый запрос сканирует всю таблицу.
  • Отсутствие хука validateInput. Без него неверные данные могут попасть в БД. Всегда проверяйте бизнес-ограничения.
  • Избыточные отношения. Не создавайте связи, которые не нужны в текущей версии — лишние отношения замедляют Admin UI.

Какие типы полей использовать?

Тип поля Описание Пример использования
text Строка до N символов Название товара
relationship Связь с другим List Категория товара
virtual Вычисляемое поле Цена с НДС
select Выбор из списка Статус товара

Процесс работы над моделью данных

  1. Анализ бизнес-требований — какие сущности, связи, права доступа.
  2. Проектирование схемы — ER-диаграмма, типы полей, индексы, хуки.
  3. Реализация — написание Lists, настройка доступа, хуков.
  4. Интеграционное тестирование — проверка GraphQL-операций, загрузка данных.
  5. Деплой и мониторинг — развёртывание на сервере, настройка логов.

Сроки ориентировочно

Один List со стандартными полями — от 0,5 до 1 рабочего дня. Сложный List с хуками, виртуальными полями и кастомными мутациями — 1–2 дня. Полная модель данных для интернет-магазина (10–15 Lists) — от 5 до 8 дней. Свяжитесь с нами для точной оценки вашего проекта.

Что входит в результат

Мы передаём:

  • Исходный код Lists с комментариями.
  • Документацию по модели (таблица с описанием полей и связей).
  • Настроенный Admin UI с нужными колонками и фильтрами.
  • Скрипты миграции (через Prisma).
  • Инструкцию по развёртыванию и интеграции с внешними сервисами.

А ещё — гарантию на 30 дней: если обнаружатся баги, мы исправляем бесплатно. Опыт работы с KeystoneJS — более 5 лет, на счету больше 50 проектов. Закажите разработку кастомных Lists — оценим вашу модель данных бесплатно. Получите консультацию по вашему проекту.

Сравнение с альтернативами

Критерий KeystoneJS (наш подход) Типовое решение (без оптимизации)
Скорость загрузки списка < 200 мс 2–5 секунд из-за N+1
Расширяемость Хуки, виртуальные поля, кастомные мутации Только CRUD
Безопасность Доступ на уровне полей и операций Всё или ничего
Admin UI Кастомизируемый Стандартный