/* * Copyright (C) 2018, 2019 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 . */ import { SCAbstractRoute, SCErrorResponse, SCFeedbackRequest, SCFeedbackResponse, SCFeedbackRoute, SCIndexRequest, SCIndexResponse, SCIndexRoute, SCInternalServerErrorResponse, SCMultiSearchRequest, SCMultiSearchResponse, SCMultiSearchRoute, SCNotFoundErrorResponse, SCRequests, SCSearchRequest, SCSearchResponse, SCSearchRoute, SCThings, } from '@openstapps/core'; import {ApiError, CoreVersionIncompatibleError, OutOfRangeError} from './errors'; import {HttpClientHeaders, HttpClientInterface} from './http-client-interface'; /** * StApps-API client */ export class Client { /** * Instance of feedback request route */ private readonly feedbackRoute = new SCFeedbackRoute(); /** * 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(); /** * 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; } } /** * Send feedback * * @param feedback Feedback to send */ async feedback(feedback: SCFeedbackRequest): Promise { return this.invokeRoute(this.feedbackRoute, undefined, feedback); } /** * Get a thing by its UID * * @param uid UID of the thing to fetch */ async getThing(uid: string): Promise { 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 { const request: SCIndexRequest = {}; const response = await this.invokeRoute(this.indexRoute, undefined, request); if (response.backend.SCVersion.split('.')[0] !== coreVersion.split('.')[0]) { throw new CoreVersionIncompatibleError(coreVersion, response.backend.SCVersion); } return response; } /** * Invoke a route * * @param route Route to invoke * @param parameters Parameters for the URL fragment * @param body Body for the request */ async invokeRoute(route: SCAbstractRoute, parameters?: { [k: string]: string; }, body?: SCRequests): Promise { // 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.getUrlFragment(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 { const preFlightRequest: SCMultiSearchRequest = {}; let preFlightNecessary = false; // gather search requests where size is not set Object.keys(multiSearchRequest) .forEach((key) => { 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( this.multiSearchRoute, undefined, preFlightRequest, ); // set size for multi search requests that were in pre flight request Object.keys(preFlightRequest) .forEach((key) => { returnMultiSearchRequest[key].size = preFlightResponse[key].pagination.total; }); } // actually invoke the route return this.invokeRoute(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 { let size: number | undefined = searchRequest.size; if (typeof size === 'undefined') { const preFlightResponse = await this.invokeRoute(this.searchRoute, undefined, { ...searchRequest, size: 0, }); size = preFlightResponse.pagination.total; } return this.invokeRoute(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), }; } }