diff --git a/tools/sp/README.md b/tools/sp/README.md index 6e24608..a9148c1 100644 --- a/tools/sp/README.md +++ b/tools/sp/README.md @@ -13,10 +13,14 @@ пользователь вводит его в браузере сам, дальше CLI for Microsoft 365 держит токен у себя. Ни пароля, ни client secret в репозитории и в командах нет. -**Работаем от имени пользователя, а не приложения.** Это не удобство, а требование: -SharePoint Search REST (`/_api/search/query`) под app-only-токеном обычно не отдаёт -ничего, ему нужен пользовательский контекст. А Search — главное, что здесь измеряют. -Побочный плюс: видно ровно то же, что видит пользователь в портале. +**Работаем от имени пользователя, а не приложения.** Главное, что это даёт: видно +ровно то же, что видит пользователь в портале, — а у заявок портала это принципиально, +потому что права разорваны НА КАЖДОМ элементе (см. «Грабли» ниже). + +Раньше здесь было написано, что Search под app-only-токеном «обычно не отдаёт ничего». +Проверено 2026-08-28 на сертификате `CN=PnP-SharePoint-Sync` (тот же, что у бэкенда +`tnsu-approval-route`): **отдаёт** — и `postquery`, и кастомные managed properties. +Так что app-only Search как таковой рабочий; ограничения у него другие, см. «Грабли». ## Разовая подготовка @@ -96,3 +100,32 @@ claims-логине частей будет четыре. Разбирать с партиций и в 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` — только +доступное вам.