Разработка кастомных Lists (моделей) KeystoneJS

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

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка кастомных Lists (моделей) KeystoneJS
Средняя
~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

Разработка кастомных Lists (моделей) KeystoneJS

Lists — основная единица модели данных в KeystoneJS. Каждый List соответствует таблице в базе данных, набору GraphQL-операций и разделу в Admin UI. Правильная архитектура Lists определяет гибкость всей системы.

Анатомия List

import { list } from '@keystone-6/core';
import { text, relationship, timestamp, integer, virtual } from '@keystone-6/core/fields';

export const Product = list({
  // Управление доступом на уровне операций и полей
  access: { ... },
  // Поля — ключевая часть
  fields: { ... },
  // Хуки жизненного цикла
  hooks: { ... },
  // Настройки Admin UI
  ui: { ... },
  // GraphQL-расширения
  graphql: { ... },
  // Описание для документации
  description: 'Товары каталога',
});

Типы полей и их применение

fields: {
  // Базовые
  name: text({ validation: { isRequired: true }, isIndexed: true }),
  sku: text({ isIndexed: 'unique' }),
  price: integer({ validation: { min: 0 } }),
  description: text({ ui: { displayMode: 'textarea' } }),

  // Файлы и изображения
  mainImage: image({ storage: 's3_images' }),
  catalogPdf: file({ storage: 's3_files' }),

  // Связи
  category: relationship({ ref: 'Category.products' }),
  tags: relationship({ ref: 'Tag', many: true }),
  variants: relationship({ ref: 'ProductVariant.product', many: true }),

  // Временные метки
  createdAt: timestamp({
    defaultValue: { kind: 'now' },
    ui: { createView: { fieldMode: 'hidden' } },
  }),
  updatedAt: timestamp({
    db: { updatedAt: true },
    ui: { createView: { fieldMode: 'hidden' } },
  }),

  // Виртуальное поле (не хранится в БД)
  fullPriceWithTax: virtual({
    field: graphql.field({
      type: graphql.Float,
      resolve(item) {
        return (item.price || 0) * 1.2;
      },
    }),
  }),
},

Хуки жизненного цикла

hooks: {
  // Перед записью в БД — модификация данных
  resolveInput: async ({ resolvedData, inputData, item, operation }) => {
    if (operation === 'create' && !inputData.sku) {
      resolvedData.sku = `PROD-${Date.now()}`;
    }
    return resolvedData;
  },

  // Валидация перед операцией
  validateInput: async ({ resolvedData, addValidationError }) => {
    if (resolvedData.price !== undefined && resolvedData.price < 0) {
      addValidationError('Цена не может быть отрицательной');
    }
  },

  // После успешного сохранения — сайд-эффекты
  afterOperation: async ({ operation, item, originalItem, context }) => {
    if (operation === 'update' && item.status === 'published' && originalItem?.status !== 'published') {
      // Инвалидация кэша, уведомление webhook, etc.
      await invalidateCache(`/products/${item.slug}`);
    }
  },

  // Перед удалением
  beforeOperation: async ({ operation, item, context }) => {
    if (operation === 'delete') {
      // Проверяем, нет ли связанных заказов
      const ordersCount = await context.db.OrderItem.count({
        where: { product: { id: { equals: item.id } } },
      });
      if (ordersCount > 0) {
        throw new Error('Нельзя удалить товар, присутствующий в заказах');
      }
    }
  },
},

Двусторонние отношения

KeystoneJS требует явного описания обеих сторон отношения:

// Category.ts
export const Category = list({
  fields: {
    name: text({ validation: { isRequired: true } }),
    products: relationship({ ref: 'Product.category', many: true }),
  },
});

// Product.ts — обратная сторона
category: relationship({ ref: 'Category.products' }),

Prisma автоматически создаёт нужные внешние ключи.

Кастомные GraphQL-мутации

Иногда нужна бизнес-логика, которую неудобно реализовывать через стандартные CRUD-операции:

// keystone.ts — extendGraphqlSchema
extendGraphqlSchema: graphql.extend((base) => ({
  mutation: {
    publishProduct: graphql.field({
      type: base.object('Product'),
      args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) },
      async resolve(source, { id }, context) {
        if (!context.session?.data?.role === 'editor') {
          throw new Error('Access denied');
        }
        return context.db.Product.updateOne({
          where: { id },
          data: { status: 'published', publishedAt: new Date() },
        });
      },
    }),
  },
})),

Admin UI: настройка отображения

ui: {
  label: 'Товары',
  singular: 'Товар',
  plural: 'Товары',
  listView: {
    initialColumns: ['name', 'sku', 'price', 'status', 'category'],
    initialSort: { field: 'createdAt', direction: 'DESC' },
    pageSize: 50,
  },
  searchFields: ['name', 'sku'],
  // Скрыть из Admin UI (только API)
  isHidden: false,
},

Сроки разработки

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