Files
request-portal/tools/sp/README.md
T
gorbenko.pavel f7979ac534 docs(tools/sp): грабли поискового индекса и прав на элементах
Проверено на тенанте 2026-08-28, при разборе «перевести отчёт по вакансиям
на Search или нет»:

1. Часть полей индекс не отдаёт ВООБЩЕ, хотя значения в списках есть
   (OpenReason/OpenReasonDetails, CandidateName/HireDate/ReplacedEmployee,
   Ticket). Рядом в тех же списках PositionOWSTEXT, RequestStatusOWSCHCS,
   SelectionRequestIdOWSNMBR и др. отдаются нормально — то есть дело не в
   типе колонки и не в задержке crawl. Добавлена таблица с тем, какие имена
   уже перепробованы, чтобы не ходить по кругу.

2. Отличить «managed property нет» от «есть, но пусто» через SelectProperties
   нельзя — на несуществующее имя молча приходит null. Отличается сортировкой:
   SortList по несуществующему свойству падает 400/500.

3. Права у заявок портала разорваны на КАЖДОМ элементе
   (HasUniqueRoleAssignments = true), поэтому под своей учёткой в списке видно
   2 элемента из 19 — не баг и не кэш браузера. App-only это не трогает.
   Отсюда же: ItemCount списка считает всё, /items — только доступное вам.

Заодно исправлено утверждение «Search под app-only обычно ничего не отдаёт» —
на сертификате CN=PnP-SharePoint-Sync отдаёт, и postquery, и кастомные
managed properties.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 14:10:36 +05:00

