оптимизация
This commit is contained in:
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
name: sp-query
|
||||||
|
description: Спросить у SharePoint-тенанта реальные данные портала заявок через tools/sp — что отдаёт поисковый индекс (managed properties, Retrievable), сколько партиций и элементов в списках, произвольный KQL или REST. Использовать, когда ответ зависит от состояния тенанта, а не выводится из кода: «отдаётся ли поле через Search», «сколько на самом деле партиций», «почему запрос возвращает пусто», «проверь на живых данных». Не использовать для правки данных.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Запросы к SharePoint портала заявок
|
||||||
|
|
||||||
|
## Когда это нужно
|
||||||
|
|
||||||
|
Рассуждать по коду можно долго и неправильно. Есть класс вопросов, где ответ живёт
|
||||||
|
только в тенанте:
|
||||||
|
|
||||||
|
- отдаёт ли индекс конкретное поле (`Retrievable`) — от этого зависит, можно ли
|
||||||
|
заменить REST-дочитывание на `SelectProperties`;
|
||||||
|
- сколько на самом деле партиций у типа заявки и сколько в них элементов (это
|
||||||
|
переменная `P` во всех оценках числа запросов);
|
||||||
|
- почему KQL возвращает пусто — не то имя managed property, не тот `Path`, нет прав;
|
||||||
|
- как выглядит реальное значение поля (`AuthorOWSUSER` — не почта, а `почта | ФИО`).
|
||||||
|
|
||||||
|
Если вопрос из этого списка — не гадай и не проси пользователя лезть в консоль
|
||||||
|
браузера. Спроси тенант.
|
||||||
|
|
||||||
|
## Порядок действий
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tools/sp
|
||||||
|
node sp.mjs status # сначала: есть ли вход вообще
|
||||||
|
```
|
||||||
|
|
||||||
|
`Logged out` → нужен вход. Он **интерактивный**: команда печатает код, который
|
||||||
|
пользователь вводит в браузере сам. Запускать `login` в фоне бессмысленно — попроси
|
||||||
|
пользователя выполнить его и вернуться.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node sp.mjs login
|
||||||
|
```
|
||||||
|
|
||||||
|
Если ругается на `SP_APP_ID` — регистрация приложения не сделана, отправь пользователя
|
||||||
|
в `tools/sp/README.md`, раздел «Разовая подготовка» (это разовая задача с admin
|
||||||
|
consent, сам её не закроешь).
|
||||||
|
|
||||||
|
Дальше:
|
||||||
|
|
||||||
|
| команда | что отвечает |
|
||||||
|
| --- | --- |
|
||||||
|
| `node sp.mjs props` | отдаёт ли индекс поля заявки; `<нет в ответе>` / `<null>` = не Retrievable |
|
||||||
|
| `node sp.mjs lists` | портальные списки, их адреса и размеры — реальное число партиций |
|
||||||
|
| `node sp.mjs search "<KQL>" --select a,b --rows N` | тот же запрос, что шлёт портал |
|
||||||
|
| `node sp.mjs get "/_api/web/..."` | произвольный GET |
|
||||||
|
|
||||||
|
## Что нельзя
|
||||||
|
|
||||||
|
- **Не вводить пароли и секреты.** Вход только device code: код вводит пользователь.
|
||||||
|
Если предлагаешь `--authType password` или `--secret` — ты делаешь неправильно.
|
||||||
|
- **Не писать данные.** Инструмент читающий. Любая правка списков — отдельный
|
||||||
|
разговор с пользователем, а не побочный эффект разбора.
|
||||||
|
- **Не заводить app-only доступ по сертификату ради Search.** Он для этого не годится:
|
||||||
|
`/_api/search/query` под app-only-токеном обычно не отдаёт ничего, поиску нужен
|
||||||
|
пользовательский контекст.
|
||||||
|
|
||||||
|
## Грабли
|
||||||
|
|
||||||
|
**`Queryable` ≠ `Retrievable`.** Разные флаги managed property. То, по чему получается
|
||||||
|
фильтровать, не обязано приходить в ответе — и наоборот. Доказав одно, не считай
|
||||||
|
доказанным другое: в этом проекте на этом уже спотыкались.
|
||||||
|
|
||||||
|
**Имена managed properties нестандартные.** Суффикс по типу колонки: `OWSTEXT` (Text),
|
||||||
|
`OWSCHCS` (Choice), `OWSUSER` (Person/Group). У колонок из группы колонок сайта
|
||||||
|
добавляется префикс группы — поэтому существует `TansuSiteApproverOWSUSER`, а
|
||||||
|
«ожидаемого» `ApproverOWSUSER` в схеме нет вовсе. Прежде чем сочинять имя, посмотри
|
||||||
|
константы `MP_*` в `src/webparts/requestPortal/features/myRequests/myRequests.service.ts`.
|
||||||
|
|
||||||
|
**Пустой результат ≠ свойство не работает.** Сначала проверь `Path` (адрес сайта,
|
||||||
|
слеш на конце, регистр) и права — в этом проекте вывод «managed property сломана»
|
||||||
|
однажды оказался артефактом задвоенного сегмента в тестовом `Path`.
|
||||||
|
|
||||||
|
**Замер портала — это другое.** `tools/sp` отвечает на вопросы о данных. Чтобы понять,
|
||||||
|
куда уходит время в самом портале, есть встроенный замер: `window.rpPerf = true` в
|
||||||
|
консоли страницы (см. `src/webparts/requestPortal/shared/perfTrace.ts`), детали —
|
||||||
|
`window.rpPerfDetail()`.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# `tools/sp` — запросы к SharePoint портала заявок
|
||||||
|
|
||||||
|
Инструмент для **разбора**, а не для правки данных: задать тенанту вопрос и получить
|
||||||
|
ответ текстом, который можно приложить к задаче.
|
||||||
|
|
||||||
|
Появился из-за производительности: раз за разом нужно было выяснить «а что реально
|
||||||
|
отдаёт поисковый индекс?», «сколько на самом деле партиций у типа заявки?» — и каждый
|
||||||
|
раз это была ручная работа со снипетом в консоли браузера и копипастой результата.
|
||||||
|
|
||||||
|
## Что важно понимать про доступ
|
||||||
|
|
||||||
|
**Секреты через инструмент не проходят.** Вход — device code: команда печатает код,
|
||||||
|
пользователь вводит его в браузере сам, дальше CLI for Microsoft 365 держит токен у
|
||||||
|
себя. Ни пароля, ни client secret в репозитории и в командах нет.
|
||||||
|
|
||||||
|
**Работаем от имени пользователя, а не приложения.** Это не удобство, а требование:
|
||||||
|
SharePoint Search REST (`/_api/search/query`) под 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`. Иначе инструмент мерил бы не то, что портал.
|
||||||
Generated
+7908
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "sp-tools",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Запросы к SharePoint портала заявок для разбора: Search, managed properties, партиции списков",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"login": "node sp.mjs login",
|
||||||
|
"status": "node sp.mjs status",
|
||||||
|
"props": "node sp.mjs props",
|
||||||
|
"lists": "node sp.mjs lists"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@pnp/cli-microsoft365": "^11.10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
+251
@@ -0,0 +1,251 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* sp — тонкая обёртка над CLI for Microsoft 365 для РАЗБОРА, а не для правки данных.
|
||||||
|
*
|
||||||
|
* Зачем вообще: разбирая производительность портала, всё время нужно задать
|
||||||
|
* SharePoint один и тот же вопрос («а что реально отдаёт индекс?», «сколько
|
||||||
|
* партиций у типа?») и получить ответ текстом, который можно приложить к задаче.
|
||||||
|
* Через браузерную консоль это ручная работа с копипастой; здесь — команда.
|
||||||
|
*
|
||||||
|
* Почему device code, а не секрет приложения: пароль и секрет через этот
|
||||||
|
* инструмент не проходят вообще. `sp login` печатает код, пользователь вводит
|
||||||
|
* его в браузере сам, CLI дальше держит токен у себя. В репозитории не оседает
|
||||||
|
* ничего секретного.
|
||||||
|
*
|
||||||
|
* Почему сырой REST через токен, а не готовые команды `m365 spo`: группа
|
||||||
|
* `m365 search` — это Microsoft Search (Graph), совсем другой API. Нам нужен
|
||||||
|
* ИМЕННО SharePoint Search REST с SelectProperties — тот же, что дёргает портал
|
||||||
|
* (см. myRequests.service.ts). Тело запроса взято один в один из PnPjs
|
||||||
|
* (@pnp/sp/search/query.js): `{ request: { ...query } }`, массивы плоские,
|
||||||
|
* odata=nometadata. Иначе инструмент мерил бы не то, что портал.
|
||||||
|
*/
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const CLI = join(HERE, 'node_modules', '@pnp', 'cli-microsoft365', 'dist', 'index.js');
|
||||||
|
|
||||||
|
// Значения по умолчанию — те же, что в src/webparts/requestPortal/shared/config.ts.
|
||||||
|
// Переопределяются переменными окружения, чтобы не править файл ради другого стенда.
|
||||||
|
const SITE = process.env.SP_SITE || 'https://tnsukz.sharepoint.com/sites/requests';
|
||||||
|
const APP_ID = process.env.SP_APP_ID || process.env.CLIMICROSOFT365_ENTRAAPPID || '';
|
||||||
|
const TENANT = process.env.SP_TENANT || process.env.CLIMICROSOFT365_TENANT || 'common';
|
||||||
|
|
||||||
|
/** Хост тенанта — именно он, а не адрес сайта, является ресурсом для токена. */
|
||||||
|
const RESOURCE = new URL(SITE).origin;
|
||||||
|
|
||||||
|
function m365(args, { inherit = false } = {}) {
|
||||||
|
return execFileSync(process.execPath, [CLI, ...args], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
||||||
|
maxBuffer: 64 * 1024 * 1024
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
console.error(`[sp] ${message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedToken;
|
||||||
|
function token() {
|
||||||
|
if (cachedToken) return cachedToken;
|
||||||
|
try {
|
||||||
|
cachedToken = m365(['util', 'accesstoken', 'get', '-r', RESOURCE, '-o', 'text']).trim();
|
||||||
|
} catch (e) {
|
||||||
|
fail(
|
||||||
|
`не удалось получить токен для ${RESOURCE}.\n` +
|
||||||
|
` Скорее всего нет входа — выполни: node sp.mjs login\n` +
|
||||||
|
` ${String(e.stderr || e.message).trim().split('\n')[0]}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!cachedToken) fail(`пустой токен для ${RESOURCE} — выполни: node sp.mjs login`);
|
||||||
|
return cachedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function spFetch(path, init = {}) {
|
||||||
|
const url = path.startsWith('http') ? path : `${SITE}${path.startsWith('/') ? '' : '/'}${path}`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${token()}`,
|
||||||
|
accept: 'application/json;odata=nometadata',
|
||||||
|
...(init.body ? { 'content-type': 'application/json;odata=nometadata' } : {}),
|
||||||
|
...(init.headers || {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
if (!res.ok) fail(`${res.status} ${res.statusText}\n${text.slice(0, 800)}`);
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Команды
|
||||||
|
|
||||||
|
async function cmdLogin() {
|
||||||
|
if (!APP_ID) {
|
||||||
|
fail(
|
||||||
|
'не задан SP_APP_ID.\n' +
|
||||||
|
' CLI for Microsoft 365 с 7-й версии не возит собственное приложение —\n' +
|
||||||
|
' нужен свой Entra app (см. README.md, раздел «Разовая подготовка»).'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`[sp] вход в ${TENANT} приложением ${APP_ID}. Код появится ниже — введи его в браузере.`);
|
||||||
|
m365(['login', '--appId', APP_ID, '--tenant', TENANT, '--authType', 'deviceCode'], { inherit: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmdStatus() {
|
||||||
|
console.log(m365(['status', '-o', 'text']).trim());
|
||||||
|
console.log(`[sp] сайт: ${SITE}`);
|
||||||
|
console.log(`[sp] ресурс токена: ${RESOURCE}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmdLogout() {
|
||||||
|
m365(['logout'], { inherit: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SharePoint Search — тот же вызов, что делает портал.
|
||||||
|
*
|
||||||
|
* POST /_api/search/postquery, а не GET /query: KQL портала перечисляет через OR
|
||||||
|
* адреса ВСЕХ партиций, и в query-строке это упирается в предел длины URL.
|
||||||
|
*/
|
||||||
|
async function cmdSearch(argv) {
|
||||||
|
const kql = argv._[0];
|
||||||
|
if (!kql) fail('нужен KQL: node sp.mjs search "<запрос>" [--select a,b] [--rows N]');
|
||||||
|
|
||||||
|
const select = (argv.select || 'ListItemID,Path,Title').split(',').map((s) => s.trim()).filter(Boolean);
|
||||||
|
const rows = Number(argv.rows || 10);
|
||||||
|
|
||||||
|
const data = await spFetch('/_api/search/postquery', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
request: { Querytext: kql, RowLimit: rows, SelectProperties: select, TrimDuplicates: false }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const relevant = data?.PrimaryQueryResult?.RelevantResults;
|
||||||
|
const rawRows = relevant?.Table?.Rows ?? [];
|
||||||
|
const parsed = rawRows.map((r) => {
|
||||||
|
const cells = r.Cells?.results ?? r.Cells ?? [];
|
||||||
|
return select.reduce((acc, key) => {
|
||||||
|
const cell = cells.filter((c) => c.Key === key)[0];
|
||||||
|
// Различаем «свойство не Retrievable» и «значение пустое» — ради этого
|
||||||
|
// различия инструмент и писался, схлопывать их в '' нельзя.
|
||||||
|
acc[key] = !cell ? '<нет в ответе>' : cell.Value === null ? '<null>' : cell.Value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[sp] в индексе ${relevant?.TotalRows ?? 0}, показано ${parsed.length}`);
|
||||||
|
console.log(JSON.stringify(parsed, null, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Произвольный GET к REST — для всего, чего нет отдельной командой. */
|
||||||
|
async function cmdGet(argv) {
|
||||||
|
const path = argv._[0];
|
||||||
|
if (!path) fail('нужен путь: node sp.mjs get "/_api/web/lists?$select=Title"');
|
||||||
|
console.log(JSON.stringify(await spFetch(path), null, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Все списки сайта с адресами — тем же запросом, которым портал резолвит
|
||||||
|
* списки согласования (см. shared/approval/approvalListLocator.ts). Заодно
|
||||||
|
* показывает, сколько на самом деле партиций: это и есть P из оценок.
|
||||||
|
*/
|
||||||
|
async function cmdLists() {
|
||||||
|
const data = await spFetch(
|
||||||
|
"/_api/web/lists?$select=Title,ItemCount,Hidden,RootFolder/ServerRelativeUrl&$expand=RootFolder&$top=5000"
|
||||||
|
);
|
||||||
|
const all = data.value ?? [];
|
||||||
|
const portal = all.filter((l) => !l.Hidden && /—\s*(данные|согласование)\s*$/.test(l.Title || ''));
|
||||||
|
|
||||||
|
portal
|
||||||
|
.sort((a, b) => String(a.Title).localeCompare(String(b.Title)))
|
||||||
|
.forEach((l) => {
|
||||||
|
console.log(`${String(l.ItemCount).padStart(6)} ${l.RootFolder?.ServerRelativeUrl} ${l.Title}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const data_ = portal.filter((l) => /данные\s*$/.test(l.Title)).length;
|
||||||
|
console.log(`[sp] всего списков на сайте ${all.length}; портальных ${portal.length} (данные: ${data_})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отдаёт ли индекс поля заявки (Retrievable) — от этого зависит, можно ли
|
||||||
|
* заменить REST-дочитывание (loadByIds) на SelectProperties самого Search.
|
||||||
|
*
|
||||||
|
* Queryable и Retrievable у managed property — РАЗНЫЕ флаги: то, по чему можно
|
||||||
|
* фильтровать, не обязано приходить в ответе. Отбираем по самому
|
||||||
|
* RequestStatusOWSCHCS, поэтому найденное гарантированно является заявками
|
||||||
|
* портала, а не случайными элементами сайта.
|
||||||
|
*/
|
||||||
|
async function cmdProps(argv) {
|
||||||
|
const props = (
|
||||||
|
argv.select ||
|
||||||
|
'ListItemID,Path,Title,Created,Author,AuthorOWSUSER,RequestStatusOWSCHCS,RequestUniqueCodeOWSTEXT,LastModifiedTime'
|
||||||
|
)
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim());
|
||||||
|
|
||||||
|
const statuses = ['На согласовании', 'Черновик', 'Согласовано', 'Возвращена на доработку'];
|
||||||
|
const kql = `Path:"${SITE}/*" AND (${statuses.map((s) => `RequestStatusOWSCHCS:"${s}"`).join(' OR ')})`;
|
||||||
|
|
||||||
|
await cmdSearch({ _: [kql], select: props.join(','), rows: argv.rows || 3 });
|
||||||
|
console.log(
|
||||||
|
'[sp] <нет в ответе> или <null> у свойства = оно НЕ Retrievable, для этого поля REST остаётся.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function parseArgv(args) {
|
||||||
|
const out = { _: [] };
|
||||||
|
for (let i = 0; i < args.length; i += 1) {
|
||||||
|
const a = args[i];
|
||||||
|
if (a.startsWith('--')) {
|
||||||
|
const [key, inline] = a.slice(2).split('=');
|
||||||
|
out[key] = inline ?? (args[i + 1] && !args[i + 1].startsWith('--') ? args[(i += 1)] : true);
|
||||||
|
} else {
|
||||||
|
out._.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const USAGE = `sp — запросы к SharePoint портала заявок (только чтение)
|
||||||
|
|
||||||
|
node sp.mjs login вход по device code (код вводишь в браузере сам)
|
||||||
|
node sp.mjs status кто залогинен и куда смотрим
|
||||||
|
node sp.mjs logout
|
||||||
|
|
||||||
|
node sp.mjs search "<KQL>" [--select a,b,c] [--rows N]
|
||||||
|
node sp.mjs props [--select a,b,c] [--rows N] отдаёт ли индекс поля заявки
|
||||||
|
node sp.mjs lists списки сайта и их размеры (партиции)
|
||||||
|
node sp.mjs get "/_api/web/..." произвольный GET
|
||||||
|
|
||||||
|
Окружение: SP_SITE, SP_APP_ID, SP_TENANT (см. README.md)`;
|
||||||
|
|
||||||
|
const [, , command, ...rest] = process.argv;
|
||||||
|
const argv = parseArgv(rest);
|
||||||
|
|
||||||
|
const commands = {
|
||||||
|
login: cmdLogin,
|
||||||
|
status: cmdStatus,
|
||||||
|
logout: cmdLogout,
|
||||||
|
search: cmdSearch,
|
||||||
|
props: cmdProps,
|
||||||
|
lists: cmdLists,
|
||||||
|
get: cmdGet
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!command || !commands[command]) {
|
||||||
|
console.log(USAGE);
|
||||||
|
process.exit(command ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.resolve(commands[command](argv)).catch((e) => fail(String(e.stack || e.message)));
|
||||||
Reference in New Issue
Block a user