@ryuzaki13/react-foundation-lib 1.2.4 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/chunks/{dateUtils-BWIaWh13.js → dateUtils-B5V5aXB-.js} +225 -201
  2. package/dist/chunks/dateUtils-B5V5aXB-.js.map +1 -0
  3. package/dist/chunks/{formatters-CEsXVZ7n.js → formatters--mhLir82.js} +4 -4
  4. package/dist/chunks/{formatters-CEsXVZ7n.js.map → formatters--mhLir82.js.map} +1 -1
  5. package/dist/chunks/{odata-service-D8oFjUah.js → odata-service-DnPlNGpU.js} +2 -2
  6. package/dist/chunks/{odata-service-D8oFjUah.js.map → odata-service-DnPlNGpU.js.map} +1 -1
  7. package/dist/chunks/{validate-DrKdjesg.js → validate-nWY_Mla5.js} +2 -2
  8. package/dist/chunks/{validate-DrKdjesg.js.map → validate-nWY_Mla5.js.map} +1 -1
  9. package/dist/error-report/index.js +2 -2
  10. package/dist/excel/index.js +3 -3
  11. package/dist/formatters/date/index.d.ts +1 -1
  12. package/dist/formatters/date/index.d.ts.map +1 -1
  13. package/dist/formatters/date/parseDate.d.ts +13 -1
  14. package/dist/formatters/date/parseDate.d.ts.map +1 -1
  15. package/dist/formatters/index.js +5 -5
  16. package/dist/odata/index.js +1 -1
  17. package/dist/odata-service/index.js +1 -1
  18. package/package.json +1 -1
  19. package/src/docs/browserAndReact.mdx +150 -0
  20. package/src/docs/filesAndExcel.mdx +163 -0
  21. package/src/docs/formatters-runtime.mdx +455 -0
  22. package/src/docs/formatters.mdx +363 -0
  23. package/src/docs/odataAndTables.mdx +178 -0
  24. package/src/docs/pureUtilities.mdx +96 -0
  25. package/src/docs/runtimeAndState.mdx +155 -0
  26. package/src/formatters/date/README.md +36 -2
  27. package/src/foundationLib.mdx +105 -0
  28. package/dist/chunks/dateUtils-BWIaWh13.js.map +0 -1
  29. package/src/file/file.mdx +0 -6
  30. package/src/formatters/date/date.mdx +0 -6
  31. package/src/formatters/number/number.mdx +0 -6
  32. package/src/formatters/valueState/valueState.mdx +0 -6
  33. package/src/formulas/formulas.mdx +0 -6
  34. package/src/pwa/pwa.mdx +0 -6
  35. package/src/query-client/queryClient.mdx +0 -6