132 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `tools/sp` — запросы к SharePoint портала заявок
Инструмент для **разбора**, а не для правки данных: задать тенанту вопрос и получить
ответ текстом, который можно приложить к задаче.
Появился из-за производительности: раз за разом нужно было выяснить «а что реально
отдаёт поисковый индекс?», «сколько на самом деле партиций у типа заявки?» — и каждый
раз это была ручная работа со снипетом в консоли браузера и копипастой результата.
## Что важно понимать про доступ
**Секреты через инструмент не проходят.** Вход — device code: команда печатает код,
пользователь вводит его в браузере сам, дальше CLI for Microsoft 365 держит токен у
себя. Ни пароля, ни client secret в репозитории и в командах нет.
**Работаем от имени пользователя, а не приложения.** Главное, что это даёт: видно
ровно то же, что видит пользователь в портале, — а у заявок портала это принципиально,
потому что права разорваны НА КАЖДОМ элементе (см. «Грабли» ниже).
Раньше здесь было написано, что Search под app-only-токеном «обычно не отдаёт ничего».
Проверено 2026-08-28 на сертификате `CN=PnP-SharePoint-Sync` (тот же, что у бэкенда
`tnsu-approval-route`): **отдаёт** — и `postquery`, и кастомные managed properties.
Так что app-only Search как таковой рабочий; ограничения у него другие, см. «Грабли».
## Разовая подготовка
CLI for Microsoft 365 начиная с 7-й версии **не возит собственное приложение** — нужен
свой Entra app. Регистрируется один раз на всю команду.
1. Entra admin center → **App registrations** → **New registration**
- Name: `Portal Tools CLI`
- Supported account types: **Single tenant**
- Redirect URI: **Public client/native** → `http://localhost`
2. **Authentication** → Advanced settings → **Allow public client flows** → **Yes**
(без этого device code не работает)
3. **API permissions** → Add a permission → **SharePoint** → **Delegated**:
- `AllSites.Read` — чтение списков
- `Sites.Search.All` — поисковый индекс
4. **Grant admin consent** (нужен админ тенанта)
5. Скопировать **Application (client) ID** и **Directory (tenant) ID**
Дальше — переменные окружения (PowerShell, на сессию):
```powershell
$env:SP_APP_ID = "<application-client-id>"
$env:SP_TENANT = "<directory-tenant-id>"
```
`SP_SITE` по умолчанию `https://tnsukz.sharepoint.com/sites/requests` — тот же адрес,
что в `src/webparts/requestPortal/shared/config.ts`. Задавать нужно, только если
списки типов заявок живут на другом сайте.
## Использование
```bash
cd tools/sp
npm install # один раз
node sp.mjs login # покажет код — ввести его в браузере
node sp.mjs status
```
| команда | зачем |
| --- | --- |
| `node sp.mjs props` | отдаёт ли индекс поля заявки (`Retrievable`) — от этого зависит, можно ли убрать REST-дочитывание в `loadByIds` |
| `node sp.mjs lists` | все портальные списки с размерами и адресами — сколько реально партиций |
| `node sp.mjs search "<KQL>" --select a,b --rows N` | тот же поисковый запрос, что делает портал |
| `node sp.mjs get "/_api/web/..."` | произвольный GET, когда отдельной команды нет |
Примеры:
```bash
# Свои заявки глазами индекса — ровно как их видит searchOwnRequests
node sp.mjs search 'AuthorOWSUSER:"i:0#.f|membership|user@tnsu.kz"' \
--select ListItemID,Title,RequestStatusOWSCHCS,Created --rows 5
# Сколько элементов в каждой партиции
node sp.mjs lists
```
## Грабли, на которые уже наступали
**`Queryable` ≠ `Retrievable`.** У managed property это разные флаги. То, по чему
получается фильтровать, не обязано приходить в ответе. Проверка от 2026-08-17
подтвердила, что по `RequestStatusOWSCHCS` можно фильтровать, и это НЕ было
доказательством, что его можно получить (проверено отдельно 2026-08-20 — можно).
Команда `props` печатает `<нет в ответе>` и `<null>` разными пометками именно
поэтому: схлопывать их в пустую строку — значит потерять ответ на вопрос.
**Имена managed properties нестандартные.** Суффикс — по типу исходной колонки:
`OWSTEXT` (Text), `OWSCHCS` (Choice), `OWSUSER` (Person/Group). У колонок из группы
колонок сайта добавляется префикс группы — поэтому «ожидаемого» `ApproverOWSUSER` в
схеме нет, а есть `TansuSiteApproverOWSUSER` (разбирались 2026-08-06). Актуальные
имена — в константах `MP_*` в `features/myRequests/myRequests.service.ts`.
**`AuthorOWSUSER` — это не почта.** Приходит строкой `почта | ФИО`, а при
claims-логине частей будет четыре. Разбирать сплитом по первому элементу нельзя.
**POST `postquery`, а не GET `query`.** KQL портала перечисляет через `OR` адреса всех
партиций и в query-строке упирается в предел длины URL. Форма тела взята один в один
из PnPjs (`@pnp/sp/search/query.js`): `{ request: { ...query } }`, массивы плоские,
`odata=nometadata`. Иначе инструмент мерил бы не то, что портал.
**Часть полей индекс не отдаёт ВООБЩЕ, хотя значения в списках есть** (проверено
2026-08-28 поштучным сравнением Search и REST на одних и тех же элементах):
| Список | Поле | Пробовали имена |
| --- | --- | --- |
| Заявка на подбор | `OpenReason`, `OpenReasonDetails` | `…OWSCHCS`, `…OWSMTXT`, `…OWSTEXT`, с префиксом `TansuSite` |
| Прием на работу | `CandidateName`, `HireDate`, `ReplacedEmployee` | `…OWSTEXT`, `…OWSDATE`, с префиксом `TansuSite` |
| — согласование | `Ticket` (Lookup) | `TicketOWSTEXT`, `TicketOWSLKID`, `TicketId`, `Ticket` |
Отличить «managed property нет вовсе» от «есть, но у строк пусто» через
`SelectProperties` НЕЛЬЗЯ — на несуществующее имя ответ молча приходит с `null`.
Отличается сортировкой: `SortList` по несуществующему свойству падает ошибкой
400/500, по существующему — работает.
При этом рядом в тех же списках нормально отдаются `PositionOWSTEXT`,
`RequestStatusOWSCHCS`, `SelectionRequestIdOWSNMBR`, `SelectionRequestListIdOWSTEXT`,
`SubmitDateOWSDATE`, `TansuSiteApproverOWSUSER` — то есть это не про тип колонки и не
про задержку crawl (у свежесозданных элементов рабочие поля появляются, а эти нет).
Из-за этого отчёт по вакансиям в `tnsu-approval-route` остался на items REST по
партициям, а не на Search — подробный расчёт в doc-комментарии `VacancyReportService`.
**Права разорваны на КАЖДОМ элементе.** У заявок портала
`HasUniqueRoleAssignments = true` поштучно: доступ выдан только участникам заявки плюс
общему админу. Поэтому под своей учёткой в списке видно 2 элемента из 19 — и это не
баг и не кэш браузера, а модель доступа. App-only токен (сертификат) это не трогает и
читает всё. Если сравниваете «сколько элементов в списке», обязательно фиксируйте, под
кем смотрите: `ItemCount` в метаданных списка считает ВСЁ, а `/items` — только
доступное вам.