UNPKG

@granito/ngx-hal-client

Version:

A HAL client to be used in Angular projects

516 lines (505 loc) 16.5 kB
import { InjectionToken, makeEnvironmentProviders } from '@angular/core'; import { map, catchError, throwError, Observable, pipe, takeUntil, switchMap, filter } from 'rxjs'; import * as URI from 'uri-template'; import { HttpClient } from '@angular/common/http'; /** * This class represents errors thrown by _Angular HAL Client_. It * unifies errors produced by _Angular 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 the `self` link exists and 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.uriFor('POST'); } /** * This property is `true` when the `self` link exists and 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.uriFor('GET'); } /** * This property is `true` when the `self` link exists and 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.uriFor('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 emits an accessor for * the newly created resource. The `self` link in the accessor * may be set to `undefined` if `Location` header is 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.withUriFor('POST', uri => this._client.post(uri, objectFrom(obj), { observe: 'response' }).pipe(map(response => response.headers.get('Location') || undefined), map(location => 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.withUriFor('DELETE', uri => this._client.delete(uri).pipe(map(() => undefined), catchError(this.handleError))); } withUriFor(method, func) { const uri = this.uriFor(method); if (!uri) return throwError(() => new HalError({ message: `no "self" relation supporting "${method}" method` })); return func(uri); } accessor(href, methods) { return this.instanceOf(Accessor, { _links: !href ? {} : { self: { href, methods } } }); } instanceOf(type, obj) { return new type({ ...obj, _client: this._client }); } roInstanceOf(type, obj) { return Object.freeze(this.instanceOf(type, obj)); } 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 })); } uriFor(method) { const selfRel = this._links[self]; if (!selfRel || !selfRel.href) return undefined; const methods = selfRel.methods; if (!Array.isArray(methods)) return selfRel.href; return methods.find(m => typeof m === 'string' && m.toUpperCase() === method) && selfRel.href || undefined; } } /** * This class represents an in-memory instance of a HAL resource. */ class Resource extends HalBase { /** * This property is `true` when the `self` link exists and 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.uriFor('PUT'); } /** * Follow the relation link. Returns an accessor for the resource. * The `self` link in the accessor is set to `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 */ follow(rel, params = {}) { const link = this._links[rel]; if (!link) return this.accessor(); const uri = link.href; return this.accessor(!link.templated ? uri : URI.parse(uri).expand(params), link.methods); } /** * Refresh 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.withUriFor('GET', uri => this._client.get(uri).pipe(map(obj => this.roInstanceOf(type, obj)), catchError(this.handleError))); } /** * Persist the resource. Uses `PUT` request to send the new resource * state. * * @returns an observable of the resource instance */ update() { return this.withUriFor('PUT', uri => this._client.put(uri, objectFrom(this)).pipe(map(() => this), 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.roInstanceOf(type, first); } return obj && this.roInstanceOf(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]; return Array.isArray(obj) ? this.roArrayOf(type, obj) : obj && this.roArrayOf(type, [obj]); } /** * Clones the resource instance and allows to modify the clone's * properties using the provided function. * * @param update the update function to apply to the clone * @returns the updated clone of the resource */ mutate(update) { const type = this.constructor; const mutable = new type(this); update(mutable); return Object.freeze(mutable); } roArrayOf(type, values) { return values.map(obj => this.roInstanceOf(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.roArrayOf(type, values); return; } } this.values = this.roArrayOf(type, []); } /** * Follow the `next` link. If the link does not exist, the accessor * will have the `self` link set to `undefined`. * * @returns the accessor for the link */ next() { return this.follow(next); } /** * Follow the `prev` link. If the link does not exist, the accessor * will have the `self` link set to `undefined`. * * @returns the accessor for the link */ prev() { return this.follow(prev); } /** * 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.withUriFor('GET', self => this._client.get(self).pipe(map(obj => new Collection(this.type, { ...obj, _client: this._client })), catchError(this.handleError))); } } /** * Convert any item to {@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 => x.create(obj)) * ``` * * @param obj the value for the resource * @returns a function that transforms the source {@link Observable} */ function create(obj) { return pipe(switchMap(base => base.create(obj))); } /** * Returns an RxJS operator to read a {@link Resource} using an * {@link Accessor}. It is equivalent to * ```ts * switchMap(accessor => accessor.read(type)) * ``` * * @param type the resource type * @returns a function that transforms the source {@link Observable} */ function read(type) { return pipe(switchMap(accessor => 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 => accessor.readCollection(type)) * ``` * * @param type the collection element type * @returns a function that transforms the source {@link Observable} */ function readCollection(type) { return pipe(switchMap(accessor => accessor.readCollection(type))); } /** * Returns an RxJS operator to refresh a {@link Resource} or * {@link Collection} of resources. It is equivalent to * ```ts * switchMap(resource => resource.read()) * ``` * * @returns a function that transforms the source {@link Observable} */ function refresh() { return pipe(switchMap(resource => resource.read())); } /** * Returns an RxJS operator to clone a {@link Resource} and modify * the cloned instance using the provided update function. It is * equivalent to * ```ts * map(resource => resource.mutate(update)) * ``` * * @param update the update function to apply * @returns a function that transforms the source {@link Observable} */ function mutate(update) { return pipe(map(resource => resource.mutate(update))); } /** * Returns an RxJS operator to update a {@link Resource}. * It is equivalent to * ```ts * switchMap(resource => resource.update()) * ``` * * @returns a function that transforms the source {@link Observable} */ function update() { return pipe(switchMap(resource => 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 => x.delete()) * ``` * * @returns a function that transforms the source {@link Observable} */ function del() { return pipe(switchMap(base => 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 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.withUriFor('GET', uri => this._client.get(uri).pipe(map(obj => this.roInstanceOf(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.withUriFor('GET', uri => this._client.get(uri).pipe(map(obj => { const collection = new Collection(type, { ...obj, _client: this._client }); Object.freeze(collection); return collection; }), catchError(this.handleError))); } } /** * The injection token for HAL root URL. */ const HAL_ROOT = new InjectionToken('hal.root'); /** * Configure HAL client using the provided the root URL for the API. * * @param url the root URL * @return an array of environment providers with a single factory * provider for the {@link Accessor} for the API root */ function provideHalRoot(url) { return makeEnvironmentProviders([ { provide: HAL_ROOT, deps: [HttpClient], useFactory: (httpClient) => new Accessor({ _client: httpClient, _links: { self: { href: url } } }) } ]); } /* * Public API Surface of ngx-hal-client */ /** * Generated bundle index. Do not edit. */ export { Accessor, Collection, HAL_ROOT, HalError, Resource, completeWith, create, defined, del, follow, isDefined, mutate, objectFrom, provideHalRoot, read, readCollection, refresh, update }; //# sourceMappingURL=granito-ngx-hal-client.mjs.map