pocketbase / js-sdk

PocketBase JavaScript SDK
https://www.npmjs.com/package/pocketbase
MIT License
2.17k stars 127 forks source link

Custom Client implementation #277

Closed sicciaman closed 10 months ago

sicciaman commented 10 months ago

Hello! I discovered the possibility of have a custom fetch function implementation, but this is only possible on GetList/GetOne etc. methods. I was trying to apply a more generical approach so I was creating my own instance of Client overriding send method.

import { HttpClient } from '@angular/common/http';
import Client, { ClientResponseError, SendOptions } from 'pocketbase';
import { firstValueFrom } from 'rxjs';

export class CustomClient extends Client {

    constructor(private http: HttpClient) {
        super();
    }

    override async send<T = any>(path: string, options: SendOptions): Promise<T> {
        options = this.initSendOptions(path, options);

        // build url + path
        let url = this.buildUrl(path);

        if (this.beforeSend) {
            const result = Object.assign({}, await this.beforeSend(url, options));
            if (typeof result.url !== 'undefined' || typeof result.options !== 'undefined') {
                url = result.url || url;
                options = result.options || options;
            } else if (Object.keys(result).length) {
                // legacy behavior
                options = result as SendOptions;
                console?.warn && console.warn('Deprecated format of beforeSend return: please use `return { url, options }`, instead of `return options`.');
            }
        }

        // serialize the query parameters
        if (typeof options.query !== 'undefined') {
            const query = this.serializeQueryParams(options.query)
            if (query) {
                url += (url.includes('?') ? '&' : '?') + query;
            }
            delete options.query;
        }

        // ensures that the json body is serialized
        if (
            this.getHeader(options.headers, 'Content-Type') == 'application/json' &&
            options.body && typeof options.body !== 'string'
        ) {
            options.body = JSON.stringify(options.body);
        }

        const method = options.method || 'GET';

        // send the request
        return firstValueFrom(this.http.request<Response>(method, url))
            .then(async (response) => {
                let data: any = {};

                try {
                    data = await response.json();
                } catch (_) {
                    // all api responses are expected to return json
                    // with the exception of the realtime event and 204
                }

                if (this.afterSend) {
                    data = await this.afterSend(response, data);
                }

                if (response.status >= 400) {
                    throw new ClientResponseError({
                        url: response.url,
                        status: response.status,
                        data: data,
                    });
                }

                return data as T;
            }).catch((err) => {
                // wrap to normalize all errors
                throw new ClientResponseError(err);
            });
    }
}

But the problem is that some functions used internally are private inside Client class so I can't use it. Do you have any idea on how I could resolve this? Is there a better solution?

Thanks

ganigeorgiev commented 10 months ago

You can provide a custom fetch implementation for any request method, not just getOne/getLits. For more details, please refer to https://github.com/pocketbase/js-sdk?tab=readme-ov-file#services (the optional SendOptions argument is usually the last one).

If you want to specify a custom fetch implementation globally for the client you can use the beforeSend hook and assign a new function to options.fetch.

sicciaman commented 10 months ago

Thank you so much! I really appreciate your work, it's awesome!

I'll attach here my custom implementation that uses Angular HttpClient so requests could be catched by any Interceptor in the app.

import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import PocketBase, { BeforeSendResult, SendOptions } from 'pocketbase';
import { lastValueFrom, map } from 'rxjs';
import { environment } from 'src/environments/environment';

@Injectable({ providedIn: 'root' })
export class PocketbaseClient {
    private readonly httpClient = inject(HttpClient);
    private serverUrl: string;
    client: PocketBase;

    constructor() {
        const { serverUrl } = environment.pocketBaseConfig;
        this.serverUrl = `${serverUrl}`;
        this.client = new PocketBase(this.serverUrl);
        this.client.autoCancellation(false);

        this.client.beforeSend = this.fetchToHttpClient; (url: string, options: SendOptions) => this.fetchToHttpClient(url, options);
    }

    private fetchToHttpClient = (url: string, options: SendOptions): BeforeSendResult => {
        options.fetch = (url: RequestInfo | URL, config: RequestInit | undefined) => {
            const method = config?.method || 'GET';
            return lastValueFrom(this.httpClient.request(method, url.toString(),
                { ...options, observe: 'response', responseType: 'arraybuffer' })
                .pipe(
                    map(({ body, headers, status, statusText }: HttpResponse<ArrayBuffer>) => {
                        const responseHeaders = this.convertHttpHeadersToHeaders(headers);
                        return new Response(body, { headers: responseHeaders, status, statusText });
                    })
                ));
        };

        return { url, options };
    }

    private convertHttpHeadersToHeaders(httpHeaders: HttpHeaders): Headers {
        const headerMap: Record<string, string> = {};

        httpHeaders.keys().forEach(key => {
            headerMap[key] = httpHeaders.get(key)!;
        });

        return new Headers(headerMap);
    }
}