UNPKG

@granito/ngx-hal-client

Version:

A HAL client to be used in Angular projects

534 lines (522 loc) 17 kB
import { map, catchError, throwError, Observable, pipe, takeUntil, switchMap, of, filter } from 'rxjs'; import * as URI from 'uri-template'; import * as i0 from '@angular/core'; import { Injectable } from '@angular/core'; import * as i1 from '@angular/common/http'; /** * This class represents errors thrown by _Angular HAL Client_. It * unifies errors produced by _Angulat HTTP Client_ and errors originating * in the API. */ class HalError extends Error { /** * @param err the object used to set properties */ constructor(err) { super(); Object.assign(this, err); this.name = 'HalError'; } } const self = 'self'; /** * The common base for resource and accessor. */ class HalBase { /** * The URI of `self` link. */ get self() { return this._links[self]?.href; } /** * This property is `true` when either `methods` array does not exist * in the `self` link or the array exists and contains `POST` string. * It is `false` otherwise. */ get canCreate() { return this.can('POST'); } /** * This property is `true` when either `methods` array does not exist * in the `self` link or the array exists and contains `GET` string. * It is `false` otherwise. */ get canRead() { return this.can('GET'); } /** * This property is `true` when either `methods` array does not exist * in the `self` link or the array exists and contains `DELETE` * string. It is `false` otherwise. */ get canDelete() { return this.can('DELETE'); } /** * @param obj the object used to assign the properties */ constructor(obj) { this._links = {}; this._embedded = {}; Object.assign(this, obj); } /** * Create a new resource in the collection identified by `self` link. * It makes a `POST` call to the URI in `self` link and returns an * observable for the call. The observable normally emits an accessor * for the newly created resource or `undefined` if `Location` header * was not returned by the call. * * @param obj the payload for the `POST` method call * @returns an observable of the resource's accessor */ create(obj) { return this.withSelf(self => this._client.post(self, objectFrom(obj), { observe: 'response' }).pipe(map(response => response.headers.get('Location') || undefined), map(location => !location ? undefined : this.accessor(location)), catchError(this.handleError))); } /** * Delete the resource identified by `self` link. * * @returns an observable that emits next signal on successful delete */ delete() { return this.withSelf(self => this._client.delete(self).pipe(map(() => undefined), catchError(this.handleError))); } withSelf(func) { const self = this.self; if (!self) return throwError(() => new HalError({ message: 'no valid "self" relation' })); return func(self); } accessor(href, methods) { return this.instanceOf(Accessor, { _links: { self: { href, methods } } }); } instanceOf(type, obj) { return new type({ ...obj, _client: this._client }); } handleError(err) { if (err.error instanceof Event) return throwError(() => new HalError({ message: err.message, path: err.url })); return throwError(() => new HalError({ message: err.message, status: err.status, error: err.statusText, path: err.url, ...err.error })); } can(method) { const methods = this._links[self]?.methods; if (!Array.isArray(methods)) return true; return !!methods.find(m => typeof m === 'string' && m.toUpperCase() === method); } } /** * This class repesents an in-memory instance of a HAL resource. */ class Resource extends HalBase { /** * This property is `true` when either `methods` array does not exist * in the `self` link or the array exists and contains `PUT` string. * It is `false` otherwise. */ get canUpdate() { return this.can('PUT'); } /** * Follow the relation link. Returns an accessor for the resource * or `undefined` if no such relation exists. * * @param rel the name of the relation link * @param params parameters for a templated link * @returns an accessor for the linked resource or `undefined` */ follow(rel, params = {}) { const link = this._links[rel]; if (!link) return undefined; const uri = link.href; return !uri ? undefined : this.accessor(!link.templated ? uri : URI.parse(uri).expand(params), link.methods); } /** * Refresh the the resource. In other words, read the resource * identified by `self` link. * * @returns an observable of the refreshed resource instance */ read() { const type = this.constructor; return this.withSelf(self => this._client.get(self).pipe(map(obj => this.instanceOf(type, obj)), catchError(this.handleError))); } /** * Persit the resource. Uses `PUT` request to send the new resource * state. * * @returns an observable of resource accessor */ update() { return this.withSelf(self => this._client.put(self, objectFrom(this)).pipe(map(() => this.accessor(self)), catchError(this.handleError))); } /** * Returns a single embedded resource or `undefined` if no such * embedded exists. If the named resource is an array it returns * the first element. * * @param type the resource type * @param rel the name of the embedded resource * @returns the resource or `undefined` */ get(type, rel) { const obj = this._embedded[rel]; if (Array.isArray(obj)) { const objs = obj; const first = objs[0]; return first && this.instanceOf(type, first); } return obj && this.instanceOf(type, obj); } /** * Returns an embedded array of resources or `undefined` if the named * embedded does not exist. If the named resource is not an array it * wraps the resource in an array. * * @param type the element resource type * @param rel the name of the embedded resource array * @returns the resource array or `undefined` */ getArray(type, rel) { const obj = this._embedded[rel]; if (Array.isArray(obj)) return this.arrayOf(type, obj); return obj && [this.instanceOf(type, obj)]; } clone(override = {}) { return new this.constructor({ ...this, ...override }); } arrayOf(type, values) { return values.map(obj => this.instanceOf(type, obj)); } } const next = 'next'; const prev = 'prev'; /** * This class represents an in-memory collection of resources. */ class Collection extends Resource { /** * @param type the element resource type * @param obj the object used to assign the properties */ constructor(type, obj) { super(obj); this.type = type; this.start = obj.start > 0 ? Math.trunc(obj.start) : 0; for (const rel in this._embedded) { const values = this._embedded[rel]; if (Array.isArray(values)) { this.values = this.arrayOf(type, values); return; } } this.values = []; } /** * Follow `next` link if it exists and can be read. * * @returns the accessor for the link or `undefined` */ next() { const accessor = this.follow(next); return !!accessor && accessor.canRead ? accessor : undefined; } /** * Follow `prev` link if it exists and can be read. * * @returns the accessor for the link or `undefined` */ previous() { const accessor = this.follow(prev); return !!accessor && accessor.canRead ? accessor : undefined; } /** * Refresh the resource collection. In other words, read * the resource collection identified by `self` link. * * @returns an observable of the refreshed resource collection instance */ read() { return this.withSelf(self => this._client.get(self).pipe(map(obj => new Collection(this.type, { ...obj, _client: this._client })), catchError(this.handleError))); } } /** * Convert any item, be that a {@link Resource} or {@link Collection} * to a plain {@link Object} or {@link Array}. On conversion * HAL-related properties (`_client`, `_links`, `_embedded`) * are not included in the resulting object. Primitive types * as well as `null` or `undefined` are returned unchanged. * If the argument is an array, every item of the returned array * is converted recursively. For {@link Collection} it returns * the array of values where each item is converted recursively. * * @param item the item to convert * @returns an object, array, primitive value, `undefined` or `null` */ function objectFrom(item) { if (typeof item !== 'object' || item === null) return item; if (item instanceof Collection) return objectFrom(item.values); if (Array.isArray(item)) return item.map(x => objectFrom(x)); const { _client, _links, _embedded, ...object } = item; return object; } /** * @param arg argument to test * @returns `false` if the argument is `null` or `undefined` and `true` * otherwise */ function isDefined(arg) { return arg !== undefined && arg !== null; } /** * Returns an RxJS operator that makes the source {@link Observable} * complete at the same time as the lifetime {@link Observable}. * * @param lifetime the lifetime observable * @returns a function that transforms the source {@link Observable} */ function completeWith(lifetime) { const terminator$ = new Observable(subscriber => lifetime.subscribe().add(() => subscriber.next())); return pipe(takeUntil(terminator$)); } /** * Returns an RxJS operator to follow a link on a {@link Resource}. It is * equivalent to * ```ts * map(resource => resource?.follow(rel, params)) * ``` * * @param rel the relation to follow * @param params the parameters to expand the link * @returns a function that transforms the source {@link Observable} */ function follow(rel, params) { return pipe(map(resource => resource?.follow(rel, params))); } /** * Returns an RxJS operator to create a new resource identified by * an {@link Accessor} or a {@link Resource}. It is equivalent to * ```ts * switchMap(x => of(undefined)) * ``` * if the stream value is `null` or `undefined` and to * ```ts * switchMap(x => x.create(obj)) * ``` * otherwise. * * @param obj the value for the resource * @returns a function that transforms the source {@link Observable} */ function create(obj) { return pipe(switchMap(base => !base ? of(undefined) : base.create(obj))); } /** * Returns an RxJS operator to read a {@link Resource} using an * {@link Accessor}. It is equivalent to * ```ts * switchMap(accessor => of(undefined)) * ``` * if `accessor` is `null` or `undefined` and to * ```ts * switchMap(accessor => accessor.read(type)) * ``` * otherwise. * * @param type the resource type * @returns a function that transforms the source {@link Observable} */ function read(type) { return pipe(switchMap(accessor => !accessor ? of(undefined) : accessor.read(type))); } /** * Returns an RxJS operator to read a {@link Collection} of resources * using an {@link Accessor}. It is equivalent to * ```ts * switchMap(accessor => of(undefined)) * ``` * if `accessor` is `null` or `undefined` and to * ```ts * switchMap(accessor => accessor.readCollection(type)) * ``` * otherwise. * * @param type the collection element type * @returns a function that transforms the source {@link Observable} */ function readCollection(type) { return pipe(switchMap(accessor => !accessor ? of(undefined) : accessor.readCollection(type))); } /** * Returns an RxJS operator to refresh a {@link Resource} or * {@link Collection} of resources. It is equivalent to * ```ts * switchMap(resource => of(undefined)) * ``` * if `resource` is `null` or `undefined` and to * ```ts * switchMap(resource => resource.read()) * ``` * otherwise. * * @returns a function that transforms the source {@link Observable} */ function refresh() { return pipe(switchMap(resource => !resource ? of(undefined) : resource.read())); } /** * Returns an RxJS operator to edit and update a {@link Resource}. * It is equivalent to * ```ts * switchMap(resource => of(undefined)) * ``` * if `resource` is `null` or `undefined` and applying edit operation to * the resource and doing * ```ts * switchMap(resource => resource.update()) * ``` * otherwise. * * @param edit the operation to edit the resource * @returns a function that transforms the source {@link Observable} */ function update(edit) { return pipe(switchMap(resource => { if (!resource) return of(undefined); return (edit(resource)).update(); })); } /** * Returns an RxJS operator to delete a resource identified by * an {@link Accessor} or a {@link Resource}. It is equivalent to * ```ts * switchMap(x => of(undefined)) * ``` * if the stream value is `null` or `undefined` and to * ```ts * switchMap(x => x.delete()) * ``` * otherwise. * * @returns a function that transforms the source {@link Observable} */ function del() { return pipe(switchMap(base => !base ? of(undefined) : base.delete())); } /** * Returns an RxJS operator to filter out `null` and `undefined` items * from the source {@link Observable}. * * @returns a function that transforms the source {@link Observable} */ function defined() { return pipe(filter(isDefined)); } /** * This service is used to obtain API root accessors. It incapsulates * {@link HttpClient}, which will be passed to all {@link Accessor} * objects originating from this service, directly or indirectly. */ class HalClientService { /** * @param httpClient Angular HTTP client */ constructor(httpClient) { this.httpClient = httpClient; } /** * Obtain an accessor for the API root entry point specified by the * URI. * * @param uri the URI for the API root * @returns an accessor for the API root */ root(uri) { return new Accessor({ _client: this.httpClient, _links: { self: { href: uri } } }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: HalClientService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: HalClientService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: HalClientService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.HttpClient }] }); /** * This class provides a way to execute operations on HAL resources * without reading them first. Accessors are obtained either by following * resource links or by getting the root entry point for the API. */ class Accessor extends HalBase { /** * Read the resource identified by `self` link. * * @param type the resource type * @returns an observable of the resource instance */ read(type) { return this._client.get(this.self).pipe(map(obj => this.instanceOf(type, obj)), catchError(this.handleError)); } /** * Read the resource collection identified by `self` link. * * @param type the collection element type * @returns an observable of the collection instance */ readCollection(type) { return this._client.get(this.self).pipe(map(obj => new Collection(type, { ...obj, _client: this._client })), catchError(this.handleError)); } } /* * Public API Surface of ngx-hal-client */ /** * Generated bundle index. Do not edit. */ export { Accessor, Collection, HalClientService, HalError, Resource, completeWith, create, defined, del, follow, isDefined, objectFrom, read, readCollection, refresh, update }; //# sourceMappingURL=granito-ngx-hal-client.mjs.map