Статьи
Паттерны 2 мин 117

Паттерн Dictionary: как сделать любую сущность гибкой без миграций

Как через JSONB field_schema добавлять произвольные поля к любой модели. Без миграций, с валидацией и AI-разбором.

Проблема

HR-система для IT-стартапа и для септик-сервиса — это разные миры. Стартапу нужно поле "GitHub username", клинике — "Номер лицензии", транспортной компании — "Категория ADR-допуска".

Классический подход — миграция на каждое поле. Не масштабируется.

Решение: Dictionary с field_schema

class Dictionary < ApplicationRecord
  # kind: :field_schema | :reference | :list
  # entity_type: "Employee" | "JobOpening" | ...
  # schema: JSONB — описание полей

  scope :field_schemas_for, ->(entity) {
    where(kind: :field_schema, entity_type: entity.class.name, company: Current.company)
  }
end

Как выглядит schema

{
  "fields": [
    {
      "key": "github_username",
      "label": { "ru": "GitHub", "en": "GitHub" },
      "type": "string",
      "required": false,
      "validation": "^[a-zA-Z0-9-]+$"
    },
    {
      "key": "adr_category",
      "label": { "ru": "Категория ADR", "en": "ADR Category" },
      "type": "select",
      "options": ["A", "B", "C", "D"],
      "required": true
    }
  ]
}

Рендеринг формы

ERB-partial _custom_fields_form.html.erb итерирует по schema и рендерит нужный input по type:

  • string → text_field
  • select → select с options
  • date → date_field
  • number → number_field
  • boolean → check_box
  • text → text_area

Валидация

class CustomFieldsValidator < ActiveModel::Validator
  def validate(record)
    schema = Dictionary.field_schemas_for(record)
    schema.each do |dict|
      dict.parsed_fields.each do |field|
        value = record.custom_data&.dig(field["key"])
        if field["required"] && value.blank?
          record.errors.add(:base, "#{field.dig('label', I18n.locale.to_s)} is required")
        end
        if field["validation"].present? && value.present?
          unless value.match?(Regexp.new(field["validation"]))
            record.errors.add(:base, "#{field.dig('label', I18n.locale.to_s)} format is invalid")
          end
        end
      end
    end
  end
end

AI Bootstrap

company_bootstrap агент через чат спрашивает владельца: "Чем занимается компания? Какие должности? Какие документы нужны?" — и генерирует полную schema для всех сущностей.

Результат

  • Один custom_data JSONB столбец вместо десятков миграций
  • HR настраивает поля через UI без разработчика
  • Валидация и AI-разбор подхватывают schema автоматически
  • Паттерн переиспользуется в Triage (custom fields per ticket type)

Комментарии (0)