From d9e9d451c98c2163fa8502991ac930074b14b05b Mon Sep 17 00:00:00 2001 From: Naz Date: Tue, 5 Jul 2022 11:22:36 +0200 Subject: [PATCH] Refactored search-index module to use class syntax refs https://github.com/TryGhost/Team/issues/1665 - The class syntax allows to use constructor DI pattern that helps with unit testing and abstracting away the dependencies - It wasn't possible to test the internals without having access to the localStorage in tests (you couls access it but that's kind of leaky for tests to know what mechanism is used in the module intenally) --- ghost/sodo-search/src/App.js | 13 +-- ghost/sodo-search/src/search-index.js | 99 ++++++++++++---------- ghost/sodo-search/src/search-index.test.js | 31 ++++++- 3 files changed, 91 insertions(+), 52 deletions(-) diff --git a/ghost/sodo-search/src/App.js b/ghost/sodo-search/src/App.js index 434107b298..41e946df22 100644 --- a/ghost/sodo-search/src/App.js +++ b/ghost/sodo-search/src/App.js @@ -2,23 +2,24 @@ import React from 'react'; import './App.css'; import AppContext from './AppContext'; import PopupModal from './components/PopupModal'; -import {init as initSearchIndex} from './search-index.js'; +import SearchIndex from './search-index.js'; export default class App extends React.Component { constructor(props) { super(props); - this.state = { + const searchIndex = new SearchIndex({ apiUrl: props.apiUrl, apiKey: props.apiKey + }); + + this.state = { + searchIndex }; } componentDidMount() { - initSearchIndex({ - apiUrl: this.state.apiUrl, - apiKey: this.state.apiKey - }); + this.state.searchIndex.init(); } render() { diff --git a/ghost/sodo-search/src/search-index.js b/ghost/sodo-search/src/search-index.js index 757671cb00..8222cfb0a5 100644 --- a/ghost/sodo-search/src/search-index.js +++ b/ghost/sodo-search/src/search-index.js @@ -1,57 +1,68 @@ import elasticlunr from 'elasticlunr'; -let index; +export default class SearchIndex { + constructor({apiUrl, apiKey, storage = localStorage}) { + this.apiUrl = apiUrl; + this.apiKey = apiKey; + this.storage = storage; -export const init = async function ({apiUrl, apiKey}) { - // remove default stop words to search of *any* word - elasticlunr.clearStopWords(); + this.index = null; - const url = `${apiUrl}/posts/?key=${apiKey}&limit=all&fields=id,title,excerpt,url,updated_at,visibility&order=updated_at%20desc&formats=plaintext`; + this.init = this.init.bind(this); + this.search = this.search.bind(this); + } - const indexDump = JSON.parse(localStorage.getItem('ease_search_index')); - - localStorage.removeItem('ease_index'); - localStorage.removeItem('ease_last'); - - function update(data) { - data.posts.forEach(function (post) { - index.addDoc(post); + #updateIndex(data) { + data.posts.forEach((post) => { + this.index.addDoc(post); }); - localStorage.setItem('ease_search_index', JSON.stringify(index)); - localStorage.setItem('ease_search_last', data.posts[0].updated_at); + this.storage.setItem('ease_search_index', JSON.stringify(this.index)); + this.storage.setItem('ease_search_last', data.posts[0].updated_at); } - if ( - !indexDump - ) { - return fetch(url) - .then(response => response.json()) - .then((data) => { - if (data.posts.length > 0) { - index = elasticlunr(function () { - this.addField('title'); - this.addField('plaintext'); - this.setRef('id'); - }); + async init() { + // remove default stop words to search of *any* word + elasticlunr.clearStopWords(); - update(data); - } - }); - } else { - index = elasticlunr.Index.load(indexDump); + const url = `${this.apiUrl}/posts/?key=${this.apiKey}&limit=all&fields=id,title,excerpt,url,updated_at,visibility&order=updated_at%20desc&formats=plaintext`; - return fetch(`${url}&filter=updated_at:>'${localStorage.getItem('ease_search_last').replace(/\..*/, '').replace(/T/, ' ')}'` - ) - .then(response => response.json()) - .then((data) => { - if (data.posts.length > 0) { - update(data); - } - }); + const indexDump = JSON.parse(this.storage.getItem('ease_search_index')); + + this.storage.removeItem('ease_index'); + this.storage.removeItem('ease_last'); + + if ( + !indexDump + ) { + return fetch(url) + .then(response => response.json()) + .then((data) => { + if (data.posts.length > 0) { + this.index = elasticlunr(function () { + this.addField('title'); + this.addField('plaintext'); + this.setRef('id'); + }); + + this.#updateIndex(data); + } + }); + } else { + this.index = elasticlunr.Index.load(indexDump); + + return fetch(`${url}&filter=updated_at:>'${this.storage.getItem('ease_search_last').replace(/\..*/, '').replace(/T/, ' ')}'` + ) + .then(response => response.json()) + .then((data) => { + if (data.posts.length > 0) { + this.#updateIndex(data); + } + }); + } } -}; -export const search = function (value) { - return index.search(value, {expand: true}); -}; + search(value) { + return this.index.search(value, {expand: true}); + } +} diff --git a/ghost/sodo-search/src/search-index.test.js b/ghost/sodo-search/src/search-index.test.js index d7af0046d1..8abdd2e3be 100644 --- a/ghost/sodo-search/src/search-index.test.js +++ b/ghost/sodo-search/src/search-index.test.js @@ -1,10 +1,15 @@ -import {init} from './search-index'; +import SearchIndex from './search-index'; import nock from 'nock'; describe('search index', function () { + afterEach(function () { + localStorage.clear(); + }); + test('initializes search index', async () => { const apiUrl = 'http://localhost/ghost/api/content'; const apiKey = 'secret_key'; + const searchIndex = new SearchIndex({apiUrl, apiKey, storage: localStorage}); const scope = nock('http://localhost/ghost/api/content') .get('/posts/?key=secret_key&limit=all&fields=id,title,excerpt,url,updated_at,visibility&order=updated_at%20desc&formats=plaintext') @@ -12,8 +17,30 @@ describe('search index', function () { posts: [{}] }); - await init({apiUrl, apiKey}); + await searchIndex.init({apiUrl, apiKey}); expect(scope.isDone()).toBeTruthy(); }); + + test('allows to search for indexed posts', async () => { + const apiUrl = 'http://localhost/ghost/api/content'; + const apiKey = 'secret_key'; + const searchIndex = new SearchIndex({apiUrl, apiKey, storage: localStorage}); + + nock('http://localhost/ghost/api/content') + .get('/posts/?key=secret_key&limit=all&fields=id,title,excerpt,url,updated_at,visibility&order=updated_at%20desc&formats=plaintext') + .reply(200, { + posts: [{ + id: 'sounique', + title: 'Amazing Barcelona Life', + plaintext: 'We are sitting by the pool and smashing out search features' + }] + }); + + await searchIndex.init({apiUrl, apiKey}); + + const searchResults = searchIndex.search('Barcelona'); + + expect(searchResults.length).toEqual(1); + }); });