заработало
This commit is contained in:
@@ -12,12 +12,6 @@ public interface ISharePointClient
|
||||
/// <summary>Заголовок списка по его серверному относительному URL (см. RequestType.ListId).</summary>
|
||||
Task<string?> GetListTitleAsync(string siteUrl, string listServerRelativeUrl, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Серверный относительный URL списка, найденного по заголовку — нужен, чтобы
|
||||
/// построить условие Path в KQL-запросе (см. SearchListItemIdsAsync).
|
||||
/// </summary>
|
||||
Task<string?> GetListServerRelativeUrlAsync(string siteUrl, string listTitle, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Элементы списка данных, найденного по серверному относительному URL.</summary>
|
||||
Task<IReadOnlyList<JsonElement>> GetItemsByListUrlAsync(
|
||||
string siteUrl, string listServerRelativeUrl, string odataQuery, CancellationToken cancellationToken);
|
||||
@@ -25,17 +19,6 @@ public interface ISharePointClient
|
||||
/// <summary>Элементы списка, найденного по заголовку (списки согласования ищутся именно так).</summary>
|
||||
Task<IReadOnlyList<JsonElement>> GetItemsByListTitleAsync(
|
||||
string siteUrl, string listTitle, string odataQuery, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Id элементов, найденных Search'ем по KQL-запросу. Именно так, а не $filter,
|
||||
/// сужается набор строк списка согласования по статусу (Choice-колонка) —
|
||||
/// $filter по ней в SharePoint REST ненадёжен: см. подробный разбор в
|
||||
/// request-portal/myRequests.service.ts (MP_STATUS/ApprovalStatusOWSCHCS,
|
||||
/// тот же список, тот же managed property, подтверждено рабочим). Значения
|
||||
/// колонок не возвращает — только Id, дочитывать поля нужно отдельным REST-запросом.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<int>> SearchListItemIdsAsync(
|
||||
string siteUrl, string queryText, int rowLimit, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -67,81 +50,6 @@ public sealed class SharePointClient(
|
||||
return json.TryGetProperty("Title", out var title) ? title.GetString() : null;
|
||||
}
|
||||
|
||||
public async Task<string?> GetListServerRelativeUrlAsync(
|
||||
string siteUrl, string listTitle, CancellationToken cancellationToken)
|
||||
{
|
||||
var url = $"{siteUrl.TrimEnd('/')}/_api/web/lists/getByTitle('{Escape(listTitle)}')" +
|
||||
"?$select=RootFolder/ServerRelativeUrl&$expand=RootFolder";
|
||||
var json = await GetAsync(siteUrl, url, cancellationToken);
|
||||
|
||||
return ParseServerRelativeUrl(json);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<int>> SearchListItemIdsAsync(
|
||||
string siteUrl, string queryText, int rowLimit, CancellationToken cancellationToken)
|
||||
{
|
||||
var url = $"{siteUrl.TrimEnd('/')}/_api/search/query" +
|
||||
$"?querytext='{Uri.EscapeDataString(queryText)}'" +
|
||||
"&selectproperties='ListItemID'" +
|
||||
$"&rowlimit={rowLimit}" +
|
||||
"&trimduplicates=false";
|
||||
var json = await GetAsync(siteUrl, url, cancellationToken);
|
||||
|
||||
return ParseSearchListItemIds(json);
|
||||
}
|
||||
|
||||
// Разбор вынесен в чистые статические функции — они не ходят в сеть и не
|
||||
// требуют токена, поэтому их (в отличие от остального клиента) можно
|
||||
// проверить тестом без живого SharePoint. Это самая непроверенная часть
|
||||
// клиента: форма ответа Search REST зафиксирована документацией, но без
|
||||
// реального тенанта убедиться в точном соответствии нельзя.
|
||||
internal static string? ParseServerRelativeUrl(JsonElement json) =>
|
||||
json.TryGetProperty("RootFolder", out var rootFolder) &&
|
||||
rootFolder.TryGetProperty("ServerRelativeUrl", out var relUrl)
|
||||
? relUrl.GetString()
|
||||
: null;
|
||||
|
||||
internal static IReadOnlyList<int> ParseSearchListItemIds(JsonElement json)
|
||||
{
|
||||
// Путь до строк результата — фиксированная форма ответа Search REST,
|
||||
// не зависит от запроса. С odata=nometadata Cells приходит уже "голым"
|
||||
// массивом (без обёртки .results) — но на случай, если сервер всё равно
|
||||
// завернёт его (разные версии API отвечали по-разному на практике в этом
|
||||
// проекте, см. комментарии в request-portal про managed properties),
|
||||
// читаем и то, и то (см. Unwrap).
|
||||
if (!json.TryGetProperty("PrimaryQueryResult", out var primary)) return [];
|
||||
if (!primary.TryGetProperty("RelevantResults", out var relevant)) return [];
|
||||
if (!relevant.TryGetProperty("Table", out var table)) return [];
|
||||
if (!table.TryGetProperty("Rows", out var rowsElement)) return [];
|
||||
|
||||
var ids = new List<int>();
|
||||
|
||||
foreach (var row in Unwrap(rowsElement))
|
||||
{
|
||||
if (!row.TryGetProperty("Cells", out var cellsElement)) continue;
|
||||
|
||||
foreach (var cell in Unwrap(cellsElement))
|
||||
{
|
||||
if (!cell.TryGetProperty("Key", out var key) || key.GetString() != "ListItemID") continue;
|
||||
if (!cell.TryGetProperty("Value", out var value)) continue;
|
||||
if (int.TryParse(value.GetString(), out var id)) ids.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
// odata=nometadata отдаёт коллекции внутри Search-ответа "голым" массивом;
|
||||
// без этого заголовка (или в старых версиях API) — обёрткой { results: [...] }.
|
||||
private static IEnumerable<JsonElement> Unwrap(JsonElement element)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Array) return element.EnumerateArray();
|
||||
|
||||
return element.TryGetProperty("results", out var results) && results.ValueKind == JsonValueKind.Array
|
||||
? results.EnumerateArray()
|
||||
: [];
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<JsonElement>> GetItemsByListUrlAsync(
|
||||
string siteUrl, string listServerRelativeUrl, string odataQuery, CancellationToken cancellationToken) =>
|
||||
GetItemsAsync(
|
||||
|
||||
Reference in New Issue
Block a user