@@ -0,0 +1,163 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Файлы и Excel" />
4
+
5
+ # Файлы, бинарные данные и Excel
6
+
7
+ Эта страница описывает browser-only API для чтения пользовательских файлов, преобразования base64 и формирования `.xlsx`. Подготовка бизнес-полей, проверка прав доступа и отправка файла на сервер остаются ответственностью host-приложения.
8
+
9
+ ## `file`: чтение файлов и HTTP-заголовки
10
+
11
+ ```ts
12
+ import {
13
+ createContentDispositionHeader,
14
+ readFile,
15
+ readImageFile,
16
+ resolveMimeTypeByExtension,
17
+ type ReadFileError,
18
+ type ReadImageError
19
+ } from "@ryuzaki13/react-foundation-lib/file";
20
+ ```
21
+
22
+ ### Когда использовать
23
+
24
+ - `readFile` — получить `dataUrl` для preview или `ArrayBuffer` для дальнейшей обработки;
25
+ - `readImageFile` — то же для изображений с проверкой image MIME и размером изображения;
26
+ - helpers Content-Disposition — сформировать безопасный заголовок в browser-compatible/edge runtime коде;
27
+ - `resolveMimeTypeByExtension` — определить MIME по расширению, если содержимого для sniffing нет.
28
+
29
+ ### `readFile`
30
+
31
+ ```ts
32
+ readFile(file, options?): Promise<ReadFileResult>
33
+ ```
34
+
35
+ | Опция | Назначение |
36
+ | --------------------------------- | ------------------------------------------------- |
37
+ | `mode?: "data-url"` | Режим по умолчанию. Результат содержит `dataUrl`. |
38
+ | `mode: "array-buffer"` | Результат содержит `buffer`. |
39
+ | `allowedMime?: readonly string[]` | Точный список разрешённых MIME. |
40
+ | `maxBytes?: number` | Максимальный размер файла в байтах. |
41
+ | `signal?: AbortSignal` | Отменяет чтение через `FileReader.abort()`. |
42
+
43
+ Возвращаемый discriminated union всегда содержит `file` и `meta` (`name`, `mime`, `size`, `lastModified`). После проверки `result.mode` TypeScript раскрывает либо `dataUrl`, либо `buffer`.
44
+
45
+ ```ts
46
+ const result = await readFile(file, {
47
+ mode: "data-url",
48
+ allowedMime: ["application/pdf"],
49
+ maxBytes: 10 * 1024 * 1024,
50
+ signal
51
+ });
52
+
53
+ if (result.mode === "data-url") {
54
+ previewUrl = result.dataUrl;
55
+ }
56
+ ```
57
+
58
+ `ReadFileError` имеет предсказуемый `code`: `NO_FILE`, `MIME_NOT_ALLOWED`, `FILE_TOO_LARGE`, `READ_ABORTED` или `READ_FAILED`. Обрабатывайте его как ожидаемую ошибку выбора файла, а остальные ошибки не скрывайте.
59
+
60
+ ### `readImageFile`
61
+
62
+ ```ts
63
+ readImageFile(image, options?): Promise<ReadImageResult>
64
+ ```
65
+
66
+ Поддерживаемые `ImageMime`: `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/svg+xml`, `image/avif`. В режиме `data-url` результат дополнительно включает `dimensions` (`width`, `height`). В режиме `array-buffer` декодирование изображения не выполняется.
67
+
68
+ `ReadImageError` расширяет коды `ReadFileError` значениями `NOT_AN_IMAGE` и `IMAGE_DECODE_FAILED`.
69
+
70
+ ### Дополнительный API `file`
71
+
72
+ | API | Контракт |
73
+ | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
74
+ | `buildMeta(file)` | Возвращает serializable метаданные `FileMeta`. |
75
+ | `attachAbort(signal, reader)` | Привязывает отмену к `FileReader`; возвращённую функцию cleanup вызывают только custom consumers низкоуровневого API. |
76
+ | `assertNotAborted(signal?)` | Бросает `ReadFileError("READ_ABORTED")`, если сигнал уже отменён. |
77
+ | `assertValidAllowedMime`, `assertValidAccept` | Проверяют конфигурацию accept/MIME до чтения. |
78
+ | `sanitizeFileName(value, fallback?)` | Удаляет unsafe символы из имени. |
79
+ | `encodeFilenameRFC5987(value)` | Кодирует Unicode filename для HTTP header. |
80
+ | `createAsciiFilenameFallback(value, fallback?)` | Создаёт ASCII fallback для старых consumers заголовка. |
81
+ | `createContentDispositionHeader(options)` | Создаёт `inline`/`attachment` Content-Disposition с `filename` и `filename*`. |
82
+
83
+ ## `binary`: base64 → `Blob`
84
+
85
+ ```ts
86
+ import { binaryToBlob, detectMimeType, useBinaryFile } from "@ryuzaki13/react-foundation-lib/binary";
87
+ ```
88
+
89
+ | API | Вход и результат |
90
+ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `binaryToBlob(binaryData, mimeType?, isBase64?)` | Создаёт `Blob` из base64/data URL (по умолчанию) или raw binary string (`false`). Пустые или некорректные данные бросают `Error`. |
92
+ | `detectMimeType(fileName?, binaryData?)` | Ищет MIME по расширению, data URL, затем сигнатуре base64. Неопределённый документ трактует как `application/pdf`. |
93
+ | `useBinaryFile(bin)` | React hook, возвращает `{ data: { blob, mime } \| null, error: string \| null }`. |
94
+
95
+ ```ts
96
+ const mime = detectMimeType("report.pdf", base64);
97
+ const blob = binaryToBlob(base64, mime);
98
+ const objectUrl = URL.createObjectURL(blob);
99
+
100
+ // Владелец UI обязан освободить URL при смене файла/размонтировании.
101
+ URL.revokeObjectURL(objectUrl);
102
+ ```
103
+
104
+ `useBinaryFile` не создаёт object URL и не освобождает его: это lifecycle конкретного preview-компонента. Hook использует `Blob` и `atob`, поэтому не вызывайте его в SSR или worker без совместимого polyfill.
105
+
106
+ ## `excel`: скачивание `.xlsx`
107
+
108
+ ```ts
109
+ import {
110
+ buildExcelAutoFilterRef,
111
+ buildExcelSheetData,
112
+ createTableExcelColumn,
113
+ downloadExcelFile,
114
+ downloadExcelWorkbook,
115
+ downloadResolvedExcelTable,
116
+ downloadResolvedExcelWorkbook,
117
+ resolveExcelCellStyleFromState,
118
+ resolveExcelSheetNames
119
+ } from "@ryuzaki13/react-foundation-lib/excel";
120
+ ```
121
+
122
+ ### Базовый экспорт
123
+
124
+ `downloadExcelFile({ fileName, columns, rows, autoFilter?, getCellStyle? })` создаёт один лист; `downloadExcelWorkbook({ fileName, sheets })` — книгу из нескольких листов. Это asynchronous browser API, который инициирует скачивание через `write-excel-file`.
125
+
126
+ ```ts
127
+ await downloadExcelFile({
128
+ fileName: "users.xlsx",
129
+ columns: [
130
+ { id: "name", header: "Имя", type: String, width: 28 },
131
+ { id: "createdAt", header: "Дата", type: Date, format: "dd.mm.yyyy" }
132
+ ],
133
+ rows: [{ name: "Анна", createdAt: new Date("2026-07-30") }],
134
+ autoFilter: true
135
+ });
136
+ ```
137
+
138
+ | Public type/API | Назначение |
139
+ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
140
+ | `ExcelColumn`, `ExcelRow`, `ExcelCellStyle` | Контракты столбца, строки и стиля ячейки. `type` передаёт в файл истинные `Number`/`Date`/`Boolean`, а не UI-строку. |
141
+ | `buildExcelSheetData(args)` | Строит данные листа без запуска download; подходит для тестируемой подготовки. |
142
+ | `buildExcelAutoFilterRef(columns, rows)` | Даёт Excel-диапазон или `undefined` для пустой сетки. |
143
+ | `resolveExcelSheetNames(names)` | Нормализует ограничения Excel (31 символ, forbidden chars) и делает имена уникальными. |
144
+ | `resolveExcelCellStyleFromState(state)` | Преобразует `State` в базовый цвет текста Excel. |
145
+
146
+ ### Экспорт подготовленной таблицы
147
+
148
+ `TableExcelColumnDescriptor` связывает generic Excel column с OData type/role и необязательным `formattersPipeline`. Используйте `createTableExcelColumn(descriptor)` для получения типизированного столбца, затем передавайте значения строк без предварительного UI-форматирования.
149
+
150
+ | API | Назначение |
151
+ | -------------------------------------------------------------- | --------------------------------------------------------------------------------- |
152
+ | `resolveTableExcelColumnType(descriptor)` | Выбирает `String`/`Number`/`Date`/`Boolean` по OData metadata и роли. |
153
+ | `createTableExcelColumn(descriptor)` | Создаёт `ExcelColumn` с Excel-маской числа/даты из published formatter contracts. |
154
+ | `downloadResolvedExcelTable({ fileName, table, autoFilter? })` | Скачивает одну уже подготовленную таблицу. |
155
+ | `downloadResolvedExcelWorkbook({ fileName, sheets })` | Скачивает несколько `ResolvedExcelTable` как листы книги. |
156
+
157
+ Не передавайте в `rows` отформатированные через `Intl` строки для числовых и date-столбцов: Excel потеряет сортировку, формулы и формат. Строки создавайте только для действительно текстовых значений.
158
+
159
+ ## Ограничения
160
+
161
+ - Все три entrypoint ориентированы на DOM/browser; не вызывайте их в server-only коде.
162
+ - MIME по имени/сигнатуре не является проверкой безопасности содержимого. Сервер обязан валидировать загружаемые файлы повторно.
163
+ - `download*` имеет пользовательский side effect. Вызывайте его из event handler, а не из render/effect при монтировании.
@@ -0,0 +1,455 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Formatters Runtime" />
4
+
5
+ # Runtime форматтеров: pipeline, row-based и value state
6
+
7
+ Эта страница описывает конфигурируемый runtime из
8
+ `@ryuzaki13/react-foundation-lib/formatters`. Он предназначен для host-кода,
9
+ который хранит сериализуемую настройку отображения, а затем компилирует её для
10
+ ячейки таблицы, дерева или другого набора данных. Конфигурация и runtime —
11
+ разные границы: в конфигурации нет функций, а скомпилированные executors нельзя
12
+ сохранять в JSON, query cache или передавать между runtime-контекстами.
13
+
14
+ ```ts
15
+ import {
16
+ compileFormattersPipelineExecutor,
17
+ formatPipelineDisplayValue,
18
+ validateFormattersPipelineConfig
19
+ } from "@ryuzaki13/react-foundation-lib/formatters";
20
+
21
+ import type {
22
+ FormattersPipelineConfig,
23
+ FormattersPipelineRuntimeField,
24
+ RowBasedFormatterDefinition,
25
+ ThresholdValueStateResolverConfig
26
+ } from "@ryuzaki13/react-foundation-lib/formatters";
27
+ ```
28
+
29
+ ## Модель данных и жизненный цикл
30
+
31
+ `FormattersPipelineConfig` имеет фиксированную версию `1` и содержит хотя бы
32
+ один источник плана:
33
+
34
+ ```ts
35
+ type FormattersPipelineConfig = {
36
+ version: 1;
37
+ graph?: FormattersPipelineGraph;
38
+ plan?: FormattersPipelinePlan;
39
+ };
40
+ ```
41
+
42
+ | Представление | Для чего хранится | Содержимое |
43
+ | -------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------- |
44
+ | `graph` | Редактор настройки | Узлы, рёбра и необязательный `viewport`. Граф обязан быть линейным от `source` к `sink`. |
45
+ | `plan` | Быстрое выполнение | Массив последовательных шагов без координат. Если он валиден, у него приоритет над `graph`. |
46
+ | `FormattersPipelineExecutor` | Только runtime | Функция `execute` и флаги. Появляется после компиляции, не сериализуется. |
47
+ | `FormattersPipelineRuntimeField` | Construction-stage объекта поля | Содержит пользовательский конфиг и может получить `formulaExecutor` / `formattersPipelineExecutor`. |
48
+
49
+ Безопасный порядок работы:
50
+
51
+ 1. Создать или восстановить конфиг как обычные JSON-данные.
52
+ 2. Для `rowBasedOverride` с `mode: "formula"` один раз настроить реестр
53
+ host-формул **до** validation и compilation.
54
+ 3. Проверить `validateFormattersPipelineConfig`; неизвестный или устаревший
55
+ конфиг не компилировать.
56
+ 4. На явной границе восстановления можно вызвать
57
+ `normalizeFormattersPipelineConfig` и положить полученную независимую копию
58
+ в модель.
59
+ 5. В construction-stage скомпилировать поле или карту полей.
60
+ 6. При отрисовке вызвать `formatPipelineDisplayValue` либо `executor.execute`.
61
+
62
+ Не нормализуйте draft при каждом вводе. `clone…` создаёт независимую копию, а
63
+ `normalize…` — проверяет внешнее `unknown` значение и допустим только на
64
+ явной границе восстановления/сохранения конфигурации.
65
+
66
+ ## Минимальный сериализуемый pipeline
67
+
68
+ ```ts
69
+ import {
70
+ compileFormattersPipelineRuntime,
71
+ formatPipelineDisplayValue,
72
+ validateFormattersPipelineConfig
73
+ } from "@ryuzaki13/react-foundation-lib/formatters";
74
+
75
+ import type { FormattersPipelineConfig } from "@ryuzaki13/react-foundation-lib/formatters";
76
+
77
+ const config = {
78
+ version: 1,
79
+ plan: {
80
+ steps: [
81
+ {
82
+ id: "pad-code",
83
+ type: "normalizeLeadingZeros",
84
+ config: { fixed: 5 }
85
+ },
86
+ {
87
+ id: "score-state",
88
+ type: "resolveValueState",
89
+ config: {
90
+ resolver: {
91
+ kind: "threshold",
92
+ thresholds: [60, 95],
93
+ states: ["warning", "success", "error"]
94
+ },
95
+ icon: { enabled: true, showValue: true, position: "left" }
96
+ }
97
+ },
98
+ {
99
+ id: "typed-value",
100
+ type: "typedValueFormat",
101
+ config: { numberPresetName: "decimal-2" }
102
+ }
103
+ ]
104
+ }
105
+ } satisfies FormattersPipelineConfig;
106
+
107
+ const validation = validateFormattersPipelineConfig(config);
108
+ if (!validation.ok) {
109
+ throw new Error(validation.errors.map((item) => item.message).join("\n"));
110
+ }
111
+
112
+ const field = {
113
+ id: "score",
114
+ role: "measure",
115
+ type: "decimal",
116
+ formattersPipeline: config
117
+ };
118
+
119
+ // Важно: эта функция записывает executors в сам объект field.
120
+ compileFormattersPipelineRuntime(field);
121
+
122
+ const display = formatPipelineDisplayValue({
123
+ field,
124
+ rawValue: "63.4",
125
+ rowData: { score: "63.4" },
126
+ rowKind: "plain"
127
+ });
128
+
129
+ // display.value === "63,40"
130
+ // display.state === "success"
131
+ ```
132
+
133
+ ## Шаги pipeline
134
+
135
+ `FormattersPipelineFormatterId` допускает только четыре значения.
136
+
137
+ | Шаг | Конфигурация | Выполнение и fallback |
138
+ | ----------------------- | ------------------------------------------- | ------------------------------------------------------------- |
139
+ | `normalizeLeadingZeros` | `{ fixed?: number }` | Передаёт текущее значение в `normalizeLeadingZeros`. |
140
+ | `rowBasedOverride` | field- или formula-вариант ниже | Подменяет текущее значение данными строки. |
141
+ | `resolveValueState` | fixed/threshold resolver и настройки иконки | Вычисляет `state`, `showIcon`, `showValue`, `iconPosition`. |
142
+ | `typedValueFormat` | `{ numberPresetName?, datePresetName? }` | Форматирует итог по OData-type поля: number, date или строка. |
143
+
144
+ Ограничения валидатора:
145
+
146
+ - В плане разрешён не более чем один шаг каждого вида.
147
+ - `typedValueFormat` не может идти раньше `resolveValueState`.
148
+ - У каждого шага должен быть непустой уникальный `id`.
149
+ - Для threshold-resolver длина `states` обязана равняться
150
+ `thresholds.length + 1`.
151
+ - `showValue: false` при выключенной иконке — warning: ячейка не покажет
152
+ значение.
153
+ - В графе должны быть ровно один `source` и один `sink`; каждый путь должен
154
+ быть линейным, без циклов, разветвлений, оборванных и отключённых узлов.
155
+
156
+ `FormattersPipelineExecutionContext` получает исходное `value`, `rowData`,
157
+ `rowKind`, уровень/группировки и `columnId`. Новый код определяет семантику
158
+ строки через `rowKind: "plain" | "tree" | "group" | "totals"` — поля
159
+ `isGroupRow` и `isTotalsRow` в этом контексте оставлены только для
160
+ совместимости.
161
+
162
+ `executor.execute(ctx)` возвращает `FormattersPipelineExecutionResult`:
163
+ `value`, `state`, опциональную `icon`, `showIcon`, `showValue`,
164
+ `iconPosition`, `hasTypedValueFormat`.
165
+
166
+ ### rowBasedOverride
167
+
168
+ ```ts
169
+ type FormattersPipelineRowBasedOverrideConfig =
170
+ | {
171
+ mode: "field";
172
+ fieldKey: string;
173
+ fallbackToRaw?: boolean;
174
+ }
175
+ | {
176
+ mode: "formula";
177
+ formulaId: string;
178
+ dependencyIds?: string[];
179
+ fallbackToRaw?: boolean;
180
+ };
181
+ ```
182
+
183
+ | Режим | Источник значения | Семантика по типу строки |
184
+ | --------- | ---------------------------------- | ----------------------------------------------------------------- |
185
+ | `field` | `rowData[fieldKey]` | Работает для `plain`, `tree`, `group`; не применяется к `totals`. |
186
+ | `formula` | `fn(context)` из row-based реестра | Работает для `plain`, `tree`, `group` и `totals`. |
187
+
188
+ Если подмена возвращает `null`/`undefined`, `fallbackToRaw` по умолчанию
189
+ сохраняет предыдущее значение. При `fallbackToRaw: false` это значение
190
+ допускается как новый результат. Ошибка formula в executor не пробрасывается:
191
+ при default fallback остаётся прежнее значение, а при `false` получается
192
+ `undefined`.
193
+
194
+ `dependencyIds` — упорядоченные ключи, которые formula читает по индексам.
195
+ Их необходимо запросить вместе с основной строкой: для этого используйте
196
+ `collectFormattersPipelineDependencyIds` или
197
+ `collectRuntimeFieldDependencyIds` до построения query.
198
+
199
+ ## Реестр row-based формул
200
+
201
+ В конфиге хранится только `formulaId`; реальная функция существует только в
202
+ runtime host-приложения. Реестр глобален для экземпляра модуля, поэтому его
203
+ надо установить один раз при bootstrap, до первого вызова валидации или
204
+ компиляции pipeline с формульным шагом.
205
+
206
+ ```ts
207
+ import { configureRowBasedFormatterRegistry, createRowBasedFormatterRegistry } from "@ryuzaki13/react-foundation-lib/formatters";
208
+
209
+ configureRowBasedFormatterRegistry(
210
+ createRowBasedFormatterRegistry([
211
+ {
212
+ id: "sum-dependencies",
213
+ name: "Сумма зависимостей",
214
+ description: "Складывает первые два поля строки.",
215
+ fn: ({ num }) => num(0) + num(1)
216
+ }
217
+ ])
218
+ );
219
+ ```
220
+
221
+ | API | Контракт |
222
+ | ---------------------------------------------- | ---------------------------------------------------------------------------------------- |
223
+ | `createRowBasedFormatterRegistry(definitions)` | Возвращает immutable registry. Бросает `Error` для пустого `id` или дубликата. |
224
+ | `configureRowBasedFormatterRegistry(registry)` | Подменяет активный module-scoped реестр. Не вызывайте повторно в обычном rendering flow. |
225
+ | `getRowBasedFormatterList()` | Список доступных host-формул. |
226
+ | `getRowBasedFormatterById(id)` | Формула или `undefined` для пустого/неизвестного id. |
227
+ | `createRowBasedFormatterContext(args)` | Создаёт контекст для прямого запуска formula или теста. |
228
+
229
+ `RowBasedFormatterDefinition` содержит `id`, `name`, `description`,
230
+ `fn`. `RowBasedFormatterContext` содержит `rowData`, `rawValue`, `columnId`
231
+ и методы:
232
+
233
+ | Метод | Результат |
234
+ | -------------- | --------------------------------------------------------------------------- |
235
+ | `key(index)` | ID зависимости или `undefined` для некорректного/вне диапазона индекса. |
236
+ | `value(index)` | Значение `rowData` зависимости или `undefined`. |
237
+ | `num(index)` | `parseNumber` значения зависимости; при отсутствии/невалидном значении `0`. |
238
+
239
+ Формула должна быть синхронной и не должна полагаться на DOM, React state или
240
+ неявные внешние данные. Валидация запускает её на тестовых положительных и
241
+ отрицательных значениях, чтобы обнаружить недостающие/неиспользуемые
242
+ `dependencyIds` и потенциальную runtime-ошибку. Это диагностический прогон, а
243
+ не гарантия корректности для всех данных.
244
+
245
+ ## Проверка, нормализация, копирование и новые ID
246
+
247
+ | API | Когда вызывать | Результат |
248
+ | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
249
+ | `validateFormattersPipelinePlan(plan)` | Перед сохранением/компиляцией готового линейного плана. | `FormattersPipelineValidationResult` с `ok`, `errors`, `warnings` и безопасным `plan` только при успехе. |
250
+ | `validateFormattersPipelineGraph(graph)` | Для editor graph. | Проверяет структуру и строит линейный `plan`. |
251
+ | `validateFormattersPipelineConfig(config)` | Основная проверка конфигурации. | Валидный `plan` имеет приоритет; если он невалиден и есть `graph`, выполняется попытка построения по графу с warning. |
252
+ | `normalizeFormattersPipelineConfig(unknownValue)` | На внешней restore/save boundary. | Возвращает валидный независимый `FormattersPipelineConfig` или `undefined`; не исправляет невалидные данные «по умолчанию». |
253
+ | `cloneFormattersPipelineConfig(config)` | Для literal draft, setter или безопасного переноса конфигурации. | Глубокая независимая копия или `undefined`; не валидирует и не нормализует. |
254
+ | `cloneTypedValueFormatConfig`, `cloneNormalizeLeadingZerosConfig`, `cloneRowBasedOverrideConfig`, `cloneResolveValueStateConfig` | Для узких UI-операций редактора. | Копия соответствующего вложенного конфига. |
255
+ | `cloneFormatterPipelinePlanStep`, `cloneFormatterPipelinePlanStepToNode` | При преобразовании plan-шага в editor node. | Независимый step или node с заданной позицией. |
256
+ | `rekeyFormattersPipelineConfig(config, idFactory?)` | Только при создании независимой копии для нового редактора/сущности. | Переиздаёт технические id пользовательских step/node и рёбер; `source`/`sink` сохраняют id. По умолчанию использует UUID. |
257
+
258
+ `FormattersPipelineValidationMessage` содержит машинный `code` и русское
259
+ `message`; не распознавайте проблему по тексту сообщения. Для UI удобно
260
+ показывать `errors`, а `warnings` отображать как необязательные рекомендации.
261
+
262
+ Каталог узлов можно получить без дублирования строк интерфейса:
263
+
264
+ ```ts
265
+ import { getFormattersPipelineDefinitionById, getFormattersPipelineDefinitions } from "@ryuzaki13/react-foundation-lib/formatters";
266
+
267
+ getFormattersPipelineDefinitions();
268
+ getFormattersPipelineDefinitionById("typedValueFormat");
269
+ ```
270
+
271
+ `getFormattersPipelineDefinitions()` и
272
+ `getFormattersPipelineDefinitionById(id)` — публичный способ получить
273
+ readonly-каталог и описание конкретного вида форматтера.
274
+
275
+ ## Компиляция и отображение
276
+
277
+ ### Низкоуровневый executor
278
+
279
+ ```ts
280
+ import { compileFormattersPipelineExecutor, formatTypedCellValue } from "@ryuzaki13/react-foundation-lib/formatters";
281
+
282
+ const compiled = compileFormattersPipelineExecutor({
283
+ config,
284
+ column: { role: "measure", type: "decimal" }
285
+ });
286
+
287
+ if (compiled.ok) {
288
+ compiled.executor.execute({
289
+ value: 12.5,
290
+ rowData: {},
291
+ rowKind: "plain",
292
+ isGroupRow: false,
293
+ isTotalsRow: false,
294
+ rowLevel: 0,
295
+ groupingIds: [],
296
+ columnId: "amount"
297
+ });
298
+ }
299
+
300
+ formatTypedCellValue(12.5, { role: "measure", type: "decimal" });
301
+ // "12,5" — default number preset "decimal"
302
+ ```
303
+
304
+ `compileFormattersPipelineExecutor` повторно валидирует config и возвращает
305
+ либо `{ ok: true, executor }`, либо `{ ok: false, reason }`. Причины ошибки:
306
+ `"pipeline_invalid"` и `"row_based_formula_not_found"`. В последнем случае
307
+ надо исправить порядок bootstrap и зарегистрировать formula, а не подменять
308
+ её локальной функцией в сериализуемом конфиге.
309
+
310
+ `formatTypedCellValue(value, column, config?)` работает и без pipeline:
311
+
312
+ | Базовый OData-type | Поведение |
313
+ | --------------------- | ------------------------------------------------------- |
314
+ | Число | `formatNumber`; default preset `decimal`. |
315
+ | Дата | `parseDate`, затем `formatDate`; default preset `date`. |
316
+ | Остальное | `toSafeString`. |
317
+ | Небезопасное значение | Пустая строка. |
318
+
319
+ Для прямого вызова неизвестное имя number/date-пресета заменяется default
320
+ пресетом. В полноценном pipeline валидатор отдельно делает неизвестный или
321
+ пустой `numberPresetName` ошибкой; неизвестный `datePresetName` в runtime
322
+ безопасно даёт preset `date`.
323
+
324
+ ### Runtime полей
325
+
326
+ | API | Важное свойство |
327
+ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
328
+ | `compileFormattersPipelineRuntime(field)` | Компилирует `formulaExecutor` и `formattersPipelineExecutor` и **мутирует переданный `field`**. Допустим только до помещения поля в readonly snapshot, memoized context или cache. |
329
+ | `compileFormattersPipelineRuntimeFields(fieldsById)` | Компилирует каждое существующее поле; также не пересобирает и не копирует сами поля. |
330
+ | `formatPipelineDisplayValue(args)` | Возвращает `FormattersPipelineDisplayValue`, пригодный для UI: значение, state, настройки иконки и `overflowTooltip`. |
331
+
332
+ Перед pipeline `formatPipelineDisplayValue` при необходимости выполняет
333
+ `field.formulaExecutor`. Если формула отсутствует/ошиблась, `purelyDerived`
334
+ поле отображается пустым, а обычное поле возвращается к `rawValue`. После
335
+ pipeline fallback-форматирование по типу применяется автоматически, если в
336
+ плане не было `typedValueFormat`. `emptyWhenZero` скрывает нулевой результат
337
+ исходной формулы/значения как пустую строку.
338
+
339
+ `formulaId` и `formulaDependencies` относятся к реестру формул host-проекта:
340
+ необходимая формула должна быть уже доступна до `compile…Runtime`, а её
341
+ зависимости нужно включить в данные строки.
342
+
343
+ ## Планирование зависимостей данных
344
+
345
+ ```ts
346
+ import { collectFormattersPipelineDependencyIds, collectRuntimeFieldDependencyIds } from "@ryuzaki13/react-foundation-lib/formatters";
347
+
348
+ const dependencies = collectFormattersPipelineDependencyIds(config);
349
+ // Только fieldKey/dependencyIds из rowBasedOverride.
350
+
351
+ const runtimePlan = collectRuntimeFieldDependencyIds(["score"], {
352
+ score: {
353
+ formattersPipeline: config,
354
+ cellRenderer: { bindings: { caption: "score-label" } }
355
+ },
356
+ "score-label": {}
357
+ });
358
+ // runtimePlan.requiredColumnIds / runtimePlan.missingColumnIds
359
+ ```
360
+
361
+ `collectFormattersPipelineDependencyIds(config)` возвращает только уникальные
362
+ IDs из valid `rowBasedOverride` в порядке первого появления.
363
+ `collectRuntimeFieldDependencyIds(fieldIds, fieldsById)` дополнительно
364
+ учитывает `cellRenderer.bindings`, отделяя известные IDs в
365
+ `requiredColumnIds` от отсутствующих в `missingColumnIds`. Функции не делают
366
+ query сами: host обязан добавить `requiredColumnIds` в запрос до исполнения
367
+ pipeline.
368
+
369
+ ## Value state
370
+
371
+ `State` допускает `""`, `"none"`, `"information"`, `"success"`,
372
+ `"warning"`, `"error"`. Resolver сопоставляет значению одно из этих состояний
373
+ и хранится в global registry данного module instance.
374
+
375
+ ### Fixed resolver
376
+
377
+ ```ts
378
+ import { createFixedResolver, registerFixedResolver, resolveValueState } from "@ryuzaki13/react-foundation-lib/formatters";
379
+
380
+ const resolverId = registerFixedResolver({
381
+ entries: { A: "success", B: "warning", C: "error" },
382
+ fallbackState: "none"
383
+ });
384
+
385
+ resolveValueState(resolverId, "B"); // "warning"
386
+
387
+ const resolve = createFixedResolver({ entries: { A: "success" } });
388
+ resolve("A"); // "success"
389
+ ```
390
+
391
+ `FixedValueStateResolverConfig` содержит `entries: Record<string, State>` и
392
+ необязательный `fallbackState` (по умолчанию `"none"`). Сопоставление точное
393
+ по `String(value)`; `null` и `undefined` сразу дают fallback. Идентичные по
394
+ содержимому конфигурации дедуплицируются, поэтому регистрация возвращает один
395
+ и тот же id.
396
+
397
+ ### Threshold resolver
398
+
399
+ ```ts
400
+ import { createThresholdResolver, registerThresholdResolver } from "@ryuzaki13/react-foundation-lib/formatters";
401
+
402
+ const resolve = createThresholdResolver({
403
+ thresholds: [{ value: 60, boundary: "lower" }, 95],
404
+ states: ["warning", "success", "error"],
405
+ invalidState: "none"
406
+ });
407
+
408
+ resolve(60); // "warning": boundary "lower" использует <=
409
+ resolve(95); // "error": числовой threshold по умолчанию использует <
410
+ ```
411
+
412
+ `ThresholdValueStateResolverConfig`:
413
+
414
+ | Поле | Правило |
415
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
416
+ | `thresholds` | Числа либо `{ value, boundary? }`; `boundary: "upper"` по умолчанию означает, что порог относится к верхнему сегменту (`<` для нижнего). `"lower"` включает его в нижний сегмент (`<=`). |
417
+ | `states` | Ровно `thresholds.length + 1` состояний в порядке диапазонов от меньших значений к большим. Иначе `create…`/`register…` бросит `Error`. |
418
+ | `invalidState` | Результат для пустого, `NaN`, бесконечного или нечислового значения; default `"none"`. |
419
+
420
+ Пороги сортируются перед compilation, но массив `states` не переставляется.
421
+ Передавайте `thresholds` сразу по возрастанию, чтобы соответствие сегментов и
422
+ states было однозначным. Для дублирующихся порогов библиотека выводит
423
+ `console.warn`, но продолжает работу.
424
+
425
+ ### Реестр и presentation helpers
426
+
427
+ | API | Поведение / side effect |
428
+ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
429
+ | `registerFixedResolver`, `registerThresholdResolver` | Регистрируют конфигурационный resolver и возвращают его детерминированный короткий id. |
430
+ | `createFixedResolver`, `createThresholdResolver` | Возвращают функцию resolver; также используют общий registry и дедупликацию. |
431
+ | `resolveValueState(id, value)` | Возвращает `"none"`, если id не найден. |
432
+ | `getValueStateResolver(id)`, `getValueStateResolverIds()` | Читают resolver или список ids. |
433
+ | `registerResolver(canonicalKey, compileFunction)`, `findResolverByCanonicalKey`, `computeShortHash` | Низкоуровневый API для специальных host-резолверов. `compileFunction` — runtime-функция, её нельзя хранить в config. |
434
+ | `resetValueStateResolvers()` | Очищает global registry. Только для изолированных тестов: сбросит resolvers, созданные скомпилированными pipeline. |
435
+ | `DEFAULT_VALUE_STATES` | Readonly список presentation-состояний без пустого значения. |
436
+ | `VALUE_STATE_COLOR_TOKENS` | Соответствие `State` CSS token. Не меняет DOM и не подключает стили. |
437
+ | `resolveValueStateClassName(state, fallbackClassName?)` | Для `success` / `warning` / `error` / `information` возвращает `status…`; для остальных — fallback. |
438
+
439
+ ## Runtime-ограничения
440
+
441
+ - Реестры row-based formatter-ов и value-state resolver-ов — module-scoped
442
+ mutable state. Инициализируйте их один раз до compilation и не меняйте во
443
+ время рендера.
444
+ - Config должен оставаться сериализуемым: IDs, строки, числа, массивы и
445
+ plain objects. Не сохраняйте `fn`, executor, `Map`, `Date`, React node или
446
+ registry в конфигурации.
447
+ - `compileFormattersPipelineRuntime` меняет входной объект. Сначала создайте
448
+ обычный mutable construction object, затем скомпилируйте его и только после
449
+ этого публикуйте readonly snapshot.
450
+ - `resetDatePresets`, `resetNumberPresets`, `resetValueStateResolvers` и
451
+ повторная настройка row-based registry предназначены для тестов/явно
452
+ изолированных окружений, а не для обычного пользовательского сценария.
453
+ - Ошибки validation следует показывать владельцу конфигурации. Не
454
+ компилируйте невалидный config и не маскируйте missing formula пустой
455
+ локальной реализацией.