diff --git a/src/http/apiCore.ts b/src/http/apiCore.ts index 9b0057c6..c9845291 100644 --- a/src/http/apiCore.ts +++ b/src/http/apiCore.ts @@ -5,8 +5,8 @@ import { InputSource, PageOptions, LocalInputSource } from "@/input/index.js"; export const TIMEOUT_SECS_DEFAULT: number = 120; export interface RequestOptions { - hostname: string; - path: string; + hostname?: string; + path?: string; method: any; timeoutSecs: number; headers: any; @@ -33,15 +33,15 @@ export async function cutDocPages(inputDoc: InputSource, pageOptions: PageOption * Reads a response from the API and processes it. * @param dispatcher custom dispatcher to use for the request. * @param options options related to the request itself. + * @param url override the URL of the request. * @returns the processed request. */ export async function sendRequestAndReadResponse( dispatcher: Dispatcher, options: RequestOptions, + url?: string, ): Promise { - const url: string = `https://${options.hostname}${options.path}`; - - logger.debug(`${options.method}: ${url}`); + url ??= `https://${options.hostname}${options.path}`; const response = await request( url, { diff --git a/src/v2/client.ts b/src/v2/client.ts index 0ec88443..b8b7f5b7 100644 --- a/src/v2/client.ts +++ b/src/v2/client.ts @@ -12,8 +12,6 @@ import { BaseProduct } from "@/v2/product/baseProduct.js"; /** * Options for the V2 Mindee Client. - * - * @category ClientV2 * @example * const client = new MindeeClientV2({ * apiKey: "YOUR_API_KEY", @@ -32,8 +30,6 @@ export interface ClientOptions { /** * Mindee Client V2 class that centralizes most basic operations. - * - * @category ClientV2 */ export class Client { /** Mindee V2 API handler. */ @@ -89,7 +85,6 @@ export class Client { * @param product the product to retrieve. * @param inferenceId id of the queue to poll. * @typeParam T an extension of an `Inference`. Can be omitted as it will be inferred from the `productClass`. - * @category Asynchronous * @returns a `Promise` containing the inference. */ async getResult

( @@ -99,7 +94,7 @@ export class Client { logger.debug( `Attempting to get inference with ID: ${inferenceId} using response type: ${product.name}` ); - return await this.mindeeApi.getProductResult(product, inferenceId); + return await this.mindeeApi.getProductResultById(product, inferenceId); } /** @@ -108,7 +103,6 @@ export class Client { * * @param jobId id of the queue to poll. * @typeParam T an extension of an `Inference`. Can be omitted as it will be inferred from the `productClass`. - * @category Asynchronous * @returns a `Promise` containing a `Job`, which also contains a `Document` if the * parsing is complete. */ @@ -126,7 +120,6 @@ export class Client { * * @param pollingOptions options for the polling loop, see {@link PollingOptions}. * @typeParam T an extension of an `Inference`. Can be omitted as it will be inferred from the `productClass`. - * @category Synchronous * @returns a `Promise` containing parsing results. */ async enqueueAndGetResult

( @@ -155,7 +148,7 @@ export class Client { protected async pollForResult

( product: typeof BaseProduct, pollingOptions: PollingOptions, - queueId: string, + jobId: string, ): Promise> { logger.debug( `Waiting ${pollingOptions.initialDelaySec} seconds before polling.` @@ -166,7 +159,7 @@ export class Client { pollingOptions.initialTimerOptions ); logger.debug( - `Start polling for inference using job ID: ${queueId}.` + `Start polling for inference using job ID: ${jobId}.` ); let retryCounter: number = 1; let pollResults: JobResponse; @@ -174,7 +167,7 @@ export class Client { logger.debug( `Attempt ${retryCounter} of ${pollingOptions.maxRetries}` ); - pollResults = await this.getJob(queueId); + pollResults = await this.getJob(jobId); const error: ErrorResponse | undefined = pollResults.job.error; if (error) { throw new MindeeHttpErrorV2(error); @@ -184,7 +177,12 @@ export class Client { break; } if (pollResults.job.status === "Processed") { - return this.getResult(product, pollResults.job.id); + if (!pollResults.job.resultUrl) { + throw new MindeeError( + "The result URL is undefined. This is a server error, try again later or contact support." + ); + } + return this.mindeeApi.getProductResultByUrl(product, pollResults.job.resultUrl); } await setTimeout( pollingOptions.delaySec * 1000, diff --git a/src/v2/http/apiSettings.ts b/src/v2/http/apiSettings.ts index 8449c150..3bbc6676 100644 --- a/src/v2/http/apiSettings.ts +++ b/src/v2/http/apiSettings.ts @@ -6,6 +6,9 @@ const API_V2_KEY_ENVVAR_NAME: string = "MINDEE_V2_API_KEY"; const API_V2_HOST_ENVVAR_NAME: string = "MINDEE_V2_API_HOST"; const DEFAULT_MINDEE_API_HOST: string = "api-v2.mindee.net"; +/** + * Settings for the V2 API. + */ export class ApiSettings extends BaseSettings { baseHeaders: Record; diff --git a/src/v2/http/mindeeApiV2.ts b/src/v2/http/mindeeApiV2.ts index 2677cb58..30d7013a 100644 --- a/src/v2/http/mindeeApiV2.ts +++ b/src/v2/http/mindeeApiV2.ts @@ -13,12 +13,14 @@ import { RequestOptions } from "@/http/apiCore.js"; import { InputSource, LocalInputSource, UrlInput } from "@/input/index.js"; -import { MindeeDeserializationError } from "@/errors/index.js"; +import { MindeeDeserializationError, MindeeError } from "@/errors/index.js"; import { MindeeHttpErrorV2 } from "./errors.js"; import { logger } from "@/logger.js"; import { BaseProduct } from "@/v2/product/baseProduct.js"; - +/** + * Mindee V2 API handler. + */ export class MindeeApiV2 { settings: ApiSettings; @@ -27,12 +29,11 @@ export class MindeeApiV2 { } /** - * Sends a file to the extraction inference queue. + * Sends a file to the product inference queue. * @param product product to enqueue. * @param inputSource Local file loaded as an input. * @param params {ExtractionParameters} parameters relating to the enqueueing options. - * @category V2 - * @throws Error if the server's response contains one. + * @throws Error if the server's response contains an error. * @returns a `Promise` containing a job response. */ async enqueueProduct( @@ -51,11 +52,10 @@ export class MindeeApiV2 { } /** - * Requests the results of a queued document from the API. - * Throws an error if the server's response contains one. - * @param jobId The document's ID in the queue. - * @category Asynchronous - * @returns a `Promise` containing information on the queue. + * Get the specified Job. + * Throws an error if the server's response contains an error. + * @param jobId The Job ID as returned by the enqueue request. + * @returns a `Promise` containing the job response. */ async getJob(jobId: string): Promise { const response = await this.#reqGetJob(jobId); @@ -63,23 +63,37 @@ export class MindeeApiV2 { } /** - * Requests the job of a queued document from the API. - * Throws an error if the server's response contains one. + * Get the result of a queued document from the API. + * Throws an error if the server's response contains an error. * @param product - * @param inferenceId The document's ID in the queue. - * @category Asynchronous - * @returns a `Promise` containing either the parsed result, or information on the queue. + * @param inferenceId The inference ID for the result. + * @returns a `Promise` containing the parsed result. */ - async getProductResult

( + async getProductResultById

( product: P, inferenceId: string, ): Promise> { const queueResponse: BaseHttpResponse = await this.#reqGetProductResult( - inferenceId, product.slug + `https://${this.settings.hostname}/v2/products/${product.slug}/results/${inferenceId}` ); return this.#processResponse(queueResponse, product.responseClass) as InstanceType; } + /** + * Get the result of a queued document from the API. + * Throws an error if the server's response contains an error. + * @param product + * @param url The URL as returned by a Job's resultUrl property. + * @returns a `Promise` containing the parsed result. + */ + async getProductResultByUrl

( + product: P, + url: string, + ): Promise> { + const queueResponse: BaseHttpResponse = await this.#reqGetProductResult(url); + return this.#processResponse(queueResponse, product.responseClass) as InstanceType; + } + #processResponse( result: BaseHttpResponse, responseClass: ResponseConstructor, @@ -114,7 +128,6 @@ export class MindeeApiV2 { /** * Sends a document to the inference queue. - * * @param product Product to enqueue. * @param inputSource Local or remote file as an input. * @param params {ExtractionParameters} parameters relating to the enqueueing options. @@ -155,19 +168,18 @@ export class MindeeApiV2 { /** * Make a request to GET the status of a document in the queue. - * @param inferenceId ID of the inference. - * @param slug "jobs" or "inferences"... - * @category Asynchronous - * @returns a `Promise` containing either the parsed result, or information on the queue. + * @param url URL path to the result. + * @returns a `Promise` containing the parsed result. */ - async #reqGetProductResult(inferenceId: string, slug: string): Promise { + async #reqGetProductResult(url: string): Promise { const options: RequestOptions = { method: "GET", headers: this.settings.baseHeaders, - hostname: this.settings.hostname, - path: `/v2/products/${slug}/results/${inferenceId}`, timeoutSecs: this.settings.timeoutSecs, }; - return await sendRequestAndReadResponse(this.settings.dispatcher, options); + if (!url.startsWith("https://")) { + throw new MindeeError(`Invalid URL: ${url}`); + } + return await sendRequestAndReadResponse(this.settings.dispatcher, options, url); } } diff --git a/tests/v2/client/client.integration.ts b/tests/v2/client/client.integration.ts index 56f29dd6..c2f1ea9e 100644 --- a/tests/v2/client/client.integration.ts +++ b/tests/v2/client/client.integration.ts @@ -1,3 +1,4 @@ +import { before, beforeEach, describe, it } from "node:test"; import assert from "node:assert/strict"; import path from "node:path"; @@ -37,7 +38,7 @@ function checkEmptyActiveOptions(inference: ExtractionInference) { assert.equal(inference.activeOptions?.textContext, false); } -describe("MindeeV2 – Integration - Client", () => { +describe("MindeeV2 – Integration - Client", { timeout: 120000 }, () => { let client: Client; let modelId: string; @@ -98,7 +99,7 @@ describe("MindeeV2 – Integration - Client", () => { assert.ok(inference.result); assert.equal(inference.result.rawText, undefined); checkEmptyActiveOptions(inference); - }).timeout(60000); + }); it("enqueueAndGetResult must succeed: Filled, single-page image – PathInput", async () => { const source = new PathInput({ inputPath: sampleImagePath }); @@ -137,7 +138,7 @@ describe("MindeeV2 – Integration - Client", () => { assert.equal(inference.activeOptions?.textContext, true); assert.equal(inference.result.rawText?.pages.length, 1); - }).timeout(120000); + }); it("enqueueAndGetResult must succeed: Filled, single-page image – Base64Input", async () => { const data = fs.readFileSync(sampleBase64Path, "utf8"); @@ -168,7 +169,7 @@ describe("MindeeV2 – Integration - Client", () => { assert.equal(supplierField.value, "Clachan"); checkEmptyActiveOptions(inference); - }).timeout(120000); + }); it("enqueue must raise 422: Invalid model ID", async () => { const source = new PathInput({ inputPath: emptyPdfPath }); @@ -180,7 +181,7 @@ describe("MindeeV2 – Integration - Client", () => { } catch (err) { check422(err); } - }).timeout(60000); + }); it("getResult must raise 422: Invalid job ID", async () => { try { @@ -192,7 +193,35 @@ describe("MindeeV2 – Integration - Client", () => { } catch (err) { check422(err); } - }).timeout(60000); + }); + + it("enqueue, getJob, and getResult must succeed", async () => { + const source = new PathInput({ inputPath: emptyPdfPath }); + const params = { + modelId, + rag: false, + rawText: false, + polygon: false, + confidence: false, + alias: "ts_integration_all_together" + }; + + const enqueueResponse = await client.enqueue( + Extraction, source, params + ); + assert.ok(enqueueResponse.job.id); + + setTimeout(async () => { + const jobResponse = await client.getJob(enqueueResponse.job.id); + assert.ok(jobResponse.job.resultUrl); + + const resultId = jobResponse.job.resultUrl?.split("/").pop() || ""; + const resultResponse = await client.getResult( + Extraction, resultId + ); + assert.strictEqual(resultId, resultResponse.inference.id); + }, 6500); + }); it("enqueueAndGetResult must succeed: HTTPS URL", async () => { const url = process.env.MINDEE_V2_SE_TESTS_BLANK_PDF_URL ?? "error-no-url-found"; @@ -211,7 +240,7 @@ describe("MindeeV2 – Integration - Client", () => { ); assert.ok(response); assert.ok(response.inference instanceof ExtractionInference); - }).timeout(60000); + }); it("should override the data schema successfully", async () => { const source = new PathInput({ inputPath: emptyPdfPath }); @@ -233,6 +262,6 @@ describe("MindeeV2 – Integration - Client", () => { assert.ok(response.inference.result.fields.get("test_replace")); assert.equal((response.inference.result.fields.get("test_replace") as SimpleField).value, "a test value"); - }).timeout(60000); + }); });