From f2b5b51a157a959e90cbd194a50857fe1b57b0c0 Mon Sep 17 00:00:00 2001 From: Paul Marechal Date: Fri, 1 Oct 2021 12:19:49 -0400 Subject: [PATCH] Support BigInt values JSON doesn't contrain numeric values to be specific sizes. In most languages this isn't an issue thanks to proper support for a wide variety of number precision. Unfortunately JavaScript struggles with integer numbers bigger than `Number.MAX_SAFE_INTEGER`. This is due to JavaScript's implementation of integer values as `double`. For anything bigger than this max safe integer value JavaScript provides a `BigInt` type. The first issue with this behavior is that when parsing JSON, there is no way of telling which "number flavor" to expect, APIs just don't allow us to define so. Another issue is that the default JSON serializer/deserializer doesn't even know how to handle `BigInt`. This commit fixes those issues as followed: 1. Use a custom JSON parser `json-bigint` that can serialize and deserialze `BigInt` values. A quirk is that it will only deserialize a number into a `BigInt` if the serialized value ends up being bigger than JavaScript's max safe integer. This means there is ambiguity when deserialing data. 2. To address the ambiguity issue, I added a TypeScript mapped type `BigintOrNumber` that given a type `T` will replace all fields that are either `bigint` or `number`. Note that this is only static typing to teach TypeScript about this behavior. Then when receiving messages, one has to define `Normalizer` functions which will take this ambiguous type as input and return a "normalized" version that matches the original type `T`. See this as post-processing to make sure the received data is using the proper data types in the right places. 3. Rewrite all the tests to validate this logic using test data as coming out of the current Trace Server. Signed-off-by: Paul Marechal Co-authored-by: Patrick Tasse --- .editorconfig | 19 ++ jest.config.json | 2 +- package.json | 2 + src/models/annotation.ts | 23 +- src/models/bookmark.ts | 15 +- src/models/entry.ts | 23 ++ src/models/experiment.ts | 16 +- src/models/filter.ts | 4 +- src/models/output-descriptor.ts | 24 +- src/models/query/query-helper.test.ts | 10 +- src/models/query/query-helper.ts | 39 ++- src/models/query/query.ts | 2 + src/models/response/responses.ts | 19 +- src/models/table.ts | 47 ++- src/models/timegraph.ts | 68 +++- src/models/trace.ts | 16 +- src/models/xy.ts | 26 +- src/protocol/__mocks__/rest-client.ts | 154 --------- .../fixtures/tsp-client/check-health-0.json | 3 + .../tsp-client/create-experiment-0.json | 19 ++ .../tsp-client/delete-experiment-0.json | 19 ++ .../fixtures/tsp-client/delete-trace-0.json | 9 + .../tsp-client/experiment-outputs-0.json | 26 ++ .../fetch-annotation-categories-0.json | 9 + .../tsp-client/fetch-annotations-0.json | 24 ++ .../tsp-client/fetch-experiment-0.json | 19 ++ .../tsp-client/fetch-experiments-0.json | 21 ++ .../tsp-client/fetch-marker-sets-0.json | 10 + .../fixtures/tsp-client/fetch-styles-0.json | 18 + .../tsp-client/fetch-table-columns-0.json | 24 ++ .../tsp-client/fetch-table-lines-0.json | 43 +++ .../tsp-client/fetch-timegraph-arrows-0.json | 18 + .../tsp-client/fetch-timegraph-states-0.json | 37 +++ .../tsp-client/fetch-timegraph-tooltip-0.json | 7 + .../tsp-client/fetch-timegraph-tree-0.json | 22 ++ .../fixtures/tsp-client/fetch-xy-0.json | 23 ++ .../fixtures/tsp-client/fetch-xy-tree-0.json | 38 +++ .../fixtures/tsp-client/open-trace-0.json | 9 + src/protocol/rest-client.ts | 109 ++++-- src/protocol/serialization.ts | 69 ++++ src/protocol/test-utils.ts | 58 ++++ src/protocol/tsp-client-response.ts | 20 +- src/protocol/tsp-client.test.ts | 309 ++++++++++++++---- src/protocol/tsp-client.ts | 132 +++++--- tsconfig.json | 5 +- yarn.lock | 17 + 46 files changed, 1265 insertions(+), 361 deletions(-) create mode 100644 .editorconfig delete mode 100644 src/protocol/__mocks__/rest-client.ts create mode 100644 src/protocol/fixtures/tsp-client/check-health-0.json create mode 100644 src/protocol/fixtures/tsp-client/create-experiment-0.json create mode 100644 src/protocol/fixtures/tsp-client/delete-experiment-0.json create mode 100644 src/protocol/fixtures/tsp-client/delete-trace-0.json create mode 100644 src/protocol/fixtures/tsp-client/experiment-outputs-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-annotation-categories-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-annotations-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-experiment-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-experiments-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-marker-sets-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-styles-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-table-columns-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-table-lines-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-timegraph-arrows-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-timegraph-states-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-timegraph-tooltip-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-timegraph-tree-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-xy-0.json create mode 100644 src/protocol/fixtures/tsp-client/fetch-xy-tree-0.json create mode 100644 src/protocol/fixtures/tsp-client/open-trace-0.json create mode 100644 src/protocol/serialization.ts create mode 100644 src/protocol/test-utils.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..13dcb55 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +indent_size = 4 +indent_style = space + +[*.test.ts] +indent_size = 2 +indent_style = space + +[*.json] +indent_size = 2 +indent_style = space diff --git a/jest.config.json b/jest.config.json index 4c02c80..d838e67 100644 --- a/jest.config.json +++ b/jest.config.json @@ -20,4 +20,4 @@ "json", "node" ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 6065dee..0f0e2ee 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "typescript": "latest" }, "dependencies": { + "@types/json-bigint": "^1.0.1", + "json-bigint": "^1.0.0", "node-fetch": "^2.5.0" }, "scripts": { diff --git a/src/models/annotation.ts b/src/models/annotation.ts index f9a048c..9d22b16 100644 --- a/src/models/annotation.ts +++ b/src/models/annotation.ts @@ -1,3 +1,4 @@ +import { assertNumber, Normalizer } from '../protocol/serialization'; import { OutputElementStyle } from './styles'; export enum Type { @@ -9,10 +10,28 @@ export interface AnnotationCategoriesModel { annotationCategories: string[]; } +export const AnnotationModel: Normalizer = input => { + const annotations: AnnotationModel['annotations'] = {}; + for (const [category, annotationArray] of Object.entries(input.annotations)) { + annotations[category] = annotationArray.map(Annotation); + } + return { annotations }; +}; + export interface AnnotationModel { annotations: { [category: string]: Annotation[] }; } +export const Annotation: Normalizer = input => { + const { duration, entryId, time, ...rest } = input; + return { + ...rest, + duration: BigInt(duration), + entryId: assertNumber(entryId), + time: BigInt(time), + }; +}; + /** * Model for annotation */ @@ -26,12 +45,12 @@ export interface Annotation { /** * Time of the annotation */ - time: number; + time: bigint; /** * Duration of the annotation */ - duration: number; + duration: bigint; /** * Entry Id of the annotation diff --git a/src/models/bookmark.ts b/src/models/bookmark.ts index cd0561e..e96e4e3 100644 --- a/src/models/bookmark.ts +++ b/src/models/bookmark.ts @@ -1,3 +1,14 @@ +import { Normalizer } from '../protocol/serialization'; + +export const Bookmark: Normalizer = input => { + const { endTime, startTime, ...rest } = input; + return { + ...rest, + endTime: BigInt(endTime), + startTime: BigInt(startTime), + }; +}; + /** * Model for bookmark */ @@ -15,12 +26,12 @@ export interface Bookmark { /** * Start time for the bookmark */ - startTime: number; + startTime: bigint; /** * End time for the bookmark */ - endTime: number; + endTime: bigint; /** * Type of the bookmark diff --git a/src/models/entry.ts b/src/models/entry.ts index e9b8417..4a52611 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -1,5 +1,18 @@ +import { assertNumber, Normalizer } from '../protocol/serialization'; import { OutputElementStyle } from './styles'; +export const Entry: Normalizer = input => { + const { id, parentId, ...rest } = input; + const entry: Entry = { + ...rest, + id: assertNumber(id), + }; + if (parentId !== undefined) { + entry.parentId = assertNumber(parentId); + } + return entry; +}; + /** * Basic entry interface */ @@ -45,6 +58,16 @@ export interface EntryHeader { tooltip: string } +export function EntryModel(normalizer: Normalizer): Normalizer> { + return input => { + let { entries, ...rest } = input; + return { + ...rest, + entries: entries.map(normalizer), + }; + }; +} + /** * Entry model that will be returned by the server */ diff --git a/src/models/experiment.ts b/src/models/experiment.ts index 986e6ea..038f8fa 100644 --- a/src/models/experiment.ts +++ b/src/models/experiment.ts @@ -1,5 +1,17 @@ +import { assertNumber, Normalizer } from '../protocol/serialization'; import { Trace } from './trace'; +export const Experiment: Normalizer = input => { + const { end, nbEvents, start, traces, ...rest } = input; + return { + ...rest, + end: BigInt(end), + nbEvents: assertNumber(nbEvents), + start: BigInt(start), + traces: traces.map(Trace), + }; +}; + /** * Model of an experiment that contain one or more traces */ @@ -17,12 +29,12 @@ export interface Experiment { /** * Experiment's start time */ - start: number; + start: bigint; /** * Experiment's end time */ - end: number; + end: bigint; /** * Current number of events diff --git a/src/models/filter.ts b/src/models/filter.ts index 7b3701a..8bc029a 100644 --- a/src/models/filter.ts +++ b/src/models/filter.ts @@ -15,12 +15,12 @@ export interface Filter { /** * Start time of the filter */ - startTime: number; + startTime: bigint; /** * End time of the filter */ - endTime: number; + endTime: bigint; /** * Expression from the filtering language diff --git a/src/models/output-descriptor.ts b/src/models/output-descriptor.ts index ede8850..105bc24 100644 --- a/src/models/output-descriptor.ts +++ b/src/models/output-descriptor.ts @@ -1,3 +1,17 @@ +import { Normalizer } from '../protocol/serialization'; + +export const OutputDescriptor: Normalizer = input => { + const { end, start, ...rest } = input; + const outputDescriptor: OutputDescriptor = rest; + if (end !== undefined) { + outputDescriptor.end = BigInt(end); + } + if (start !== undefined) { + outputDescriptor.start = BigInt(start); + } + return outputDescriptor; +}; + /** * Descriptor of a specific output provider */ @@ -26,26 +40,26 @@ export interface OutputDescriptor { /** * Map of query parameters that the provider accept */ - queryParameters: Map; + queryParameters?: Record; /** * Start time */ - start: number; + start?: bigint; /** * End time */ - end: number; + end?: bigint; /** * Indicate if the start, end times and current model are final, * or if they will need to be refreshed later to represent a more up to date version */ - final: boolean; + final?: boolean; /** * List of compatible outputs that can be used in the same view (ex. as overlay) */ - compatibleProviders: string[]; + compatibleProviders?: string[]; } diff --git a/src/models/query/query-helper.test.ts b/src/models/query/query-helper.test.ts index 6750a8a..5879f33 100644 --- a/src/models/query/query-helper.test.ts +++ b/src/models/query/query-helper.test.ts @@ -13,7 +13,7 @@ describe('Query helper tests', () => { }); it('Should build a simple time query', () => { - const array = [1, 2, 3]; + const array = [BigInt(1), BigInt(2), BigInt(3)]; const query = new Query({ [QueryHelper.REQUESTED_TIMES_KEY]: array }); const test = QueryHelper.timeQuery(array); @@ -21,7 +21,7 @@ describe('Query helper tests', () => { }); it('Should build a simple time query with selected items', () => { - const times = [1, 2, 3]; + const times = [BigInt(1), BigInt(2), BigInt(3)]; const items = [4, 5, 6]; const query = new Query({ [QueryHelper.REQUESTED_TIMES_KEY]: times, @@ -47,10 +47,10 @@ describe('Query helper tests', () => { }); it('Should split the range into equal parts', () => { - const start = 10; - const end = 20; + const start = BigInt(10); + const end = BigInt(20); const parts = 3; - const array = [10, 15, 20]; + const array = [BigInt(10), BigInt(15), BigInt(20)]; const test = QueryHelper.splitRangeIntoEqualParts(start, end, parts); expect(test).toEqual(array); diff --git a/src/models/query/query-helper.ts b/src/models/query/query-helper.ts index b722de0..5b1e538 100644 --- a/src/models/query/query-helper.ts +++ b/src/models/query/query-helper.ts @@ -44,25 +44,25 @@ export class QueryHelper { /** * Build a simple time query - * @param timeRequested Array of requested times + * @param requestedTimes Array of requested times * @param additionalProperties Use this optional parameter to add custom properties to your query */ - public static timeQuery(timeRequested: number[], additionalProperties?: { [key: string]: any }): Query { + public static timeQuery(requestedTimes: bigint[], additionalProperties?: { [key: string]: any }): Query { const timeObj = { - [this.REQUESTED_TIMES_KEY]: timeRequested + [this.REQUESTED_TIMES_KEY]: requestedTimes }; return new Query({ ...timeObj, ...additionalProperties }); } /** * Build a simple time query with selected items - * @param timeRequested Array of requested times + * @param requestedTimes Array of requested times * @param items Array of item IDs * @param additionalProperties Use this optional parameter to add custom properties to your query */ - public static selectionTimeQuery(timeRequested: number[], items: number[], additionalProperties?: { [key: string]: any }): Query { + public static selectionTimeQuery(requestedTimes: bigint[], items: number[], additionalProperties?: { [key: string]: any }): Query { const selectionTimeObj = { - [this.REQUESTED_TIMES_KEY]: timeRequested, + [this.REQUESTED_TIMES_KEY]: requestedTimes, [this.REQUESTED_ITEMS_KEY]: items }; @@ -90,22 +90,27 @@ export class QueryHelper { * Split the range into equal parts * @param start Start time * @param end End time - * @param nb Number of element or resolution + * @param nb Number of elements */ - public static splitRangeIntoEqualParts(start: number, end: number, nb: number): number[] { - const result: number[] = new Array(nb); + public static splitRangeIntoEqualParts(start: bigint, end: bigint, nb: number): bigint[] { + if (nb <= 0) { + return []; + } if (nb === 1) { - if (start === end) { - result[0] = start; - return result; - } + return [start]; } - - const stepSize: number = Math.abs(end - start) / (nb - 1); + if (start > end) { + const tmp = end; + end = start; + start = tmp; + } + + const result: bigint[] = new Array(nb); + const stepSize: number = Number(end - start) / (nb - 1); for (let i = 0; i < nb; i++) { - result[i] = Math.min(start, end) + Math.round(i * stepSize); + result[i] = start + BigInt(Math.floor(i * stepSize)); } - result[result.length - 1] = Math.max(start, end); + result[result.length - 1] = end; return result; } } diff --git a/src/models/query/query.ts b/src/models/query/query.ts index 8e020b1..4fb591c 100644 --- a/src/models/query/query.ts +++ b/src/models/query/query.ts @@ -4,9 +4,11 @@ * The output response will contain only elements that pass these filters. */ export class Query { + /** * Map of parameters used for the query */ + // @ts-expect-error TS doesn't like unused private fields. private parameters: object; /** diff --git a/src/models/response/responses.ts b/src/models/response/responses.ts index f93c278..de7cdb3 100644 --- a/src/models/response/responses.ts +++ b/src/models/response/responses.ts @@ -1,4 +1,4 @@ -import { OutputDescriptor } from '../output-descriptor'; +import { BigintOrNumber, Normalizer } from '../../protocol/serialization'; /** * Response status @@ -24,6 +24,18 @@ export enum ResponseStatus { CANCELLED = 'CANCELLED' } +export function GenericResponse(): Normalizer>>; +export function GenericResponse(normalizer: Normalizer): Normalizer>; +export function GenericResponse(normalizer?: Normalizer): Normalizer> | Normalizer>> { + return input => { + const { model, ...rest } = input; + return { + ...rest, + model: normalizer ? normalizer(model) : model, + }; + }; +} + /** * Generic response that contains a model */ @@ -33,11 +45,6 @@ export interface GenericResponse { */ model: T; - /** - * Output descriptor - */ - output: OutputDescriptor; - /** * Response status as described by ResponseStatus */ diff --git a/src/models/table.ts b/src/models/table.ts index d86f53d..b4680ae 100644 --- a/src/models/table.ts +++ b/src/models/table.ts @@ -1,3 +1,13 @@ +import { assertNumber, Normalizer } from '../protocol/serialization'; + +export const ColumnHeaderEntry: Normalizer = input => { + const { id, ...rest } = input; + return { + ...rest, + id: assertNumber(id), + }; +}; + /** * Column header */ @@ -23,6 +33,19 @@ export interface ColumnHeaderEntry { type: string; } +export const Line: Normalizer = input => { + const { cells, index, tags, ...rest } = input; + const line: Line = { + ...rest, + cells: cells.map(Cell), + index: assertNumber(index), + }; + if (tags !== undefined) { + line.tags = assertNumber(tags); + } + return line; +}; + /** * Line of a table */ @@ -40,9 +63,18 @@ export interface Line { /** * Tag associated to the line, used when the line pass a filter */ - tags: number; + tags?: number; } +export const Cell: Normalizer = input => { + const { tags, ...rest } = input; + const cell: Cell = rest; + if (tags !== undefined) { + cell.tags = assertNumber(tags); + } + return cell; +}; + /** * Cell inside a table line */ @@ -55,9 +87,20 @@ export interface Cell { /** * Tag associated to the cell, used when the cell pass a filter */ - tags: number; + tags?: number; } +export const TableModel: Normalizer = input => { + const { columnIds, lines, lowIndex, size, ...rest } = input; + return { + ...rest, + columnIds: columnIds.map(assertNumber), + lines: lines.map(Line), + lowIndex: assertNumber(lowIndex), + size: assertNumber(size), + }; +}; + /** * Model of a table */ diff --git a/src/models/timegraph.ts b/src/models/timegraph.ts index 1f89f15..3389306 100644 --- a/src/models/timegraph.ts +++ b/src/models/timegraph.ts @@ -1,6 +1,21 @@ +import { assertNumber, Normalizer } from '../protocol/serialization'; import { Entry } from './entry'; import { OutputElementStyle } from './styles'; +export const TimeGraphEntry: Normalizer = input => { + const { end, id, start, parentId, ...rest } = input; + const timeGraphEntry: TimeGraphEntry = { + ...rest, + end: BigInt(end), + id: assertNumber(id), + start: BigInt(start), + }; + if (parentId !== undefined) { + timeGraphEntry.parentId = assertNumber(parentId); + } + return timeGraphEntry; +}; + /** * Entry in a time graph */ @@ -8,14 +23,22 @@ export interface TimeGraphEntry extends Entry { /** * Start time of the entry */ - start: number; + start: bigint; /** * End time of the entry */ - end: number; + end: bigint; } +export const TimeGraphModel: Normalizer = input => { + const { rows, ...rest1 } = input; + return { + ...rest1, + rows: rows.map(TimeGraphRow), + }; +}; + /** * Time Graph model that will be returned by the server */ @@ -23,6 +46,15 @@ export interface TimeGraphModel { rows: TimeGraphRow[]; } +export const TimeGraphRow: Normalizer = input => { + const { entryId, states, ...rest } = input; + return { + ...rest, + entryId: assertNumber(entryId), + states: states.map(TimeGraphState), + }; +}; + /** * Time graph row described by an array of states for a specific entry */ @@ -38,6 +70,19 @@ export interface TimeGraphRow { states: TimeGraphState[]; } +const TimeGraphState: Normalizer = input => { + const { end, start, tags, ...rest } = input; + const timegraphState: TimeGraphState = { + ...rest, + end: BigInt(end), + start: BigInt(start), + }; + if (tags !== undefined) { + timegraphState.tags = assertNumber(tags); + } + return timegraphState; +}; + /** * Time graph state */ @@ -45,12 +90,12 @@ export interface TimeGraphState { /** * Start time of the state */ - start: number; + start: bigint; /** * End time of the state */ - end: number; + end: bigint; /** * Label to apply to the state @@ -68,6 +113,17 @@ export interface TimeGraphState { style?: OutputElementStyle; } +export const TimeGraphArrow: Normalizer = input => { + const { end, sourceId, start, targetId, ...rest } = input; + return { + ...rest, + end: BigInt(end), + sourceId: assertNumber(sourceId), + targetId: assertNumber(targetId), + start: BigInt(start), + }; +}; + /** * Arrow for time graph */ @@ -85,12 +141,12 @@ export interface TimeGraphArrow { /** * Start time of the arrow */ - start: number; + start: bigint; /** * Duration of the arrow */ - end: number; + end: bigint; /** * Optional information on the style to format this arrow diff --git a/src/models/trace.ts b/src/models/trace.ts index 9872a92..6d382a3 100644 --- a/src/models/trace.ts +++ b/src/models/trace.ts @@ -1,3 +1,15 @@ +import { assertNumber, Normalizer } from '../protocol/serialization'; + +export const Trace: Normalizer = input => { + const { end, start, nbEvents, ...rest } = input; + return { + ...rest, + end: BigInt(end), + start: BigInt(start), + nbEvents: assertNumber(nbEvents), + }; +}; + /** * Model of a single trace */ @@ -15,12 +27,12 @@ export interface Trace { /** * Trace's start time */ - start: number; + start: bigint; /** * Trace's end time */ - end: number; + end: bigint; /** * URI of the trace diff --git a/src/models/xy.ts b/src/models/xy.ts index 775481f..bf9df38 100644 --- a/src/models/xy.ts +++ b/src/models/xy.ts @@ -1,3 +1,13 @@ +import { assertNumber, Normalizer } from '../protocol/serialization'; + +export const XYModel: Normalizer = input => { + const { series, ...rest } = input; + return { + ...rest, + series: series.map(XYSeries), + }; +}; + /** * Model of a XY chart, contains at least one XY series */ @@ -18,6 +28,20 @@ export interface XYModel { series: XYSeries[]; } +const XYSeries: Normalizer = input => { + const { seriesId, tags, xValues, yValues, ...rest } = input; + const xySeries: XYSeries = { + ...rest, + seriesId: assertNumber(seriesId), + xValues: xValues.map(Number), + yValues: yValues.map(Number), + }; + if (tags !== undefined) { + xySeries.tags = tags.map(assertNumber); + } + return xySeries; +}; + /** * Represent a XY series and its values */ @@ -55,7 +79,7 @@ export interface XYSeries { /** * Array of tags for each XY value, used when a value passes a filter */ - tags: number[]; + tags?: number[]; } /** diff --git a/src/protocol/__mocks__/rest-client.ts b/src/protocol/__mocks__/rest-client.ts deleted file mode 100644 index cc29d68..0000000 --- a/src/protocol/__mocks__/rest-client.ts +++ /dev/null @@ -1,154 +0,0 @@ -import fetch from 'node-fetch'; -import { TspClientResponse } from '../tsp-client-response'; -import { OutputDescriptor } from '../../models/output-descriptor'; - -const response = { - text: 'Test', - status: 200, - statusText: 'Success' -}; - -const output: OutputDescriptor = { - id: 'One', - name: 'Test', - description: 'Test description', - type: 'Test type', - queryParameters: new Map([ - ['key1', 'value1'], - ['key2', 'value2'] - ]), - start: 1, - end: 10, - final: false, - compatibleProviders: ['One', 'Two'] -}; - -export class RestClient { - /** - * Perform request - * Mocks the request call without calling the real backend. - * @param verb RestAPI verb - * @param url URL to query - * @param body Query object as defined by the Query interface - * @return A TspClientResponse object - */ - private static async performRequest(verb: string, url: string, body?: any): Promise> { - return new Promise((resolve, reject) => { - let client: TspClientResponse = new TspClientResponse(response.text, response.status, response.statusText); - - if (verb === 'get') { - // In case a GET request contains parameters in the URL, extract them - let params = this.extractParameters(url); - if (params.length > 0) { - if (params.includes('output')) { - params = params.slice(0, params.indexOf('/')); - let ar = [output]; - ar[0].id = params; - client = new TspClientResponse(JSON.stringify(ar), response.status, response.statusText); - } else { - client = new TspClientResponse(params, response.status, response.statusText); - } - } - } else if (verb === 'post') { - // Check if POST request contains parameters in body - if (typeof body === 'object' && body.parameters !== null) { - const result = body.parameters.test; - client = new TspClientResponse(result, response.status, response.statusText); - } else { - reject(); - } - } else if (verb === 'delete' || verb === 'put') { - const params = this.extractParameters(url); - if (params.length > 0) { - client = new TspClientResponse(params, response.status, response.statusText); - } else { - reject(); - } - } - - resolve(client); - }); - } - - /** - * Perform GET - * T is the expected type of the json object returned by this request - * @param url URL to query without query parameters - * @param parameters Query parameters. Mapped keys and values are used to build the final URL - * @return A TspClientResponse object - */ - public static async get(url: string, parameters?: Map): Promise> { - let getUrl = url; - if (parameters) { - const urlParameters = this.encodeURLParameters(parameters); - getUrl = getUrl.concat(urlParameters); - } - return this.performRequest('get', getUrl); - } - - /** - * Perform POST - * T is the expected type of the json object returned by this request - * @param url URL to query - * @param body Query object as defined by the Query interface - * @return A TspClientResponse object - */ - public static async post(url: string, body?: any): Promise> { - return this.performRequest('post', url, body); - } - - /** - * Perform PUT - * T is the expected type of the json object returned by this request - * @param url URL to query - * @param body Query object as defined by the Query interface - * @return A TspClientResponse object - */ - public static async put(url: string, body?: any): Promise> { - return this.performRequest('put', url, body); - } - - /** - * Perform DELETE - * T is the expected type of the json object returned by this request - * @param url URL to query without query parameters - * @param parameters Query parameters. Mapped keys and values are use to build the final URL - * @return A TspClientResponse object - */ - public static async delete(url: string, parameters?: Map): Promise> { - let deleteUrl = url; - if (parameters) { - const urlParameters = this.encodeURLParameters(parameters); - deleteUrl = deleteUrl.concat(urlParameters); - } - return this.performRequest('delete', deleteUrl); - } - - /** - * Encode URL parameters - * @param parameters Query parameters. Mapped keys and values are used to build the final URL - * @return Encoded string - */ - private static encodeURLParameters(parameters: Map): string { - if (parameters.size) { - const urlParameters: string = '?'; - const parametersArray: string[] = []; - parameters.forEach((value, key) => { - parametersArray.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); - }); - return urlParameters.concat(parametersArray.join('&')); - } - return ''; - } - - /** - * Extract parameters - * @param url URL string - * @return String containing the parameters extracted from the url - */ - public static extractParameters(url: string): string { - const topic = url.includes('traces') ? 'traces' : 'experiments'; - const ar = url.split(topic + '/'); - return url.slice(url.indexOf(topic) + topic.length + 1); - } -} diff --git a/src/protocol/fixtures/tsp-client/check-health-0.json b/src/protocol/fixtures/tsp-client/check-health-0.json new file mode 100644 index 0000000..87fdfb6 --- /dev/null +++ b/src/protocol/fixtures/tsp-client/check-health-0.json @@ -0,0 +1,3 @@ +{ + "status": "UP" +} diff --git a/src/protocol/fixtures/tsp-client/create-experiment-0.json b/src/protocol/fixtures/tsp-client/create-experiment-0.json new file mode 100644 index 0000000..e7ac11d --- /dev/null +++ b/src/protocol/fixtures/tsp-client/create-experiment-0.json @@ -0,0 +1,19 @@ +{ + "name": "kernel", + "UUID": "22222222-2222-2222-2222-222222222222", + "nbEvents": 999999, + "start": 1234567890123456789, + "end": 9876543210987654321, + "indexingStatus": "COMPLETED", + "traces": [ + { + "name": "kernel", + "path": "/path/kernel", + "UUID": "11111111-1111-1111-1111-111111111111", + "nbEvents": 999999, + "start": 1234567890123456789, + "end": 9876543210987654321, + "indexingStatus": "COMPLETED" + } + ] +} diff --git a/src/protocol/fixtures/tsp-client/delete-experiment-0.json b/src/protocol/fixtures/tsp-client/delete-experiment-0.json new file mode 100644 index 0000000..087f624 --- /dev/null +++ b/src/protocol/fixtures/tsp-client/delete-experiment-0.json @@ -0,0 +1,19 @@ +{ + "name": "kernel", + "UUID": "22222222-2222-2222-2222-222222222222", + "nbEvents": 0, + "start": 0, + "end": 0, + "indexingStatus": "CLOSED", + "traces": [ + { + "name": "kernel", + "path": "/path/kernel", + "UUID": "11111111-1111-1111-1111-111111111111", + "nbEvents": 0, + "start": 0, + "end": 0, + "indexingStatus": "CLOSED" + } + ] +} diff --git a/src/protocol/fixtures/tsp-client/delete-trace-0.json b/src/protocol/fixtures/tsp-client/delete-trace-0.json new file mode 100644 index 0000000..63cb227 --- /dev/null +++ b/src/protocol/fixtures/tsp-client/delete-trace-0.json @@ -0,0 +1,9 @@ +{ + "name": "kernel", + "path": "/path/kernel", + "UUID": "11111111-1111-1111-1111-111111111111", + "nbEvents": 0, + "start": 0, + "end": 0, + "indexingStatus": "CLOSED" +} diff --git a/src/protocol/fixtures/tsp-client/experiment-outputs-0.json b/src/protocol/fixtures/tsp-client/experiment-outputs-0.json new file mode 100644 index 0000000..cb4fc5a --- /dev/null +++ b/src/protocol/fixtures/tsp-client/experiment-outputs-0.json @@ -0,0 +1,26 @@ +[ + { + "id": "xy.output.id", + "name": "Output name", + "description": "Output description", + "type": "TREE_TIME_XY" + }, + { + "id": "eventtable.output.id", + "name": "Output name", + "description": "Output description", + "type": "TABLE" + }, + { + "id": "data.output.id", + "name": "Output name", + "description": "Output description", + "type": "DATA_TREE" + }, + { + "id": "timegraph.output.id", + "name": "Output name", + "description": "Output description", + "type": "TIME_GRAPH" + } +] diff --git a/src/protocol/fixtures/tsp-client/fetch-annotation-categories-0.json b/src/protocol/fixtures/tsp-client/fetch-annotation-categories-0.json new file mode 100644 index 0000000..9340b35 --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-annotation-categories-0.json @@ -0,0 +1,9 @@ +{ + "model": { + "annotationCategories": [ + "Annotation category" + ] + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-annotations-0.json b/src/protocol/fixtures/tsp-client/fetch-annotations-0.json new file mode 100644 index 0000000..211d701 --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-annotations-0.json @@ -0,0 +1,24 @@ +{ + "model": { + "annotations": { + "Annotation category": [ + { + "time": 1111111111111111111, + "duration": 1111111, + "entryId": -1, + "type": "CHART", + "label": "label", + "style": { + "parentKey": "Style key", + "values": { + "color": "#FF0000", + "opacity": 0.2 + } + } + } + ] + } + }, + "statusMessage": "Running", + "status": "RUNNING" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-experiment-0.json b/src/protocol/fixtures/tsp-client/fetch-experiment-0.json new file mode 100644 index 0000000..e7ac11d --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-experiment-0.json @@ -0,0 +1,19 @@ +{ + "name": "kernel", + "UUID": "22222222-2222-2222-2222-222222222222", + "nbEvents": 999999, + "start": 1234567890123456789, + "end": 9876543210987654321, + "indexingStatus": "COMPLETED", + "traces": [ + { + "name": "kernel", + "path": "/path/kernel", + "UUID": "11111111-1111-1111-1111-111111111111", + "nbEvents": 999999, + "start": 1234567890123456789, + "end": 9876543210987654321, + "indexingStatus": "COMPLETED" + } + ] +} diff --git a/src/protocol/fixtures/tsp-client/fetch-experiments-0.json b/src/protocol/fixtures/tsp-client/fetch-experiments-0.json new file mode 100644 index 0000000..ceae11a --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-experiments-0.json @@ -0,0 +1,21 @@ +[ + { + "name": "kernel", + "UUID": "22222222-2222-2222-2222-222222222222", + "nbEvents": 999999, + "start": 1234567890123456789, + "end": 9876543210987654321, + "indexingStatus": "COMPLETED", + "traces": [ + { + "name": "kernel", + "path": "/path/kernel", + "UUID": "11111111-1111-1111-1111-111111111111", + "nbEvents": 999999, + "start": 1234567890123456789, + "end": 9876543210987654321, + "indexingStatus": "COMPLETED" + } + ] + } +] diff --git a/src/protocol/fixtures/tsp-client/fetch-marker-sets-0.json b/src/protocol/fixtures/tsp-client/fetch-marker-sets-0.json new file mode 100644 index 0000000..5f7fa20 --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-marker-sets-0.json @@ -0,0 +1,10 @@ +{ + "model": [ + { + "name": "Marker Set name", + "id": "marker.set.id" + } + ], + "statusMessage": "Completed", + "status": "COMPLETED" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-styles-0.json b/src/protocol/fixtures/tsp-client/fetch-styles-0.json new file mode 100644 index 0000000..0fd04bb --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-styles-0.json @@ -0,0 +1,18 @@ +{ + "model": { + "styles": { + "Style key": { + "parentKey": null, + "values": { + "style-name": "Style name", + "background-color": "#646464", + "height": 0.33, + "opacity": 1.0, + "style-group": "Group name" + } + } + } + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-table-columns-0.json b/src/protocol/fixtures/tsp-client/fetch-table-columns-0.json new file mode 100644 index 0000000..134759d --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-table-columns-0.json @@ -0,0 +1,24 @@ +{ + "model": [ + { + "name": "Trace", + "id": 0, + "type": null, + "description": null + }, + { + "name": "Timestamp", + "id": 1, + "type": null, + "description": null + }, + { + "name": "Content", + "id": 2, + "type": null, + "description": null + } + ], + "statusMessage": "Completed", + "status": "COMPLETED" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-table-lines-0.json b/src/protocol/fixtures/tsp-client/fetch-table-lines-0.json new file mode 100644 index 0000000..07195ba --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-table-lines-0.json @@ -0,0 +1,43 @@ +{ + "model": { + "size": 999999, + "lowIndex": 0, + "columnIds": [ + 0, + 1, + 2 + ], + "lines": [ + { + "index": 0, + "cells": [ + { + "content": "kernel" + }, + { + "content": "1111111111.111 111 111" + }, + { + "content": "Message 1" + } + ] + }, + { + "index": 0, + "cells": [ + { + "content": "kernel" + }, + { + "content": "2222222222.222 222 222" + }, + { + "content": "Message 2" + } + ] + } + ] + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-timegraph-arrows-0.json b/src/protocol/fixtures/tsp-client/fetch-timegraph-arrows-0.json new file mode 100644 index 0000000..4fae46c --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-timegraph-arrows-0.json @@ -0,0 +1,18 @@ +{ + "model": [ + { + "start": 1111111111111111111, + "end": 2222222222222222222, + "sourceId": 1111, + "targetId": 2222, + "style": { + "parentKey": "Style key", + "values": { + + } + } + } + ], + "statusMessage": "Completed", + "status": "COMPLETED" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-timegraph-states-0.json b/src/protocol/fixtures/tsp-client/fetch-timegraph-states-0.json new file mode 100644 index 0000000..f6cff53 --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-timegraph-states-0.json @@ -0,0 +1,37 @@ +{ + "model": { + "rows": [ + { + "entryId": 1234, + "states": [ + { + "start": 1111111111111111111, + "end": 1111111111111111111 + }, + { + "start": 2222222222222222222, + "end": 2222222222222222222, + "style": { + "parentKey": "Style key", + "values": { + + } + } + }, + { + "start": 3333333333333333333, + "end": 3333333333333333333, + "style": { + "parentKey": "Style key", + "values": { + "background-color": "#646464" + } + } + } + ] + } + ] + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-timegraph-tooltip-0.json b/src/protocol/fixtures/tsp-client/fetch-timegraph-tooltip-0.json new file mode 100644 index 0000000..6e2059b --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-timegraph-tooltip-0.json @@ -0,0 +1,7 @@ +{ + "model": { + "key": "value" + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-timegraph-tree-0.json b/src/protocol/fixtures/tsp-client/fetch-timegraph-tree-0.json new file mode 100644 index 0000000..c0720f1 --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-timegraph-tree-0.json @@ -0,0 +1,22 @@ +{ + "model": { + "entries": [ + { + "id": 1234, + "parentId": -1, + "style": null, + "labels": [ + "entry name" + ], + "start": 1234567890123456789, + "end": 9876543210987654321, + "hasData": true + } + ], + "headers": [ + + ] + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-xy-0.json b/src/protocol/fixtures/tsp-client/fetch-xy-0.json new file mode 100644 index 0000000..2da46ff --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-xy-0.json @@ -0,0 +1,23 @@ +{ + "model": { + "title": "Chart name", + "series": [ + { + "seriesId": 111, + "seriesName": "Series name", + "xValues": [ + 1111111111111111111, + 2222222222222222222, + 3333333333333333333 + ], + "yValues": [ + 0.0, + 50.0, + 100.0 + ] + } + ] + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} diff --git a/src/protocol/fixtures/tsp-client/fetch-xy-tree-0.json b/src/protocol/fixtures/tsp-client/fetch-xy-tree-0.json new file mode 100644 index 0000000..3ea57b3 --- /dev/null +++ b/src/protocol/fixtures/tsp-client/fetch-xy-tree-0.json @@ -0,0 +1,38 @@ +{ + "model": { + "entries": [ + { + "id": 111, + "parentId": -1, + "style": null, + "labels": [ + "process", + "123", + "100.000 %", + "10.000 s" + ], + "hasData": true + } + ], + "headers": [ + { + "name": "Process", + "tooltip": "" + }, + { + "name": "TID", + "tooltip": "" + }, + { + "name": "%", + "tooltip": "" + }, + { + "name": "Time", + "tooltip": "" + } + ] + }, + "statusMessage": "Running", + "status": "RUNNING" +} diff --git a/src/protocol/fixtures/tsp-client/open-trace-0.json b/src/protocol/fixtures/tsp-client/open-trace-0.json new file mode 100644 index 0000000..63cb227 --- /dev/null +++ b/src/protocol/fixtures/tsp-client/open-trace-0.json @@ -0,0 +1,9 @@ +{ + "name": "kernel", + "path": "/path/kernel", + "UUID": "11111111-1111-1111-1111-111111111111", + "nbEvents": 0, + "start": 0, + "end": 0, + "indexingStatus": "CLOSED" +} diff --git a/src/protocol/rest-client.ts b/src/protocol/rest-client.ts index c78cab4..5a7616f 100644 --- a/src/protocol/rest-client.ts +++ b/src/protocol/rest-client.ts @@ -1,5 +1,24 @@ import fetch from 'node-fetch'; +import { BigintOrNumber, Normalizer } from './serialization'; import { TspClientResponse } from './tsp-client-response'; +import JSONBigConfig = require('json-bigint'); + +const JSONBig = JSONBigConfig({ + useNativeBigInt: true, +}); + +export interface HttpRequest { + method: string, + url: string, + body?: string + headers?: Record +} + +export interface HttpResponse { + text: string + status: number + statusText: string +} /** * Rest client helper to make request. @@ -7,71 +26,94 @@ import { TspClientResponse } from './tsp-client-response'; * The json object in the response may be undefined when an error occurs. */ export class RestClient { - private static async performRequest(verb: string, url: string, body?: any): Promise> { - const jsonBody: string = JSON.stringify(body); - const response = await fetch(url, { - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - method: verb, - body: jsonBody - }); - const text = await response.text(); - return new TspClientResponse(text, response.status, response.statusText); - } + public static get(url: string, parameters?: Map): Promise>>; + public static get(url: string, parameters?: Map, normalizer?: Normalizer): Promise>; /** * Perform GET - * T is the expected type of the json object returned by this request + * @template T is the expected type of the json object returned by this request * @param url URL to query without query parameters * @param parameters Query parameters. Mapped keys and values are used to build the final URL */ - public static async get(url: string, parameters?: Map): Promise> { + public static async get(url: string, parameters?: Map, normalizer?: Normalizer) { let getUrl = url; if (parameters) { const urlParameters = this.encodeURLParameters(parameters); getUrl = getUrl.concat(urlParameters); } - return this.performRequest('get', getUrl); + return this.performRequest('get', getUrl, undefined, normalizer); } + public static post(url: string, body?: any): Promise>>; + public static post(url: string, body?: any, normalizer?: Normalizer): Promise>; /** * Perform POST - * T is the expected type of the json object returned by this request + * @template T is the expected type of the json object returned by this request * @param url URL to query * @param body Query object as defined by the Query interface */ - public static async post(url: string, body?: any): Promise> { - return this.performRequest('post', url, body); + public static async post(url: string, body?: any, normalizer?: Normalizer) { + return this.performRequest('post', url, body, normalizer); } + public static put(url: string, body?: any): Promise>>; + public static put(url: string, body?: any, normalizer?: Normalizer): Promise>; /** * Perform PUT - * T is the expected type of the json object returned by this request + * @template T is the expected type of the json object returned by this request * @param url URL to query * @param body Query object as defined by the Query interface */ - public static async put(url: string, body?: any): Promise> { - return this.performRequest('put', url, body); + public static async put(url: string, body?: any, normalizer?: Normalizer) { + return this.performRequest('put', url, body, normalizer); } + public static delete(url: string, parameters?: Map): Promise>>; + public static delete(url: string, parameters?: Map, normalizer?: Normalizer): Promise>; /** * Perform DELETE - * T is the expected type of the json object returned by this request + * @template T is the expected type of the json object returned by this request * @param url URL to query without query parameters * @param parameters Query parameters. Mapped keys and values are used to build the final URL */ - public static async delete(url: string, parameters?: Map): Promise> { + public static async delete(url: string, parameters?: Map, normalizer?: Normalizer) { let deleteUrl = url; if (parameters) { const urlParameters = this.encodeURLParameters(parameters); deleteUrl = deleteUrl.concat(urlParameters); } - return this.performRequest('delete', deleteUrl); + return this.performRequest('delete', deleteUrl, undefined, normalizer); + } + + protected static performRequest(method: string, url: string, body?: any): Promise>>; + protected static performRequest(method: string, url: string, body: any, normalizer?: Normalizer): Promise>; + protected static async performRequest(method, url, body?, normalizer?) { + const response = await this.httpRequest({ + url, + method, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: this.jsonStringify(body) + }); + const parsed = this.jsonParse(response.text); + const responseModel = normalizer ? normalizer(parsed) : parsed; + return new TspClientResponse(response.text, response.status, response.statusText, responseModel); } - private static encodeURLParameters(parameters: Map): string { + protected static async httpRequest(req: HttpRequest): Promise { + const { url, method, body, headers } = req; + const response = await fetch(url, { method, headers, body }); + const text = await response.text(); + return { + text, + status: response.status, + statusText: response.statusText, + }; + } + + protected static encodeURLParameters(parameters: Map): string { if (parameters.size) { const urlParameters: string = '?'; const parametersArray: string[] = []; @@ -82,4 +124,19 @@ export class RestClient { } return ''; } + + /** + * Stringify JS objects. Can stringify `BigInt`s. + */ + protected static jsonStringify(data: any): string { + return JSONBig.stringify(data); + } + + /** + * Parse JSON-encoded data. If a number is too large to fit + * into a regular `number` then it will be deserialized as `BigInt`. + */ + protected static jsonParse(text: string): any { + return JSONBig.parse(text); + } } diff --git a/src/protocol/serialization.ts b/src/protocol/serialization.ts new file mode 100644 index 0000000..3fc9f99 --- /dev/null +++ b/src/protocol/serialization.ts @@ -0,0 +1,69 @@ +/** + * Whenever a protocol message contains a `bigint` field, it may + * or may not be deserialized as `bigint` but as `number` instead. + * `BigintOrNumber` is a mapped type that reflects that behavior. + */ +export type BigintOrNumber = + T extends bigint | number + ? bigint | number + : T extends { [key: string]: any } + ? { [K in keyof T]: BigintOrNumber } + : T extends any[] + ? T extends (infer U)[] ? (U extends any ? BigintOrNumber : never)[] : never + : T + ; + +// export type MustBeNormalized = +// T extends bigint | number +// ? T +// : T extends { [key: string]: any } +// ? { +// [K in keyof T as T[K] extends MustBeNormalized ? ( +// K +// ) : never +// ]: MustBeNormalized } +// : never +// ; + +// type a = MustBeNormalized<{ +// a: string +// b: number +// c: { +// d: string +// e: bigint +// } +// f: { +// g: string +// } +// }>; + +// export type NormalizerDescriptor = +// T extends bigint +// ? (number: bigint | number) => bigint +// : T extends number +// ? (number: bigint | number) => number +// : T extends { [key: string]: any } +// ? { [K in keyof T]: NormalizerDescriptor } +// : T extends (infer U)[] +// ? (array: U[]) => U[] +// : never +// ; + +/** + * Given a possibly altered input, get a normalized output. + */ +export type Normalizer = (input: BigintOrNumber) => T; + +/** + * Create a normalizer that operates on JS Array objects. + */ +export function array(normalizer: Normalizer): Normalizer { + return arr => arr.map(element => normalizer(element)); +} + +export function assertNumber(number: bigint | number): number { + if (typeof number !== 'number') { + throw new TypeError(); + } + return number; +} diff --git a/src/protocol/test-utils.ts b/src/protocol/test-utils.ts new file mode 100644 index 0000000..0e97322 --- /dev/null +++ b/src/protocol/test-utils.ts @@ -0,0 +1,58 @@ +import fs = require('fs'); +import path = require('path'); +import type { HttpResponse } from './rest-client'; + +export class FixtureSet { + + static async fromFolder(...paths: string[]): Promise { + const folder = path.resolve(...paths); + const files = await fs.promises.readdir(folder); + const fixtures = new Map(); + for (const file of files) { + fixtures.set(file, path.resolve(folder, file)); + } + return new this(fixtures); + } + + /** + * Set of unused fixtures (file names). + */ + protected unused: Set; + + protected constructor( + /** + * Key: fixture file name. + * Value: fixture file path. + */ + protected fixtures: Map + ) { + this.unused = new Set(fixtures.keys()); + } + + async asResponse(fixtureName: string, options: Partial = {}): Promise { + const { status = 200, statusText = 'Success' } = options; + return { + status, + statusText, + text: await this.readFixture(fixtureName), + }; + } + + assertUsedAllFixtures(): void { + if (this.unused.size > 0) { + throw new Error(`Some fixtures were not used!\n${ + Array.from(this.unused, fixture => ` - ${fixture}`).join('\n') + }`); + } + } + + protected async readFixture(fixtureName: string): Promise { + const fixturePath = this.fixtures.get(fixtureName); + if (!fixturePath) { + throw new Error(`no fixture named ${fixtureName}`); + } + const content = await fs.promises.readFile(fixturePath, 'utf8'); + this.unused.delete(fixtureName); + return content; + } +} diff --git a/src/protocol/tsp-client-response.ts b/src/protocol/tsp-client-response.ts index f175950..908b5c3 100644 --- a/src/protocol/tsp-client-response.ts +++ b/src/protocol/tsp-client-response.ts @@ -4,26 +4,20 @@ * the status code and message of the HTTP response, and the plain text attached to this response. */ export class TspClientResponse { - private readonly responseModel: T | undefined; - private readonly statusCode: number; - private readonly statusMessage: string; - private readonly text: string; /** * Constructor * @param text Plain text of the response from the server * @param statusCode Status code from the HTTP response * @param statusMessage Status message from the HTTP response + * @param responseModel Optional parsed value from `text` (usually from JSON). */ - constructor(text: string, statusCode: number, statusMessage: string) { - this.text = text; - this.statusCode = statusCode; - this.statusMessage = statusMessage; - try { - this.responseModel = JSON.parse(text) as T; - } catch (error) { - } - } + constructor( + private readonly text: string, + private readonly statusCode: number, + private readonly statusMessage: string, + private readonly responseModel?: T, + ) {} /** * Get the model from the server, or undefined diff --git a/src/protocol/tsp-client.test.ts b/src/protocol/tsp-client.test.ts index 60ae254..ded0f0b 100644 --- a/src/protocol/tsp-client.test.ts +++ b/src/protocol/tsp-client.test.ts @@ -1,105 +1,274 @@ -import { TspClient } from './tsp-client'; -import { TspClientResponse } from './tsp-client-response'; -import { Trace } from '../models/trace'; +// tslint:disable: no-unused-expression + import { Query } from '../models/query/query'; -import { OutputDescriptor } from '../models/output-descriptor'; +import { HttpRequest, HttpResponse, RestClient } from './rest-client'; +import { FixtureSet } from './test-utils'; +import { TspClient } from './tsp-client'; + +describe('TspClient Deserialization', () => { + + const client = new TspClient('not-relevant'); + const httpRequestMock = jest.fn, [req: HttpRequest]>(); + + let fixtures: FixtureSet; + + beforeAll(async () => { + fixtures = await FixtureSet.fromFolder(__dirname, 'fixtures/tsp-client'); + RestClient['httpRequest'] = httpRequestMock; + }); + + afterAll(() => { + fixtures.assertUsedAllFixtures(); + }); + + beforeEach(() => { + httpRequestMock.mockReset(); + httpRequestMock.mockResolvedValue({ text: '', status: 404, statusText: 'Not found' }); + }); + + it('checkHealth', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('check-health-0.json')); + const response = await client.checkHealth(); + const health = response.getModel()!; + + expect(health.status).toEqual('UP'); + }); -jest.mock('./rest-client'); + it('createExperiment', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('create-experiment-0.json')); + const response = await client.createExperiment(new Query({})); + const experiment = response.getModel()!; -const client = new TspClient('http://localhost'); + expect(typeof experiment.end).toEqual('bigint'); + expect(typeof experiment.start).toEqual('bigint'); + expect(typeof experiment.nbEvents).toEqual('number'); + }); -describe('Tsp client tests', () => { - it('Should fetch traces', async () => { - const input = new TspClientResponse('Test', 200, 'Success'); - const response = await client.fetchTraces(); + it('deleteExperiment', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('delete-experiment-0.json')); + const response = await client.deleteExperiment('not-relevant'); + const experiment = response.getModel()!; - expect(response).toEqual(input); + expect(typeof experiment.end).toEqual('bigint'); + expect(typeof experiment.start).toEqual('bigint'); + expect(typeof experiment.nbEvents).toEqual('number'); }); - it('Should fetch a specific trace', async () => { - const traceUUID = '123'; - const input = new TspClientResponse(traceUUID, 200, 'Success'); - const response = await client.fetchTrace(traceUUID); + it('deleteTrace', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('delete-trace-0.json')); + const response = await client.deleteTrace('not-relevant'); + const trace = response.getModel()!; - expect(response).toEqual(input); + expect(typeof trace.end).toEqual('bigint'); + expect(typeof trace.start).toEqual('bigint'); + expect(typeof trace.nbEvents).toEqual('number'); }); - it('Should open a trace in the server', async () => { - const check = '123'; - const query = new Query({ test: check }); - const input = new TspClientResponse(check, 200, 'Success'); - const response = await client.openTrace(query); + it('experimentOutputs', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('experiment-outputs-0.json')); + const response = await client.experimentOutputs('not-relevant'); + const outputs = response.getModel()!; - expect(response).toEqual(input); + expect(outputs).toHaveLength(4); }); - it('Should delete a trace', async () => { - const traceUUID = '123'; - const input = new TspClientResponse(traceUUID, 200, 'Success'); - const response = await client.deleteTrace(traceUUID); + it('fetchAnnotationsCategories', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-annotation-categories-0.json')); + const response = await client.fetchAnnotationsCategories('not-relevant', 'not-relevant'); + const categories = response.getModel()!; - expect(response).toEqual(input); + expect(categories.model.annotationCategories[0]).toEqual('Annotation category'); }); - it('Should fetch experiments', async () => { - const input = new TspClientResponse('Test', 200, 'Success'); + it('fetchAnnotations', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-annotations-0.json')); + const response = await client.fetchAnnotations('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const annotations = genericResponse.model.annotations['Annotation category']; + + expect(annotations).toHaveLength(1); + for (const annotation of annotations) { + expect(typeof annotation.time).toEqual('bigint'); + expect(typeof annotation.entryId).toEqual('number'); + expect(typeof annotation.time).toEqual('bigint'); + } + }); + + it('fetchExperiment', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-experiment-0.json')); + const response = await client.fetchExperiment('not-relevant'); + const experiment = response.getModel()!; + + expect(typeof experiment.end).toEqual('bigint'); + expect(typeof experiment.nbEvents).toEqual('number'); + expect(typeof experiment.start).toEqual('bigint'); + + expect(experiment.traces).toHaveLength(1); + for (const trace of experiment.traces) { + expect(typeof trace.end).toEqual('bigint'); + expect(typeof trace.nbEvents).toEqual('number'); + expect(typeof trace.start).toEqual('bigint'); + } + }); + + it('fetchExperiments', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-experiments-0.json')); const response = await client.fetchExperiments(); + const experiments = response.getModel()!; + + expect(experiments).toHaveLength(1); + for (const experiment of experiments) { + expect(typeof experiment.end).toEqual('bigint'); + expect(typeof experiment.nbEvents).toEqual('number'); + expect(typeof experiment.start).toEqual('bigint'); + } + }); - expect(response).toEqual(input); + it('fetchMarkerSets', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-marker-sets-0.json')); + const response = await client.fetchMarkerSets('not-relevant'); + const genericResponse = response.getModel()!; + const markerSets = genericResponse.model; + + expect(markerSets).toHaveLength(1); }); - it('Should fetch an experiment', async () => { - const expUUID = '123'; - const input = new TspClientResponse(expUUID, 200, 'Success'); - const response = await client.fetchExperiment(expUUID); + it('fetchStyles', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-styles-0.json')); + const response = await client.fetchStyles('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const testStyle = genericResponse.model.styles['Style key']; - expect(response).toEqual(input); + expect(typeof testStyle.values.height).toEqual('number'); + expect(typeof testStyle.values.opacity).toEqual('number'); }); - it('Should create an experiment on the server', async () => { - const query = new Query({ test: 'Test' }); - const input = new TspClientResponse('Test', 200, 'Success'); - const response = await client.createExperiment(query); + it('fetchTableColumns', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-table-columns-0.json')); + const response = await client.fetchTableColumns('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const tableColumns = genericResponse.model; - expect(response).toEqual(input); + expect(tableColumns).toHaveLength(3); + for (const tableColumn of tableColumns) { + expect(typeof tableColumn.id).toEqual('number'); + } }); - it('Should update an experiment', async () => { - const expUUID = '123'; - const query = new Query({ test: expUUID }); - const input = new TspClientResponse(expUUID, 200, 'Success'); - const response = await client.updateExperiment(expUUID, query); + it('fetchTableLines', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-table-lines-0.json')); + const response = await client.fetchTableLines('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const model = genericResponse.model; + + expect(typeof model.size).toEqual('number'); + expect(typeof model.lowIndex).toEqual('number'); + expect(model.lines).toHaveLength(2); + for (const line of model.lines) { + expect(typeof line.index).toEqual('number'); + } + }); + + it('fetchTimeGraphArrows', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-timegraph-arrows-0.json')); + const response = await client.fetchTimeGraphArrows('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const arrows = genericResponse.model; + + expect(arrows).toHaveLength(1); + for (const arrow of arrows) { + expect(typeof arrow.end).toEqual('bigint'); + expect(typeof arrow.sourceId).toEqual('number'); + expect(typeof arrow.start).toEqual('bigint'); + expect(typeof arrow.targetId).toEqual('number'); + } + }); + + it('fetchTimeGraphStates', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-timegraph-states-0.json')); + const response = await client.fetchTimeGraphStates('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const rows = genericResponse.model.rows; + + expect(rows).toHaveLength(1); + for (const row of rows) { + expect(typeof row.entryId).toEqual('number'); + for (const state of row.states) { + expect(typeof state.end).toEqual('bigint'); + expect(typeof state.start).toEqual('bigint'); + if (state.tags !== undefined) { + expect(typeof state.tags).toEqual('number'); + } + } + } + }); + + it('fetchTimeGraphTooltip', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-timegraph-tooltip-0.json')); + const response = await client.fetchTimeGraphTooltip('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const tooltips = genericResponse.model; + + expect(tooltips.key).toEqual('value'); + }); + + it('fetchTimeGraphTree', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-timegraph-tree-0.json')); + const response = await client.fetchTimeGraphTree('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const model = genericResponse.model; + + expect(model.entries).toHaveLength(1); + expect(model.headers).toHaveLength(0); + for (const entry of model.entries) { + expect(typeof entry.end).toEqual('bigint'); + expect(typeof entry.id).toEqual('number'); + expect(typeof entry.start).toEqual('bigint'); + } + }); + + it('fetchXY', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-xy-0.json')); + + // To throw or not to throw? + // The `fetch-xy-0.json` fixture contains values too big for JS `number` + // type, but the current implementation lossly converts those. + // return expect(client.fetchXY('not-relevant', 'not-relevant', new Query({}))).rejects.toBe(Error); + + const response = await client.fetchXY('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const xy = genericResponse.model; - expect(response).toEqual(input); + expect(xy.series).toHaveLength(1); + for (const serie of xy.series) { + expect(typeof serie.seriesId).toEqual('number'); + expect(serie.xValues).toHaveLength(3); + for (const xValue of serie.xValues) { + expect(typeof xValue).toEqual('number'); + } + } }); - it('Should delete an experiment', async () => { - const expUUID = '123'; - const input = new TspClientResponse(expUUID, 200, 'Success'); - const response = await client.deleteExperiment(expUUID); + it('fetchXYTree', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-xy-tree-0.json')); + const response = await client.fetchXYTree('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const model = genericResponse.model; - expect(response).toEqual(input); + expect(model.entries).toHaveLength(1); + expect(model.headers).toHaveLength(4); + for (const entry of model.entries) { + expect(typeof entry.id).toEqual('number'); + } }); - it('Should list all the outputs associated to this experiment', async () => { - const expUUID = '123'; - const output: OutputDescriptor = { - id: expUUID, - name: 'Test', - description: 'Test description', - type: 'Test type', - queryParameters: new Map([ - ['key1', 'value1'], - ['key2', 'value2'] - ]), - start: 1, - end: 10, - final: false, - compatibleProviders: ['One', 'Two'] - }; - const input = new TspClientResponse(JSON.stringify([output]), 200, 'Success'); - const response = await client.experimentOutputs(expUUID); + it('openTrace', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('open-trace-0.json')); + const response = await client.openTrace(new Query({})); + const trace = response.getModel()!; - expect(response).toEqual(input); + expect(typeof trace.end).toEqual('bigint'); + expect(typeof trace.nbEvents).toEqual('number'); + expect(typeof trace.start).toEqual('bigint'); }); }); diff --git a/src/protocol/tsp-client.ts b/src/protocol/tsp-client.ts index 9184c8b..3544f74 100644 --- a/src/protocol/tsp-client.ts +++ b/src/protocol/tsp-client.ts @@ -8,11 +8,12 @@ import { Trace } from '../models/trace'; import { RestClient } from './rest-client'; import { Experiment } from '../models/experiment'; import { OutputDescriptor } from '../models/output-descriptor'; -import { EntryModel, Entry, EntryHeader } from '../models/entry'; +import { EntryModel, Entry } from '../models/entry'; import { TspClientResponse } from './tsp-client-response'; import { OutputStyleModel } from '../models/styles'; import { HealthStatus } from '../models/health'; import { MarkerSet } from '../models/markerset'; +import { array } from './serialization'; /** * Trace Server Protocol client @@ -34,7 +35,7 @@ export class TspClient { */ public async fetchTraces(): Promise> { const url = this.baseUrl + '/traces'; - return RestClient.get(url); + return RestClient.get(url, undefined, array(Trace)); } /** @@ -43,7 +44,7 @@ export class TspClient { */ public async fetchTrace(traceUUID: string): Promise> { const url = this.baseUrl + '/traces/' + traceUUID; - return RestClient.get(url); + return RestClient.get(url, undefined, Trace); } /** @@ -53,7 +54,7 @@ export class TspClient { */ public async openTrace(parameters: Query): Promise> { const url = this.baseUrl + '/traces'; - return RestClient.post(url, parameters); + return RestClient.post(url, parameters, Trace); } /** @@ -72,7 +73,7 @@ export class TspClient { if (removeCache) { deleteParameters.set('removeCache', removeCache.toString()); } - return await RestClient.delete(url, deleteParameters); + return RestClient.delete(url, deleteParameters, Trace); } /** @@ -81,7 +82,7 @@ export class TspClient { */ public async fetchExperiments(): Promise> { const url = this.baseUrl + '/experiments'; - return await RestClient.get(url); + return RestClient.get(url, undefined, array(Experiment)); } /** @@ -91,7 +92,7 @@ export class TspClient { */ public async fetchExperiment(expUUID: string): Promise> { const url = this.baseUrl + '/experiments/' + expUUID; - return await RestClient.get(url); + return RestClient.get(url, undefined, Experiment); } /** @@ -101,7 +102,7 @@ export class TspClient { */ public async createExperiment(parameters: Query): Promise> { const url = this.baseUrl + '/experiments'; - return await RestClient.post(url, parameters); + return RestClient.post(url, parameters, Experiment); } /** @@ -112,7 +113,7 @@ export class TspClient { */ public async updateExperiment(expUUID: string, parameters: Query): Promise> { const url = this.baseUrl + '/experiments/' + expUUID; - return await RestClient.put(url, parameters); + return RestClient.put(url, parameters, Experiment); } /** @@ -122,7 +123,7 @@ export class TspClient { */ public async deleteExperiment(expUUID: string): Promise> { const url = this.baseUrl + '/experiments/' + expUUID; - return await RestClient.delete(url); + return RestClient.delete(url, undefined, Experiment); } /** @@ -132,7 +133,7 @@ export class TspClient { */ public async experimentOutputs(expUUID: string): Promise> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs'; - return await RestClient.get(url); + return RestClient.get(url, undefined, array(OutputDescriptor)); } /** @@ -142,10 +143,13 @@ export class TspClient { * @param parameters Query object * @returns Generic entry response with entries */ - public async fetchXYTree(expUUID: string, - outputID: string, parameters: Query): Promise>>> { + public async fetchXYTree( + expUUID: string, + outputID: string, + parameters: Query, + ): Promise>>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/XY/' + outputID + '/tree'; - return await RestClient.post>>(url, parameters); + return RestClient.post(url, parameters, GenericResponse(EntryModel(Entry))); } /** @@ -155,9 +159,13 @@ export class TspClient { * @param parameters Query object * @returns XY model response with the model */ - public async fetchXY(expUUID: string, outputID: string, parameters: Query): Promise>> { + public async fetchXY( + expUUID: string, + outputID: string, + parameters: Query, + ): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/XY/' + outputID + '/xy'; - return await RestClient.post>(url, parameters); + return RestClient.post(url, parameters, GenericResponse(XYModel)); } /** @@ -169,8 +177,13 @@ export class TspClient { * @param seriesID Optional series ID * @returns Map of key=name of the property and value=string value associated */ - public async fetchXYToolTip(expUUID: string, outputID: string, xValue: number, - yValue?: number, seriesID?: string): Promise>> { + public async fetchXYToolTip( + expUUID: string, + outputID: string, + xValue: number, + yValue?: number, + seriesID?: string, + ): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/XY/' + outputID + '/tooltip'; const parametersMap: Map = new Map(); @@ -181,7 +194,7 @@ export class TspClient { if (seriesID) { parametersMap.set('seriesId', seriesID); } - return await RestClient.get>(url, parametersMap); + return RestClient.get(url, parametersMap); } /** @@ -191,10 +204,13 @@ export class TspClient { * @param parameters Query object * @returns Time graph entry response with entries of type TimeGraphEntry */ - public async fetchTimeGraphTree(expUUID: string, - outputID: string, parameters: Query): Promise>>> { + public async fetchTimeGraphTree( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/timeGraph/' + outputID + '/tree'; - return await RestClient.post>>(url, parameters); + return RestClient.post(url, parameters, GenericResponse(EntryModel(TimeGraphEntry))); } /** @@ -204,9 +220,13 @@ export class TspClient { * @param parameters Query object * @returns Generic response with the model */ - public async fetchTimeGraphStates(expUUID: string, outputID: string, parameters: Query): Promise>> { + public async fetchTimeGraphStates( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/timeGraph/' + outputID + '/states'; - return await RestClient.post>(url, parameters); + return RestClient.post(url, parameters, GenericResponse(TimeGraphModel)); } /** @@ -216,19 +236,22 @@ export class TspClient { * @param parameters Query object * @returns Generic response with the model */ - public async fetchTimeGraphArrows(expUUID: string, outputID: string, parameters: Query): Promise>> { + public async fetchTimeGraphArrows( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/timeGraph/' + outputID + '/arrows'; - return await RestClient.post>(url, parameters); + return RestClient.post(url, parameters, GenericResponse(array(TimeGraphArrow))); } /** * Fetch marker sets. * @returns Generic response with the model */ - public async fetchMarkerSets(expUUID: string): Promise>> { + public async fetchMarkerSets(expUUID: string): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/markerSets'; - let parametersMap: Map | undefined = undefined; - return await RestClient.get>(url); + return RestClient.get(url, undefined); } /** @@ -238,14 +261,18 @@ export class TspClient { * @param markerSetId Marker Set ID * @returns Generic response with the model */ - public async fetchAnnotationsCategories(expUUID: string, outputID: string, markerSetId?: string): Promise>> { + public async fetchAnnotationsCategories( + expUUID: string, + outputID: string, + markerSetId?: string, + ): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/' + outputID + '/annotations'; let parametersMap: Map | undefined = undefined; if (markerSetId) { parametersMap = new Map(); parametersMap.set('markerSetId', markerSetId); } - return await RestClient.get>(url, parametersMap); + return RestClient.get(url, parametersMap); } /** @@ -255,9 +282,13 @@ export class TspClient { * @param parameters Query object * @returns Generic response with the model */ - public async fetchAnnotations(expUUID: string, outputID: string, parameters: Query): Promise>> { + public async fetchAnnotations( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/' + outputID + '/annotations'; - return await RestClient.post>(url, parameters); + return RestClient.post(url, parameters, GenericResponse(AnnotationModel)); } /** @@ -267,9 +298,13 @@ export class TspClient { * @param parameters Query object * @returns Map of key=name of the property and value=string value associated */ - public async fetchTimeGraphTooltip(expUUID: string, outputID: string, parameters: Query): Promise>> { + public async fetchTimeGraphTooltip( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/timeGraph/' + outputID + '/tooltip'; - return await RestClient.post>(url, parameters); + return RestClient.post(url, parameters); } /** @@ -279,10 +314,13 @@ export class TspClient { * @param parameters Query object * @returns Generic entry response with columns headers as model */ - public async fetchTableColumns(expUUID: string, - outputID: string, parameters: Query): Promise>> { + public async fetchTableColumns( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/table/' + outputID + '/columns'; - return await RestClient.post>(url, parameters); + return RestClient.post(url, parameters, GenericResponse(array(ColumnHeaderEntry))); } /** @@ -292,9 +330,13 @@ export class TspClient { * @param parameters Query object * @returns Generic response with the model */ - public async fetchTableLines(expUUID: string, outputID: string, parameters: Query): Promise>> { + public async fetchTableLines( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/table/' + outputID + '/lines'; - return await RestClient.post>(url, parameters); + return RestClient.post(url, parameters, GenericResponse(TableModel)); } /** @@ -304,9 +346,13 @@ export class TspClient { * @param parameters Query object * @returns Generic response with the model */ - public async fetchStyles(expUUID: string, outputID: string, parameters: Query): Promise>> { + public async fetchStyles( + expUUID: string, + outputID: string, + parameters: Query, + ): Promise>> { const url = this.baseUrl + '/experiments/' + expUUID + '/outputs/' + outputID + '/style'; - return await RestClient.post>(url, parameters); + return RestClient.post(url, parameters); } /** @@ -315,6 +361,6 @@ export class TspClient { */ public async checkHealth(): Promise> { const url = this.baseUrl + '/health'; - return RestClient.get(url); + return RestClient.get(url); } } diff --git a/tsconfig.json b/tsconfig.json index a4a07f3..aec17fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,9 +18,10 @@ } ], "outDir": "lib", - "rootDir": "src" + "rootDir": "src", + "noUnusedLocals": true }, "include": [ "src" ] -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 3c52ac7..28ae786 100644 --- a/yarn.lock +++ b/yarn.lock @@ -594,6 +594,11 @@ jest-diff "^27.0.0" pretty-format "^27.0.0" +"@types/json-bigint@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/json-bigint/-/json-bigint-1.0.1.tgz#201062a6990119a8cc18023cfe1fed12fc2fc8a7" + integrity sha512-zpchZLNsNuzJHi6v64UBoFWAvQlPhch7XAi36FkH6tL1bbbmimIF+cS7vwkzY4u5RaSWMoflQfu+TshMPPw8uw== + "@types/node-fetch@^2.3.3": version "2.5.7" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" @@ -791,6 +796,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +bignumber.js@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" + integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1908,6 +1918,13 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json5@2.x, json5@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"