mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2025-12-13 01:36:22 +00:00
312 lines
8.9 KiB
TypeScript
312 lines
8.9 KiB
TypeScript
/*
|
|
* Copyright (C) 2018-2022 Open StApps
|
|
* This program is free software: you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation, version 3.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
import {
|
|
SCAbstractRoute,
|
|
SCErrorResponse,
|
|
SCFeatureConfiguration,
|
|
SCFeatureConfigurationPlugin,
|
|
SCIndexRequest,
|
|
SCIndexResponse,
|
|
SCIndexRoute,
|
|
SCInternalServerErrorResponse,
|
|
SCMultiSearchRequest,
|
|
SCMultiSearchResponse,
|
|
SCMultiSearchRoute,
|
|
SCNotFoundErrorResponse,
|
|
SCRequests,
|
|
SCSearchRequest,
|
|
SCSearchResponse,
|
|
SCSearchRoute,
|
|
SCThings,
|
|
} from '@openstapps/core';
|
|
import {ApiError, CoreVersionIncompatibleError, OutOfRangeError, PluginNotAvailableError} from './errors';
|
|
import {HttpClientHeaders, HttpClientInterface} from './http-client-interface';
|
|
|
|
/**
|
|
* StApps-API client
|
|
*/
|
|
export class Client {
|
|
/**
|
|
* Instance of index route
|
|
*/
|
|
private readonly indexRoute = new SCIndexRoute();
|
|
|
|
/**
|
|
* Instance of multi search request route
|
|
*/
|
|
private readonly multiSearchRoute = new SCMultiSearchRoute();
|
|
|
|
/**
|
|
* Instance of search request route
|
|
*/
|
|
private readonly searchRoute = new SCSearchRoute();
|
|
|
|
/**
|
|
* Features supported by backend
|
|
*/
|
|
private supportedFeatures?: SCFeatureConfiguration = undefined;
|
|
|
|
/**
|
|
* Default headers
|
|
*
|
|
* TODO: remove headers
|
|
*/
|
|
protected readonly headers: HttpClientHeaders = {};
|
|
|
|
/**
|
|
* Create a new search request with altered pagination parameters to move to the next result window
|
|
*
|
|
* @param searchRequest Last search request
|
|
* @param searchResponse Search response for supplied search request
|
|
* @throws OutOfRangeError Throws an error if the next window is beyond the total number of results
|
|
*/
|
|
static nextWindow(searchRequest: SCSearchRequest, searchResponse: SCSearchResponse): SCSearchRequest {
|
|
// calculate next from
|
|
const from = searchResponse.pagination.offset + searchResponse.pagination.count;
|
|
|
|
// throw an error if the next window is beyond the total number of results
|
|
if (from >= searchResponse.pagination.total) {
|
|
throw new OutOfRangeError(searchRequest);
|
|
}
|
|
|
|
// return a search request with the next window
|
|
return {
|
|
...searchRequest,
|
|
from,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Instantiate a new StApps-API client to communicate with a StApps-backend.
|
|
*
|
|
* @param httpClient HTTP client to use
|
|
* @param url URL of the backend
|
|
* @param version App version to use when requesting data *(only necessary if URL is ambiguous)*
|
|
*
|
|
* TODO: remove headers/version
|
|
*/
|
|
constructor(protected httpClient: HttpClientInterface, protected url: string, protected version?: string) {
|
|
// cut trailing slash if needed
|
|
this.url = this.url.replace(/\/$/, '');
|
|
|
|
this.headers = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (typeof version === 'string') {
|
|
this.headers['X-StApps-Version'] = this.version;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a thing by its UID
|
|
*
|
|
* @param uid UID of the thing to fetch
|
|
*/
|
|
async getThing(uid: string): Promise<SCThings> {
|
|
const response = await this.search({
|
|
filter: {
|
|
arguments: {
|
|
field: 'uid',
|
|
value: uid,
|
|
},
|
|
type: 'value',
|
|
},
|
|
size: 1,
|
|
});
|
|
|
|
if (response.data.length === 1 && response.data[0].uid === uid) {
|
|
return response.data[0];
|
|
}
|
|
|
|
throw new SCInternalServerErrorResponse(new SCNotFoundErrorResponse(true), true);
|
|
}
|
|
|
|
/**
|
|
* Make a handshake with the backend and check StAppsCore version
|
|
*
|
|
* @param coreVersion StAppsCore version to check
|
|
*/
|
|
async handshake(coreVersion: string): Promise<SCIndexResponse> {
|
|
const request: SCIndexRequest = {};
|
|
|
|
const response = await this.invokeRoute<SCIndexResponse>(this.indexRoute, undefined, request);
|
|
|
|
if (response.backend.SCVersion.split('.')[0] !== coreVersion.split('.')[0]) {
|
|
throw new CoreVersionIncompatibleError(coreVersion, response.backend.SCVersion);
|
|
}
|
|
/* istanbul ignore next */
|
|
this.supportedFeatures = response?.app?.features;
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Invoke a plugin route
|
|
*
|
|
* @param name name of the plugin
|
|
* @param parameters Parameters for the URL fragment
|
|
* @param body Body for the request
|
|
*/
|
|
async invokePlugin<T>(name: string, parameters?: {[k: string]: string}, body?: SCRequests): Promise<T> {
|
|
if (typeof this.supportedFeatures === 'undefined') {
|
|
const request: SCIndexRequest = {};
|
|
const response = await this.invokeRoute<SCIndexResponse>(this.indexRoute, undefined, request);
|
|
if (typeof response?.app?.features !== 'undefined') {
|
|
/* istanbul ignore next */
|
|
this.supportedFeatures = response?.app?.features;
|
|
}
|
|
}
|
|
const pluginInfo: SCFeatureConfigurationPlugin | undefined = this.supportedFeatures?.plugins?.[name];
|
|
if (typeof pluginInfo === 'undefined') {
|
|
throw new PluginNotAvailableError(name);
|
|
}
|
|
|
|
const route = new SCIndexRoute();
|
|
route.urlPath = pluginInfo.urlPath;
|
|
|
|
return this.invokeRoute<T>(route, parameters, body);
|
|
}
|
|
|
|
/**
|
|
* Invoke a route
|
|
*
|
|
* @param route Route to invoke
|
|
* @param parameters Parameters for the URL fragment
|
|
* @param body Body for the request
|
|
*/
|
|
async invokeRoute<T>(
|
|
route: SCAbstractRoute,
|
|
parameters?: {[k: string]: string},
|
|
body?: SCRequests,
|
|
): Promise<T> {
|
|
// make the request
|
|
const response = await this.httpClient.request({
|
|
body: body,
|
|
// TODO: remove headers
|
|
headers: this.headers,
|
|
method: route.method,
|
|
url: new URL(this.url + route.getUrlPath(parameters)),
|
|
});
|
|
|
|
if (response.statusCode === route.statusCodeSuccess) {
|
|
return response.body as T;
|
|
}
|
|
|
|
throw new ApiError(response.body as SCErrorResponse);
|
|
}
|
|
|
|
/**
|
|
* Send a multi search request to the backend
|
|
*
|
|
* All results will be returned for requests where no size is set.
|
|
*
|
|
* @param multiSearchRequest Multi search request
|
|
*/
|
|
async multiSearch(multiSearchRequest: SCMultiSearchRequest): Promise<SCMultiSearchResponse> {
|
|
const preFlightRequest: SCMultiSearchRequest = {};
|
|
let preFlightNecessary = false;
|
|
|
|
// gather search requests where size is not set
|
|
for (const key of Object.keys(multiSearchRequest)) {
|
|
const searchRequest = multiSearchRequest[key];
|
|
|
|
if (typeof searchRequest.size === 'undefined') {
|
|
preFlightRequest[key] = {
|
|
...searchRequest,
|
|
};
|
|
preFlightRequest[key].size = 0;
|
|
preFlightNecessary = true;
|
|
}
|
|
}
|
|
|
|
let returnMultiSearchRequest = multiSearchRequest;
|
|
|
|
if (preFlightNecessary) {
|
|
// copy multi search request
|
|
returnMultiSearchRequest = {
|
|
...multiSearchRequest,
|
|
};
|
|
|
|
// make pre flight request
|
|
const preFlightResponse = await this.invokeRoute<SCMultiSearchResponse>(
|
|
this.multiSearchRoute,
|
|
undefined,
|
|
preFlightRequest,
|
|
);
|
|
|
|
// set size for multi search requests that were in pre flight request
|
|
for (const key of Object.keys(preFlightRequest)) {
|
|
returnMultiSearchRequest[key].size = preFlightResponse[key].pagination.total;
|
|
}
|
|
}
|
|
|
|
// actually invoke the route
|
|
return this.invokeRoute<SCMultiSearchResponse>(
|
|
this.multiSearchRoute,
|
|
undefined,
|
|
returnMultiSearchRequest,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Send a search request to the backend
|
|
*
|
|
* All results will be returned if no size is set in the request.
|
|
*
|
|
* @param searchRequest Search request
|
|
*/
|
|
async search(searchRequest: SCSearchRequest): Promise<SCSearchResponse> {
|
|
let size: number | undefined = searchRequest.size;
|
|
|
|
if (typeof size === 'undefined') {
|
|
const preFlightResponse = await this.invokeRoute<SCSearchResponse>(this.searchRoute, undefined, {
|
|
...searchRequest,
|
|
size: 0,
|
|
});
|
|
|
|
size = preFlightResponse.pagination.total;
|
|
}
|
|
|
|
return this.invokeRoute<SCSearchResponse>(this.searchRoute, undefined, {
|
|
...searchRequest,
|
|
size,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the next search results
|
|
*
|
|
* @param searchRequest Last search request
|
|
* @param searchResponse Search response for supplied search request
|
|
*/
|
|
async searchNext(
|
|
searchRequest: SCSearchRequest,
|
|
searchResponse: SCSearchResponse,
|
|
): Promise<{
|
|
/* tslint:disable:completed-docs */
|
|
searchRequest: SCSearchRequest;
|
|
searchResponse: SCSearchResponse;
|
|
/* tslint:enable:completed-docs */
|
|
}> {
|
|
const nextSearchRequest = Client.nextWindow(searchRequest, searchResponse);
|
|
|
|
return {
|
|
searchRequest: nextSearchRequest,
|
|
searchResponse: await this.search(nextSearchRequest),
|
|
};
|
|
}
|
|
}
|