@vertesia/api-fetch-client 0.42.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/lib/cjs/base.js +190 -0
  4. package/lib/cjs/base.js.map +1 -0
  5. package/lib/cjs/client.js +107 -0
  6. package/lib/cjs/client.js.map +1 -0
  7. package/lib/cjs/errors.js +57 -0
  8. package/lib/cjs/errors.js.map +1 -0
  9. package/lib/cjs/index.js +21 -0
  10. package/lib/cjs/index.js.map +1 -0
  11. package/lib/cjs/package.json +3 -0
  12. package/lib/cjs/sse/EventSourceParserStream.js +41 -0
  13. package/lib/cjs/sse/EventSourceParserStream.js.map +1 -0
  14. package/lib/cjs/sse/TextDecoderStream.js +51 -0
  15. package/lib/cjs/sse/TextDecoderStream.js.map +1 -0
  16. package/lib/cjs/sse/index.js +27 -0
  17. package/lib/cjs/sse/index.js.map +1 -0
  18. package/lib/cjs/utils.js +38 -0
  19. package/lib/cjs/utils.js.map +1 -0
  20. package/lib/esm/base.js +185 -0
  21. package/lib/esm/base.js.map +1 -0
  22. package/lib/esm/client.js +101 -0
  23. package/lib/esm/client.js.map +1 -0
  24. package/lib/esm/errors.js +51 -0
  25. package/lib/esm/errors.js.map +1 -0
  26. package/lib/esm/index.js +5 -0
  27. package/lib/esm/index.js.map +1 -0
  28. package/lib/esm/sse/EventSourceParserStream.js +37 -0
  29. package/lib/esm/sse/EventSourceParserStream.js.map +1 -0
  30. package/lib/esm/sse/TextDecoderStream.js +49 -0
  31. package/lib/esm/sse/TextDecoderStream.js.map +1 -0
  32. package/lib/esm/sse/index.js +24 -0
  33. package/lib/esm/sse/index.js.map +1 -0
  34. package/lib/esm/utils.js +33 -0
  35. package/lib/esm/utils.js.map +1 -0
  36. package/lib/tsconfig.tsbuildinfo +1 -0
  37. package/lib/types/base.d.ts +67 -0
  38. package/lib/types/base.d.ts.map +1 -0
  39. package/lib/types/client.d.ts +36 -0
  40. package/lib/types/client.d.ts.map +1 -0
  41. package/lib/types/errors.d.ts +18 -0
  42. package/lib/types/errors.d.ts.map +1 -0
  43. package/lib/types/index.d.ts +5 -0
  44. package/lib/types/index.d.ts.map +1 -0
  45. package/lib/types/sse/EventSourceParserStream.d.ts +23 -0
  46. package/lib/types/sse/EventSourceParserStream.d.ts.map +1 -0
  47. package/lib/types/sse/TextDecoderStream.d.ts +8 -0
  48. package/lib/types/sse/TextDecoderStream.d.ts.map +1 -0
  49. package/lib/types/sse/index.d.ts +13 -0
  50. package/lib/types/sse/index.d.ts.map +1 -0
  51. package/lib/types/utils.d.ts +4 -0
  52. package/lib/types/utils.d.ts.map +1 -0
  53. package/package.json +61 -0
  54. package/src/base.ts +225 -0
  55. package/src/client.ts +126 -0
  56. package/src/errors.ts +61 -0
  57. package/src/index.ts +4 -0
  58. package/src/sse/EventSourceParserStream.ts +39 -0
  59. package/src/sse/TextDecoderStream.ts +62 -0
  60. package/src/sse/index.ts +27 -0
  61. package/src/utils.ts +31 -0
