From 917961a570d9c13a9fcfdb6a974314b87735bd11 Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Sun, 12 Jul 2020 12:04:59 +0200 Subject: [PATCH] Cleanup of structure, documentation --- .../src/main/typescript/ApiClient.ts | 354 ++++++++++++++++++ .../main/typescript/RequestInitProvider.ts | 28 -- .../main/typescript/ccm-apiclient-commons.ts | 198 ++++------ .../src/main/typescript/entities.ts | 8 +- 4 files changed, 426 insertions(+), 162 deletions(-) create mode 100644 ccm-apiclient-commons/src/main/typescript/ApiClient.ts delete mode 100644 ccm-apiclient-commons/src/main/typescript/RequestInitProvider.ts diff --git a/ccm-apiclient-commons/src/main/typescript/ApiClient.ts b/ccm-apiclient-commons/src/main/typescript/ApiClient.ts new file mode 100644 index 000000000..3c8c51942 --- /dev/null +++ b/ccm-apiclient-commons/src/main/typescript/ApiClient.ts @@ -0,0 +1,354 @@ +/** + * The interface of a client for the LibreCcm RESTful API. + */ +export interface LibreCcmApiClient { + /** + * Performs a `GET` request for the provided endpoint. + * Implementations should throw an {@link ApiError} if an error + * occurs. + * + * @param endpoint The API endpoint to use. + * @param searchParams Optional search parameters. + */ + get( + endpoint: string, + searchParams?: Record + ): Promise; + /** + * Performs a `POST` request for the provided endpoint. + * Implementations should throw an {@link ApiError} if an error + * occurs. + * + * @param endpoint The API endpoint to use. + * @param body The request body. + * @param searchParams Optional search parameters. + */ + post( + endpoint: string, + body: unknown, + searchParams?: Record + ): Promise; + /** + * Performs a `PUT` request for the provided endpoint. + * Implementations should throw an {@link ApiError} if an error + * occurs. + * + * @param endpoint The API endpoint to use. + * @param body The request body. + * @param searchParams Optional search parameters. + */ + put( + endpoint: string, + body: Record, + searchParams?: Record + ): Promise; + /** + * Performs a `DELETE` request for the provided endpoint. + * Implementations should throw an {@link ApiError} if an error + * occurs. + * + * @param endpoint The API endpoint to use. + * @param searchParams Optional search parameters. + */ + delete( + endpoint: string, + searchParams?: Record + ): Promise; + /** + * Performs a `HEAD` request for the provided endpoint. + * Implementations should throw an {@link ApiError} if an error + * occurs. + * + * @param endpoint The API endpoint to use. + */ + head(endpoint: string): Promise; + /** + * Performs a `OPTIONS` request for the provided endpoint. + * Implementations should throw an {@link ApiError} if an error + * occurs. + * + * @param endpoint The API endpoint to use. + */ + options(endpoint: string): Promise; +} + +/** + * The response from the API. + */ +export interface ApiResponse { + /** + * The HTTP status code returned by the RESTful API. + */ + status: number; + /** + * The status text returned by the RESTful API. + */ + statusText: string; + /** + * Gets the Response Body as JSON. + */ + json(): Promise>; + /** + * Gets the Response Body as ArrayBuffer. + */ + arrayBuffer(): Promise; + /** + * Gets the Response Body as `string`. + */ + text(): Promise; +} + +export interface ApiError { + /** + * The HTTP status code reported by the API. `-1` if no status is available. + */ + status: number; + /** + * The status text reported by the API. + */ + statusText: string; + /** + * The HTTP method used for the failed request. + */ + method: "get" | "post" | "put" | "delete" | "head" | "option"; + /** + * A error message providing (additional) details about the error. + */ + message: string; + /** + * The URL used in the failed request. + */ + url: string; +} + +/** + * A helper function for building an URL. Because it might be useful for implementing + * client for specific APIs in the exported from this module. + * + * @param base The base URL pointing the an LibreCCM installation. + * @param endpoint The endpoint to address. + * @param searchParams Optional search parameter to append to the URL. + */ +export function buildUrl( + base: string, + endpoint: string, + searchParams?: Record +): string { + const url = new URL(base); + url.pathname = endpoint; + if (searchParams) { + const urlSearchParams: URLSearchParams = new URLSearchParams(); + for (const key in searchParams) { + urlSearchParams.append(key, searchParams[key]); + } + url.search = urlSearchParams.toString(); + } + + return url.href; +} + +/** + * An implementation of the {@link ApiResponse} for the Fetch-API supported + * by browsers. + */ +class FetchResponse implements ApiResponse { + readonly status: number; + readonly statusText: string; + #response: Response; + + constructor(response: Response) { + this.status = response.status; + this.statusText = response.statusText; + this.#response = response; + } + + json(): Promise> { + return this.#response.json(); + } + + arrayBuffer(): Promise { + return this.#response.arrayBuffer(); + } + + text(): Promise { + return this.#response.text(); + } +} + +/** + * An implementation of the {@link LibreCcmApiClient} interface + * using the Fetch-API supported by browsers. + */ +export class ApiClientFetchImpl implements LibreCcmApiClient { + readonly #baseUrl: string; + readonly #fetchOptions: RequestInit; + + /** + * + * @param baseUrl The URL of the LibreCCM installation to use. + * @param fetchOptions Basic fetch options for requests. + */ + constructor(baseUrl: string, fetchOptions: RequestInit) { + this.#baseUrl = baseUrl; + this.#fetchOptions = fetchOptions; + } + + private buildFetchOptions( + method: "get" | "post" | "put" | "delete" | "head" | "options" + ): RequestInit { + const fetchOptions: RequestInit = {}; + Object.assign(fetchOptions, this.#fetchOptions); + fetchOptions.method = method; + + return fetchOptions; + } + + async get( + endpoint: string, + searchParams?: Record + ): Promise { + const url = buildUrl(this.#baseUrl, endpoint, searchParams); + try { + const response = await fetch(url, this.buildFetchOptions("get")); + if (response.ok) { + return new FetchResponse(response); + } else { + throw { + status: response.status, + statusText: response.statusText, + method: "get", + message: "API responded with an error.", + url, + }; + } + } catch (err) { + throw { + status: -1, + statusText: "n/a", + method: "get", + message: `Failed to execute get: ${err}`, + url, + }; + } + } + + post( + endpoint: string, + body: unknown, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + put( + endpoint: string, + body: unknown, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + delete( + endpoint: string, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + head(endpoint: string): Promise { + throw "Not implemented yet."; + } + + options(endpoint: string): Promise { + throw "Not implemented yet."; + } +} + +/** + * An implementation of the {@link LibreCcmApiClient} interface using the HTTP API + * provided by node.js. + */ +export class ApiClientNodeImpl implements LibreCcmApiClient { + get( + endpoint: string, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + post( + endpoint: string, + body: unknown, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + put( + endpoint: string, + body: unknown, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + delete( + endpoint: string, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + head(endpoint: string): Promise { + throw "Not implemented yet."; + } + + options(endpoint: string): Promise { + throw "Not implemented yet."; + } +} + +/** + * An isomorphic implementation of the {@link LibreCcmApiClient} interface. + * Under the hood the implementation checks if the the Fetch-API is avaiable + * and used either the {@link ApiClientFetchImpl} or the {@link ApiClientNodeImpl}. + */ +export class IsomorphicClientImpl implements LibreCcmApiClient { + get( + endpoint: string, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + post( + endpoint: string, + body: unknown, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + put( + endpoint: string, + body: unknown, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + delete( + endpoint: string, + searchParams?: Record + ): Promise { + throw "Not implemented yet."; + } + + head(endpoint: string): Promise { + throw "Not implemented yet."; + } + + options(endpoint: string): Promise { + throw "Not implemented yet."; + } +} diff --git a/ccm-apiclient-commons/src/main/typescript/RequestInitProvider.ts b/ccm-apiclient-commons/src/main/typescript/RequestInitProvider.ts deleted file mode 100644 index 15e949c14..000000000 --- a/ccm-apiclient-commons/src/main/typescript/RequestInitProvider.ts +++ /dev/null @@ -1,28 +0,0 @@ -export interface RequestInitProvider { - buildRequestInit(): RequestInit; -} - -export class EmbeddedRequestInitProvider implements RequestInitProvider { - buildRequestInit(): RequestInit { - return { - credentials: "include", - }; - } -} - -export class RemoteRequestInitProvider implements RequestInitProvider { - #jwt: string; - - constructor(jwt: string) { - this.#jwt = jwt; - } - - buildRequestInit(): RequestInit { - return { - credentials: "omit", - headers: { - authorization: this.#jwt, - }, - }; - } -} diff --git a/ccm-apiclient-commons/src/main/typescript/ccm-apiclient-commons.ts b/ccm-apiclient-commons/src/main/typescript/ccm-apiclient-commons.ts index aaa04415d..7f96fdeb5 100644 --- a/ccm-apiclient-commons/src/main/typescript/ccm-apiclient-commons.ts +++ b/ccm-apiclient-commons/src/main/typescript/ccm-apiclient-commons.ts @@ -1,58 +1,27 @@ -import http from "http"; -import { AnyARecord } from "dns"; +import { + LibreCcmApiClient, + ApiResponse, + ApiError, + buildUrl, + ApiClientFetchImpl, + ApiClientNodeImpl, + IsomorphicClientImpl, +} from "./ApiClient"; export * from "./entities"; -export * from "./RequestInitProvider"; - -export interface LibreCcmApiClient { - get( - endpoint: string, - searchParams: Record - ): Promise; - post( - endpoint: string, - searchParams: Record, - body: unknown - ): Promise; - put(endpoint: string, body: Record): Promise; - delete(endpoint: string): Promise; - head(endpoint: string): Promise; - options(endpoint: string): Promise; -} - -export interface ApiResponse { - status: number; - statusText: string; - json(): Promise>; - blob(): Promise; - text(): Promise -} - -export interface ApiError { - status: number; - statusText: string; - method: "get" | "post" | "put" | "delete" | "head" | "option"; - message: string; - url: string; -} - -export function buildUrl(base: string, endpoint: string, searchParams?: Record): string { - const url = new URL(base); - url.pathname = endpoint; - if (searchParams) { - const urlSearchParams: URLSearchParams = new URLSearchParams(); - for (const key in searchParams) { - urlSearchParams.append(key, searchParams[key]); - } - url.search = urlSearchParams.toString(); - } - - return url.href; -} +export { LibreCcmApiClient, ApiResponse, ApiError, buildUrl }; +/** + * Build an client for the LibreCCM RESTful API suitable for use in + * clientside code served by the Application Server. + * + * The base URL for accessing the LibreCCM RESTful API is automatically + * determined from the URL of the current document. The API client will + * use the credentials stored in the browser (cookies) to authenticate itself. + */ export function buildEmbeddedApiClient(): LibreCcmApiClient { - if (!document) { - throw "document global variable is not available. Please use the buildRemoteApiClient."; + if (!fetch) { + throw "Fetch API is not available. Please use buildIsomorpicApiClient."; } const baseUrl = new URL(document.documentURI); baseUrl.hash = ""; @@ -61,95 +30,58 @@ export function buildEmbeddedApiClient(): LibreCcmApiClient { baseUrl.search = ""; baseUrl.username = ""; - return new EmbeddedApiClient(baseUrl.href, { + return new ApiClientFetchImpl(baseUrl.href, { credentials: "include", mode: "same-origin", }); } -class FetchResponse implements ApiResponse { - readonly status: number; - readonly statusText: string; - #response: Response; - - constructor(response: Response) { - this.status = response.status; - this.statusText = response.statusText; - this.#response = response; +/** + * Builds a client for the LibreCCM RESTful API suitable for running in a browser. + * + * @param baseUrl The URL of the LibreCCM installation to access, including the port. + * @param jwt The JSON Web Token to use by the client to authenticate itself. + */ +export function buildRemoteApiClient( + baseUrl: string, + jwt: string +): LibreCcmApiClient { + if (!fetch) { + throw "Fetch API is not available. Please use buildIsomorpicApiClient."; } - json(): Promise> { - return this.#response.json(); - } - - blob(): Promise { - return this.#response.blob(); - } - - text(): Promise { - return this.#response.text(); - } + return new ApiClientFetchImpl(baseUrl, { + headers: { + Authorization: jwt, + }, + }); } -class EmbeddedApiClient implements LibreCcmApiClient { - #baseUrl: string; - #fetchOptions: RequestInit; - - constructor(baseUrl: string, fetchOptions: RequestInit) { - this.#baseUrl = baseUrl; - this.#fetchOptions = fetchOptions; - } - - async get( - endpoint: string, - searchParams?: Record - ): Promise { - const fetchOptions: RequestInit = {}; - Object.assign(fetchOptions, this.#fetchOptions); - fetchOptions.method = "get"; - - const url = buildUrl(this.#baseUrl, endpoint, searchParams); - try { - const response = await fetch(url, this.#fetchOptions); - if (response.ok) { - return new FetchResponse(response); - } else { - throw { - status: response.status, - statusText: response.statusText, - method: "get", - message: "Received an error from the API endpoint. ", - url - } - } - } catch(err) { - throw `Failed to execute GET on ${url}: ${err}`; - } - } - - async post( - endpoint: string, - searchParams: Record, - body: unknown - ): Promise { - throw "Not implemented yet"; - } - - put(endpoint: string, body: Record): Promise { - throw "Not implemented yet"; - } - - delete(endpoint: string): Promise { - throw "Not implemented yet"; - } - - head(endpoint: string): Promise { - throw "Not implemented yet"; - } - - options(endpoint: string): Promise { - throw "Not implemented yet"; - } - - +/** + * Builds a client for the LibreCCM RESTful API suitable for running inside a node.js + * environment. + * + * @param baseUrl The URL of the LibreCCM installation to access, including the port. + * @param jwt The JSON Web Token to use by the client to authenticate itself. + */ +export function buildNodeApiClient( + baseUrl: string, + jwt: string +): LibreCcmApiClient { + return new ApiClientNodeImpl(); +} + +/** + * Builds an isomorphic client for the LibreCCM RESTful API which will work in the + * browser and in a node.js environment. Use this function to create an API client + * for JavaScript applications which use Server-Side-Rendering. + * + * @param baseUrl The URL of the LibreCCM installation to access, including the port. + * @param jwt The JSON Web Token to use by the client to authenticate itself. + */ +export function buildIsomorpicApiClient( + baseUrl: string, + jwt: string +): LibreCcmApiClient { + return new IsomorphicClientImpl(); } diff --git a/ccm-apiclient-commons/src/main/typescript/entities.ts b/ccm-apiclient-commons/src/main/typescript/entities.ts index 70590baa1..e341e94ce 100644 --- a/ccm-apiclient-commons/src/main/typescript/entities.ts +++ b/ccm-apiclient-commons/src/main/typescript/entities.ts @@ -1,3 +1,9 @@ +/** + * Properties of the type `LocalizedString` are used in several entities + * returned by various of the endpoints the RESTful API of LibreCCM. + * Each `LocalizedString` consists of various values with the language + * as key. + */ export interface LocalizedString { - [language: string]: string; + values: Record }