From 19d4a76733062548a283feba21adfc213d02a9bf Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 21 Apr 2020 18:17:22 +0200 Subject: [PATCH 1/3] Changed Errors naming and added waitForPendingUpdate --- .../meilisearch-error.ts} | 23 +-- src/errors/meilisearch-timeout-error.ts | 11 ++ src/indexes.ts | 28 +++- src/meili-axios-wrapper.ts | 12 +- src/types.ts | 139 +++++++++--------- src/utils.ts | 8 + tests/wait_for_pending_update_tests.ts | 85 +++++++++++ 7 files changed, 221 insertions(+), 85 deletions(-) rename src/{meili-axios-error.ts => errors/meilisearch-error.ts} (70%) create mode 100644 src/errors/meilisearch-timeout-error.ts create mode 100644 src/utils.ts create mode 100644 tests/wait_for_pending_update_tests.ts diff --git a/src/meili-axios-error.ts b/src/errors/meilisearch-error.ts similarity index 70% rename from src/meili-axios-error.ts rename to src/errors/meilisearch-error.ts index 2da1230f1..973a74d73 100644 --- a/src/meili-axios-error.ts +++ b/src/errors/meilisearch-error.ts @@ -1,15 +1,17 @@ import { AxiosError } from 'axios' -import * as Types from './types' +import * as Types from '../types' -const MeiliAxiosError: Types.MeiliAxiosErrorConstructor = class extends Error - implements Types.MeiliAxiosErrorInterface { - response?: Types.MeiliAxiosErrorResponse - request?: Types.MeiliAxiosErrorRequest +const MeiliSearchApiError: Types.MeiliSearchApiErrorConstructor = class + extends Error + implements Types.MeiliSearchApiErrorInterface { + response?: Types.MeiliSearchApiErrorResponse + request?: Types.MeiliSearchApiErrorRequest + type: string constructor(error: AxiosError, cachedStack?: string) { super(error.message) - - this.name = 'MeiliSearch Error' + this.type = this.constructor.name + this.name = 'MeiliSearchApiError' // Fetch the native error message but add our application name in front of it. // This means slicing the "Error" string at the start of the message. @@ -40,8 +42,11 @@ const MeiliAxiosError: Types.MeiliAxiosErrorConstructor = class extends Error } // use cached Stack on error object to keep the call stack if (cachedStack && error.stack) { - this.stack = cachedStack + this.stack = `${this.name}: ${this.message}\n${cachedStack + .split('\n') + .slice(1) + .join('\n')}` } } } -export default MeiliAxiosError +export default MeiliSearchApiError diff --git a/src/errors/meilisearch-timeout-error.ts b/src/errors/meilisearch-timeout-error.ts new file mode 100644 index 000000000..54e75e19f --- /dev/null +++ b/src/errors/meilisearch-timeout-error.ts @@ -0,0 +1,11 @@ +class MeiliSearchTimeOutError extends Error { + type: string + constructor(message: string) { + super(message) + this.name = 'MeiliSearchTimeOutError' + this.type = this.constructor.name + Error.captureStackTrace(this, MeiliSearchTimeOutError) + } +} + +export { MeiliSearchTimeOutError } diff --git a/src/indexes.ts b/src/indexes.ts index daeeddd35..5a71dfb7d 100644 --- a/src/indexes.ts +++ b/src/indexes.ts @@ -7,10 +7,14 @@ 'use strict' +import { MeiliSearchTimeOutError } from './errors/meilisearch-timeout-error' import MeiliAxiosWrapper from './meili-axios-wrapper' import * as Types from './types' +import { sleep } from './utils' -class Indexes extends MeiliAxiosWrapper implements Types.Indexes{ + + +class Indexes extends MeiliAxiosWrapper implements Types.Indexes { indexUid: string constructor(config: Types.Config, indexUid: string) { super(config) @@ -32,6 +36,24 @@ class Indexes extends MeiliAxiosWrapper implements Types.Indexes{ return this.get(url) } + async waitForPendingUpdate( + updateId: number, + { + timeOutMs = 5000, + intervalMs = 50, + }: { timeOutMs?: number; intervalMs?: number } = {} + ) { + const startingTime = Date.now() + while (Date.now() - startingTime < timeOutMs) { + const response = await this.getUpdateStatus(updateId) + if (response.status !== 'enqueued') return response + await sleep(intervalMs) + } + throw new MeiliSearchTimeOutError( + `timeout of ${timeOutMs}ms has exceeded on process ${updateId} when waiting for pending update to resolve.` + ) + } + /** * Get the list of all updates * @memberof Indexes @@ -319,7 +341,7 @@ class Indexes extends MeiliAxiosWrapper implements Types.Indexes{ * @memberof Indexes * @method updateSynonyms */ - updateSynonyms(synonyms: object): Promise { + updateSynonyms(synonyms: object): Promise { const url = `/indexes/${this.indexUid}/settings/synonyms` return this.post(url, synonyms) @@ -330,7 +352,7 @@ class Indexes extends MeiliAxiosWrapper implements Types.Indexes{ * @memberof Indexes * @method resetSynonyms */ - resetSynonyms(): Promise { + resetSynonyms(): Promise { const url = `/indexes/${this.indexUid}/settings/synonyms` return this.delete(url) diff --git a/src/meili-axios-wrapper.ts b/src/meili-axios-wrapper.ts index 827bfbd02..808ff6e2a 100644 --- a/src/meili-axios-wrapper.ts +++ b/src/meili-axios-wrapper.ts @@ -13,7 +13,7 @@ import instance, { AxiosResponse, CancelTokenSource, } from 'axios' -import MeiliAxiosError from './meili-axios-error' +import MeiliSearchApiError from './errors/meilisearch-error' import * as Types from './types' class MeiliAxiosWrapper { @@ -57,7 +57,7 @@ class MeiliAxiosWrapper { .get(url, config) .then((response: any) => response) .catch((e) => { - const meiliError = new MeiliAxiosError(e, cachedStack) + const meiliError = new MeiliSearchApiError(e, cachedStack) throw meiliError }) } @@ -73,7 +73,7 @@ class MeiliAxiosWrapper { .post(url, data, config) .then((response: any) => response) .catch((e) => { - throw new MeiliAxiosError(e, cachedStack) + throw new MeiliSearchApiError(e, cachedStack) }) } @@ -88,7 +88,7 @@ class MeiliAxiosWrapper { .put(url, data, config) .then((response: any) => response) .catch((e) => { - const meiliError = new MeiliAxiosError(e, cachedStack) + const meiliError = new MeiliSearchApiError(e, cachedStack) throw meiliError }) } @@ -103,7 +103,7 @@ class MeiliAxiosWrapper { .patch(url, data, config) .then((response: any) => response) .catch((e) => { - const meiliError = new MeiliAxiosError(e, cachedStack) + const meiliError = new MeiliSearchApiError(e, cachedStack) throw meiliError }) } @@ -117,7 +117,7 @@ class MeiliAxiosWrapper { .delete(url, config) .then((response: any) => response) .catch((e) => { - const meiliError = new MeiliAxiosError(e, cachedStack) + const meiliError = new MeiliSearchApiError(e, cachedStack) throw meiliError }) } diff --git a/src/types.ts b/src/types.ts index ae9c796a2..de646f7e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,10 @@ -import { AxiosError, AxiosInstance, CancelTokenSource, AxiosRequestConfig, AxiosResponse } from 'axios' +import { + AxiosError, + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + CancelTokenSource, +} from 'axios' /// /// Global interfaces @@ -203,127 +209,126 @@ export interface SysInfoPretty { ** MeiliSearch Class */ - -export interface Indexes extends MeiliAxiosWrapper{ - indexUid: string; - getUpdateStatus(updateId: number): Promise; - getAllUpdateStatus(): Promise; - search(query: string, options?: SearchParams): Promise; - show(): Promise; - updateIndex(data: UpdateIndexRequest): Promise; - deleteIndex(): Promise; - getStats(): Promise; - getDocuments(options?: GetDocumentsParams): Promise; - getDocument(documentId: string | number): Promise; +export interface Indexes extends MeiliAxiosWrapper { + indexUid: string + getUpdateStatus(updateId: number): Promise + getAllUpdateStatus(): Promise + search(query: string, options?: SearchParams): Promise + show(): Promise + updateIndex(data: UpdateIndexRequest): Promise + deleteIndex(): Promise + getStats(): Promise + getDocuments(options?: GetDocumentsParams): Promise + getDocument(documentId: string | number): Promise addDocuments( documents: Document[], options?: AddDocumentParams - ): Promise; + ): Promise updateDocuments( documents: Document[], options?: AddDocumentParams - ): Promise; - deleteDocument(documentId: string | number): Promise; - deleteDocuments(documentsIds: string[] | number[]): Promise; - deleteAllDocuments(): Promise; - getSettings(): Promise; - updateSettings(settings: Settings): Promise; - resetSettings(): Promise; - getSynonyms(): Promise; - updateSynonyms(synonyms: object): Promise; - resetSynonyms(): Promise; - getStopWords(): Promise; - updateStopWords(stopWords: string[]): Promise; - resetStopWords(): Promise; - getRankingRules(): Promise; - updateRankingRules(rankingRules: string[]): Promise; - resetRankingRules(): Promise; - getDistinctAttribute(): Promise; - updateDistinctAttribute(distinctAttribute: string): Promise; - resetDistinctAttribute(): Promise; - getSearchableAttributes(): Promise; + ): Promise + deleteDocument(documentId: string | number): Promise + deleteDocuments(documentsIds: string[] | number[]): Promise + deleteAllDocuments(): Promise + getSettings(): Promise + updateSettings(settings: Settings): Promise + resetSettings(): Promise + getSynonyms(): Promise + updateSynonyms(synonyms: object): Promise + resetSynonyms(): Promise + getStopWords(): Promise + updateStopWords(stopWords: string[]): Promise + resetStopWords(): Promise + getRankingRules(): Promise + updateRankingRules(rankingRules: string[]): Promise + resetRankingRules(): Promise + getDistinctAttribute(): Promise + updateDistinctAttribute(distinctAttribute: string): Promise + resetDistinctAttribute(): Promise + getSearchableAttributes(): Promise updateSearchableAttributes( searchableAttributes: string[] - ): Promise; - resetSearchableAttributes(): Promise; - getDisplayedAttributes(): Promise; + ): Promise + resetSearchableAttributes(): Promise + getDisplayedAttributes(): Promise updateDisplayedAttributes( displayedAttributes: string[] - ): Promise; - resetDisplayedAttributes(): Promise; - getAcceptNewFields(): Promise; - updateAcceptNewFields(acceptNewFields: boolean): Promise; + ): Promise + resetDisplayedAttributes(): Promise + getAcceptNewFields(): Promise + updateAcceptNewFields(acceptNewFields: boolean): Promise } -export interface Meilisearch extends MeiliAxiosErrorInterface { - config: Config; - getIndex(indexUid: string): Indexes; - listIndexes(): Promise; - createIndex(data: IndexRequest): Promise; - getKeys(): Promise; - isHealthy(): Promise; - setHealthy(): Promise; - setUnhealthy(): Promise; - changeHealthTo(health: boolean): Promise; - stats(): Promise; - version(): Promise; - sysInfo(): Promise; - prettySysInfo(): Promise; +export interface Meilisearch extends MeiliSearchApiErrorInterface { + config: Config + getIndex(indexUid: string): Indexes + listIndexes(): Promise + createIndex(data: IndexRequest): Promise + getKeys(): Promise + isHealthy(): Promise + setHealthy(): Promise + setUnhealthy(): Promise + changeHealthTo(health: boolean): Promise + stats(): Promise + version(): Promise + sysInfo(): Promise + prettySysInfo(): Promise } export interface MeiliAxiosWrapper { - instance: AxiosInstance; - cancelTokenSource: CancelTokenSource; + instance: AxiosInstance + cancelTokenSource: CancelTokenSource get>( url: string, config?: AxiosRequestConfig - ): Promise; + ): Promise post>( url: string, data?: any, config?: AxiosRequestConfig - ): Promise; + ): Promise put>( url: string, data?: any, config?: AxiosRequestConfig - ): Promise; + ): Promise patch>( url: string, data?: any, config?: AxiosRequestConfig - ): Promise; + ): Promise delete>( url: string, config?: AxiosRequestConfig - ): Promise; + ): Promise } /* ** ERROR HANDLER */ -export interface MeiliAxiosErrorInterface extends Error { +export interface MeiliSearchApiErrorInterface extends Error { name: string message: string stack?: string } -export interface MeiliAxiosErrorResponse { +export interface MeiliSearchApiErrorResponse { status?: number statusText?: string path?: string method?: string body?: object } -export interface MeiliAxiosErrorRequest { +export interface MeiliSearchApiErrorRequest { url?: string path?: string method?: string } -export type MeiliAxiosErrorConstructor = new ( +export type MeiliSearchApiErrorConstructor = new ( error: AxiosError, cachedStack?: string ) => void -export default Indexes; +export default Indexes diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 000000000..a147d0a54 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,8 @@ + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +export { + sleep +} diff --git a/tests/wait_for_pending_update_tests.ts b/tests/wait_for_pending_update_tests.ts new file mode 100644 index 000000000..de75b0ba9 --- /dev/null +++ b/tests/wait_for_pending_update_tests.ts @@ -0,0 +1,85 @@ +import * as Types from '../src/types' +import { + clearAllIndexes, + sleep, + config, + masterClient, +} from './meilisearch-test-utils' + +const index = { + uid: 'movies_test', +} + +const dataset = [ + { id: 123, title: 'Pride and Prejudice', comment: 'A great book' }, + { id: 456, title: 'Le Petit Prince', comment: 'A french book' }, + { id: 2, title: 'Le Rouge et le Noir', comment: 'Another french book' }, + { id: 1, title: 'Alice In Wonderland', comment: 'A weird book' }, + { id: 1344, title: 'The Hobbit', comment: 'An awesome book' }, + { + id: 4, + title: 'Harry Potter and the Half-Blood Prince', + comment: 'The best book', + }, + { id: 42, title: "The Hitchhiker's Guide to the Galaxy" }, +] + +jest.setTimeout(100 * 1000) + +beforeAll(async () => { + await clearAllIndexes(config) + await masterClient.createIndex(index) + await sleep(500) +}) + +afterAll(() => { + return clearAllIndexes(config) +}) + +describe.each([ + { client: masterClient, permission: 'Master' }, + // { client: privateClient, permission: 'Private' }, +])('Test on wait-for-pending-update', ({ client, permission }) => { + beforeEach(async () => { + await clearAllIndexes(config) + await masterClient.createIndex(index) + }) + test(`${permission} key: Get WaitForPendingStatus until done and resolved`, async () => { + const { updateId } = await client.getIndex(index.uid).addDocuments(dataset) + const update = await client + .getIndex(index.uid) + .waitForPendingUpdate(updateId) + expect(update).toHaveProperty('status', 'processed') + }) + test(`${permission} key: Get WaitForPendingStatus with custom interval and timeout until done and resolved`, async () => { + const { updateId } = await client.getIndex(index.uid).addDocuments(dataset) + const update = await client + .getIndex(index.uid) + .waitForPendingUpdate(updateId, { + timeOutMs: 6000, + intervalMs: 100, + }) + expect(update).toHaveProperty('status', 'processed') + }) + test(`${permission} key: Get WaitForPendingStatus with custom timeout and interval at 0 done and resolved`, async () => { + const { updateId } = await client.getIndex(index.uid).addDocuments(dataset) + const update = await client + .getIndex(index.uid) + .waitForPendingUpdate(updateId, { + timeOutMs: 6000, + intervalMs: 0, + }) + expect(update).toHaveProperty('status', 'processed') + }) + + test(`${permission} key: Try to WaitForPendingStatus with small timeout and raise an error`, async () => { + const { updateId } = await client.getIndex(index.uid).addDocuments(dataset) + await expect( + client + .getIndex(index.uid) + .waitForPendingUpdate(updateId, { timeOutMs: 0 }) + ).rejects.toThrowError( + `timeout of 0ms has exceeded on process 0 when waiting for pending update to resolve.` + ) + }) +}) From 9353bfea95ef5d235e410c62d4ab7cfaff94e5a0 Mon Sep 17 00:00:00 2001 From: Charlotte Vermandel Date: Tue, 21 Apr 2020 18:17:51 +0200 Subject: [PATCH 2/3] Style fixes --- src/indexes.ts | 2 -- src/utils.ts | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/indexes.ts b/src/indexes.ts index 5a71dfb7d..cbb433373 100644 --- a/src/indexes.ts +++ b/src/indexes.ts @@ -12,8 +12,6 @@ import MeiliAxiosWrapper from './meili-axios-wrapper' import * as Types from './types' import { sleep } from './utils' - - class Indexes extends MeiliAxiosWrapper implements Types.Indexes { indexUid: string constructor(config: Types.Config, indexUid: string) { diff --git a/src/utils.ts b/src/utils.ts index a147d0a54..57c803ca6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,5 @@ - function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } -export { - sleep -} +export { sleep } From 356108175e64255776778467b60d725ed9046443 Mon Sep 17 00:00:00 2001 From: cvermand <33010418+bidoubiwa@users.noreply.github.com> Date: Wed, 22 Apr 2020 12:05:33 +0200 Subject: [PATCH 3/3] Update tests/wait_for_pending_update_tests.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Clémentine Urquizar --- tests/wait_for_pending_update_tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wait_for_pending_update_tests.ts b/tests/wait_for_pending_update_tests.ts index de75b0ba9..7d5a8e299 100644 --- a/tests/wait_for_pending_update_tests.ts +++ b/tests/wait_for_pending_update_tests.ts @@ -38,7 +38,7 @@ afterAll(() => { describe.each([ { client: masterClient, permission: 'Master' }, - // { client: privateClient, permission: 'Private' }, + { client: privateClient, permission: 'Private' }, ])('Test on wait-for-pending-update', ({ client, permission }) => { beforeEach(async () => { await clearAllIndexes(config)