UNPKG

spfx-kql-data-retriever

Version:

Search Query Service for retrieve data with Kql with Sharepoint 365

364 lines (289 loc) 12.7 kB
import { Text } from '@microsoft/sp-core-library'; import { IWebPartContext } from '@microsoft/sp-webpart-base'; import { ConsoleListener, Logger, LogLevel } from '@pnp/logging'; import { SearchQuery, SearchResults, SearchSuggestQuery, Sort, SortDirection, sp, SPRest, Web } from '@pnp/sp'; import { groupBy, mapKeys, mapValues, sortBy } from 'lodash-es'; import * as moment from 'moment'; import LocalizationHelper from './../helpers/LocalizationHelper'; import { IRefinementFilter, IRefinementResult, IRefinementValue, ISearchResult, ISearchResults } from './../models/ISearchResult'; import { ISearchService } from './ISearchService'; declare var System: any; export class SearchService implements ISearchService { static ParseSelectedProperties(value: string): string[] { return value ? value.replace(/\s|,+$/g, '').split(',') : []; } private _initialSearchResult: SearchResults = null; private _resultsCount: number; private _context: IWebPartContext; private _selectedProperties: string[]; private _templateQuery: string; private _resultSourceId: string; private _sortList: string; private _enableQueryRules: boolean; public get resultsCount(): number { return this._resultsCount; } public set resultsCount(value: number) { this._resultsCount = value; } public set selectedProperties(value: string[]) { this._selectedProperties = value; } public get selectedProperties(): string[] { return this._selectedProperties; } public set templateQuery(value: string) { this._templateQuery = value; } public get templateQuery(): string { return this._templateQuery; } public set resultSourceId(value: string) { this._resultSourceId = value; } public get resultSourceId(): string { return this._resultSourceId; } public set sortList(value: string) { this._sortList = value; } public get sortList(): string { return this._sortList; } public set enableQueryRules(value: boolean) { this._enableQueryRules = value; } public get enableQueryRules(): boolean { return this._enableQueryRules; } private _localPnPSetup: SPRest; public constructor(webPartContext: IWebPartContext) { this._context = webPartContext; // Setup Moment Locales moment.locale(this._context.pageContext.cultureInfo.currentUICultureName); // Setup the PnP JS instance const consoleListener = new ConsoleListener(); Logger.subscribe(consoleListener); // To limit the payload size, we set odata=nometadata // We just need to get list items here // We use a local configuration to avoid conflicts with other Web Parts this._localPnPSetup = sp.configure({ headers: { Accept: 'application/json; odata=nometadata', }, }, this._context.pageContext.web.absoluteUrl); } /** * Performs a search query against SharePoint * @param query The search query in KQL format * @return The search results */ public async search(query: string, refiners?: string, refinementFilters?: IRefinementFilter[], pageNumber?: number): Promise<ISearchResults> { const searchQuery: SearchQuery = {}; let sortedRefiners: string[] = []; // Search paging option is one based const page = pageNumber ? pageNumber : 1; searchQuery.ClientType = 'ContentSearchRegular'; searchQuery.Querytext = query; // Disable query rules by default if not specified searchQuery.EnableQueryRules = this._enableQueryRules ? this._enableQueryRules : false; if (this._resultSourceId) { searchQuery.SourceId = this._resultSourceId; } else { // To be able to use search query variable according to the current context // http://www.techmikael.com/2015/07/sharepoint-rest-do-support-query.html searchQuery.QueryTemplate = this._templateQuery; } const defaultRowLimit = 50; searchQuery.RowLimit = this._resultsCount ? this._resultsCount : defaultRowLimit; searchQuery.SelectProperties = this._selectedProperties; searchQuery.TrimDuplicates = false; let sortList: Sort[] = [ { Direction: SortDirection.Descending, Property: 'Created' }, { Direction: SortDirection.Ascending, Property: 'Size' } ]; if (this._sortList) { const sortOrders = this._sortList.split(','); sortList = sortOrders.map((sorter) => { const sort = sorter.split(':'); const s: Sort = { Property: sort[0].trim(), Direction: SortDirection.Descending }; if (sort.indexOf('[') !== -1) { s.Direction = SortDirection.FQLFormula; } else if (sort.length > 1) { const direction = sort[1].trim().toLocaleLowerCase(); s.Direction = direction === "ascending" ? SortDirection.Ascending : SortDirection.Descending; } return s; }); } searchQuery.SortList = sortList; if (refiners) { // Get the refiners order specified in the property pane sortedRefiners = refiners.split(','); searchQuery.Refiners = refiners ? refiners : ''; } if (refinementFilters) { if (refinementFilters.length > 0) { searchQuery.RefinementFilters = [this._buildRefinementQueryString(refinementFilters)]; } } const results: ISearchResults = { RefinementResults: [], RelevantResults: [], TotalRows: 0 }; try { if (!this._initialSearchResult || page === 1) { this._initialSearchResult = await this._localPnPSetup.search(searchQuery); } const allItemsPromises: Array<Promise<any>> = []; let refinementResults: IRefinementResult[] = []; // Need to do this check // More info here: https://github.com/SharePoint/PnP-JS-Core/issues/337 if (this._initialSearchResult.RawSearchResults.PrimaryQueryResult) { // Be careful, there was an issue with paging calculation under 2.0.8 version of sp-pnp-js library // More info https://github.com/SharePoint/PnP-JS-Core/issues/535 let r2 = this._initialSearchResult; if (page > 1) { r2 = await this._initialSearchResult.getPage(page, this._resultsCount); } const resultRows = r2.RawSearchResults.PrimaryQueryResult.RelevantResults.Table.Rows; const refinementResultsRows = r2.RawSearchResults.PrimaryQueryResult.RefinementResults; const refinementRows = refinementResultsRows ? refinementResultsRows['Refiners'] : []; if (refinementRows.length > 0) { // const component = await System.import( // /* webpackChunkName: 'search-handlebars-helpers' */ // 'handlebars-helpers' // ); // // this._helper = component({ // handlebars: Handlebars // }); } // Map search results resultRows.map((elt) => { const p1 = new Promise<ISearchResult>((resolvep1, rejectp1) => { // Build item result dynamically // We can't type the response here because search results are by definition too heterogeneous so we treat them as key-value object const result: ISearchResult = {}; elt.Cells.map((item) => { result[item.Key] = item.Value; }); // Get the icon source URL this._mapToIcon(result.Filename ? result.Filename : Text.format('.{0}', result.FileExtension)).then((IconUrl) => { result.IconSrc = IconUrl; resolvep1(result); }).catch((error) => { rejectp1(error); }); }); allItemsPromises.push(p1); }); // Map refinement results refinementRows.map((refiner) => { refinementResults.push({ FilterName: refiner.Name, Values: [{ RefinementCount: parseInt(refiner.Entries.RefinementCount, 10), RefinementName: this._formatDate(refiner.Entries.RefinementName), // This value will appear in the selected filter bar RefinementToken: refiner.Entries.RefinementToken, RefinementValue: this._formatDate(refiner.Entries.RefinementValue), // This value will appear in the filter panel }], }); }); // Resolve all the promises once to get news const relevantResults: ISearchResult[] = await Promise.all(allItemsPromises); // Sort refiners according to the property pane value refinementResults = sortBy(refinementResults, (refinement) => { // Get the index of the corresponding filter name return sortedRefiners.indexOf(refinement.FilterName); }); results.RelevantResults = relevantResults; results.RefinementResults = refinementResults; results.TotalRows = this._initialSearchResult.TotalRows; } return results; } catch (error) { Logger.write('[SharePointDataProvider.search()]: Error: ' + error, LogLevel.Error); throw error; } } /** * Retrieves search query suggestions * @param query the term to suggest from */ public async suggest(query: string): Promise<string[]> { let suggestions: string[] = []; const searchSuggestQuery: SearchSuggestQuery = { count: 10, culture: LocalizationHelper.getLocaleId(this._context.pageContext.cultureInfo.currentUICultureName).toString(), hitHighlighting: true, preQuery: true, prefixMatch: true, querytext: query }; try { const response = await this._localPnPSetup.searchSuggest(searchSuggestQuery); if (response.Queries.length > 0) { // Get only the suggesiton string value suggestions = response.Queries.map((elt) => { return elt.Query; }); } return suggestions; } catch (error) { Logger.write("[SharePointDataProvider.suggest()]: Error: " + error, LogLevel.Error); throw error; } } /** * Gets the icon corresponding to the file name extension * @param filename The file name (ex: file.pdf) */ private async _mapToIcon(filename: string): Promise<string> { const webAbsoluteUrl = this._context.pageContext.web.absoluteUrl; const web = new Web(webAbsoluteUrl); try { const encodedFileName = filename ? filename.replace(/['']/g, '') : ''; const iconFileName = await web.mapToIcon(encodedFileName, 1); const iconUrl = webAbsoluteUrl + '/_layouts/15/images/' + iconFileName; return iconUrl; } catch (error) { Logger.write('[SharePointDataProvider._mapToIcon()]: Error: ' + error, LogLevel.Error); throw error; } } /** * Find and eeplace ISO 8601 dates in the string by a friendly value * @param inputValue The string to format */ private _formatDate(inputValue: string): string { const iso8061rgx = /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/g; const matches = inputValue.match(iso8061rgx); let updatedInputValue = inputValue; if (matches) { matches.map((match) => { updatedInputValue = updatedInputValue.replace(match, moment(match).format("LL")); }); } return updatedInputValue; } /** * Build the refinement condition in FQL format * @param selectedFilters The selected filter array */ private _buildRefinementQueryString(selectedFilters: IRefinementFilter[]): string { const refinementQueryConditions: string[] = []; let refinementQueryString: string = null; const refinementFilters = mapValues(groupBy(selectedFilters, 'FilterName'), (values) => { const refinementFilter = values.map((filter) => { return filter.Value.RefinementToken; }); return refinementFilter.length > 1 ? Text.format('or({0})', refinementFilter) : refinementFilter.toString(); }); mapKeys(refinementFilters, (value, key) => { refinementQueryConditions.push(key + ':' + value); }); const conditionsCount = refinementQueryConditions.length; switch (true) { // No filters case (conditionsCount === 0): { refinementQueryString = null; break; } // Just one filter case (conditionsCount === 1): { refinementQueryString = refinementQueryConditions[0].toString(); break; } // Multiple filters case (conditionsCount > 1): { refinementQueryString = Text.format('and({0})', refinementQueryConditions.toString()); break; } } return refinementQueryString; } }