mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
chore: extract json-api from core
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
f1f2634490
commit
04a23fc4f9
20 files changed
+164
-55
No files matched your search
@@ -3,14 +3,13 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { RequestInit, Response } from "@k8slens/node-fetch";
|
||||
import fetch from "@k8slens/node-fetch";
|
||||
|
||||
export type Fetch = (url: string, init?: RequestInit) => Promise<Response>;
|
||||
export type Fetch = typeof fetch;
|
||||
|
||||
const fetchInjectable = getInjectable({
|
||||
id: "fetch",
|
||||
instantiate: () => fetch as Fetch,
|
||||
instantiate: () => fetch,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import type { RequestInit } from "@k8slens/node-fetch";
|
||||
import lensProxyCertificateInjectable from "../certificate/lens-proxy-certificate.injectable";
|
||||
import fetchInjectable from "../fetch/fetch.injectable";
|
||||
import { loggerInjectionToken } from "@k8slens/logger";
|
||||
import type { JsonApiConfig, JsonApiData, JsonApiDependencies, JsonApiParams } from "./json-api";
|
||||
import { JsonApi } from "./json-api";
|
||||
import type { JsonApiConfig, JsonApiData, JsonApiDependencies, JsonApiParams } from "@k8slens/json-api";
|
||||
import { JsonApi } from "@k8slens/json-api";
|
||||
|
||||
export type CreateJsonApi = <Data = JsonApiData, Params extends JsonApiParams<Data> = JsonApiParams<Data>>(config: JsonApiConfig, reqInit?: RequestInit) => JsonApi<Data, Params>;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { RequestInit } from "@k8slens/node-fetch";
|
||||
import lensProxyCertificateInjectable from "../certificate/lens-proxy-certificate.injectable";
|
||||
import fetchInjectable from "../fetch/fetch.injectable";
|
||||
import { loggerInjectionToken } from "@k8slens/logger";
|
||||
import type { JsonApiConfig, JsonApiDependencies } from "./json-api";
|
||||
import type { JsonApiConfig, JsonApiDependencies } from "@k8slens/json-api";
|
||||
import { KubeJsonApi } from "./kube-json-api";
|
||||
|
||||
export type CreateKubeJsonApi = (config: JsonApiConfig, reqInit?: RequestInit) => KubeJsonApi;
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Base http-service / json-api class
|
||||
|
||||
import { Agent as HttpAgent } from "http";
|
||||
import { Agent as HttpsAgent } from "https";
|
||||
import { merge } from "lodash";
|
||||
import type { Response, RequestInit } from "@k8slens/node-fetch";
|
||||
import { stringify } from "querystring";
|
||||
import type { Patch } from "rfc6902";
|
||||
import type { PartialDeep, ValueOf } from "type-fest";
|
||||
import { EventEmitter } from "@k8slens/event-emitter";
|
||||
import type { Logger } from "@k8slens/logger";
|
||||
import type { Fetch } from "../fetch/fetch.injectable";
|
||||
import type { Defaulted } from "@k8slens/utilities";
|
||||
import { isObject, isString, json } from "@k8slens/utilities";
|
||||
|
||||
export interface JsonApiData {}
|
||||
|
||||
export interface JsonApiError {
|
||||
code?: number;
|
||||
message?: string;
|
||||
errors?: { id: string; title: string; status?: number }[];
|
||||
}
|
||||
|
||||
export interface KubeJsonApiErrorCause {
|
||||
reason: string;
|
||||
message: string;
|
||||
field: string;
|
||||
}
|
||||
|
||||
export interface KubeJsonApiErrorDetails {
|
||||
name: string;
|
||||
group: string;
|
||||
kind: string;
|
||||
causes: KubeJsonApiErrorCause[];
|
||||
}
|
||||
|
||||
export interface KubeJsonApiError {
|
||||
kind: "Status";
|
||||
apiVersion: "v1";
|
||||
metadata: object;
|
||||
status: string;
|
||||
message: string;
|
||||
reason: string;
|
||||
details: KubeJsonApiErrorDetails;
|
||||
code: number;
|
||||
}
|
||||
|
||||
export interface JsonApiParams<D> {
|
||||
data?: PartialDeep<D>; // request body
|
||||
}
|
||||
|
||||
export interface JsonApiLog {
|
||||
method: string;
|
||||
reqUrl: string;
|
||||
reqInit: RequestInit;
|
||||
data?: any;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
export type GetRequestOptions = () => Promise<RequestInit>;
|
||||
|
||||
export interface JsonApiConfig {
|
||||
apiBase: string;
|
||||
serverAddress: string;
|
||||
debug?: boolean;
|
||||
getRequestOptions?: GetRequestOptions;
|
||||
}
|
||||
|
||||
const httpAgent = new HttpAgent({ keepAlive: true });
|
||||
const httpsAgent = new HttpsAgent({ keepAlive: true });
|
||||
|
||||
export type QueryParam = string | number | boolean | null | undefined | readonly string[] | readonly number[] | readonly boolean[];
|
||||
export type QueryParams = Partial<Record<string, QueryParam | undefined>>;
|
||||
|
||||
export type ParamsAndQuery<Params, Query> = (
|
||||
ValueOf<Query> extends QueryParam
|
||||
? Params & { query?: Query }
|
||||
: Params & { query?: undefined }
|
||||
);
|
||||
|
||||
export interface JsonApiDependencies {
|
||||
fetch: Fetch;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = JsonApiParams<Data>> {
|
||||
static readonly reqInitDefault = {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
};
|
||||
protected readonly reqInit: Defaulted<RequestInit, keyof typeof JsonApi["reqInitDefault"]>;
|
||||
|
||||
static readonly configDefault: Partial<JsonApiConfig> = {
|
||||
debug: false,
|
||||
};
|
||||
|
||||
constructor(protected readonly dependencies: JsonApiDependencies, public readonly config: JsonApiConfig, reqInit?: RequestInit) {
|
||||
this.config = Object.assign({}, JsonApi.configDefault, config);
|
||||
this.reqInit = merge({}, JsonApi.reqInitDefault, reqInit);
|
||||
this.parseResponse = this.parseResponse.bind(this);
|
||||
this.getRequestOptions = config.getRequestOptions ?? (() => Promise.resolve({}));
|
||||
}
|
||||
|
||||
public readonly onData = new EventEmitter<[Data, Response]>();
|
||||
public readonly onError = new EventEmitter<[JsonApiErrorParsed, Response]>();
|
||||
private readonly getRequestOptions: GetRequestOptions;
|
||||
|
||||
async getResponse<Query>(
|
||||
path: string,
|
||||
params?: ParamsAndQuery<Params, Query>,
|
||||
init: RequestInit = {},
|
||||
): Promise<Response> {
|
||||
let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`;
|
||||
const reqInit = merge(
|
||||
{
|
||||
method: "get",
|
||||
agent: reqUrl.startsWith("https:") ? httpsAgent : httpAgent,
|
||||
},
|
||||
this.reqInit,
|
||||
await this.getRequestOptions(),
|
||||
init,
|
||||
);
|
||||
const { query } = params ?? {};
|
||||
|
||||
if (query && Object.keys(query).length > 0) {
|
||||
const queryString = stringify(query as unknown as QueryParams);
|
||||
|
||||
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
|
||||
}
|
||||
|
||||
return this.dependencies.fetch(reqUrl, reqInit);
|
||||
}
|
||||
|
||||
get<OutData = Data, Query = QueryParams>(
|
||||
path: string,
|
||||
params?: ParamsAndQuery<Params, Query>,
|
||||
reqInit: RequestInit = {},
|
||||
) {
|
||||
return this.request<OutData, Query>(path, params, { ...reqInit, method: "get" });
|
||||
}
|
||||
|
||||
post<OutData = Data, Query = QueryParams>(
|
||||
path: string,
|
||||
params?: ParamsAndQuery<Params, Query>,
|
||||
reqInit: RequestInit = {},
|
||||
) {
|
||||
return this.request<OutData, Query>(path, params, { ...reqInit, method: "post" });
|
||||
}
|
||||
|
||||
put<OutData = Data, Query = QueryParams>(
|
||||
path: string,
|
||||
params?: ParamsAndQuery<Params, Query>,
|
||||
reqInit: RequestInit = {},
|
||||
) {
|
||||
return this.request<OutData, Query>(path, params, { ...reqInit, method: "put" });
|
||||
}
|
||||
|
||||
patch<OutData = Data, Query = QueryParams>(
|
||||
path: string,
|
||||
params?: (ParamsAndQuery<Omit<Params, "data">, Query> & { data?: Patch | PartialDeep<Data> }),
|
||||
reqInit: RequestInit = {},
|
||||
) {
|
||||
return this.request<OutData, Query>(path, params, { ...reqInit, method: "patch" });
|
||||
}
|
||||
|
||||
del<OutData = Data, Query = QueryParams>(
|
||||
path: string,
|
||||
params?: ParamsAndQuery<Params, Query>,
|
||||
reqInit: RequestInit = {},
|
||||
) {
|
||||
return this.request<OutData, Query>(path, params, { ...reqInit, method: "delete" });
|
||||
}
|
||||
|
||||
protected async request<OutData, Query = QueryParams>(
|
||||
path: string,
|
||||
params: (ParamsAndQuery<Omit<Params, "data">, Query> & { data?: unknown }) | undefined,
|
||||
init: Defaulted<RequestInit, "method">,
|
||||
) {
|
||||
let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`;
|
||||
const reqInit = merge(
|
||||
{},
|
||||
this.reqInit,
|
||||
await this.getRequestOptions(),
|
||||
init,
|
||||
);
|
||||
const { data, query } = params || {};
|
||||
|
||||
if (data && !reqInit.body) {
|
||||
reqInit.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
if (query && Object.keys(query).length > 0) {
|
||||
const queryString = stringify(query as unknown as QueryParams);
|
||||
|
||||
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
|
||||
}
|
||||
const infoLog: JsonApiLog = {
|
||||
method: reqInit.method.toUpperCase(),
|
||||
reqUrl,
|
||||
reqInit,
|
||||
};
|
||||
|
||||
const res = await this.dependencies.fetch(reqUrl, reqInit);
|
||||
|
||||
return await this.parseResponse(res, infoLog) as OutData;
|
||||
}
|
||||
|
||||
protected async parseResponse(res: Response, log: JsonApiLog): Promise<Data> {
|
||||
const { status } = res;
|
||||
|
||||
const text = await res.text();
|
||||
const parseResponse = json.parse(text || "{}");
|
||||
const data = parseResponse.callWasSuccessful
|
||||
? parseResponse.response as Data
|
||||
: text as Data;
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
this.onData.emit(data, res);
|
||||
this.writeLog({ ...log, data });
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
if (log.method === "GET" && res.status === 403) {
|
||||
this.writeLog({ ...log, error: data });
|
||||
throw data;
|
||||
}
|
||||
|
||||
const error = new JsonApiErrorParsed(data as JsonApiError, this.parseError(data, res));
|
||||
|
||||
this.onError.emit(error, res);
|
||||
this.writeLog({ ...log, error });
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
protected parseError(error: unknown, res: Response): string[] {
|
||||
if (isString(error)) {
|
||||
return [error];
|
||||
}
|
||||
|
||||
if (!isObject(error)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { errors, message } = error as { errors?: { title: string }[]; message?: string };
|
||||
|
||||
if (Array.isArray(errors)) {
|
||||
return errors.map(error => error.title);
|
||||
}
|
||||
|
||||
if (isString(message)) {
|
||||
return [message];
|
||||
}
|
||||
|
||||
return [res.statusText || "Error!"];
|
||||
}
|
||||
|
||||
protected writeLog(log: JsonApiLog) {
|
||||
const { method, reqUrl, ...params } = log;
|
||||
|
||||
this.dependencies.logger.debug(`[JSON-API] request ${method} ${reqUrl}`, params);
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonApiErrorParsed {
|
||||
isUsedForNotification = false;
|
||||
|
||||
constructor(private error: JsonApiError | DOMException | KubeJsonApiError, private messages: string[]) {
|
||||
}
|
||||
|
||||
get isAborted() {
|
||||
return this.error.code === DOMException.ABORT_ERR;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.messages.join("\n");
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { JsonApiError } from "./json-api";
|
||||
import { JsonApi } from "./json-api";
|
||||
import type { JsonApiError } from "@k8slens/json-api";
|
||||
import { JsonApi } from "@k8slens/json-api";
|
||||
import type { Response } from "@k8slens/node-fetch";
|
||||
import type { KubeJsonApiData } from "@k8slens/kube-object";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { JsonApiErrorParsed } from "../k8s-api/json-api";
|
||||
import { JsonApiErrorParsed } from "@k8slens/json-api";
|
||||
|
||||
export const getErrorMessage = (error: unknown): string => {
|
||||
if (typeof error === "string") {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { loggerInjectionToken } from "@k8slens/logger";
|
||||
import maybeKubeApiInjectable from "../../common/k8s-api/maybe-kube-api.injectable";
|
||||
import { DeploymentApi as InternalDeploymentApi, IngressApi as InternalIngressApi, NodeApi, PersistentVolumeClaimApi, PodApi } from "../../common/k8s-api/endpoints";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../../common/k8s-api/stores-apis-can-be-created.token";
|
||||
import type { JsonApiConfig } from "../../common/k8s-api/json-api";
|
||||
import type { JsonApiConfig } from "@k8slens/json-api";
|
||||
import type { KubeJsonApi as InternalKubeJsonApi } from "../../common/k8s-api/kube-json-api";
|
||||
import createKubeJsonApiInjectable from "../../common/k8s-api/create-kube-json-api.injectable";
|
||||
import type { RequestInit } from "@k8slens/node-fetch";
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { ApiKubeGet } from "../../../renderer/k8s/api-kube-get.injectable";
|
||||
import apiKubePatchInjectable from "../../../renderer/k8s/api-kube-patch.injectable";
|
||||
import apiKubeGetInjectable from "../../../renderer/k8s/api-kube-get.injectable";
|
||||
import type { BaseKubeJsonApiObjectMetadata, KubeObjectScope, KubeJsonApiData } from "@k8slens/kube-object";
|
||||
import { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
|
||||
import { JsonApiErrorParsed } from "@k8slens/json-api";
|
||||
import type { ShowNotification } from "../../../renderer/components/notifications";
|
||||
import React from "react";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import type React from "react";
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import type { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
|
||||
import type { JsonApiErrorParsed } from "@k8slens/json-api";
|
||||
import type { SetRequired } from "type-fest";
|
||||
import autoBind from "auto-bind";
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import "./notifications.scss";
|
||||
import React from "react";
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
|
||||
import { JsonApiErrorParsed } from "@k8slens/json-api";
|
||||
import type { Disposer } from "@k8slens/utilities";
|
||||
import { cssNames, prevDefault } from "@k8slens/utilities";
|
||||
import type { CreateNotificationOptions, Notification, NotificationMessage, NotificationsStore } from "./notifications.store";
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
|
||||
import { JsonApiErrorParsed } from "@k8slens/json-api";
|
||||
import { loggerInjectionToken } from "@k8slens/logger";
|
||||
import type { Disposer } from "@k8slens/utilities";
|
||||
import type { CreateNotificationOptions } from "./notifications.store";
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ForwardedPort } from "../port-forward-item";
|
||||
import { PortForwardItem } from "../port-forward-item";
|
||||
import { waitUntilFree } from "tcp-port-used";
|
||||
import type { Logger } from "@k8slens/logger";
|
||||
import type { JsonApi } from "../../../common/k8s-api/json-api";
|
||||
import type { JsonApi } from "@k8slens/json-api";
|
||||
import type { RequestActivePortForward } from "./request-active-port-forward.injectable";
|
||||
import autoBind from "auto-bind";
|
||||
|
||||
|
||||
Reference in new issue
Block a user