// / /** @module restSearchClient */ /** * @typedef {import('./urlGenerator.js').UrlGenerator} UrlGenerator */ const fetchJson = require( './fetch.js' ); /** * @typedef {Object} RestResponse * @property {RestResult[]} pages */ /** * @typedef {Object} SearchResponse * @property {string} query * @property {SearchResult[]} results */ /** * Nullish coalescing operator (??) helper * * @param {any} a * @param {any} b * @return {any} */ function nullish( a, b ) { return ( a !== null && a !== undefined ) ? a : b; } /** * @param {UrlGenerator} urlGeneratorInstance * @param {string} query * @param {RestResponse} restResponse * @param {boolean} showDescription * @return {SearchResponse} */ function adaptApiResponse( urlGeneratorInstance, query, restResponse, showDescription ) { return { query, results: restResponse.pages.map( ( page, index ) => { const thumbnail = page.thumbnail; return { id: page.id, value: page.id || -( index + 1 ), label: page.title, key: page.key, title: page.title, description: showDescription ? page.description : undefined, url: urlGeneratorInstance.generateUrl( page ), thumbnail: thumbnail ? { url: thumbnail.url, width: nullish( thumbnail.width, undefined ), height: nullish( thumbnail.height, undefined ) } : undefined }; } ) }; } /** * @typedef {Object} AbortableSearchFetch * @property {Promise} fetch * @property {Function} abort */ /** * @callback fetchByTitle * @param {string} query The search term. * @param {number} [limit] Maximum number of results. * @param {boolean} [showDescription] Whether descriptions should be added to the results. * @return {AbortableSearchFetch} */ /** * @callback loadMore * @param {string} query The search term. * @param {number} offset The number of search results that were already loaded. * @param {number} [limit] How many further search results to load (at most). * @param {boolean} [showDescription] Whether descriptions should be added to the results. * @return {AbortableSearchFetch} */ /** * @typedef {Object} SearchClient * @property {fetchRecommendationByTitle} fetchRecommendationByTitle * @property {fetchByTitle} fetchByTitle * @property {loadMore} [loadMore] */ /** * @param {string} searchApiUrl * @param {UrlGenerator} urlGeneratorInstance * @param {string} recommendationApiUrl * @return {SearchClient} */ function restSearchClient( searchApiUrl, urlGeneratorInstance, recommendationApiUrl = null ) { return { /** * @type {fetchRecommendationByTitle} */ fetchRecommendationByTitle: recommendationApiUrl ? ( currentTitle, showDescription = true ) => { const result = fetchJson( recommendationApiUrl.replace( /\$1/g, currentTitle ), { headers: { accept: 'application/json' } } ); const recommendationResponsePromise = result.fetch .then( ( /** @type {RestResponse} */ res ) => adaptApiResponse( urlGeneratorInstance, '', res, showDescription ) ); return { abort: result.abort, fetch: recommendationResponsePromise }; } : undefined, /** * @type {fetchByTitle} */ fetchByTitle: ( q, limit = 10, showDescription = true ) => { const params = { q, limit: limit.toString() }; const search = new URLSearchParams( params ); mw.hook( 'typeaheadSearch.appendUrlParams' ).fire( ( key, value ) => { search.append( key, value ); } ); const url = `${ searchApiUrl }/v1/search/title?${ search.toString() }`; const result = fetchJson( url, { headers: { accept: 'application/json' } } ); const searchResponsePromise = result.fetch .then( ( /** @type {RestResponse} */ res ) => adaptApiResponse( urlGeneratorInstance, q, res, showDescription ) ); return { abort: result.abort, fetch: searchResponsePromise }; } }; } module.exports = restSearchClient;