From 569eeb9cf600ec487ce5aaf77a8d3ac50b019da6 Mon Sep 17 00:00:00 2001 From: Patrick Tasse Date: Fri, 10 Oct 2025 20:23:57 +0200 Subject: [PATCH] Support generic object data provider Implement fetchObject for /obj endpoint Add ObjectModel for response Add selectionRange to OutputCapabilities Add optional replacer and space arguments to JSONBigUtils.stringify Add unit tests Signed-off-by: Patrick Tasse --- .../tsp-client/experiment-outputs-0.json | 11 +++++++- .../fixtures/tsp-client/fetch-object-0.json | 16 +++++++++++ tsp-typescript-client/src/models/object.ts | 27 ++++++++++++++++++ .../src/models/output-capabilities.ts | 8 ++++++ .../src/models/output-descriptor.ts | 4 +++ .../src/protocol/http-tsp-client.ts | 27 ++++++++++++++++++ .../src/protocol/tsp-client.test.ts | 28 ++++++++++++++++++- .../src/protocol/tsp-client.ts | 14 ++++++++++ .../src/utils/jsonbig-utils.ts | 4 +-- 9 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 tsp-typescript-client/fixtures/tsp-client/fetch-object-0.json create mode 100644 tsp-typescript-client/src/models/object.ts diff --git a/tsp-typescript-client/fixtures/tsp-client/experiment-outputs-0.json b/tsp-typescript-client/fixtures/tsp-client/experiment-outputs-0.json index 7809522..73bace0 100644 --- a/tsp-typescript-client/fixtures/tsp-client/experiment-outputs-0.json +++ b/tsp-typescript-client/fixtures/tsp-client/experiment-outputs-0.json @@ -12,7 +12,7 @@ "type": "TABLE" }, { - "id": "data.output.id", + "id": "datatree.output.id", "name": "Output name", "description": "Output description", "type": "DATA_TREE" @@ -52,5 +52,14 @@ "canCreate": false, "canDelete": true } + }, + { + "id": "data.output.id", + "name": "Output name", + "description": "Output description", + "type": "DATA", + "capabilities": { + "selectionRange": true + } } ] diff --git a/tsp-typescript-client/fixtures/tsp-client/fetch-object-0.json b/tsp-typescript-client/fixtures/tsp-client/fetch-object-0.json new file mode 100644 index 0000000..e41892d --- /dev/null +++ b/tsp-typescript-client/fixtures/tsp-client/fetch-object-0.json @@ -0,0 +1,16 @@ +{ + "model": { + "object": { + "fooNumber": 1234567890123456789, + "fooString": "foo", + "fooObj": { "fooA": { "A1key": "A1val", "A2key": 1 }, + "fooB": { "B1key": "B1val", "B2key": 2 } }, + "fooNumArray": [1, 2, 3], + "fooObjArray": [{"k": "v1"}, {"k": "v2"}, {"k": "v3"}] + }, + "next": 9876543210987654321, + "previous": { "rank": 0 } + }, + "statusMessage": "Completed", + "status": "COMPLETED" +} \ No newline at end of file diff --git a/tsp-typescript-client/src/models/object.ts b/tsp-typescript-client/src/models/object.ts new file mode 100644 index 0000000..efb91f5 --- /dev/null +++ b/tsp-typescript-client/src/models/object.ts @@ -0,0 +1,27 @@ +import { createNormalizer } from '../protocol/serialization'; + +export const ObjectModel = createNormalizer({ + object: undefined, + next: undefined, + previous: undefined +}); + +/** + * Object model that will be returned by the server + */ +export interface ObjectModel { + /** + * Generic object + */ + object: any; + + /** + * Next navigation parameter object + */ + next?: any; + + /** + * Previous navigation parameter object + */ + previous?: any; +} diff --git a/tsp-typescript-client/src/models/output-capabilities.ts b/tsp-typescript-client/src/models/output-capabilities.ts index 8a9514a..564b720 100644 --- a/tsp-typescript-client/src/models/output-capabilities.ts +++ b/tsp-typescript-client/src/models/output-capabilities.ts @@ -5,6 +5,9 @@ * "canCreate" indicates that a given data provider can create a derived data * provider. "canDelete" indicates that a given data provider can be deleted. * + * "selectionRange" indicates that a given data provider can use the selection + * range to compute its data. Clients should include the selection range in + * query parameters and refresh the data when the selection range changes. */ export class OutputCapabilities { /** @@ -16,4 +19,9 @@ export class OutputCapabilities { * Whether the data provider can be deleted. 'false' if absent. */ canDelete?: boolean; + + /** + * Whether the data provider uses the selection range. 'false' if absent. + */ + selectionRange?: boolean; } diff --git a/tsp-typescript-client/src/models/output-descriptor.ts b/tsp-typescript-client/src/models/output-descriptor.ts index b07f02a..4d472b6 100644 --- a/tsp-typescript-client/src/models/output-descriptor.ts +++ b/tsp-typescript-client/src/models/output-descriptor.ts @@ -43,6 +43,10 @@ export enum ProviderType { * A provider for a gantt chart (abitrary x-axis). E.g. flame graph. */ GANTT_CHART = "GANTT_CHART", + /** + * A provider of generic data objects. + */ + DATA = "DATA", } /** diff --git a/tsp-typescript-client/src/protocol/http-tsp-client.ts b/tsp-typescript-client/src/protocol/http-tsp-client.ts index e934f20..34b065b 100644 --- a/tsp-typescript-client/src/protocol/http-tsp-client.ts +++ b/tsp-typescript-client/src/protocol/http-tsp-client.ts @@ -13,6 +13,7 @@ import { MarkerSet } from "../models/markerset"; import { OutputDescriptor } from "../models/output-descriptor"; import { ConfigurationQuery, OutputConfigurationQuery, Query } from "../models/query/query"; import { GenericResponse } from "../models/response/responses"; +import { ObjectModel } from "../models/object"; import { OutputStyleModel } from "../models/styles"; import { ColumnHeaderEntry, TableModel } from "../models/table"; import { @@ -167,6 +168,32 @@ export class HttpTspClient implements ITspClient { return RestClient.get(url, undefined, array(OutputDescriptor)); } + /** + * Fetch object + * @param expUUID Experiment UUID + * @param outputID Output ID + * @param parameters Query object + * @returns Generic object response + */ + public async fetchObject( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>> { + const url = + this.baseUrl + + "/experiments/" + + expUUID + + "/outputs/data/" + + outputID + + "/obj"; + return RestClient.post( + url, + parameters, + GenericResponse(ObjectModel) + ); + } + /** * Fetch Data tree * @param expUUID Experiment UUID diff --git a/tsp-typescript-client/src/protocol/tsp-client.test.ts b/tsp-typescript-client/src/protocol/tsp-client.test.ts index f65e16c..ed16b62 100644 --- a/tsp-typescript-client/src/protocol/tsp-client.test.ts +++ b/tsp-typescript-client/src/protocol/tsp-client.test.ts @@ -126,7 +126,7 @@ describe('HttpTspClient Deserialization', () => { httpRequestMock.mockReturnValueOnce(fixtures.asResponse('experiment-outputs-0.json')); const response = await client.experimentOutputs('not-relevant'); const outputs = response.getModel()!; - expect(outputs).toHaveLength(6); + expect(outputs).toHaveLength(7); let output = outputs.find((item) => item.id === 'timegraph.output.id1'); expect(output).toBeDefined(); @@ -141,6 +141,11 @@ describe('HttpTspClient Deserialization', () => { expect(output?.capabilities?.canCreate).toBeFalsy(); expect(output?.capabilities?.canDelete).toBeTruthy(); expect(output?.configuration).toBeDefined(); + + output = outputs.find((item) => item.id === 'data.output.id'); + expect(output).toBeDefined(); + expect(output?.capabilities).toBeDefined(); + expect(output?.capabilities?.selectionRange).toBeTruthy(); }); it('fetchAnnotationsCategories', async () => { @@ -210,6 +215,27 @@ describe('HttpTspClient Deserialization', () => { expect(identifier.productId).toBeDefined(); }); + it('fetchObject', async () => { + httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-object-0.json')); + const response = await client.fetchObject('not-relevant', 'not-relevant', new Query({})); + const genericResponse = response.getModel()!; + const object = genericResponse.model.object; + const next = genericResponse.model.next; + const previous = genericResponse.model.previous; + + expect(object).toBeDefined(); + expect(object['fooNumber']).toEqual(BigInt('1234567890123456789')); + expect(object['fooString']).toEqual('foo'); + expect(object['fooObj']['fooA']).toEqual({ "A1key": "A1val", "A2key": 1 }); + expect(object['fooObj']['fooB']).toEqual({ "B1key": "B1val", "B2key": 2 }); + expect(object['fooNumArray']).toEqual([1, 2, 3]); + expect(object['fooObjArray']).toEqual([{"k": "v1"}, {"k": "v2"}, {"k": "v3"}]); + expect(next).toBeDefined(); + expect(next).toEqual(BigInt('9876543210987654321')); + expect(previous).toBeDefined(); + expect(previous).toEqual({ "rank": 0 }); + }); + it('fetchMarkerSets', async () => { httpRequestMock.mockReturnValueOnce(fixtures.asResponse('fetch-marker-sets-0.json')); const response = await client.fetchMarkerSets('not-relevant'); diff --git a/tsp-typescript-client/src/protocol/tsp-client.ts b/tsp-typescript-client/src/protocol/tsp-client.ts index 564f669..777c4c1 100644 --- a/tsp-typescript-client/src/protocol/tsp-client.ts +++ b/tsp-typescript-client/src/protocol/tsp-client.ts @@ -16,6 +16,7 @@ import { Experiment } from "../models/experiment"; import { OutputDescriptor } from "../models/output-descriptor"; import { EntryModel } from "../models/entry"; import { TspClientResponse } from "./tsp-client-response"; +import { ObjectModel } from "../models/object"; import { OutputStyleModel } from "../models/styles"; import { HealthStatus } from "../models/health"; import { MarkerSet } from "../models/markerset"; @@ -111,6 +112,19 @@ export interface ITspClient { expUUID: string ): Promise>; + /** + * Fetch object + * @param expUUID Experiment UUID + * @param outputID Output ID + * @param parameters Query object + * @returns Generic object response + */ + fetchObject( + expUUID: string, + outputID: string, + parameters: Query + ): Promise>>; + /** * Fetch Data tree * @param expUUID Experiment UUID diff --git a/tsp-typescript-client/src/utils/jsonbig-utils.ts b/tsp-typescript-client/src/utils/jsonbig-utils.ts index 732303e..55d8400 100644 --- a/tsp-typescript-client/src/utils/jsonbig-utils.ts +++ b/tsp-typescript-client/src/utils/jsonbig-utils.ts @@ -42,7 +42,7 @@ export class JSONBigUtils { /** * Stringify JS objects. Can stringify `BigInt` values. */ - public static stringify(data: any): string { - return JSONBig.stringify(data); + public static stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string { + return JSONBig.stringify(value, replacer, space); } }