Розробка кастомних 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('Ціна не може бути від\'ємною');
    }
  },

  // Після успішного збереження — side effects
  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 днів.