package/src/client.ts ADDED
@@ -0,0 +1,126 @@
1
+ import { ClientBase, FETCH_FN, IRequestParamsWithPayload } from "./base.js";
2
+ import { RequestError } from "./errors.js";
3
+
4
+ function isAuthorizationHeaderSet(headers: HeadersInit | undefined): boolean {
5
+ if (!headers) return false;
6
+ return "authorization" in headers;
7
+ }
8
+
9
+ export class AbstractFetchClient<T extends AbstractFetchClient<T>> extends ClientBase {
10
+
11
+ headers: Record<string, string>;
12
+ _auth?: () => Promise<string>;
13
+ // callbacks usefull to log requests and responses
14
+ onRequest?: (req: Request) => void;
15
+ onResponse?: (res: Response, req: Request) => void;
16
+ // the last response. Can be used to inspect the response headers
17
+ response?: Response;
18
+
19
+ constructor(baseUrl: string, fetchImpl?: FETCH_FN | Promise<FETCH_FN>) {
20
+ super(baseUrl, fetchImpl);
21
+ this.baseUrl = baseUrl[baseUrl.length - 1] === '/' ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
22
+ this.headers = this.initialHeaders;
23
+ }
24
+
25
+ get initialHeaders() {
26
+ return { accept: 'application/json' };
27
+ }
28
+
29
+ /**
30
+ * Install an auth callback. If the callback is undefined or null then remove the auth callback.
31
+ * @param authCb a fucntion returning a promise that resolves to the value to use for the authorization header
32
+ * @returns the client instance
33
+ */
34
+ withAuthCallback(authCb?: (() => Promise<string>) | null) {
35
+ this._auth = authCb || undefined;
36
+ return this;
37
+ }
38
+
39
+ withErrorFactory(factory: (err: RequestError) => Error) {
40
+ this.errorFactory = factory;
41
+ return this as unknown as T;
42
+ }
43
+
44
+ withLang(locale: string | undefined | null) {
45
+ if (locale) {
46
+ this.headers['accept-language'] = locale;
47
+ } else {
48
+ delete this.headers['accept-language'];
49
+ }
50
+ return this as unknown as T;
51
+ }
52
+
53
+ withHeaders(headers: Record<string, string>) {
54
+ const thisHeaders = this.headers;
55
+ for (const key in headers) {
56
+ const value = headers[key];
57
+ if (value != null) {
58
+ thisHeaders[key.toLowerCase()] = value;
59
+ }
60
+ }
61
+ return this as unknown as T;
62
+ }
63
+
64
+ setHeader(key: string, value: string | undefined) {
65
+ if (!value) {
66
+ delete this.headers[key.toLowerCase()];
67
+ } else {
68
+ this.headers[key.toLowerCase()] = value;
69
+ }
70
+ }
71
+
72
+ async createRequest(url: string, init: RequestInit) {
73
+ if (this._auth && !isAuthorizationHeaderSet(init.headers)) {
74
+ const headers = (init.headers ? init.headers : {}) as Record<string, string>;
75
+ init.headers = headers;
76
+ const auth = await this._auth();
77
+ if (auth) {
78
+ init.headers["authorization"] = auth;
79
+ }
80
+ }
81
+ this.response = undefined;
82
+ const request = await super.createRequest(url, init);
83
+ this.onRequest && this.onRequest(request);
84
+ return request;
85
+ }
86
+
87
+ async handleResponse(req: Request, res: Response, params: IRequestParamsWithPayload | undefined): Promise<any> {
88
+ this.response = res; // store last repsonse
89
+ this.onResponse && this.onResponse(res, req);
90
+ return super.handleResponse(req, res, params);
91
+ }
92
+
93
+ }
94
+
95
+ export class FetchClient extends AbstractFetchClient<FetchClient> {
96
+
97
+ constructor(baseUrl: string, fetchImpl?: FETCH_FN | Promise<FETCH_FN>) {
98
+ super(baseUrl, fetchImpl);
99
+ }
100
+
101
+ }
102
+
103
+ export abstract class ApiTopic extends ClientBase {
104
+
105
+ constructor(public client: ClientBase, basePath: string) {
106
+ //TODO we should refactor the way ClientBase and ApiTopic is created
107
+ // to avoid cloning all customizations
108
+ super(client.getUrl(basePath), client._fetch);
109
+ this.createServerError = client.createServerError
110
+ this.errorFactory = client.errorFactory;
111
+ this.verboseErrors = client.verboseErrors;
112
+ }
113
+
114
+ createRequest(url: string, init: RequestInit): Promise<Request> {
115
+ return this.client.createRequest(url, init);
116
+ }
117
+
118
+ handleResponse(req: Request, res: Response, params: IRequestParamsWithPayload | undefined): Promise<any> {
119
+ return this.client.handleResponse(req, res, params);
120
+ }
121
+
122
+ get headers() {
123
+ return this.client.headers;
124
+ }
125
+
126
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,61 @@
1
+
2
+ function createMessage(message: string, request: Request, status: number, payload: any, displayDetails: boolean) {
3
+ let msg = message;
4
+ if (displayDetails) {
5
+ msg += '\nRequest: ' + request.method + ' ' + request.url + ' => ' + status;
6
+ const details = payload?.details || payload?.error?.details;
7
+ if (details) {
8
+ const detailsType = typeof details;
9
+ if (detailsType === 'string') {
10
+ msg += '\nDetails: ' + details;
11
+ } else if (detailsType === "object") {
12
+ msg += '\nDetails: ' + JSON.stringify(details, undefined, 2);
13
+ }
14
+ }
15
+ msg += '\nStack Trace: ';
16
+ }
17
+ return msg;
18
+ }
19
+
20
+ export class RequestError extends Error {
21
+ status: number;
22
+ payload: any;
23
+ request: Request;
24
+ request_info: string;
25
+ displayDetails: boolean;
26
+ original_message: string;
27
+ constructor(message: string, request: Request, status: number, payload: any, displayDetails = true) {
28
+ super(createMessage(message, request, status, payload, displayDetails));
29
+ this.original_message = message;
30
+ this.request = request;
31
+ this.status = status;
32
+ this.payload = payload;
33
+ this.request_info = request.method + ' ' + request.url + ' => ' + status;
34
+ this.displayDetails = displayDetails;
35
+ }
36
+
37
+ get details() {
38
+ return this.payload?.details || this.payload?.error?.details;
39
+ }
40
+
41
+ }
42
+
43
+ export class ServerError extends RequestError {
44
+ constructor(message: string, req: Request, status: number, payload: any, displayDetails = true) {
45
+ super(message, req, status, payload, displayDetails);
46
+ }
47
+
48
+ updateDetails(details: any) {
49
+ if (details !== this.details) {
50
+ return new ServerError(this.original_message, this.request, this.status, { ...this.payload, details }, this.displayDetails)
51
+ } else {
52
+ return this;
53
+ }
54
+ }
55
+ }
56
+
57
+ export class ConnectionError extends RequestError {
58
+ constructor(req: Request, err: Error) {
59
+ super("Failed to connect to server: " + err.message, req, 0, err);
60
+ }
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./base.js";
2
+ export * from "./client.js";
3
+ export * from "./errors.js";
4
+ export * from "./sse/index.js";
@@ -0,0 +1,39 @@
1
+ import { createParser, ReconnectInterval, type EventSourceParser, type ParsedEvent } from 'eventsource-parser'
2
+
3
+ /**
4
+ * We copied this file from the eventsource-parser/stream package and made it a part of our project.
5
+ * because importing the eventsource-parser/stream breaks tsc build when buuilding the commonjs version
6
+ * see for a similar error:
7
+ * https://stackoverflow.com/questions/77280140/why-typescript-dont-see-exports-of-package-with-module-commonjs-and-moduleres
8
+ */
9
+
10
+ /**
11
+ * A TransformStream that ingests a stream of strings and produces a stream of ParsedEvents.
12
+ *
13
+ * @example
14
+ * ```
15
+ * const eventStream =
16
+ * response.body
17
+ * .pipeThrough(new TextDecoderStream())
18
+ * .pipeThrough(new EventSourceParserStream())
19
+ * ```
20
+ * @public
21
+ */
22
+ export class EventSourceParserStream extends TransformStream<string, ParsedEvent> {
23
+ constructor() {
24
+ let parser!: EventSourceParser
25
+
26
+ super({
27
+ start(controller) {
28
+ parser = createParser((event: ParsedEvent | ReconnectInterval) => {
29
+ if (event.type === 'event') {
30
+ controller.enqueue(event)
31
+ }
32
+ })
33
+ },
34
+ transform(chunk) {
35
+ parser.feed(chunk)
36
+ },
37
+ })
38
+ }
39
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Decode a stream of bytes into a stream of characters.
3
+ * Some javascript env like Bun.js doesn't supports the TextDecoderStream (as for jan 2024)
4
+ * This is a polyfill for bunjs
5
+ */
6
+ let _TextDecoderStream: typeof TextDecoderStream;
7
+ if (globalThis.TextDecoderStream && typeof globalThis.TextDecoderStream === 'function') {
8
+ _TextDecoderStream = globalThis.TextDecoderStream;
9
+ } else {
10
+ class MyTextDecoderStream extends TransformStream<ArrayBuffer | Uint8Array, string> {
11
+ private _options: {
12
+ encoding: string,
13
+ fatal?: boolean,
14
+ ignoreBOM?: boolean
15
+ }
16
+ constructor(encoding = "utf-8", { fatal = false, ignoreBOM = false }: {
17
+ fatal?: boolean,
18
+ ignoreBOM?: boolean
19
+ } = {}) {
20
+ super(new TextDecodeTransformer(new TextDecoder(encoding, { fatal, ignoreBOM })));
21
+ this._options = { fatal, ignoreBOM, encoding };
22
+ }
23
+
24
+ get encoding() {
25
+ return this._options.encoding;
26
+ }
27
+ get fatal() {
28
+ return this._options.fatal;
29
+ }
30
+ get ignoreBOM() {
31
+ return this._options.ignoreBOM;
32
+ }
33
+ }
34
+ class TextDecodeTransformer implements Transformer<ArrayBuffer | Uint8Array, string> {
35
+ private decoder: TextDecoder;
36
+
37
+ constructor(decoder: TextDecoder) {
38
+ this.decoder = decoder;
39
+ }
40
+
41
+ transform(chunk: ArrayBuffer | Uint8Array, controller: TransformStreamDefaultController<string>) {
42
+ if (!(chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk))) {
43
+ throw new TypeError("Input must be a compatible with: ArrayBuffer | Uint8Array");
44
+ }
45
+ const text = this.decoder.decode(chunk, { stream: true });
46
+ if (text.length !== 0) {
47
+ controller.enqueue(text);
48
+ }
49
+ }
50
+
51
+ flush(controller: TransformStreamDefaultController<string>) {
52
+ const text = this.decoder.decode();
53
+ if (text.length !== 0) {
54
+ controller.enqueue(text);
55
+ }
56
+ }
57
+ }
58
+ _TextDecoderStream = MyTextDecoderStream as any;
59
+ }
60
+
61
+ export { _TextDecoderStream as TextDecoderStream };
62
+
@@ -0,0 +1,27 @@
1
+ import { TextDecoderStream } from "./TextDecoderStream.js";
2
+ import { EventSourceParserStream } from "./EventSourceParserStream.js";
3
+ import { ParsedEvent, ReconnectInterval } from "eventsource-parser";
4
+
5
+ export type ServerSentEvent = ParsedEvent | ReconnectInterval;
6
+ /**
7
+ * A SSE response reader.
8
+ * Usage client.get('/path', {reader: sse}) or client.post('/path', {reader: sse})
9
+ * where sse is this function
10
+ * @param response
11
+ * @returns
12
+ */
13
+ export async function sse(response: Response): Promise<ReadableStream<ServerSentEvent>> {
14
+ if (!response.ok) {
15
+ const text = await response.text();
16
+ const error = new Error("SSE error: " + response.status + ". Content:\n" + text);
17
+ (error as any).status = response.status;
18
+ throw error;
19
+ }
20
+ if (!response.body) {
21
+ throw new Error('No body in response');
22
+ }
23
+ return response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream());
24
+ }
25
+
26
+ // re-export TextDecoderStream (in case it was polyfilled)
27
+ export { TextDecoderStream }
package/src/utils.ts ADDED
@@ -0,0 +1,31 @@
1
+
2
+ export function buildQueryString(query: any) {
3
+ const parts = [];
4
+ for (const key of Object.keys(query)) {
5
+ const val = query[key];
6
+ if (val != null) {
7
+ parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(val)));
8
+ }
9
+ }
10
+ return parts.join("&");
11
+ }
12
+
13
+ export function join(left: string, right: string) {
14
+ if (left.endsWith('/')) {
15
+ if (right.startsWith('/')) {
16
+ return left + right.substring(1);
17
+ } else {
18
+ return left + right;
19
+ }
20
+ } else if (right.startsWith('/')) {
21
+ return left + right;
22
+ } else {
23
+ return left + '/' + right;
24
+ }
25
+ }
26
+ export function removeTrailingSlash(path: string) {
27
+ if (path[path.length - 1] === '/') {
28
+ return path.slice(0, -1);
29
+ }
30
+ return path;
31
+ }