@granito/ngx-hal-client
Version:
A HAL client to be used in Angular projects
534 lines (522 loc) • 17 kB
JavaScript
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