379 lines
35 KiB
JavaScript
379 lines
35 KiB
JavaScript
import { readFile } from 'node:fs/promises';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const webRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const articlesPath = join(webRoot, 'data', 'articles.json');
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
function paragraph(text) {
|
|
return '<p>' + text + '</p>';
|
|
}
|
|
|
|
function heading(text) {
|
|
return '<h2>' + text + '</h2>';
|
|
}
|
|
|
|
function codeBlock(lines) {
|
|
return '<pre><code>' + escapeHtml(lines.join('\n')) + '</code></pre>';
|
|
}
|
|
|
|
function figure(src, alt, caption) {
|
|
return '<figure><img src="' + src + '" alt="' + alt + '" /><figcaption>' + caption + '</figcaption></figure>';
|
|
}
|
|
|
|
function orderedList(items) {
|
|
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
|
}
|
|
|
|
function bulletList(items) {
|
|
return '<ul>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ul>';
|
|
}
|
|
|
|
function dataTable(headers, rows) {
|
|
const head = '<thead><tr>' + headers.map((header) => '<th scope="col">' + header + '</th>').join('') + '</tr></thead>';
|
|
const body = '<tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody>';
|
|
return '<div class="table-scroll"><table>' + head + body + '</table></div>';
|
|
}
|
|
|
|
function sourceList(items) {
|
|
return '<ul>' + items.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
|
|
}
|
|
|
|
const bitrixAdd = {
|
|
title: 'CIBlockElement::Add',
|
|
url: 'https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/add.php?print=Y',
|
|
note: 'контракт метода, обработчики до и после записи, ID и LAST_ERROR',
|
|
};
|
|
|
|
const bitrixProperties = {
|
|
title: 'CIBlockElement::SetPropertyValuesEx',
|
|
url: 'https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/setpropertyvaluesex.php',
|
|
note: 'точечное сохранение свойств и особенности пустых значений',
|
|
};
|
|
|
|
const bitrixBeforeAdd = {
|
|
title: 'OnBeforeIBlockElementAdd',
|
|
url: 'https://dev.1c-bitrix.ru/api_help/iblock/events/onbeforeiblockelementadd.php',
|
|
note: 'как обработчик может изменить поля или отменить запись',
|
|
};
|
|
|
|
const bitrixGetList = {
|
|
title: 'CIBlockElement::GetList',
|
|
url: 'https://dev.1c-bitrix.ru/api_help/iblock/classes/ciblockelement/getlist.php',
|
|
note: 'фильтры ACTIVE, ACTIVE_DATE и выборка полей элемента',
|
|
};
|
|
|
|
const catalogProduct = {
|
|
title: 'CCatalogProduct::Add и актуальная модель Catalog',
|
|
url: 'https://dev.1c-bitrix.ru/api_help/catalog/classes/ccatalogproduct/add.php',
|
|
note: 'параметры товарного элемента и версия API',
|
|
};
|
|
|
|
const practiceArticle = {
|
|
slug: 'editorial-2018-01-practice-bitrix-elements',
|
|
title: 'Bitrix API. Создаём элемент инфоблока так, чтобы ошибка не исчезла',
|
|
categories: ['Bitrix', 'PHP', 'Практика'],
|
|
cover: '/assets/editorial/2018/bitrix-catalog-workflow.png',
|
|
excerpt: 'Разбираем создание элемента инфоблока как полноценную операцию: контракт полей, обработка LAST_ERROR, свойства, контрольная выборка и проверка публичного сценария.',
|
|
readingMinutes: 9,
|
|
contentHtml: [
|
|
paragraph('Иногда задача формулируется очень просто: «добавь товар через API». Первая версия обычно занимает десять строк — создаём <code>CIBlockElement</code>, вызываем <code>Add</code>, получаем ID. А через день приходит сообщение: товар есть в админке, но карточка пустая, ссылка ведёт не туда или импорт тихо пропустил половину ошибок. Давайте сразу сделаем операцию так, чтобы её можно было проверить, повторить и поддерживать.'),
|
|
heading('Ситуация: ID — это ещё не готовый результат'),
|
|
paragraph('Элемент инфоблока — лишь одна часть пользовательского сценария. Для каталога могут быть важны символьный код, раздел, обязательные свойства, активность, картинка, цена и остаток. Метод <code>CIBlockElement::Add</code> действительно возвращает ID при успехе и <code>false</code> при ошибке, а текст причины лежит в <code>LAST_ERROR</code>. Поэтому нормальный критерий готовности состоит из двух вопросов: запись создана и потребитель этой записи видит ожидаемые данные.'),
|
|
figure('/assets/editorial/2018/bitrix-catalog-workflow.png', 'Разработчик проверяет путь от формы к карточке товара и фиксирует схему процесса', 'Не начинаем с большого импорта. Сначала рисуем путь данных и называем контрольные точки.'),
|
|
heading('Сначала формулируем контракт операции'),
|
|
paragraph('Перед вызовом API полезно выписать, какие поля обязательны именно для нашего инфоблока. Это выглядит занудно до первой ошибки импорта, а после неё экономит часы. Не нужно создавать универсальный валидатор Bitrix: достаточно проверить входные данные, зафиксировать системные значения и вернуть диагностируемую ошибку вызывающему коду.'),
|
|
dataTable(
|
|
['Участок', 'Что фиксируем', 'Чем доказываем'],
|
|
[
|
|
['Вход', '<code>name</code>, внешний ID, категория, файлы', 'Валидация до вызова Bitrix и понятная ошибка для вызывающего кода'],
|
|
['Элемент', '<code>IBLOCK_ID</code>, <code>NAME</code>, <code>CODE</code>, <code>ACTIVE</code>', 'Массив <code>$fields</code> можно залогировать без секретов'],
|
|
['Свойства', 'Какие свойства обязательны при первом сохранении', 'Они передаются в <code>PROPERTY_VALUES</code> или проверяются отдельно'],
|
|
['Результат', 'ID, URL, видимость в нужной выборке', 'Контрольный запрос и тест пользовательского сценария'],
|
|
],
|
|
),
|
|
heading('Рабочий пример'),
|
|
paragraph('Ниже пример для черновика товара. Я намеренно сохраняю элемент неактивным: пока импорт не завершил все обязательные действия, пользователю незачем видеть полуготовую карточку. Конкретные коды свойств и ID инфоблока должны быть вынесены в конфигурацию проекта, а не спрятаны в середине функции.'),
|
|
codeBlock([
|
|
'<?php',
|
|
'',
|
|
'use Bitrix\\Main\\Loader;',
|
|
'',
|
|
'const PRODUCT_IBLOCK_ID = 12;',
|
|
'',
|
|
'function createProductDraft(array $input): int',
|
|
'{',
|
|
' if (!Loader::includeModule("iblock")) {',
|
|
' throw new RuntimeException("Модуль iblock не подключён");',
|
|
' }',
|
|
'',
|
|
' $name = trim((string)($input["name"] ?? ""));',
|
|
' $code = trim((string)($input["code"] ?? ""));',
|
|
'',
|
|
' if ($name === "" || $code === "") {',
|
|
' throw new InvalidArgumentException("Нужны NAME и CODE");',
|
|
' }',
|
|
'',
|
|
' $element = new CIBlockElement();',
|
|
' $id = $element->Add([',
|
|
' "IBLOCK_ID" => PRODUCT_IBLOCK_ID,',
|
|
' "NAME" => $name,',
|
|
' "CODE" => $code,',
|
|
' "ACTIVE" => "N",',
|
|
' "PROPERTY_VALUES" => [',
|
|
' "EXTERNAL_ID" => (string)($input["externalId"] ?? ""),',
|
|
' "BRAND" => (int)($input["brandId"] ?? 0),',
|
|
' ],',
|
|
' ]);',
|
|
'',
|
|
' if ($id === false) {',
|
|
' throw new RuntimeException($element->LAST_ERROR ?: "Не удалось создать элемент");',
|
|
' }',
|
|
'',
|
|
' return (int)$id;',
|
|
'}',
|
|
]),
|
|
heading('Почему свойства лучше не «доклеивать» вслепую'),
|
|
paragraph('Для обязательных свойств, без которых объект не имеет смысла, удобнее передавать <code>PROPERTY_VALUES</code> в том же вызове <code>Add</code>. Метод <code>SetPropertyValuesEx</code> полезен, когда нужно сознательно обновить небольшую часть свойств: он не требует передавать полный набор и экономнее по запросам. Но он возвращает <code>null</code>, поэтому его нельзя использовать как удобный индикатор успеха. Если частичное обновление критично, его надо окружить собственным журналированием и контрольным чтением.'),
|
|
codeBlock([
|
|
'<?php',
|
|
'',
|
|
'// Осознанное точечное изменение, а не «попробуем и забудем».',
|
|
'CIBlockElement::SetPropertyValuesEx(',
|
|
' $elementId,',
|
|
' PRODUCT_IBLOCK_ID,',
|
|
' ["SYNC_STATUS" => "ready"]',
|
|
');',
|
|
'',
|
|
'// После важного изменения читаем нужное свойство в контрольном сценарии.',
|
|
]),
|
|
heading('Четыре проверки после Add'),
|
|
orderedList([
|
|
'Проверяем, что вернулся положительный ID; при <code>false</code> сохраняем <code>LAST_ERROR</code>, входной внешний идентификатор и контекст операции.',
|
|
'Читаем элемент в том же инфоблоке и убеждаемся, что поля <code>NAME</code>, <code>CODE</code> и нужные свойства действительно сохранены.',
|
|
'Проверяем публичную выборку с теми же фильтрами, которые использует компонент каталога: активность, даты, раздел, права, цена и остатки — если они участвуют в сценарии.',
|
|
'Только после этого включаем элемент или помечаем импортированную запись как готовую.',
|
|
]),
|
|
heading('Чего я бы не делал'),
|
|
bulletList([
|
|
'Не игнорировал бы результат <code>Add</code> в надежде, что ошибка «сама попадёт в журнал».',
|
|
'Не делал бы элемент активным до заполнения зависимых данных.',
|
|
'Не генерировал бы <code>CODE</code> без правила уникальности: два одинаковых названия неизбежно встретятся.',
|
|
'Не очищал бы весь кеш первым действием. Сначала нужно доказать, что проблема именно в кеше, а не в данных или фильтре.',
|
|
]),
|
|
heading('Проверяемые источники'),
|
|
sourceList([bitrixAdd, bitrixProperties, bitrixGetList]),
|
|
heading('Итог'),
|
|
paragraph('Сам вызов <code>CIBlockElement::Add</code> несложен. Сложность в том, чтобы не потерять границу между «запись появилась» и «сценарий закончен». Если хранить контракт полей рядом с кодом, проверять <code>LAST_ERROR</code> и делать контрольную выборку, импорт перестаёт быть магией. А дальше уже можно спокойно добавлять цены, остатки и любые проектные правила.'),
|
|
].join('\n'),
|
|
};
|
|
|
|
const mechanismArticle = {
|
|
slug: 'editorial-2018-01-mechanism-bitrix-elements',
|
|
title: 'Bitrix API. Что на самом деле происходит вокруг CIBlockElement::Add',
|
|
categories: ['Bitrix', 'PHP', 'Архитектура'],
|
|
cover: '/assets/editorial/2018/bitrix-add-lifecycle.svg',
|
|
excerpt: 'Разбираем жизненный цикл добавления элемента: кто проверяет поля, где срабатывают события Bitrix, почему глобальный обработчик не заменяет сервис и как тестировать эту границу.',
|
|
readingMinutes: 9,
|
|
contentHtml: [
|
|
paragraph('Когда Bitrix-проект разрастается, вокруг простого <code>CIBlockElement::Add</code> появляется невидимый код: обработчики событий, правила символьного кода, импортеры, каталог, поиск и шаблоны. Из-за этого одинаковый вызов сегодня работает из формы, а завтра падает из консольного скрипта. Давайте разложим путь записи по шагам и не будем прятать бизнес-правило в месте, где его трудно обнаружить.'),
|
|
heading('Карта жизненного цикла'),
|
|
paragraph('Документация Bitrix говорит важную вещь: перед добавлением вызывается <code>OnBeforeIBlockElementAdd</code>. Обработчик получает поля по ссылке, поэтому способен их изменить; чтобы отменить запись, он должен установить исключение через <code>$APPLICATION->ThrowException()</code> и вернуть <code>false</code>. После успешной записи срабатывают события после добавления. Это значит, что обработчик — реальная часть контракта метода, а не декоративная «магия в init.php».'),
|
|
figure('/assets/editorial/2018/bitrix-add-lifecycle.svg', 'Последовательность от формы до контрольной публичной выборки при создании элемента Bitrix', 'ID возвращается из слоя инфоблока, но качество результата подтверждается уже в пользовательском сценарии.'),
|
|
heading('Где живёт каждое правило'),
|
|
dataTable(
|
|
['Место', 'Хорошая ответственность', 'Что туда не стоит класть'],
|
|
[
|
|
['Сервис создания', 'Проверка входа, подготовка полей, перевод ошибки в понятный результат', 'Глобальные побочные эффекты для любого инфоблока'],
|
|
['OnBeforeIBlockElementAdd', 'Последний общий барьер: запрет пустого CODE, аудит общей политики', 'Внешние HTTP-вызовы, тяжёлую обработку файлов, правила одного экрана'],
|
|
['После записи', 'Отправка события, фоновая реакция, журналирование успешной операции', 'Изменение результата, от которого зависит успех текущего Add'],
|
|
['Публичный компонент', 'Фильтрация и отображение данных', 'Исправление отсутствующих обязательных данных «на лету»'],
|
|
],
|
|
),
|
|
heading('Минимальный предохранитель в событии'),
|
|
paragraph('Ниже — не замена сервису, а общий барьер для конкретного инфоблока. Он предотвращает запись элемента без символьного кода независимо от того, откуда пришёл вызов: админка, импорт или самописный endpoint. Важно, что код не пытается угадать всё бизнес-правило товара. Он проверяет только инвариант, который действительно должен быть общим.'),
|
|
codeBlock([
|
|
'<?php',
|
|
'',
|
|
'const PRODUCT_IBLOCK_ID = 12;',
|
|
'',
|
|
'AddEventHandler(',
|
|
' "iblock",',
|
|
' "OnBeforeIBlockElementAdd",',
|
|
' ["CatalogElementGuard", "beforeAdd"]',
|
|
');',
|
|
'',
|
|
'final class CatalogElementGuard',
|
|
'{',
|
|
' public static function beforeAdd(array &$fields): bool',
|
|
' {',
|
|
' if ((int)($fields["IBLOCK_ID"] ?? 0) !== PRODUCT_IBLOCK_ID) {',
|
|
' return true;',
|
|
' }',
|
|
'',
|
|
' if (trim((string)($fields["CODE"] ?? "")) === "") {',
|
|
' global $APPLICATION;',
|
|
' $APPLICATION->ThrowException("Для товара нужен символьный код");',
|
|
' return false;',
|
|
' }',
|
|
'',
|
|
' return true;',
|
|
' }',
|
|
'}',
|
|
]),
|
|
heading('Почему событие не должно быть единственным валидатором'),
|
|
paragraph('Потому что событие не знает намерения конкретной операции. Один экран может создавать черновик без картинки, другой — импортировать поставщика, третий — мигрировать старые записи. Если все проверки спрятать в <code>OnBeforeIBlockElementAdd</code>, получится глобальная функция с десятком условий и неожиданными побочными эффектами. Сервис создания должен объяснять, почему он принимает или отклоняет вход. Событие лишь страхует инвариант, который действует для всех.'),
|
|
heading('Сервис остаётся точкой диагностики'),
|
|
codeBlock([
|
|
'<?php',
|
|
'',
|
|
'function addCatalogElement(array $fields): int',
|
|
'{',
|
|
' if (!\\Bitrix\\Main\\Loader::includeModule("iblock")) {',
|
|
' throw new RuntimeException("Модуль iblock не подключён");',
|
|
' }',
|
|
'',
|
|
' $element = new CIBlockElement();',
|
|
' $id = $element->Add($fields);',
|
|
'',
|
|
' if ($id === false) {',
|
|
' $message = $element->LAST_ERROR ?: "Bitrix не вернул причину ошибки";',
|
|
' throw new RuntimeException($message);',
|
|
' }',
|
|
'',
|
|
' return (int)$id;',
|
|
'}',
|
|
]),
|
|
heading('После записи — это уже другой разговор'),
|
|
paragraph('Обработчик после добавления удобен для журналирования, запуска поиска или отправки внутреннего уведомления. Но он не должен молча решать судьбу уже созданного элемента. Если побочный шаг упал после того, как <code>Add</code> вернул ID, повторный вызов <code>Add</code> из обработчика легко создаст дубль, а внешний сервис получит два одинаковых запроса. Поэтому после записи я бы сохранял ID, внешний ключ и понятный статус операции, а ошибку реакции разбирал как отдельную задачу.'),
|
|
paragraph('Если синхронизация с внешней системой действительно обязательна для публикации товара, полезно разделить два состояния: «элемент сохранён» и «элемент готов для пользователя». Первый факт подтверждает сервис создания, второй — контрольная проверка после всех зависимых действий. Тогда временный сбой индексации или уведомления не превращается в неясную историю, где никто не понимает, можно ли безопасно повторить импорт.'),
|
|
heading('Как тестировать такую связку'),
|
|
paragraph('В 2018-м легко ограничиться ручной проверкой в админке, но здесь полезно хотя бы зафиксировать короткую матрицу. Она не требует сложного тестового фреймворка: часть сценариев можно выполнить на тестовом инфоблоке и сохранить как чек-лист релиза. Главное — проверять и прямой сервис, и поведение глобального события.'),
|
|
dataTable(
|
|
['Сценарий', 'Ожидание', 'Где искать ошибку при сбое'],
|
|
[
|
|
['Корректный элемент', 'Сервис возвращает ID, элемент читается', 'Поля сервиса и конфигурация инфоблока'],
|
|
['Пустой CODE', 'Запись отменена, причина понятна вызывающему коду', 'Обработчик OnBeforeIBlockElementAdd'],
|
|
['Другой инфоблок', 'Охранник не вмешивается', 'Слишком широкое условие в обработчике'],
|
|
['Импорт или CLI', 'Результат тот же, что из формы', 'Скрытая зависимость от HTTP-сессии или интерфейса'],
|
|
],
|
|
),
|
|
heading('Проверяемые источники'),
|
|
sourceList([bitrixAdd, bitrixBeforeAdd, bitrixProperties]),
|
|
heading('Итог'),
|
|
paragraph('События Bitrix полезны, когда их граница ясна. Общий инвариант — в обработчик. Намерение операции, логирование и перевод ошибки — в сервис. Публичная видимость — в отдельную проверку после создания. С такой схемой даже старый проект перестаёт выглядеть набором случайных <code>init.php</code>-заклинаний: у каждого правила появляется место и причина.'),
|
|
].join('\n'),
|
|
};
|
|
|
|
const fieldArticle = {
|
|
slug: 'editorial-2018-01-field-bitrix-elements',
|
|
title: 'Bitrix API. Элемент есть в админке, но не виден в каталоге',
|
|
categories: ['Bitrix', 'PHP', 'Диагностика'],
|
|
cover: '/assets/editorial/2018/bitrix-visibility-diagnostic.svg',
|
|
excerpt: 'Полевой разбор частой ошибки Bitrix: Add вернул ID, админка показывает элемент, но пользователь не видит его в каталоге. Ищем причину по слоям, а не очищаем кеш наугад.',
|
|
readingMinutes: 10,
|
|
contentHtml: [
|
|
paragraph('Знакомая картина: скрипт вернул ID, в админке новый товар есть, а на сайте его нет. Первый импульс — «почистить кеш». Иногда это действительно помогает, но чаще кеш просто оказывается первым подозреваемым, потому что его легко назвать. Давайте сначала отделим факт записи от публичной видимости и пройдём путь теми же условиями, которыми живёт каталог.'),
|
|
heading('Постановка проблемы'),
|
|
paragraph('Админка и публичный компонент редко показывают одинаковую выборку. Админка может отобразить неактивный элемент, а каталог фильтрует по <code>ACTIVE</code>, датам активности, разделу, правам, цене, наличию и проектным свойствам. Поэтому вопрос «почему элемент не виден?» нельзя решать одной командой. Нужен короткий список слоёв и доказательство на каждом.'),
|
|
figure('/assets/editorial/2018/bitrix-visibility-diagnostic.svg', 'Дерево диагностики: от результата Add к условиям публичного каталога', 'Начинаем не с кеша, а с самого раннего условия, которое может исключить элемент из публичной выборки.'),
|
|
heading('Проверяем по слоям'),
|
|
dataTable(
|
|
['Слой', 'Что проверяем', 'Как получить доказательство'],
|
|
[
|
|
['Запись', '<code>Add</code> вернул ID, <code>LAST_ERROR</code> пуст', 'Лог результата и внешний ID операции'],
|
|
['Инфоблок', '<code>ACTIVE</code>, даты, символьный код, раздел', 'Контрольная выборка с теми же базовыми фильтрами'],
|
|
['Свойства', 'Обязательная связь, SKU, картинка, проектные флаги', 'Чтение конкретных свойств для созданного ID'],
|
|
['Каталог', 'Цена, остаток, доступность — если компонент их требует', 'Проверка конфигурации каталога и товарных параметров'],
|
|
['Публичный путь', 'Фильтр компонента, права, кеш и индекс', 'Повтор сценария от имени нужного пользователя'],
|
|
],
|
|
),
|
|
heading('Контрольный запрос вместо догадки'),
|
|
paragraph('Документация <code>CIBlockElement::GetList</code> описывает фильтры <code>ACTIVE</code>, <code>ACTIVE_DATE</code> и выбор нужных полей. Ниже не универсальный каталоговый запрос, а диагностическая проба. Она отвечает на первый важный вопрос: проходит ли наш элемент хотя бы базовые условия публичной выдачи. Если нет — проблему надо искать в данных, а не в шаблоне.'),
|
|
codeBlock([
|
|
'<?php',
|
|
'',
|
|
'const PRODUCT_IBLOCK_ID = 12;',
|
|
'',
|
|
'$result = CIBlockElement::GetList(',
|
|
' [],',
|
|
' [',
|
|
' "IBLOCK_ID" => PRODUCT_IBLOCK_ID,',
|
|
' "=ID" => $elementId,',
|
|
' "ACTIVE" => "Y",',
|
|
' "ACTIVE_DATE" => "Y",',
|
|
' ],',
|
|
' false,',
|
|
' ["nTopCount" => 1],',
|
|
' ["ID", "IBLOCK_ID", "NAME", "CODE", "ACTIVE", "DATE_ACTIVE_FROM", "DATE_ACTIVE_TO"]',
|
|
');',
|
|
'',
|
|
'$row = $result->Fetch();',
|
|
'if ($row === false) {',
|
|
' throw new RuntimeException("Элемент не проходит базовый публичный фильтр");',
|
|
'}',
|
|
]),
|
|
heading('Где здесь каталог'),
|
|
paragraph('Элемент инфоблока и товарная часть каталога — соседние, но разные уровни. Если публичный компонент требует цену, остаток или связь торгового предложения с товаром, одного <code>CIBlockElement::Add</code> недостаточно. Документация каталога отдельно описывает товарные параметры; в старом коде можно встретить <code>CCatalogProduct::Add</code>, но текущая документация помечает его устаревшим и рекомендует модель <code>\\Bitrix\\Catalog\\Model\\Product</code>. Для исторического проекта это не повод переписывать всё за вечер, а повод явно зафиксировать используемую версию API и не смешивать создание элемента с догадкой о его товарном состоянии.'),
|
|
heading('Мини-матрица симптомов'),
|
|
dataTable(
|
|
['Симптом', 'Самая частая причина', 'Безопасное следующее действие'],
|
|
[
|
|
['Нет ID', 'Ошибка обязательного поля, свойства или прав', 'Вывести <code>LAST_ERROR</code> и входной внешний ID'],
|
|
['ID есть, базовый GetList пуст', 'ACTIVE, дата, инфоблок или неверный ID', 'Сначала читать поля элемента без публичных фильтров'],
|
|
['GetList есть, карточки нет', 'Дополнительный фильтр компонента, раздел, права, URL', 'Сравнить фильтр и маршрут компонента с контрольной выборкой'],
|
|
['Карточка есть, нельзя купить', 'Не настроены параметры каталога, цена или остаток', 'Проверить товарный слой отдельно от инфоблока'],
|
|
['После изменения появляется не сразу', 'Кеш или индекс', 'Подтвердить корректность данных и только затем адресно обновлять кеш/индекс'],
|
|
],
|
|
),
|
|
heading('Почему не стоит начинать с очистки кеша'),
|
|
paragraph('Потому что очистка кеша скрывает различие между двумя ситуациями: данные корректны, но слой кеширования устарел; или данные с самого начала не удовлетворяют фильтру. В первом случае нужна адресная стратегия инвалидирования. Во втором — очистка не решит проблему, а только добавит шума. Хорошая диагностика оставляет после себя не только исправленный товар, но и понимание, какое условие не было выполнено.'),
|
|
heading('Чек-лист перед закрытием задачи'),
|
|
orderedList([
|
|
'Зафиксировать ID созданного элемента и внешний идентификатор операции.',
|
|
'Считать элемент без публичных ограничений и проверить, что ожидаемые поля и свойства сохранены.',
|
|
'Повторить контрольную выборку с <code>ACTIVE</code> и <code>ACTIVE_DATE</code>.',
|
|
'Проверить условия конкретного компонента: раздел, права, проектные фильтры, URL.',
|
|
'Если это товар — отдельно проверить цену, остаток и доступность, не смешивая этот слой с данными инфоблока.',
|
|
'Только после этого проверять кеш и индекс; зафиксировать, какое именно действие обновляет их в данном проекте.',
|
|
]),
|
|
heading('Проверяемые источники'),
|
|
sourceList([bitrixAdd, bitrixGetList, catalogProduct]),
|
|
heading('Итог'),
|
|
paragraph('Фраза «элемент есть в админке» говорит только о том, что одна запись сохранилась. Для каталога этого недостаточно. Если идти от ID к базовой выборке, от неё к товарному слою и только затем к кешу, причина обычно находится быстро. И самое приятное: на следующей похожей задаче уже не нужно вспоминать магическую кнопку очистки — есть нормальный порядок проверки.'),
|
|
].join('\n'),
|
|
};
|
|
|
|
const revisions = [practiceArticle, mechanismArticle, fieldArticle];
|
|
const archive = JSON.parse(await readFile(articlesPath, 'utf8'));
|
|
const revisionBySlug = new Map(revisions.map((article) => [article.slug, article]));
|
|
|
|
for (const revision of revisions) {
|
|
if (!archive.some((article) => article.slug === revision.slug)) {
|
|
throw new Error('Article not found: ' + revision.slug);
|
|
}
|
|
if (!revision.contentHtml.includes('<figure>') || !revision.contentHtml.includes('<table>')) {
|
|
throw new Error('Visual or table missing: ' + revision.slug);
|
|
}
|
|
}
|
|
|
|
const updated = archive.map((article) => {
|
|
const revision = revisionBySlug.get(article.slug);
|
|
return revision ? { ...article, ...revision } : article;
|
|
});
|
|
|
|
if (process.argv.includes('--print-revisions')) {
|
|
console.log(JSON.stringify(revisions, null, 2));
|
|
} else {
|
|
console.log('Usage: node web/scripts/upgrade-2018-01.mjs --print-revisions');
|
|
}
|