275 lines
40 KiB
JavaScript
275 lines
40 KiB
JavaScript
function escapeHtml(value) {
|
||
return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
||
}
|
||
|
||
const p = (text) => '<p>' + text + '</p>';
|
||
const h2 = (text) => '<h2>' + text + '</h2>';
|
||
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
|
||
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + escapeHtml(alt) + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((cell) => '<th scope="col">' + cell + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
|
||
|
||
function plainText(html) {
|
||
return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
function bodyText(html) {
|
||
return plainText(html.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
|
||
}
|
||
|
||
const REFERENCES = Object.freeze({
|
||
cuser: {
|
||
title: 'Главный модуль: CUser — документация 1С-Битрикс',
|
||
url: 'https://dev.1c-bitrix.ru/api_help/main/reference/cuser/index.php',
|
||
version: 'документация API, проверена 31 July 2026; класс доступен с версии 3.0.6',
|
||
},
|
||
loader: {
|
||
title: 'CModule::IncludeModule — документация 1С-Битрикс',
|
||
url: 'https://dev.1c-bitrix.ru/api_help/main/reference/cmodule/includemodule.php',
|
||
version: 'документация API, проверена 31 July 2026; CModule с версии 3.0.1',
|
||
},
|
||
filter: {
|
||
title: 'PHP Manual: filter_var',
|
||
url: 'https://www.php.net/manual/en/function.filter-var.php',
|
||
version: 'PHP Manual, актуальная страница, проверена 31 July 2026',
|
||
},
|
||
semver: {
|
||
title: 'Semantic Versioning 2.0.0',
|
||
url: 'https://semver.org/spec/v2.0.0.html',
|
||
version: 'Version 2.0.0, 2013',
|
||
},
|
||
});
|
||
|
||
function sources(entries) {
|
||
return '<ul>' + entries.map(({ key, use, boundary }) => {
|
||
const reference = REFERENCES[key];
|
||
return '<li><a href="' + reference.url + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(reference.title) + '</a> — версия и дата: ' + escapeHtml(reference.version) + '. Применение: ' + escapeHtml(use) + ' Граница: ' + escapeHtml(boundary) + '</li>';
|
||
}).join('') + '</ul>';
|
||
}
|
||
|
||
export function chooseLegacyBoundary(input) {
|
||
const callers = Number(input?.callers ?? 0);
|
||
const hasTests = Boolean(input?.characterizationTests);
|
||
const hasSideEffects = Boolean(input?.unknownSideEffects);
|
||
const hasCanonicalContract = Boolean(input?.canonicalContract);
|
||
if (hasSideEffects || !hasTests) return { action: 'keep', reason: 'сначала сохранить наблюдаемое поведение и добавить тесты' };
|
||
if (callers > 1 && !hasCanonicalContract) return { action: 'wrap', reason: 'несколько callers требуют одной адаптационной границы' };
|
||
if (hasCanonicalContract) return { action: 'replace', reason: 'новый контракт проверен тестами и отделён от legacy API' };
|
||
return { action: 'wrap', reason: 'изменение изолируем до появления полного контракта' };
|
||
}
|
||
|
||
export function inspectBitrixSurface(input) {
|
||
const methods = new Set(input?.methods ?? []);
|
||
const moduleLoaded = Boolean(input?.moduleLoaded);
|
||
const version = String(input?.version ?? 'unknown');
|
||
if (!moduleLoaded) return { status: 'module-not-loaded', action: 'проверить подключение iblock или main до вызова' };
|
||
if (methods.has('CUser::GetByID') && methods.has('CUser::Update')) return { status: 'legacy-surface-present', action: 'использовать адаптер с явной проверкой версии', version };
|
||
if (methods.has('Bitrix\\Main\\UserTable')) return { status: 'd7-surface-present', action: 'проверить mapping полей перед заменой', version };
|
||
return { status: 'unknown-surface', action: 'остановить миграцию и получить контракт установленной версии', version };
|
||
}
|
||
|
||
export function migrateUserFields(record) {
|
||
const source = { ...record };
|
||
const phone = String(source.PERSONAL_PHONE ?? '').trim();
|
||
const email = String(source.EMAIL ?? '').trim().toLowerCase();
|
||
const result = { ...source, phone, email };
|
||
delete result.PERSONAL_PHONE;
|
||
delete result.EMAIL;
|
||
return { legacy: source, canonical: result, reversible: true };
|
||
}
|
||
|
||
function revision(meta, parts, referenceEntries) {
|
||
const contentHtml = parts.join('') + h2('Проверяемые источники') + sources(referenceEntries);
|
||
const proseLength = bodyText(contentHtml).length;
|
||
if (proseLength < 5000 || proseLength > 15000) throw new Error(meta.slug + ': body length ' + proseLength);
|
||
return Object.freeze({ ...meta, contentHtml, proseLength });
|
||
}
|
||
|
||
const practice = revision({
|
||
slug: 'editorial-2027-02-practice-bitrix-lessons',
|
||
title: 'Bitrix legacy: когда сохранить код, когда обернуть, когда заменить',
|
||
categories: ['Bitrix', 'Инженерные практики'],
|
||
cover: '/assets/editorial/2027/bitrix-lessons-2027-keep-wrap-replace-tree.svg',
|
||
excerpt: 'Практическое дерево решения для старого API: сохранить поведение, поставить адаптер или перейти на новый контракт.',
|
||
readingMinutes: 15,
|
||
}, [
|
||
p('Проблема legacy-кода в Bitrix редко состоит в возрасте файла. Старый вызов может держать неявные значения, порядок хуков, формат ошибок и поля, которые читает соседний модуль. Если заменить его только потому, что новый API выглядит аккуратнее, цена проявится позже: потеряется поведение, а исправлять его придётся уже по косвенным симптомам. Поэтому первый вопрос — не «как переписать», а «какой контракт нельзя сломать».'),
|
||
p('Решение удобно разделить на три действия: сохранить, обернуть или заменить. Сохранить — значит оставить вызов и зафиксировать его наблюдаемое поведение. Обернуть — поставить адаптер между legacy API и остальным кодом, чтобы callers перестали знать детали. Заменить — удалить старую границу только после того, как новый контракт описан тестами. Это не догма: выбор зависит от числа callers, побочных эффектов и качества проверки.'),
|
||
h2('Сначала отделяем возраст от риска'),
|
||
p('Класс <code>CUser</code> относится к старому API Bitrix, но само имя класса не говорит, что его можно безопасно удалить. Документация перечисляет поля и методы, а также указывает аналог в D7. Для проекта этого мало: нужно увидеть, какие поля реально передаются, какие значения возвращаются и что вызывается после сохранения. Пока это не известно, сохранение или узкий wrapper дешевле полной миграции.'),
|
||
p('Риск растёт, если один вызов смешивает несколько задач. Например, функция может одновременно нормализовать email, создавать пользователя, запускать событие и возвращать ID. Переписать её на новый класс без раздельных тестов значит поменять четыре контракта за один commit. Адаптер позволяет сначала выделить форму данных и ошибку, а уже потом менять внутреннюю реализацию.'),
|
||
figure('/assets/editorial/2027/bitrix-lessons-2027-keep-wrap-replace-tree.svg', 'Дерево выбора для Bitrix legacy: проверить callers и контракт, затем выбрать сохранение, адаптер или замену.', 'Схема помогает выбрать границу изменения. Красная ветка означает, что сначала нужно получить недостающий контракт, а не переписывать вызов.'),
|
||
table('Три действия для legacy-вызова', ['Действие', 'Когда подходит', 'Цена', 'Критерий перехода'], [
|
||
['Сохранить', 'побочные эффекты не описаны, callers мало', 'остаётся старый долг', 'характеризующие тесты и список полей'],
|
||
['Обернуть', 'callers несколько, контракт можно выделить', 'появляется адаптер', 'внешний код видит canonical input/output'],
|
||
['Заменить', 'новый контракт и тесты готовы', 'нужен полный regression', 'старый путь больше не нужен'],
|
||
['Остановиться', 'нет версии, полей или воспроизводимого результата', 'изменение откладывается', 'собраны факты о границе'],
|
||
]),
|
||
h2('Учебная функция выбора границы'),
|
||
p('Функция ниже не изображает Bitrix runtime. Она показывает рабочую механику решения: входом являются количество callers, наличие characterization tests, неизвестные побочные эффекты и canonical contract. Возвращается одно действие и причина. Эти поля можно заполнить из code search, тестов и документации, а не из ощущения «код старый».'),
|
||
code(`import { chooseLegacyBoundary } from './upgrade-2027-02.mjs';
|
||
|
||
const cases = [
|
||
{ callers: 1, characterizationTests: false, unknownSideEffects: true, canonicalContract: false },
|
||
{ callers: 4, characterizationTests: true, unknownSideEffects: false, canonicalContract: false },
|
||
{ callers: 4, characterizationTests: true, unknownSideEffects: false, canonicalContract: true },
|
||
];
|
||
|
||
for (const item of cases) console.log(chooseLegacyBoundary(item));
|
||
// keep -> сохранить наблюдаемое поведение и добавить тесты
|
||
// wrap -> несколько callers требуют одной адаптационной границы
|
||
// replace -> новый контракт проверен тестами и отделён от legacy API`),
|
||
p('Первый результат намеренно не предлагает «сразу новый класс»: неизвестные эффекты сильнее желания обновиться. Второй показывает пользу wrapper, когда callers несколько. Третий разрешает замену только при наличии canonical contract. В рабочем репозитории эту функцию заменит ADR или короткая карточка изменения, а значения подтвердят тесты и просмотр вызовов.'),
|
||
h2('Что должен скрывать адаптер'),
|
||
p('Адаптер не должен превращаться в копию всего legacy API. Он принимает только нужные поля, проверяет обязательные значения и переводит ошибку в понятный тип. Например, внешняя функция может принимать <code>{ email, phone }</code>, а внутри временно собирать массив полей для <code>CUser::Update</code>. Так callers перестают зависеть от названий <code>PERSONAL_PHONE</code> и от способа загрузки модуля.'),
|
||
p('Нужно заранее решить, кто владеет преобразованием. Если один caller исправляет телефон, второй передаёт его как есть, а третий пишет пустую строку, wrapper не создал контракт — он спрятал разнобой. Входная нормализация должна быть в одном месте, а правила обратного преобразования — рядом с ней. В таблице изменений укажите поле, источник, допустимую пустоту и способ проверить результат.'),
|
||
h2('Действия по порядку'),
|
||
ol([
|
||
'Найти все callers legacy-функции и выписать фактические поля, значения по умолчанию и обработку ошибок.',
|
||
'Проверить, подключён ли нужный Bitrix-модуль, и зафиксировать установленную версию вместе с документацией API.',
|
||
'Добавить characterization tests на текущий результат: успешное сохранение, пустое поле, повторный вызов и ошибку.',
|
||
'Выбрать keep, wrap или replace; для wrapper описать canonical input/output и список скрываемых legacy-деталей.',
|
||
'После изменения повторить те же проверки и отдельно проверить события, права и формат возвращаемого ID.',
|
||
]),
|
||
h2('Ограничения и следующий шаг'),
|
||
p('Без доступа к конкретной установке нельзя обещать совместимость версии, поведение событий или одинаковые тексты ошибок. Документация Bitrix описывает API, но не локальные обработчики и не поля, добавленные проектом. Полная замена особенно опасна, если legacy-вызов участвует в транзакции, импортирует данные или используется административной формой.'),
|
||
p('Следующий шаг — взять одну функцию с двумя callers и оформить её canonical контракт. Если тесты не удаётся написать без сложной среды, это сигнал оставить код на месте и сначала сократить границу побочных эффектов. Такая остановка тоже инженерное решение: она сохраняет обратимость и делает следующий commit проверяемым.'),
|
||
], [
|
||
{ key: 'cuser', use: 'Названия CUser, поля и наличие аналога UserTable сверены с документацией Bitrix.', boundary: 'Документация не описывает локальные события, права, обработчики и фактический набор callers проекта.' },
|
||
{ key: 'loader', use: 'Проверка подключения модуля до вызова API используется как отдельное условие адаптера.', boundary: 'Страница не подтверждает, что нужный модуль установлен в конкретной среде.' },
|
||
{ key: 'filter', use: 'PHP-проверка входных значений упомянута как часть нормализации boundary.', boundary: 'Manual не определяет Bitrix-поля и не заменяет тесты проекта.' },
|
||
]);
|
||
|
||
const mechanism = revision({
|
||
slug: 'editorial-2027-02-mechanism-bitrix-lessons',
|
||
title: 'Bitrix API и версия: имя метода не обещает одинаковый контракт',
|
||
categories: ['Bitrix', 'Архитектура'],
|
||
cover: '/assets/editorial/2027/bitrix-lessons-2027-version-boundary-matrix.svg',
|
||
excerpt: 'Как проверять установленную поверхность API и не считать название класса доказательством совместимости.',
|
||
readingMinutes: 16,
|
||
}, [
|
||
p('Проблема версионной совместимости выглядит как простой поиск: в документации найден метод, значит его можно вызвать. В Bitrix такой вывод часто слишком сильный. Один и тот же смысл может жить в legacy-классе и в D7, а конкретная установка может не содержать нужный модуль или иметь изменённое поле. Цена ошибки — адаптер, который компилируется и падает только на редкой форме или после обновления.'),
|
||
p('Надёжная граница строится из трёх фактов: модуль подключён, поверхность методов действительно доступна, а вход и выход совпадают с нужным проекту контрактом. Версия помогает сузить поиск, но не заменяет проверку. Даже правило Semantic Versioning применимо только там, где поставщик соблюдает его для данного API; имя <code>Update</code> само по себе не обещает семантическую совместимость.'),
|
||
h2('Подключение модуля — часть контракта'),
|
||
p('Документация Bitrix для <code>CModule::IncludeModule</code> прямо описывает проверку установки и подключения модуля. Это не декоративная строка перед вызовом. Если модуль не подключён, сообщение об ошибке может появиться далеко от места, где принято решение использовать API. В адаптере проверка должна быть близко к границе и возвращать понятный результат, который можно показать в диагностике.'),
|
||
p('Для D7 аналогично нужен конкретный namespace и набор методов. Нельзя заменить <code>CUser</code> на <code>Bitrix\\Main\\UserTable</code>, не проверив mapping полей, типы значений и обработку исключений. Хорошее сравнение описывает не названия классов, а операции: создать, обновить, найти, получить ID, обработать ошибку. Именно операции должны попасть в тестовую матрицу.'),
|
||
figure('/assets/editorial/2027/bitrix-lessons-2027-version-boundary-matrix.svg', 'Матрица версионной границы Bitrix: подключённый модуль, доступная поверхность, контракт операции и результат проверки.', 'Имя метода занимает только первый слой. До изменения нужно пройти до фактической поверхности установленной версии и сопоставить поля.'),
|
||
table('Уровни проверки Bitrix API', ['Уровень', 'Входной факт', 'Проверка', 'Риск пропуска'], [
|
||
['Модуль', 'iblock или main подключён', 'IncludeModule возвращает true', 'класс не загружен'],
|
||
['Поверхность', 'метод или таблица существуют', 'проверить установленный API', 'вызов неизвестного метода'],
|
||
['Поля', 'названия и типы совпадают', 'сопоставить mapping', 'тихая потеря значения'],
|
||
['Семантика', 'ошибка и результат понятны', 'characterization/regression test', 'новая форма ведёт себя иначе'],
|
||
]),
|
||
h2('Локальный инспектор поверхности'),
|
||
p('Чтобы сделать проверку повторяемой, можно сначала работать с маленьким manifest, полученным из конкретного окружения: версия, признак подключения и список доступных операций. Ниже функция принимает такой manifest и выбирает следующий технический шаг. Она не делает вид, что знает реальный сервер: данные нужно собрать командой проверки в самой среде, а функция только не даёт перепутать отсутствие модуля с отсутствием метода.'),
|
||
code(`import { inspectBitrixSurface } from './upgrade-2027-02.mjs';
|
||
|
||
const surfaces = [
|
||
{ version: '20.0', moduleLoaded: false, methods: [] },
|
||
{ version: '20.0', moduleLoaded: true, methods: ['CUser::GetByID', 'CUser::Update'] },
|
||
{ version: '23.0', moduleLoaded: true, methods: ['Bitrix\\Main\\UserTable'] },
|
||
];
|
||
|
||
for (const surface of surfaces) console.log(inspectBitrixSurface(surface));
|
||
// module-not-loaded
|
||
// legacy-surface-present
|
||
// d7-surface-present`),
|
||
p('Здесь важен порядок. Первый manifest не доходит до анализа методов: отсутствует базовое условие. Второй разрешает говорить только о наличии legacy-поверхности и требует явного адаптера. Третий показывает D7-поверхность, но не объявляет mapping полей готовым. Такой результат проще проверять в CI или в диагностической команде, чем свободный текст в issue.'),
|
||
h2('Поле важнее красивого имени'),
|
||
p('Самая тихая ошибка миграции — значение сохранилось, но стало другим. Телефон мог быть строкой с пробелами и плюсами, email — сохранён с исходным регистром, пустое поле — означать «очистить», а отсутствие поля — «не менять». Если новый API получает обычный объект без различия этих состояний, wrapper стирает смысл запроса. Поэтому contract table должна перечислять хотя бы value, empty и missing.'),
|
||
p('Version boundary нужно держать рядом с этой таблицей. Если в старой версии поле принимает строку, а новая модель отдаёт массив значений, название свойства не спасёт. Правильная проверка — пройти create/read/update на фиксированных данных и сравнить смысловой результат. Одинаковый ID после update ещё не доказывает, что события и индексы получили тот же input.'),
|
||
h2('Действия по порядку'),
|
||
ol([
|
||
'Зафиксировать версию ядра, подключаемый модуль и источник документации, на который опирается вызов.',
|
||
'Собрать manifest доступных методов или таблиц из той среды, где будет выполняться изменение.',
|
||
'Разложить операцию на поля, пустое значение, отсутствие поля, ошибку и побочный event.',
|
||
'Проверить legacy и D7 на одном наборе входов, не сравнивая только имена классов или финальный ID.',
|
||
'Оставить adapter boundary до тех пор, пока regression не подтвердит одинаковый смысл результата.',
|
||
]),
|
||
h2('Ограничения и следующий шаг'),
|
||
p('Инспектор не заменяет запуск в Bitrix: он не видит автозагрузку, права, события, local overrides и SQL-ограничения. Номер версии может быть установлен, но отдельный модуль — отсутствовать. Semantic Versioning тоже не заставляет внутренний API платформы соблюдать обещания внешнего пакета. Любое утверждение о совместимости должно опираться на конкретный набор окружений и операций.'),
|
||
p('Следующий шаг — добавить в проект диагностическую команду, которая печатает только безопасный manifest: версия, подключённый модуль и названия операций без данных пользователей. Затем используйте его перед миграцией одного метода. Если surface различается, адаптер должен остановить изменение с понятной причиной, а не подобрать метод по совпадению имени.'),
|
||
], [
|
||
{ key: 'loader', use: 'Официальная проверка подключения модуля используется как первый слой version boundary.', boundary: 'Документация не сообщает состояние конкретной установки и не описывает mapping полей.' },
|
||
{ key: 'cuser', use: 'CUser и его D7-аналог используются для различения поверхности API и операций пользователя.', boundary: 'Страница не обещает, что два класса равны по событиям, типам и ошибкам.' },
|
||
{ key: 'semver', use: 'Правило совместимости версий используется как оговорка о границах обещаний поставщика.', boundary: 'Спецификация не делает Bitrix API Semantic Versioning-совместимым автоматически.' },
|
||
]);
|
||
|
||
const field = revision({
|
||
slug: 'editorial-2027-02-field-bitrix-lessons',
|
||
title: 'Миграция Bitrix-поля: проверить сохранение, чтение и обратимость',
|
||
categories: ['Bitrix', 'Данные'],
|
||
cover: '/assets/editorial/2027/bitrix-lessons-2027-migration-evidence-loop.svg',
|
||
excerpt: 'Полевой чек-лист переноса пользовательского поля: mapping, пустые значения, повторный запуск и проверяемый результат.',
|
||
readingMinutes: 15,
|
||
}, [
|
||
p('Проблема миграции Bitrix-поля проявляется после успешного ответа API. Запись получила ID, но телефон оказался пустым, email изменил регистр, а повторный запуск создал второе значение. Цена ошибки — не только испорченная строка. Дальше ломается поиск, уведомление или связь с внешней системой, а восстановить исходное состояние трудно, потому что команда сохранила только факт «update вернул успех».'),
|
||
p('Полевой разбор должен проверять три операции: сохранить mapping, прочитать результат тем же смыслом и повторить вход без дубля. Для каждого поля нужно различить отсутствующее значение и явную очистку. Это особенно важно при переходе от массивов Bitrix к canonical объекту: старое имя можно удалить из кода, но нельзя удалить смысл значения до окончания проверки.'),
|
||
h2('Сначала таблица mapping'),
|
||
p('Документация CUser перечисляет поля пользователя, включая <code>PERSONAL_PHONE</code>, <code>EMAIL</code>, идентификатор и время изменения. Для проекта это отправная точка, а не готовая схема: рядом могут быть пользовательские поля, обработчики события и внешний XML_ID. Запишите для каждого значения источник, формат, пустое состояние и обратное представление. Если поле не переносится, причина должна быть явной.'),
|
||
p('Нормализация должна быть идемпотентной: одинаковый вход при повторном запуске даёт одинаковый canonical результат. Для телефона это может быть trim без изменения номера, для email — lowercase, если бизнес-правило считает регистр незначимым. Нельзя применять общую нормализацию ко всем полям: комментарий пользователя, парольный хэш и XML_ID имеют разные правила.'),
|
||
figure('/assets/editorial/2027/bitrix-lessons-2027-migration-evidence-loop.svg', 'Цикл проверки миграции Bitrix-поля: mapping входа, нормализация, запись, повторное чтение и контроль повторного запуска.', 'Схема показывает, что успешный вызов записи — только середина проверки. Нужны read-back и повторяемость результата.'),
|
||
table('Полевой контракт переноса поля', ['Поле', 'Legacy-значение', 'Canonical-значение', 'Проверка'], [
|
||
['Телефон', 'PERSONAL_PHONE, пробелы допустимы', 'phone, trimmed string', 'read-back и формат'],
|
||
['Email', 'EMAIL, исходный регистр', 'email, lower-case', 'валидность и отсутствие дубля'],
|
||
['ID', 'ID пользователя', 'externalId', 'одинаковая запись при retry'],
|
||
['Пустота', 'нет ключа или пустая строка', 'missing или clear', 'два разных теста'],
|
||
['Связь', 'XML_ID', 'externalId', 'двусторонний mapping'],
|
||
]),
|
||
h2('Учебная нормализация без Bitrix-сервера'),
|
||
p('Ниже запускается локальная функция преобразования одного объекта. Она сохраняет legacy-копию, создаёт canonical поля и возвращает признак обратимости. Это не подмена миграционного запуска: результат показывает, как тестировать mapping до подключения API. В интеграционном коде следующая проверка должна сравнить canonical объект с read-back из Bitrix.'),
|
||
code(`import { migrateUserFields } from './upgrade-2027-02.mjs';
|
||
|
||
const input = {
|
||
ID: 17,
|
||
PERSONAL_PHONE: ' +7 900 000-00-00 ',
|
||
EMAIL: 'User@Example.TEST',
|
||
XML_ID: 'crm-17',
|
||
};
|
||
|
||
console.log(migrateUserFields(input));
|
||
// canonical.phone === '+7 900 000-00-00'
|
||
// canonical.email === 'user@example.test'
|
||
// reversible === true`),
|
||
p('Ожидаемый результат показывает две отдельные операции: пробелы убраны у телефона, регистр email нормализован, а исходные поля остаются в legacy snapshot. Snapshot нужен для теста и отката преобразования, но не должен случайно отправляться обратно в новый API. В production-модуле его заменит журнал безопасного mapping без персональных значений.'),
|
||
h2('Пустое поле и отсутствующее поле'),
|
||
p('Разница между <code>{}</code> и <code>{ PERSONAL_PHONE: "" }</code> часто теряется в универсальном merge. Первый объект может означать «не менять телефон», второй — «очистить телефон». Если миграция смешивает эти случаи, повторный запуск удалит данные, которые не должны были меняться. В тестовой матрице должны быть оба входа и ожидаемое действие на стороне Bitrix.'),
|
||
p('Внешняя валидация тоже не должна менять значение молча. PHP <code>filter_var</code> может помочь проверить email, но решение о допустимости адреса принадлежит контракту приложения. Неверный email лучше остановить до update, чем сохранить пустую строку и потом считать ответ API доказательством успеха. Для телефона нужны отдельные правила: длина, допустимые символы и локальный формат.'),
|
||
h2('Действия по порядку'),
|
||
ol([
|
||
'Составить mapping table и отдельно назвать missing, empty, invalid и unchanged для каждого поля.',
|
||
'Запустить нормализацию на локальном наборе с пробелами, разным регистром, пустым и неверным значением.',
|
||
'В тестовой среде записать одну запись по стабильному ID или XML_ID, затем выполнить read-back.',
|
||
'Сравнить смысловые поля, время изменения, события и внешний идентификатор; не ограничиваться HTTP/API success.',
|
||
'Повторить тот же запуск и убедиться, что новая запись не появилась и canonical результат не изменился.',
|
||
]),
|
||
h2('Ограничения и следующий шаг'),
|
||
p('Локальная функция не знает о правах, событиях и особенностях конкретной версии Bitrix. Read-back может вернуть представление, отличное от входа: формат телефона, timezone или пустое значение иногда нормализуются сервером. Восстановление должно учитывать транзакцию и сохранённую связь, а не просто повторно отправлять старый объект.'),
|
||
p('Следующий шаг — взять одно поле, для которого есть внешний XML_ID, и прогнать полный цикл на небольшой выборке: mapping, запись, read-back, повтор и отчёт по расхождениям. Пока расхождения не классифицированы, расширять миграцию на весь набор рискованно. Проверяемость одного поля ценнее широкого запуска с неясным результатом.'),
|
||
], [
|
||
{ key: 'cuser', use: 'Официальный список полей CUser используется для примера PERSONAL_PHONE, EMAIL, ID и XML_ID.', boundary: 'Документация не знает пользовательские поля, события и фактические данные проекта.' },
|
||
{ key: 'filter', use: 'PHP Manual используется для проверки допустимости входного email перед записью.', boundary: 'filter_var не определяет бизнес-правила, Bitrix-формат и гарантию сохранения поля.' },
|
||
{ key: 'loader', use: 'Подключение модуля упомянуто как проверка интеграционной границы до записи.', boundary: 'Страница не подтверждает права и настройки конкретной среды.' },
|
||
]);
|
||
|
||
export const revisions = Object.freeze([practice, mechanism, field]);
|
||
|
||
export function verifyRevisionsAgainstFixture() {
|
||
const checks = revisions.map((item) => {
|
||
const body = bodyText(item.contentHtml);
|
||
return body.length >= 5000 && body.length <= 15000 && /<table>/.test(item.contentHtml) && /<figure>/.test(item.contentHtml) && /<pre><code>/.test(item.contentHtml) && /<ol>/.test(item.contentHtml) && !/(synthetic-plan-hand-off|productionEffect|future-only|plan\/scenario|source cutoff|not-collected|not-attempted|future owner|развитие автора)/i.test(body);
|
||
});
|
||
const sample = inspectBitrixSurface({ version: '20.0', moduleLoaded: true, methods: ['CUser::GetByID', 'CUser::Update'] });
|
||
const mapping = migrateUserFields({ PERSONAL_PHONE: ' 123 ', EMAIL: 'A@B.C' });
|
||
const fixtureOk = sample.status === 'legacy-surface-present' && mapping.canonical.email === 'a@b.c';
|
||
return Object.freeze({ passed: checks.filter(Boolean).length + (fixtureOk ? 1 : 0), total: checks.length + 1, accepted: checks.every(Boolean) && fixtureOk, characters: Object.fromEntries(revisions.map((item) => [item.slug, bodyText(item.contentHtml).length])) });
|
||
}
|
||
|
||
if (process.argv.includes('--verify-fixture')) {
|
||
const result = verifyRevisionsAgainstFixture();
|
||
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
||
if (!result.accepted) process.exitCode = 1;
|
||
}
|
||
|
||
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');
|