mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 00:23:03 +00:00
refactor: move api to monorepo
This commit is contained in:
311
packages/api/src/client.ts
Normal file
311
packages/api/src/client.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user