Cleanup of structure, documentation

Former-commit-id: 917961a570
restapi
Jens Pelzetter 2020-07-12 12:04:59 +02:00
parent 1fdb60b045
commit 82a982ebf9
4 changed files with 426 additions and 162 deletions

View File

@ -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<string, string>
): Promise<ApiResponse>;
/**
* 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<string, string>
): Promise<ApiResponse>;
/**
* 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<string, unknown>,
searchParams?: Record<string, string>
): Promise<ApiResponse>;
/**
* 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<string, string>
): Promise<ApiResponse>;
/**
* 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<ApiResponse>;
/**
* 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<ApiResponse>;
}
/**
* 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<Record<string, unknown>>;
/**
* Gets the Response Body as ArrayBuffer.
*/
arrayBuffer(): Promise<ArrayBuffer>;
/**
* Gets the Response Body as `string`.
*/
text(): Promise<string>;
}
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, string>
): 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<Record<string, unknown>> {
return this.#response.json();
}
arrayBuffer(): Promise<ArrayBuffer> {
return this.#response.arrayBuffer();
}
text(): Promise<string> {
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<string, string>
): Promise<ApiResponse> {
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<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
put(
endpoint: string,
body: unknown,
searchParams?: Record<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
delete(
endpoint: string,
searchParams?: Record<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
head(endpoint: string): Promise<ApiResponse> {
throw "Not implemented yet.";
}
options(endpoint: string): Promise<ApiResponse> {
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<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
post(
endpoint: string,
body: unknown,
searchParams?: Record<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
put(
endpoint: string,
body: unknown,
searchParams?: Record<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
delete(
endpoint: string,
searchParams?: Record<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
head(endpoint: string): Promise<ApiResponse> {
throw "Not implemented yet.";
}
options(endpoint: string): Promise<ApiResponse> {
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<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
post(
endpoint: string,
body: unknown,
searchParams?: Record<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
put(
endpoint: string,
body: unknown,
searchParams?: Record<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
delete(
endpoint: string,
searchParams?: Record<string, string>
): Promise<ApiResponse> {
throw "Not implemented yet.";
}
head(endpoint: string): Promise<ApiResponse> {
throw "Not implemented yet.";
}
options(endpoint: string): Promise<ApiResponse> {
throw "Not implemented yet.";
}
}

View File

@ -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,
},
};
}
}

View File

@ -1,58 +1,27 @@
import http from "http"; import {
import { AnyARecord } from "dns"; LibreCcmApiClient,
ApiResponse,
ApiError,
buildUrl,
ApiClientFetchImpl,
ApiClientNodeImpl,
IsomorphicClientImpl,
} from "./ApiClient";
export * from "./entities"; export * from "./entities";
export * from "./RequestInitProvider"; export { LibreCcmApiClient, ApiResponse, ApiError, buildUrl };
export interface LibreCcmApiClient {
get(
endpoint: string,
searchParams: Record<string, string>
): Promise<ApiResponse>;
post(
endpoint: string,
searchParams: Record<string, string>,
body: unknown
): Promise<Response>;
put(endpoint: string, body: Record<string, unknown>): Promise<Response>;
delete(endpoint: string): Promise<Response>;
head(endpoint: string): Promise<Response>;
options(endpoint: string): Promise<Response>;
}
export interface ApiResponse {
status: number;
statusText: string;
json(): Promise<Record<string, unknown>>;
blob(): Promise<Blob>;
text(): Promise<string>
}
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, string>): 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;
}
/**
* 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 { export function buildEmbeddedApiClient(): LibreCcmApiClient {
if (!document) { if (!fetch) {
throw "document global variable is not available. Please use the buildRemoteApiClient."; throw "Fetch API is not available. Please use buildIsomorpicApiClient.";
} }
const baseUrl = new URL(document.documentURI); const baseUrl = new URL(document.documentURI);
baseUrl.hash = ""; baseUrl.hash = "";
@ -61,95 +30,58 @@ export function buildEmbeddedApiClient(): LibreCcmApiClient {
baseUrl.search = ""; baseUrl.search = "";
baseUrl.username = ""; baseUrl.username = "";
return new EmbeddedApiClient(baseUrl.href, { return new ApiClientFetchImpl(baseUrl.href, {
credentials: "include", credentials: "include",
mode: "same-origin", mode: "same-origin",
}); });
} }
class FetchResponse implements ApiResponse { /**
readonly status: number; * Builds a client for the LibreCCM RESTful API suitable for running in a browser.
readonly statusText: string; *
#response: Response; * @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.
constructor(response: Response) { */
this.status = response.status; export function buildRemoteApiClient(
this.statusText = response.statusText; baseUrl: string,
this.#response = response; jwt: string
): LibreCcmApiClient {
if (!fetch) {
throw "Fetch API is not available. Please use buildIsomorpicApiClient.";
} }
json(): Promise<Record<string, unknown>> { return new ApiClientFetchImpl(baseUrl, {
return this.#response.json(); headers: {
} Authorization: jwt,
},
blob(): Promise<Blob> { });
return this.#response.blob();
}
text(): Promise<string> {
return this.#response.text();
}
} }
class EmbeddedApiClient implements LibreCcmApiClient { /**
#baseUrl: string; * Builds a client for the LibreCCM RESTful API suitable for running inside a node.js
#fetchOptions: RequestInit; * environment.
*
constructor(baseUrl: string, fetchOptions: RequestInit) { * @param baseUrl The URL of the LibreCCM installation to access, including the port.
this.#baseUrl = baseUrl; * @param jwt The JSON Web Token to use by the client to authenticate itself.
this.#fetchOptions = fetchOptions; */
} export function buildNodeApiClient(
baseUrl: string,
async get( jwt: string
endpoint: string, ): LibreCcmApiClient {
searchParams?: Record<string, string> return new ApiClientNodeImpl();
): Promise<ApiResponse> { }
const fetchOptions: RequestInit = {};
Object.assign(fetchOptions, this.#fetchOptions); /**
fetchOptions.method = "get"; * 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
const url = buildUrl(this.#baseUrl, endpoint, searchParams); * for JavaScript applications which use Server-Side-Rendering.
try { *
const response = await fetch(url, this.#fetchOptions); * @param baseUrl The URL of the LibreCCM installation to access, including the port.
if (response.ok) { * @param jwt The JSON Web Token to use by the client to authenticate itself.
return new FetchResponse(response); */
} else { export function buildIsomorpicApiClient(
throw { baseUrl: string,
status: response.status, jwt: string
statusText: response.statusText, ): LibreCcmApiClient {
method: "get", return new IsomorphicClientImpl();
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<string, string>,
body: unknown
): Promise<Response> {
throw "Not implemented yet";
}
put(endpoint: string, body: Record<string, unknown>): Promise<Response> {
throw "Not implemented yet";
}
delete(endpoint: string): Promise<Response> {
throw "Not implemented yet";
}
head(endpoint: string): Promise<Response> {
throw "Not implemented yet";
}
options(endpoint: string): Promise<Response> {
throw "Not implemented yet";
}
} }

View File

@ -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 { export interface LocalizedString {
[language: string]: string; values: Record<string, string>
} }