UNPKG

baqend

Version:

Baqend JavaScript SDK

554 lines (469 loc) 14.6 kB
// eslint-disable-next-line max-classes-per-file import type { Request, RequestBody, RequestBodyType, Response, ResponseBodyType, } from './Connector'; import { CommunicationError } from '../error'; import { Acl } from '../Acl'; import { TokenStorage } from '../intersection/TokenStorage'; export type RestSpecification = { method: string; status: number[]; path: string; }; export type MessageSpec = { status: number[]; dynamic: boolean; method: string; path: string[]; query: string[]; }; /** * The progress callback is called, when you send a message to the server and a progress is noticed * @param event The Progress Event * @return unused */ export type ProgressListener = (event: ProgressEvent) => any; /** * Checks whether the user uses a browser which does support revalidation. */ // only chromium based browsers are supporting cache revalidations with the cache-control: no-cache directive // @ts-ignore export const REVALIDATION_SUPPORTED = typeof navigator === 'undefined' || navigator.userAgentData?.brands?.some(data => data.brand === 'Chromium'); // webkit does not support cache replacement https://stackoverflow.com/questions/32571769/cache-control-no-cache-in-request-header-response-does-not-replace-previously-c // @ts-ignore export const CACHE_REPLACEMENT_SUPPORTED = typeof navigator === 'undefined' || REVALIDATION_SUPPORTED || /firefox/i.test(navigator.userAgent); export const StatusCode = { NOT_MODIFIED: 304, BAD_CREDENTIALS: 460, BUCKET_NOT_FOUND: 461, INVALID_PERMISSION_MODIFICATION: 462, INVALID_TYPE_VALUE: 463, FORBIDDEN: 403, OBJECT_NOT_FOUND: 404, OBJECT_OUT_OF_DATE: 412, PERMISSION_DENIED: 466, QUERY_DISPOSED: 467, QUERY_NOT_SUPPORTED: 468, SCHEMA_NOT_COMPATIBLE: 469, SCHEMA_STILL_EXISTS: 470, SYNTAX_ERROR: 471, TYPE_ALREADY_EXISTS: 473, TYPE_STILL_REFERENCED: 474, SCRIPT_ABORTION: 475, }; /** * Appends the given query parameters to the url * @param url - on which the parameters will be appended * @param queryParams - The Query parameters which should be appended * @return The URL with the appended parameters */ export function appendQueryParams(url: string, queryParams: string | { [key: string]: string | undefined }) { const queryString = typeof queryParams === 'string' ? queryParams : Object.entries(queryParams) .filter(([, value]) => value !== undefined) .map(([key, value]) => `${key}=${encodeURIComponent(value as string)}`) .join('&'); if (!queryString) { return url; } const sep = url.indexOf('?') >= 0 ? '&' : '?'; return url + sep + queryString; } export abstract class Message { static readonly StatusCode = StatusCode; static readonly BINARY = { blob: true, buffer: true, stream: true, arraybuffer: true, 'data-url': true, base64: true, }; public withCredentials: boolean = false; public progressCallback: null | ProgressListener = null; public request: Request; private _tokenStorage: TokenStorage | null = null; private _responseType: ResponseBodyType | null = null; /** * Returns the specification of this message */ public get spec(): MessageSpec { return null as any; } /** * Creates a new message class with the given message specification * @return A created message object for the specification */ static create<T>(specification: RestSpecification): T { const parts = specification.path.split('?'); const path = parts[0].split(/[:*]\w*/); const query: string[] = []; if (parts[1]) { parts[1].split('&').forEach((arg) => { const part = arg.split('='); query.push(part[0]); }); } const spec: MessageSpec = { path, query, status: specification.status, method: specification.method, dynamic: specification.path.indexOf('*') !== -1, }; return class extends Message { get spec() { return spec; } } as any as T; } get isBinary() { return (this.request.type && this.request.type in Message.BINARY) || this._responseType!! in Message.BINARY; } /** * @param args The path arguments */ constructor(...args: string[]) { let index = 0; let path = this.spec.path[0]; const len = this.spec.path.length; for (let i = 1; i < len; i += 1) { if (this.spec.dynamic && len - 1 === i) { path += args[index].split('/').map(encodeURIComponent).join('/'); } else { path += encodeURIComponent(args[index]) + this.spec.path[i]; } index += 1; } const queryParams: { [key: string]: string } = {}; for (let i = 0; i < this.spec.query.length; i += 1) { const arg = args[index]; index += 1; if (arg !== undefined && arg !== null) { queryParams[this.spec.query[i]] = arg; } } this.request = { method: this.spec.method, path: appendQueryParams(path, queryParams), entity: null, headers: {}, }; if (args[index]) { this.entity(args[index], 'json'); } this.responseType('json'); } /** * Gets the tokenStorage which stored credentials are used to authorize this message * @return The header value */ tokenStorage(): TokenStorage | null; /** * Sets the tokenStorage which stored credentials are used to authorize this message * @param value The new tokenStorage used to authorize this message * @return This message object */ tokenStorage(value: TokenStorage | null): this; tokenStorage(value?: TokenStorage | null): this | TokenStorage | null { if (value === undefined) { return this._tokenStorage; } this._tokenStorage = value; return this; } /** * Gets the request path * @return The path of the message value */ path(): string; /** * Sets the request path * @param path The new path value, any query parameters provided with the path will be merged with the * exiting query params * @return This message object */ path(path: string): this; path(path?: string): this | string { if (path !== undefined) { const queryIndex = this.request.path.indexOf('?') + 1; this.request.path = path + (queryIndex > 0 ? (path.indexOf('?') > -1 ? '&' : '?') + this.request.path.substring(queryIndex) : ''); return this; } return this.request.path; } /** * Gets the value of a the specified request header * @param name The header name * @return The header value */ header(name: string): string; /** * Sets the value of a the specified request header * @param name The header name * @param value The header value if omitted the value will be returned * @return This message object */ header(name: string, value: string | null): this; header(name: string, value?: string | null): this | string; header(name: string, value?: string | null): this | string { if (value === null) { delete this.request.headers[name]; return this; } if (value !== undefined) { this.request.headers[name] = value; return this; } return this.request.headers[name]; } /** * Sets the entity type * @param data - The data to send * @param type - the type of the data one of 'json'|'text'|'blob'|'arraybuffer' * defaults detect the type based on the body data * @return This message object */ entity(data: RequestBody, type?: RequestBodyType): this { let requestType = type; if (!requestType) { if (typeof data === 'string') { if (/^data:(.+?)(;base64)?,.*$/.test(data)) { requestType = 'data-url'; } else { requestType = 'text'; } } else if (typeof Blob !== 'undefined' && data instanceof Blob) { requestType = 'blob'; } else if (typeof Buffer !== 'undefined' && data instanceof Buffer) { requestType = 'buffer'; } else if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer) { requestType = 'arraybuffer'; } else if (typeof FormData !== 'undefined' && data instanceof FormData) { requestType = 'form'; } else { requestType = 'json'; } } this.request.type = requestType; this.request.entity = data; return this; } /** * Get the mimeType * @return This message object */ mimeType(): string; /** * Sets the mimeType * @param mimeType the mimeType of the data * @return This message object */ mimeType(mimeType: string | null): this; mimeType(mimeType?: string | null): this | string { return this.header('content-type', mimeType); } /** * Gets the contentLength * @return */ contentLength(): number; /** * Sets the contentLength * @param contentLength the content length of the data * @return This message object */ contentLength(contentLength: number): this; contentLength(contentLength?: number): this | number { if (contentLength !== undefined) { return this.header('content-length', `${contentLength}`); } return Number(this.header('content-length')); } /** * Gets the request conditional If-Match header * @return This message object */ ifMatch(): string; /** * Sets the request conditional If-Match header * @param eTag the If-Match ETag value * @return This message object */ ifMatch(eTag: string | number | null): this; ifMatch(eTag?: string | number | null): this | string { return this.header('If-Match', this.formatETag(eTag)); } /** * Gets the request a ETag based conditional header * @return */ ifNoneMatch(): string; /** * Sets the request a ETag based conditional header * @param eTag The ETag value * @return This message object */ ifNoneMatch(eTag: string): this; ifNoneMatch(eTag?: string): this | string { return this.header('If-None-Match', this.formatETag(eTag)); } /** * Gets the request date based conditional header * @return */ ifUnmodifiedSince(): string; /** * Sets the request date based conditional header * @param date The date value * @return This message object */ ifUnmodifiedSince(date: Date): this; ifUnmodifiedSince(date?: Date): this | string { // IE 10 returns UTC strings and not an RFC-1123 GMT date string return this.header('if-unmodified-since', date && date.toUTCString().replace('UTC', 'GMT')); } /** * Indicates that the request should not be served by a local cache * @return */ noCache(): this { if (!REVALIDATION_SUPPORTED) { this.ifMatch('') // is needed for firefox or safari (but forbidden for chrome) .ifNoneMatch('-'); // is needed for edge and ie (but forbidden for chrome) } return this.cacheControl('max-age=0, no-cache'); } /** * Gets the cache control header * @return */ cacheControl(): string; /** * Sets the cache control header * @param value The cache control flags * @return This message object */ cacheControl(value: string): this; cacheControl(value?: string): this | string { return this.header('cache-control', value); } /** * Gets the ACL of a file into the Baqend-Acl header * @return */ acl(): string; /** * Sets and encodes the ACL of a file into the Baqend-Acl header * @param acl the file ACLs * @return This message object */ acl(acl: Acl): this; acl(acl?: Acl): this | string { return this.header('baqend-acl', acl && JSON.stringify(acl)); } /** * Gets and encodes the custom headers of a file into the Baqend-Custom-Headers header * @return */ customHeaders(): string; /** * Sets and encodes the custom headers of a file into the Baqend-Custom-Headers header * @param customHeaders the file custom headers * @return This message object */ customHeaders(customHeaders: { [headers: string]: string }): this; customHeaders(customHeaders?: { [headers: string]: string }): this | string { return this.header('baqend-custom-headers', customHeaders && JSON.stringify(customHeaders)); } /** * Gets the request accept header * @return */ accept(): string; /** * Sets the request accept header * @param accept the accept header value * @return This message object */ accept(accept: string): this; accept(accept?: string): this | string { return this.header('accept', accept); } /** * Gets the response type which should be returned * @return This message object */ responseType(): ResponseBodyType | null; /** * Sets the response type which should be returned * @param type The response type one of 'json'|'text'|'blob'|'arraybuffer' defaults to 'json' * @return This message object */ responseType(type: ResponseBodyType | null): this; responseType(type?: ResponseBodyType | null): this | ResponseBodyType | null { if (type !== undefined) { this._responseType = type; return this; } return this._responseType; } /** * Gets the progress callback * @return The callback set */ progress(): ProgressListener | null; /** * Sets the progress callback * @param callback * @return This message object */ progress(callback: ProgressListener | null): this; progress(callback?: ProgressListener | null): this | ProgressListener | null { if (callback !== undefined) { this.progressCallback = callback; return this; } return this.progressCallback; } /** * Adds the given string to the request path * * If the parameter is an object, it will be serialized as a query string. * * @param query which will added to the request path * @return */ addQueryString(query: string | { [key: string]: string }): this { this.request.path = appendQueryParams(this.request.path, query); return this; } formatETag(eTag?: string | number | null): string | undefined | null { if (eTag === null || eTag === undefined || eTag === '*') { return eTag; } let tag = `${eTag}`; if (tag.indexOf('"') === -1) { tag = `"${tag}"`; } return tag; } /** * Handle the receive * @param response The received response headers and data * @return */ doReceive(response: Response) { if (this.spec.status.indexOf(response.status) === -1) { throw new CommunicationError(this, response); } } } export class OAuthMessage extends Message { get spec() { return { method: 'OAUTH', dynamic: false, path: [''], query: [], status: [200], }; } }