@lagoshny/ngx-hateoas-client
Version:
This client used to develop `Angular 12+` applications working with RESTfulll server API with HAL/JSON response type (supports server implementation by Spring HATEOAS)
1,540 lines (1,183 loc) • 128 kB
Markdown
# NgxHateoasClient
<a href="https://www.npmjs.com/package/@lagoshny/ngx-hateoas-client/v/latest">
<img src="https://img.shields.io/npm/v/@lagoshny/ngx-hateoas-client/latest?label=npm" alt="Last released npm version" />
</a>
<a href="https://github.com/lagoshny/ngx-hateoas-client/actions?query=workflow%3ABuild">
<img src="https://github.com/lagoshny/ngx-hateoas-client/actions/workflows/nodejs.yml/badge.svg?branch=master" alt="Pipeline info" />
</a>
<a href="https://github.com/lagoshny/ngx-hateoas-client/issues">
<img src="https://img.shields.io/github/issues/lagoshny/ngx-hateoas-client" alt="Total open issues" />
</a>
<a href="https://www.npmjs.com/package/@lagoshny/ngx-hateoas-client">
<img src="https://img.shields.io/npm/dt/@lagoshny/ngx-hateoas-client" alt="Total downloads by npm" />
</a>
<a href="https://mit-license.org/">
<img src="https://img.shields.io/npm/l/@lagoshny/ngx-hateoas-client" alt="License info" />
</a>
<br />
<br />
## ⭐Compatible with Angular 12.x.x - 21.x.x versions that uses `Ivy compilation`.
### ⚠ If you use old `View Engine` compilation or Angular 6.x.x - 11.x.x you need to use [2.x.x](https://github.com/lagoshny/ngx-hateoas-client/tree/ng-ve) lib version.
>
>See more about it [here](https://github.com/lagoshny/ngx-hateoas-client/blob/master/CHANGELOG.md#303-2021-12-23).
### 💡 Old versioning policy.
- Versions that work with old `View Engine` compilation [`2.0.0`-`2.x.x`].
- Versions that work with new `Ivy` compilation [`3.0.0`-`x.x.x`] and supports Angular from 12.x.x - 21.x.x.
### 💡 New versioning policy.
>New releases will respect with Angular versions. For example current Angular has 21 version, then lib version will be 21.x.x. The lib will be respect only with *major* version. Minor and patched version will be own not respect to Angular version. New lib versioning policy helps you choose the right release, depends on your Angular version.
This client can be used to develop `Angular 12+` applications working with RESTfull server API.
By `RESTfull API` means when the server application implements all the layers of the [Richardson Maturity Model](https://martinfowler.com/articles/richardsonMaturityModel.html)
and the server provides [HAL/JSON](http://stateless.co/hal_specification.html) response type.
This client compatible with Java server-side applications based on [Spring HATEOAS](https://spring.io/projects/spring-hateoas) or [Spring Data REST](https://docs.spring.io/spring-data/rest/docs/current/reference/html/#reference).
>This client is a continuation of the [@lagoshny/ngx-hal-client](https://github.com/lagoshny/ngx-hal-client).
You can find out about the motivation to create a new client [here](https://github.com/lagoshny/ngx-hateoas-client/blob/master/migration-guide.md#Motivation).
To migrate from `@lagoshny/ngx-hal-client` to this client you can use the [migration guide](https://github.com/lagoshny/ngx-hateoas-client/blob/master/migration-guide.md#Motivation).
You can found examples of usage this client with [task-manager-front](https://github.com/lagoshny/task-manager-front) application that uses server-side [task-manager-back](https://github.com/lagoshny/task-manager-back) application.
## Contents
1. [Changelog](#Changelog)
2. [Migrate to standalone](#migrate-to-standalone)
2. [Getting started](#Getting-started)
- [Installation](#Installation)
- [Configuration](#Configuration)
- [Common Resource URL](#using-common-url-for-retrieve-resources)
- [Multiple Resource URLs](#using-multiple-urls-to-retrieve-resources)
- [Usage](#Usage)
- [Define resource classes](#Define-resource-classes)
- [Built-in HateoasResourceService](#built-in-hateoasresourceservice)
- [Create custom Resource service](#Create-custom-Resource-service)
3. [Testing](#Testing)
4. [Resource types](#Resource-types)
- [Decorators](#decorators)
- [@HateoasResource](#hateoasresource)
- [resourceName](#resourcename)
- [options](#options)
- [@HateoasEmbeddedResource](#hateoasembeddedresource)
- [@HateoasProjection](#hateoasprojection)
- [@ProjectionRel](#projectionrel)
- [BaseResource](#BaseResource)
- [HasRelation](#HasRelation)
- [GetRelation](#GetRelation)
- [GetRelatedCollection](#GetRelatedCollection)
- [GetRelatedPage](#GetRelatedPage)
- [PostRelation](#PostRelation)
- [PatchRelation](#PatchRelation)
- [PutRelation](#PutRelation)
- [Resource](#Resource)
- [AddCollectionRelation](#AddCollectionRelation)
- [BindRelation](#BindRelation)
- [UnbindRelation](#UnbindRelation)
- [UnbindCollectionRelation](#UnbindCollectionRelation)
- [DeleteRelation](#DeleteRelation)
- [EmbeddedResource](#EmbeddedResource)
- [ResourceCollection](#ResourceCollection)
- [PagedResourceCollection](#PagedResourceCollection)
- [Subtypes support](#Subtypes-support)
- [Resource projection support](#resource-projection-support)
- [ProjectionRelType](#projectionreltype)
5. [Resource service](#Resource-service)
- [GetResource](#GetResource)
- [GetCollection](#GetCollection)
- [GetPage](#GetPage)
- [CreateResource](#CreateResource)
- [UpdateResource](#UpdateResource)
- [UpdateResourceById](#UpdateResourceById)
- [PatchResource](#PatchResource)
- [PatchResourceById](#PatchResourceById)
- [DeleteResource](#DeleteResource)
- [DeleteResourceById](#DeleteResourceById)
- [SearchResource](#SearchResource)
- [SearchCollection](#SearchCollection)
- [SearchPage](#SearchPage)
- [CustomQuery](#CustomQuery)
- [CustomSearchQuery](#CustomSearchQuery)
6. [Settings](#settings)
- [Configuration params](#Configuration-params)
- [Http params](#http-params)
- [UseTypes](#usetypes-params)
- [TypesFormat](#typesformat)
- [HALFormat](#halformat)
- [Cache support](#cache-support)
- [Evict all cache](#evict-all-cache-data)
- [Logging](#Logging)
7. [Public classes](#Public-classes)
- [RequestOption](#RequestOption)
- [GetOption](#GetOption)
- [PagedGetOption](#PagedGetOption)
- [RequestBody](#RequestBody)
- [NULL_VALUES](#nullvalues)
- [REL_RESOURCES_AS_OBJECTS](#relresourcesasobjects)
- [Sort](#Sort)
- [SortedPageParam](#SortedPageParam)
## Changelog
[Learn about the latest improvements](https://github.com/lagoshny/ngx-hateoas-client/blob/master/CHANGELOG.md).
## Migrate to standalone
To migrate to standalone system instead `NgModule` use need to change lib configuration.
Before with `NgModule` system you configured library as:
```ts
import { NgxHateoasClientModule } from '@lagoshny/ngx-hateoas-client';
...
@NgModule({
...
imports: [
HttpClientModule,
...
NgxHateoasClientModule.forRoot()
...
]
...
})
export class AppModule {
constructor(hateoasConfig: NgxHateoasClientConfigurationService) {
hateoasConfig.configure({
http: {
rootUrl: 'http://localhost:8080/api/v1'
}
});
}
}
---
```
> `NgModule` lib configuration method will be removed in the next lib releases, please migrate your code to new standalone configuration
> No matter your project used `NgModule` system or standalone you can configure standalone lib in both types
Now with standalone system:
```ts
import { provideNgxHateoasClient } from '@lagoshny/ngx-hateoas-client';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideHttpClient(),
provideNgxHateoasClient({
http: {
rootUrl: 'http://localhost:8080/api/v1'
}
}),
// ...
],
}
```
Note that you don't need anymore inject manually `NgxHateoasClientConfigurationService` and configure it.
Now you simple use `provideNgxHateoasClient` and pass all configuration there.
If you use old `NgModule` system then you can use standalone lib version like this:
```ts
import { provideNgxHateoasClient } from '@lagoshny/ngx-hateoas-client';
@NgModule({
...
imports: [
HttpClientModule,
],
providers: [
...
provideNgxHateoasClient(
{
http: {
rootUrl: 'http://localhost:8080/api/v1'
}
}
),
...
]
...
})
export class AppModule {
// removed constructor with NgxHateoasClientConfigurationService
}
```
## Getting started
### Installation
To install the latest version use command:
```
npm i @lagoshny/ngx-hateoas-client@latest --save
```
### Configuration
To configure lib you need to import `provideNgxHateoasClient` provider and setup lib configuration.
Minimal configuration looks like this:
```ts
import { provideNgxHateoasClient } from '@lagoshny/ngx-hateoas-client';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideHttpClient(),
provideNgxHateoasClient({
http: {
rootUrl: 'http://localhost:8080/api/v1'
}
}),
// ...
],
}
```
If you use `NgModule` system and want configure lib use the next approach:
```ts
import { provideNgxHateoasClient } from '@lagoshny/ngx-hateoas-client';
@NgModule({
...
imports: [
HttpClientModule,
],
providers: [
provideNgxHateoasClient({
http: {
rootUrl: 'http://localhost:8080/api/v1'
}
}),
...
]
...
})
export class AppModule {
...
}
```
### Different configuration URLs
#### Using common URL to retrieve `Resources`
```ts
import { provideNgxHateoasClient } from '@lagoshny/ngx-hateoas-client';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideHttpClient(),
provideNgxHateoasClient({
// All Resources will use this url as base url
http: {
rootUrl: 'http://localhost:8080/api/v1'
}
}),
// ...
],
}
```
#### Using multiple URLs to retrieve `Resources`
```ts
import { provideNgxHateoasClient } from '@lagoshny/ngx-hateoas-client';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideHttpClient(),
provideNgxHateoasClient({
http: {
// Use this router name for default Resources route
defaultRoute: {
rootUrl: 'http://localhost:8080/api/v1'
},
anotherRoute: {
rootUrl: 'http://localhost:9090/api/v1'
}
}
}),
// ...
],
}
```
`defaultRoute` - it is special `router name` that all `Resources` used by default.
`anotherRoute` - additional `Resource` route that can be used in `Resource` [@HateoasResource#options](#options) to specify it.
<br>
<br>
>See more about other configuration params [here](#configuration-params).
### Usage
### Define resource classes
To represent model class as a resource model extend model class by `Resource` class.
Besides you need to decorate the class with [@HateoasResource](#hateoasresource) decorator that will register your resource class with passed `resourceName` in `hateoas-client`.
Suppose you have some `Product` model class:
```ts
export class Product {
public name: string;
public cost: number;
}
```
After making it as a `Resource` it will look like this:
```ts
import { Resource, HateoasResource } from '@lagoshny/ngx-hateoas-client';
/*
Resource name 'products' should be map to server-side resource name that used to build resource self URL.
For example in this case it can be: http://localhost:8080/api/v1/products
*/
@HateoasResource('products')
export class Product extend Resource {
public name: string;
public cost: number;
}
```
Thereafter, the `Product` class will have `Resource` methods to work with the product's relations through resource links.
>You can create a resource projection class that will map to a server-side projection model. How to do it read in [this section](#resource-projection-support).
>Also, you can extend model classes with the `EmbeddedResource` class and decorate with [@HateasEmbeddedResource](#hateoasembeddedresource) decorator when the model class used as an [embeddable](https://docs.oracle.com/javaee/6/api/javax/persistence/Embeddable.html) entity.
You can read more about `EmbeddedResource` [here](#embeddedresource).
It is recommended also to declare the `Product` resource class (others `Resources` and `EmbeddedResources` too) in the `hateoas-client` configuration:
```ts
...
provideNgxHateoasClient(
{
// ...
useTypes: {
resources: [Product]
}
}
),
...
```
>See more about `useTypes` in the configuration section [here](#usetypes-params).
Now you have created a resource class and ready to perform requests for this resource.
For this you can use universal on resource built-in [HateoasResourceService](#built-in-hateoasresourceservice) or a create [custom resource service](#create-custom-resource-service) for concrete resource class.
### Built-in HateoasResourceService
The library has built-in universal on resource `HateoasResourceService`.
It is a simple service with methods to `get`/`create`/`update`/`delete`/`search` resources.
To use it inject `HateoasResourceService` to a component or a service class after that you can perform resource requests by passing the resource type.
```ts
import { Resource, HateoasResource, HateoasResourceService } from '@lagoshny/ngx-hateoas-client';
@HateoasResource('products')
export class Product extends Resource {
...
}
@Component({
...
})
export class SomeComponent {
constructor(private resourceService: HateoasResourceService) {
}
onSomeAction() {
const product = new Product();
product.cost = 100;
product.name = 'Fruit';
this.resourceService.createResource(Product, product)
.subscribe((createdResource: Product) => {
// TODO something
});
};
}
```
Each `HateoasResourceService` method has the first param is the resource type that extends [Resource](#resource) class and decorated with [@HateoasResource](#hateoasresource).
The resource type uses to build a URL for resource requests and create resources with a concrete class when parsing the server's answer.
More about `HateoasResourceService` methods see [here](#resource-service).
>`HateoasResourceService` is a universal service that can work with several `Resources` at a time, you need only pass a desired resource type as the first param.
>If you have not extra logic to work with your resources then you can inject once `HateoasResourceService` and use it to work with your resources.
When you have some logic that should be preparing resource before a request you need to create a custom resource service extends `HateoasResourceOperation` to see more about this [here](#create-custom-resource-service).
### Create custom Resource service
To create a custom resource service create a new service and extends it with `HateoasResourceOperation` and pass `resourceType` to the parent constructor.
```ts
import { Resource, HateoasResource, HateoasResourceOperation } from '@lagoshny/ngx-hateoas-client';
@HateoasResource('products')
export class Product extends Resource {
...
}
@Injectable({providedIn: 'root'})
export class ProductService extends HateoasResourceOperation<Product> {
constructor() {
super(Product);
}
}
```
`HateoasResourceOperation` has the same [methods](#resource-service) as `HateoasResourceService` without `resourceType` as the first param (because you pass `resourceType` with service constructor).
## Testing
To test your services, that are using `HateoasResourceOperation` or `HateoasResourceService` you need import `NgxHateoasClientModule.forRoot()` in the test module.
After that you can inject or mock the next services (if you need it):
- `NgxHateoasClientConfigurationService`
- `HateoasResourceService`
Below you can find simple lib test examples:
Suppose you have the `User` and some `UserService` like that:
```ts
import {
HateoasResourceOperation,
HateoasResourceService,
} from '@lagoshny/ngx-hateoas-client';
export class User {
name: string;
age: number;
}
@Injectable()
export class UserService extends HateoasResourceOperation<User> {
constructor(public resourceService: HateoasResourceService) {
super(User);
}
public create(user: User): Observable<Observable<never> | User> {
return super.createResource({body: user});
}
public getAllUsersByAge(age: number): Observable<PagedResourceCollection<User>> {
return this.resourceService.searchPage(User, 'allByAge', {
params: {
age
}
}
);
}
}
```
Note, `UserService` extends `HateoasResourceOperation` and uses `HateoasResourceService` to perform requests.
### Using TestBed
#### Standalone system
>If you have the app with standalone components then you don't need any lib configuration to test it.
>You don't need to configure TestBed additionally.
#### NgModule system
If you prefer to use standard `TestBed` for testing, you can do that in the following way:
````ts
import {
HateoasResourceService,
NgxHateoasClientModule,
PagedResourceCollection,
ResourceCollection,
provideNgxHateoasClientTesting
} from '@lagoshny/ngx-hateoas-client';
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
describe('UserServiceTest', () => {
let hateoasResourceServiceSpy: Mocked<Pick<HateoasResourceService, 'createResource' | 'searchPage'>>;
beforeEach(() => {
hateoasResourceServiceSpy = {
createResource: vi.fn(),
searchPage: vi.fn(),
};
TestBed.configureTestingModule({
providers: [
UserService,
provideNgxHateoasClientTesting(), // also you can provide custom configuration if need it
{provide: HateoasResourceService, useValue: hateoasResourceServiceSpy}
]
});
});
it('should init service', () => {
const userService = TestBed.inject(UserService);
expect(userService).toBeTruthy();
});
it('should create new user', () => {
const userService = TestBed.inject(UserService);
const newUser = new User();
newUser.id = '1';
hateoasResourceServiceSpy.createResource.mockReturnValue(of(newUser));
const user = new User();
user.name = 'Test user';
userService.create(user).subscribe((createdUser: User) => {
expect(createdUser).toBeTruthy();
expect(createdUser.id).toEqual('1');
});
});
it('should return paged user list', () => {
const userService = TestBed.inject(UserService);
const returnedUser = new User();
returnedUser.id = '1';
const resourceCollection = new ResourceCollection<User>();
resourceCollection.resources = [returnedUser];
hateoasResourceServiceSpy.searchPage.mockReturnValue(of(new PagedResourceCollection(resourceCollection)));
userService.getAllUsersByAge(35).subscribe((users: PagedResourceCollection<User>) => {
expect(users).toBeTruthy();
expect(users.resources).toBeTruthy();
expect(users.resources[0]).toBeTruthy();
expect(users.resources[0].id).toEqual('1');
});
});
});
````
### Using Spectator
If you prefer to use [@ngneat/spectator](https://www.npmjs.com/package/@ngneat/spectator) for testing, you can do that in the following way:
```ts
import {
HateoasResourceService,
NgxHateoasClientModule,
PagedResourceCollection,
ResourceCollection,
provideNgxHateoasClientTesting
} from '@lagoshny/ngx-hateoas-client';
import { of } from 'rxjs';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
describe('UserServiceTest', () => {
let spectator: SpectatorService<UserService>;
const createService = createServiceFactory({
providers: [provideNgxHateoasClientTesting()],
service: UserService,
mocks: [HateoasResourceService]
});
beforeEach(() => {
spectator = createService();
});
it('should init service', () => {
const userService = spectator.inject(UserService);
expect(userService).toBeTruthy();
});
it('should create new user', waitForAsync(() => {
const userService = spectator.inject(UserService);
const newUser = new User();
newUser.id = '1';
const hateoasResourceServiceMock = spectator.inject(HateoasResourceService);
hateoasResourceServiceMock.createResource.and.returnValue(of(newUser));
const user = new User();
user.name = 'Test user';
userService.create(user).subscribe((createdUser: User) => {
expect(createdUser).toBeTruthy();
expect(createdUser.id).toEqual('1');
});
}));
it('should return paged user list', waitForAsync(() => {
const userService = spectator.inject(UserService);
const returnedUser = new User();
returnedUser.id = '1';
const resourceCollection = new ResourceCollection<User>();
resourceCollection.resources = [returnedUser];
const hateoasResourceServiceMock = spectator.inject(HateoasResourceService);
hateoasResourceServiceMock.searchPage.and.returnValue(of(new PagedResourceCollection(resourceCollection)));
userService.getAllUsersByAge(35).subscribe((users: PagedResourceCollection<User>) => {
expect(users).toBeTruthy();
expect(users.resources).toBeTruthy();
expect(users.resources[0]).toBeTruthy();
expect(users.resources[0].id).toEqual('1');
});
}));
});
```
## Resource types
There are several types of resources: the main resource type is [Resource](#resource) represents the server-side entity model class.
If the server-side model has Embeddable entity type then use [EmbeddedResource](#embeddedresource) type instead [Resource](#resource) type.
Both [Resource](#resource) and [EmbeddedResource](#embeddedresource) have some the same methods therefore they have common parent [BaseResource](#baseresource) class implements these methods.
Also, you can create `Resource` class to represent resource projection, see more about resource projection [here](#resource-projection-support).
>Each `Resource`/`EmbeddedResource` has decorators [@HateoasResource](#hateoasresource)/[@HateoasEmbeddedResource](#hateoasembeddedresource) respectively. They're used to register info about your resources in `hateoas-client`. For example, `resourceName` that used to build resource requests.
To work with resource collections uses [ResourceCollection](#resourcecollection) type its holds an array of the resources.
When you have a paged collection of resources result use an extension of [ResourceCollection](#resourcecollection) is [PagedResourceCollection](#pagedresourcecollection) that allows you to navigate by pages and perform custom page requests.
In some cases, the server-side can have an entity inheritance model how to work with entity subtypes, you can found [here](#subtypes-support).
## Decorators
### @HateoasResource
`@HateoasResource` decorator use to register your `Resource` classes in `hateoas-client` with passed `resourceName` as decorator's param.
- `resourceName`: `string` should be equals to the server-side resource name that uses to represent self resource link.
- `options`: `ResourceOption` additional resource options. Find more about these options [here](#options).
#### resourceName
Using to specify resource name.
For example, you need to work with `Shop`resource:
```ts
import { Resource, HateoasResource } from '@lagoshny/ngx-hateoas-client';
@HateoasResource('shops')
export class Shop extends Resource {
...
}
```
It means that server-side use `shops` as resource name for `Shop` entity and it is resource self-link seem like: `http://localhost:8080/api/v1/shops` (with the assumption that server's root URL is `http://localhost:8080/api/v1`)
>It is required to mark your `Resource` classes with this decorator otherwise you will get an error when performing resource request
#### options
`ResourceOption` contains properties:
```ts
{
routeName: string; // name of Resource route
}
```
If you want to use special `URL` to get `Resource` use `options#routeName` param:
```ts
import { Resource, HateoasResource } from '@lagoshny/ngx-hateoas-client';
@HateoasResource('shops', { routeName: 'yourRoute' })
export class Shop extends Resource {
...
}
```
Make sure that you configure route with name `yourRoute` in lib configuration, see [http](#using-multiple-urls-to-retrieve-resources) section.
> If you not specify `routerName` it will be used default router name as `defaultRouter`.
### @HateoasEmbeddedResource
`@HateoasEmbeddedResource` decorator use to register your `EmbeddedResource` classes in `hateoas-client` with passed `relationNames` as decorator's param.
- `relationNames` is an array of the names where each is the name of relation with which `EmbeddedResource` using in `Resource` class.
For example, you have `Client` resource that using `Address` embedded resource:
```ts
import { EmbeddedResource, Resource, HateoasEmbeddedResource, HateoasResource } from '@lagoshny/ngx-hateoas-client';
@HateoasEmbeddedResource(['clientAddress'])
export class Address extends EmbeddedResource {
...
}
@HateoasResource('clients')
export class Client extends Resource {
...
public clientAddress: Address;
...
}
```
In this case, `@HateoasEmbeddedResource.relationNames` has one element (`clientAddress`) that equals to property `Address` name in `Client` resource.
If embedded resource `Address` will use in several resources then `@HateoasEmbeddedResource.relationNames` should have all different property `Address` names that used in these resources.
>It is required to mark your `EmbeddedResource` classes with this decorator if wou want get concrete embedded resource class (in this example `Address` class). Otherwise you will get a warning about creating an embedded resource when parsing server's answer with the default `EmbeddedResource` class when performing resource request that using an embedded resource.
### @HateoasProjection
`@HateoasProjection` decorator use to register your projection resource classes in `hateoas-client` with passed `resourceType` and `projectionName` as decorator's params.
- `resourceType` equals to resource type that use this resource projection.
- `projectionName` should be equals to the server-side resource projection name.
For example, you have `Shop` resource projection with the name `shopProjection`:
```ts
import { Resource, HateoasResource, HateoasProjection } from '@lagoshny/ngx-hateoas-client';
@HateoasResource('shops')
export class Shop extends Resource {
...
}
@HateoasProjection(Shop, 'shopProjection')
export class ShopProjection extends Resource {
...
}
```
Using `@HateoasProjection` you can create separate resource projection classes with desired resource properties and relations.
For resource projection relations to another resource, you need to wrap these relations with [ProjectionRelType](#projectionreltype) type and mark these relations with [@ProjectionRel](#projectionrel) decorator with a relation resource type.
- [ProjectionRelType](#projectionreltype) will hide `Resource`/`EmbeddedResource` methods for projection relation that will lead clear projection relation interface.
- [@ProjectionRel](#projectionrel) decorator will be used to create relation with concrete resource type when parsing server-side answer.
See more about resource projection support [here](#resource-projection-support).
#### @ProjectionRel
`@ProjectionRel` decorator use to register projection relation resource type in `hateoas-client` with passed `relationType` as decorator's param.
- `relationType` equals to resource type that used as a relation property in resource projection.
For example, you have `Shop` resource projection with the name `shopProjection` and this projection has relation to `Cart` resource:
```ts
import { Resource, HateoasResource, HateoasProjection, ProjectionRel, ProjectionRelType } from '@lagoshny/ngx-hateoas-client';
import { ProjectionRelType } from './declarations';
@HateoasResource('carts')
export class Cart extends Resource {
...
}
@HateoasProjection(Shop, 'shopProjection')
export class ShopProjection extends Resource {
...
@ProjectionRel(Cart)
public cart: ProjectionRelType<Cart>;
...
}
```
Using `@ProjectionRel` decorator allows knowing`hateoas-client` which relation resource type needs to use when creating a resource from the serv-side answer.
> Here used `ProjectionRelType` type as a wrapper for `Cart` resource class, to hide `Resource`/`EmbeddedResource` methods in projection relation.
> See more [here](#projectionreltype) about this type.
### Resource presets
Examples of usage resource relation methods rely on presets.
- Resource classes are
```ts
import { Resource, HateoasResource } from '@lagoshny/ngx-hateoas-client';
@HateoasResource('carts')
export class Cart extends Resource {
public shop: Shop;
public products: Array<Product>;
public status: string;
public client: PhysicalClient;
}
@HateoasResource('shops')
export class Shop extends Resource {
public name: string;
public rating: number;
}
@HateoasResource('products')
export class Product extends Resource {
public name: string;
public cost: number;
public description: string;
}
@HateoasResource('clients')
export class Client extends Resource {
public address: string;
}
@HateoasResource('physicalClients')
export class PhysicalClient extends Client {
public fio: string;
}
@HateoasResource('juridicalClients')
export class JuridicalClient extends Client {
public inn: string;
}
```
- HateoasClientConfiguration:
```ts
provideNgxHateoasClient({
http: {
rootUrl: 'http://localhost:8080/api/v1'
},
useTypes: {
resources: [Cart, Shop, Product, Client, PhysicalClient, JuridicalClient]
}
})
```
- Suppose we have existed resources:
```json
Cart:
{
"status": "New",
"_links": {
"self": {
"href": "http://localhost:8080/api/v1/carts/1"
},
"cart": {
"href": "http://localhost:8080/api/v1/carts/1{?projection}",
"templated": true
},
"shop": {
"href": "http://localhost:8080/api/v1/carts/1/shop"
},
"products": {
"href": "http://localhost:8080/api/v1/carts/1/products"
},
"productsPage": {
"href": "http://localhost:8080/api/v1/carts/1/productPage?page={page}&size={size}&sort={sort}&projection={projection}",
"templated": true
},
"client": {
"href": "http://localhost:8080/api/v1/carts/1/client"
},
"postExample": {
"href": "http://localhost:8080/api/v1/cart/1/postExample"
},
"putExample": {
"href": "http://localhost:8080/api/v1/cart/1/putExample"
},
"patchExample": {
"href": "http://localhost:8080/api/v1/cart/1/patchExample"
},
}
}
Shop:
{
"name": "Some Name",
"ratings": 5
"_links": {
"self": {
"href": "http://localhost:8080/api/v1/shops/1"
},
"shop": {
"href": "http://localhost:8080/api/v1/shops/1"
}
}
}
Product:
{
"name": "Milk",
"cost": 2,
"description": "Some description"
"_links": {
"self": {
"href": "http://localhost:8080/api/v1/produtcs/1"
},
"produtc": {
"href": "http://localhost:8080/api/v1/produtcs/1"
}
}
}
Client:
{
"fio": "Some fio",
"_links": {
"self": {
"href": "http://localhost:8080/api/v1/physicalClients/1"
},
"physicalClient": {
"href": "http://localhost:8080/api/v1/physicalClients/1"
}
}
}
```
## BaseResource
Parent class for [Resource](#resource) and [EmbeddedResource](#embeddedresource) classes.
Contains common resource methods to work with resource relations through resource links (see below).
### HasRelation
Checks that passed relation name contains in relation `_link` array of the `BaseResource`.
Method signature:
```
hasRelation(relationName: string): boolean;
```
- `relationName` - resource relation name used to check.
- `return value` - `true` if resource has that relation, `false` otherwise.
### GetRelation
Getting resource relation object by relation name.
This method takes [GetOption](#getoption) parameter with it you can pass `projection` param
Method signature:
```
getRelation<T extends BaseResource>(relationName: string, options?: GetOption): Observable<T>;
```
- `relationName` - resource relation name used to get request URL.
- `options` - [GetOption](#getoption) additional options applied to the request.
- `return value` - [Resource](#resource) with type `T`.
- `throws error` - when required params are not valid or link not found by relation name or returned value is not [Resource](#resource).
##### Examples of usage ([given the presets](#resource-presets)):
```ts
// Performing GET request by the URL: http://localhost:8080/api/v1/carts/1/shop
cart.getRelation<Shop>('shop')
.subscribe((shop: Shop) => {
// some logic
});
```
With options:
```ts
// Performing GET request by the URL: http://localhost:8080/api/v1/carts/1/shop?projection=shopProjection&testParam=test&sort=name,ASC
cart.getRelation<Shop>('shop', {
params: {
testParam: 'test',
projection: 'shopProjection'
},
sort: {
name: 'ASC'
},
// useCache: true | false, by default true
})
.subscribe((shop: Shop) => {
// some logic
});
```
### GetRelatedCollection
Getting related resource collection by relation name.
This method takes [GetOption](#getoption) parameter with it you can pass `projection` param.
Method signature:
```
getRelatedCollection<T extends ResourceCollection<BaseResource>>(relationName: string, options?: GetOption): Observable<T>;
```
- `relationName` - resource relation name used to get request URL.
- `options` - [GetOption](#getoption) additional options applied to the request.
- `return value` - [ResourceCollection](#resourcecollection) collection of resources with type `T`.
- `throws error` - when required params are not valid or link not found by relation name or returned value is not [ResourceCollection](#resourcecollection).
##### Examples of usage ([given the presets](#resource-presets)):
```ts
// Performing GET request by the URL: http://localhost:8080/api/v1/carts/1/products
cart.getRelatedCollection<ResourceCollection<Product>>('products')
.subscribe((collection: ResourceCollection<Product>) => {
const products: Array<Product> = collection.resources;
// some logic
});
```
With options:
```ts
// Performing GET request by the URL: http://localhost:8080/api/v1/carts/1/products?projection=productProjection&testParam=test&sort=name,ASC
cart.getRelatedCollection<ResourceCollection<Product>>('products', {
params: {
testParam: 'test',
projection: 'productProjection'
},
sort: {
name: 'ASC'
},
// useCache: true | false, by default true
})
.subscribe((collection: ResourceCollection<Product>) => {
const products: Array<Product> = collection.resources;
// some logic
});
```
### GetRelatedPage
Getting related resource collection with pagination by relation name.
This method takes [PagedGetOption](#pagedgetoption) parameter with it you can pass `projection` param (see below).
>If do not pass `pageParams` with `PagedGetOption` then will be used [default page options](#default-page-values).
>Also, you can set up own default page params through [configuration](#pagination-params).
Method signature:
```
getRelatedPage<T extends PagedResourceCollection<BaseResource>>(relationName: string, options?: PagedGetOption): Observable<T>;
```
- `relationName` - resource relation name used to get request URL.
- `options` - [PagedGetOption](#pagedgetoption) additional options applied to the request, if not passed `pageParams` then used [default page params](#default-page-values).
- `return value` - [PagedResourceCollection](#pagedresourcecollection) paged collection of resources with type `T`.
- `throws error` - when required params are not valid or link not found by relation name or returned value is not [PagedResourceCollection](#pagedresourcecollection).
##### Examples of usage ([given the presets](#resource-presets)):
```ts
// Performing GET request by the URL: http://localhost:8080/api/v1/carts/1/productPage?page=0&size=20
cart.getRelatedPage<PagedResourceCollection<Product>>('productsPage')
.subscribe((page: PagedResourceCollection<Product>) => {
const products: Array<Product> = page.resources;
/* can use page methods
page.first();
page.last();
page.next();
page.prev();
page.customPage();
*/
});
```
With options:
```ts
// Performing GET request by the URL: http://localhost:8080/api/v1/carts/1/productPage?page=1&size=40&projection=productProjection&testParam=test&sort=name,ASC
cart.getRelatedPage<PagedResourceCollection<Product>>('productsPage', {
pageParams: {
page: 1,
size: 40
},
params: {
testParam: 'test',
projection: 'productProjection'
},
sort: {
name: 'ASC'
},
// useCache: true | false, by default true
})
.subscribe((page: PagedResourceCollection<Product>) => {
const products: Array<Product> = page.resources;
/* can use page methods
page.first();
page.last();
page.next();
page.prev();
page.customPage();
*/
});
```
### PostRelation
Performing POST request with request body by relation link URL.
Method signature:
```
postRelation(relationName: string, requestBody: RequestBody<any>, options?: RequestOption): Observable<HttpResponse<any> | any>;
```
- `relationName` - resource relation name used to get request URL.
- `requestBody` - [RequestBody](#requestbody) contains request body and additional body options.
- `options` - [RequestOption](#requestoption) additional options applied to the request.
- `return value` - by default `raw response data` or Angular `HttpResponse` when `options` param has a `observe: 'response'` value.
##### Examples of usage ([given the presets](#resource-presets)):
```ts
// Performing POST request by the URL: http://localhost:8080/api/v1/cart/1/postExample
cart.postRelation('postExample', {
// In this case null values in someBody will be ignored
body: someBody
})
.subscribe((rawResult: any) => {
// some logic
});
```
With options:
```ts
// Performing POST request by the URL: http://localhost:8080/api/v1/cart/1/postExample?testParam=test
cart.postRelation('postExample', {
// In this case null values in someBody will be NOT ignored
body: someBody,
valuesOption: {
include: Include.NULL_VALUES
}
}, {
params: {
testParam: 'test'
},
observe: 'response'
})
.subscribe((response: HttpResponse<any>) => {
// some logic
});
```
### PatchRelation
Performing PATCH request with request body by relation link URL.
Method signature:
```
patchRelation(relationName: string, requestBody: RequestBody<any>, options?: RequestOption): Observable<HttpResponse<any> | any>;
```
- `relationName` - resource relation name used to get request URL.
- `requestBody` - [RequestBody](#requestbody) contains request body and additional body options.
- `options` - [RequestOption](#requestoption) additional options applied to the request.
- `return value` - by default `raw response data` or Angular `HttpResponse` when `options` param has a `observe: 'response'` value.
##### Examples of usage ([given the presets](#resource-presets)):
```ts
// Performing PATCH request by the URL: http://localhost:8080/api/v1/cart/1/patchExample
cart.patchRelation('patchExample', {
// In this case null values in someBody will be ignored
body: someBody
})
.subscribe((rawResult: any) => {
// some logic
});
```
With options:
```ts
// Performing PATCH request by the URL: http://localhost:8080/api/v1/cart/1/patchExample?testParam=test
cart.patchRelation('patchExample', {
// In this case null values in someBody will be NOT ignored
body: someBody,
valuesOption: {
include: Include.NULL_VALUES
}
}, {
params: {
testParam: 'test'
},
observe: 'response'
})
.subscribe((response: HttpResponse<any>) => {
// some logic
});
```
### PutRelation
Performing PUT request with request body by relation link URL.
Method signature:
```
putRelation(relationName: string, requestBody: RequestBody<any>, options?: RequestOption): Observable<HttpResponse<any> | any>;
```
- `relationName` - resource relation name used to get request URL.
- `requestBody` - [RequestBody](#requestbody) contains request body and additional body options.
- `options` - [RequestOption](#requestoption) additional options applied to the request.
- `return value` - by default `raw response data` or Angular `HttpResponse` when `options` param has a `observe: 'response'` value.
##### Examples of usage ([given the presets](#resource-presets)):
```ts
// Performing PUT request by the URL: http://localhost:8080/api/v1/cart/1/putExample
cart.putRelation('putExample', {
// In this case null values in someBody will be ignored
body: someBody
})
.subscribe((rawResult: any) => {
// some logic
});
```
With options:
```ts
// Performing PUT request by the URL: http://localhost:8080/api/v1/cart/1/putExample?testParam=test
cart.putRelation('putExample', {
// In this case null values in someBody will be NOT ignored
body: someBody,
valuesOption: {
include: Include.NULL_VALUES
}
}, {
params: {
testParam: 'test'
},
observe: 'response'
})
.subscribe((response: HttpResponse<any>) => {
// some logic
});
```
## Resource
Main resource class. Extend model classes with `Resource` class to have the ability to use resource methods.
The difference between the `Resource` type and [EmbeddedResource](#embeddedresource) is `Resource` class has a self link therefore it has an id property, `EmbeddedResource` has not.
`Resource` classes are `@Entity` server-side classes. [EmbeddedResource](#embeddedresource) classes are `@Embeddable` entities.
`Resource` class extend [BaseResource](#baseresource) with additional resource relations methods that used only with `Resource` type.
>Each `Resource` class should be declared with [@HateoasResource](#hateoasresource) decorator.
### AddCollectionRelation
Adding passed entities to the resource collection behind the relation name.
Used `POST` method with `'Content-Type': 'text/uri-list'`.
>This method **DOES NOT REPLACED** existing resources in the collection instead it adds new ones.
To replace collection resource with passed entities use [bindRelation](#bindrelation) method.
Method signature:
```
addCollectionRelation<T extends Resource>(relationName: string, entities: Array<T>): Observable<HttpResponse<any>>;
```
- `relationName` - resource relation name used to get request URL mapped to resource collection.
- `entities` - an array of entities that should be added to resource collection.
- `return value` - Angular `HttpResponse` result.
##### Examples of usage ([given the presets](#resource-presets)):
```ts
// Suppose product1 already exists with id = 1
const product1 = ...;
// Suppose product2 already exists with id = 2
const product2 = ...;
cart.addCollectionRelation('products', [product1, product2])
/*
Performing POST request by the URL: http://localhost:8080/api/v1/carts/1/products
Content-type: 'text/uri-list'
Body: [http://localhost:8080/api/v1/products/1, http://localhost:8080/api/v1/products/2]
*/
.subscribe((result: HttpResponse<any>) => {
// some logic
});
```
### BindRelation
Bounding the passed entity or collection of entities to this resource by the relation name.
Used `PUT` method with `'Content-Type': 'text/uri-list'`.
>This method also **REPLACED** existing resources in the collection by passed entities.
To add entities to collection resource use [addCollectionRelation](#addCollectionRelation) method.
Method signature:
```
bindRelation<T extends Resource>(relationName: string, entities: T | Array<T>): Observable<HttpResponse<any>>;
```
- `relationName` - resource relation name used to get request URL.
- `entities` - an array of entities that should be bound to resource.
- `return value` - Angular `HttpResponse` result.
##### Examples of usage ([given the presets](#resource-presets)):
With single resource relation:
```ts
// Suppose shopToBind already exists with id = 1
const shopToBind = ...;
cart.bindRelation('shop', shopToBind)
/*
Performing PUT request by the URL: http://localhost:8080/api/v1/carts/1/shop
Content-type: 'text/uri-list'
Body: http://localhost:8080/api/v1/shops/1
*/
.subscribe((result: HttpResponse<any>) => {
// some logic
});
```
With collection resource relation:
```ts
// Suppose product1 already exists with id = 1
const product1 = ...;
// Suppose product2 already exists with id = 2
const product2 = ...;
cart.bindRelation('products', [product1, product2])
/*
Performing PUT request by the URL: http://localhost:8080/api/v1/carts/1/products
Content-type: 'text/uri-list'
Body: [http://localhost:8080/api/v1/products/1, http://localhost:8080/api/v1/products/2]
*/
.subscribe((result: HttpResponse<any>) => {
// some logic
});
```
### UnbindRelation
Unbinding single resource relation behind resource name.
Used `DELETE` method to relation resource link URL.
>This method does not work with collection resource relations.
> To unbind collection resource relation use [UnbindCollectionRelation](#unbindCollectionRelation) method.
> To delete one resource from resource collection use [deleteRelation](#deleterelation) method.
Method signature:
```
unbindRelation<T extends Resource>(relationName: string): Observable<HttpResponse<any>>;
```
- `relationName` - resource relation name to unbind.
- `return value` - Angular `HttpResponse` result.
##### Examples of usage ([given the presets](#resource-presets)):
```ts
// Suppose cart already bound shop resource by relation name 'shop'
cart.unbindRelation('shop')
/*
Performing DELETE request by the URL: http://localhost:8080/api/v1/carts/1/shop
*/
.subscribe((result: HttpResponse<any>) => {
// some logic
});
```
### UnbindCollectionRelation
Unbinding all resources from resource collection behind resource name.
Used `PUT` method with `'Content-Type': 'text/uri-list'` and `EMPTY` body to clear relations.
>This method does not work with SINGLE resource relations.
> To delete single resource relations use [unbindRelation](#unbindrelation) or [deleteRelation](#deleterelation) methods.
> To delete one resource from collection use [deleteRelation](#deleterelation) method.
Method signature:
```
unbindCollectionRelation<T extends Resource>(relationName: string): Observable<HttpResponse<any>>;
```
- `relationName` - resource relation name used to get request URL.
- `return value` - Angular `HttpResponse` result.
##### Examples of usage ([given the presets](#resource-presets)):
```ts
/*
Performing PUT request by the URL: http://localhost:8080/api/v1/carts/1/products
Content-type: 'text/uri-list'
Body: ''
*/
cart.unbindCollectionRelation('products')
.subscribe((result: HttpResponse<any>) => {
// some logic
});
```
### DeleteRelation
Deleting resource relation.
For collection, means that only passed entity will be unbind from the collection.
For single resource, deleting relation the same as [unbindRelation](#unbindrelation) method.
>To delete all resource relations from collection use [unbindCollectionRelation](#unbindcollectionrelation) method.
Method signature:
```
deleteRelation<T extends Resource>(relationName: string, entity: T): Observable<HttpResponse<any>>;
```
- `relationName` - resource relation name used to get request URL.
- `entity` - entity to unbind.
- `return value` - Angular `HttpResponse` result.
##### Examples of usage ([given the presets](#resource-presets)):
```ts
// Performing DELETE request by the URL: http://localhost:8080/api/v1/carts/1/shop/1
// Suppose shopToDelete already exists with id = 1
const shopToDelete = ...;
cart.deleteRelation('shop', shopToDelete)
.subscribe((result: HttpResponse<any>) => {
// some logic
});
```
## EmbeddedResource
This resource type uses when a server-side entity is [@Embeddable](https://docs.jboss.org/hibernate/orm/5.0/userguide/html_single/chapters/domain/embeddables.html).
It means that this entity has not an id property and can't exist standalone.
Because embedded resources have not an id then it can use only [BaseResource](#baseresource) methods.
>Each `EmbeddedResource` class should be declared with [@HateoasEmbeddedResource](#hateoasembeddedresource) decorator.
## ResourceCollection
This resource type represents collection of resources.
You can get this type as result [GetRelatedCollection](#getrelatedcollection), [GetResourceCollection](#getresourcecollection) or perform [CustomQuery](#customquery)/[CustomSearchQuery](#customsearchquery) with passed return type as `ResourceCollection`.
Resource collection holds resources in the public property with the name `resources`.
## PagedResourceCollection
This resource type represents paged collection of resources.
You can get this type as result [GetRelatedPage](#getrelatedpage), [GetPage](#getpage) or perform [CustomQuery](#customquery)/[CustomSearchQuery](#customsearchquery) with passed return type as PagedResourceCollection.
PagedResourceCollection extends [ResourceCollection](#resourcecollection) type and adds methods to work with a page.
### Default page values
When you do not pass `page` or `size` params in methods with [PagedGetOption](#pagedgetoption) then used [default page values](#default-page-values).
>Also, you can set up own default page params through [configuration](#pagination-params).
### HasFirst
Checks that `PagedResourceCollection` has the link to get the first-page result.
Method signature:
```
hasFirst(): boolean;
```
- `return value` - `true` when the link to get the first page exists, `false` otherwise.
### Ha