3.8.1 • Published 5 months ago

@lagoshny/ngx-hateoas-client v3.8.1

Weekly downloads
6
License
MIT
Repository
github
Last release
5 months ago

NgxHateoasClient

Compatible with Angular 10.

This client can be used to develop Angular 4.3+ applications working with RESTful server API. By RESTful API means when the server application implements all the layers of the Richardson Maturity Model and the server provides HAL/JSON response type.

This client compatible with Java server-side applications based on Spring HATEOAS or Spring Data REST.

This client is a continuation of the @lagoshny/ngx-hal-client. You can find out about the motivation to create a new client here. To migrate from @lagoshny/ngx-hal-client to this client you can use the migration guide.

Contents

  1. Changelog
  2. Getting started
  3. Resource types
  4. Resource service
  5. Settings
  6. Public classes

Changelog

Learn about the latest improvements.

Getting started

Installation

To install the latest version use command:

npm i @lagoshny/ngx-hateoas-client --save

Configuration

Before start, configure NgxHateoasClientModule and pass configuration through HateoasConfigurationService.

1) NgxHalClientModule configuration:

import { NgxHateoasClientModule } from '@lagoshny/ngx-hateoas-client';

...

@NgModule({
  ...
  imports: [
    ...
    NgxHateoasClientModule.forRoot()
    ...
  ]
  ...
})
export class AppModule {
  ...
}

2) In constructor app root module inject HalConfigurationService and pass a configuration:

import { ..., HateoasConfigurationService } from '@lagoshny/ngx-hateoas-client';

...

export class AppModule {

  constructor(hateoasConfig: HateoasConfigurationService) {
    hateoasConfig.configure({
      http: {
        rootUrl: 'http://localhost:8080/api/v1'
      }
    });
  }

}

Configuration has only one required param is rootUrl mapped to the server API URL. Also, you can configure proxyUrl when use it in resource links. See more about other a configuration params here.

Usage

Define resource classes

To represent model class as a resource model extend model class by Resource class. Suppose you have some Product model class:

export class Product {

    public name: string;

    public cost: number;

}

When extending it with Resource class it will look like:

import { Resource } from '@lagoshny/ngx-hateoas-client';

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.

Also, you can extend model classes with the EmbeddedResource class when the model class used as an embeddable entity. You can read more about EmbeddedResource here.

To perform resource requests you can use built-in HateoasResourceService or a create custom resource service.

Built-in HateoasResourceService

The library has built-in HateoasResourceService. It is a simple service with methods to get/create/update/delete resources.

To use it injecting HateoasResourceService to a component or a service class and set the resource type to a generic param.

@Component({
  ...
})
export class SomeComponent {

  constructor(private hateoasProductService: HateoasResourceService<Product>) {
  }

  onSomeAction() {
    const product = new Product();
    product.cost = 100;
    product.name = 'Fruit';

    this.hateoasProductService.createResource('product', product)
            .subscribe((createdResource: Product) => {
                // TODO something
            });
  };

}

Each HateoasResourceService method has the first param is the resource name that should be equals to the resource name in backend API. The resource name uses to build a URL for resource requests.

More about available HateoasResourceService methods see here.

HateoasResourceService is the best choice for simple resources that has not extra logic for requests. When you have some logic that should be preparing resource before a request, or you do not want always pass the resource name as first method param you can create a custom resource service extends HateoasResourceOperation to see more about this here.

Create custom Resource service

To create custom resource service create a new service and extends it with HateoasResourceOperation and pass resourceName to parent constructor.

import { HateoasResourceOperation } from '@lagoshny/ngx-hateoas-client';

@Injectable({providedIn: 'root'})
export class ProductService extends HateoasResourceOperation<Product> {

  constructor() {
    super('products');
  }

}

HateoasResourceOperation has the same methods as HateoasResourceService without resourceName as the first param.

Resource types

There are several types of resources, the main resource type is Resource represents the server-side entity model class. If the server-side model has Embeddable entity type then use EmbeddedResource type instead Resource type.

Both Resource and EmbeddedResource have some the same methods therefore they have common parent BaseResource class implements these methods.

To work with resource collections uses ResourceCollection type its holds an array of the resources. When you have a paged resource collection result use an extension of ResourceCollection is 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.

Resource presets

Examples of usage resource relation methods rely on presets.

  • Server root url = http://localhost:8080/api/v1

  • Resource classes are

    import { Resource } from '@lagoshny/ngx-hal-client';
    
    export class Cart extends Resource {
    
        public shop: Shop;
    
        public products: Array<Product>;
    
        public status: string;
    
        public client: PhysicalClient;
    
    }
    
    export class Shop extends Resource {
    
        public name: string;
    
        public rating: number;
    
    }
     
    export class Product extends Resource {
    
        public name: string;
    
        public cost: number;
    
        public description: string;
    
    }
    
    export class Client extends Resource {
    
      public address: string;
    
    }
    
    export class PhysicalClient extends Client {
    
      public fio: string;
    
    }
    
    export class JuridicalClient extends Client {
    
      public inn: string;
    
    }
  • Suppose we have existed resources:

    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 and EmbeddedResource classes. Contains common resource methods to work with resource relations through resource links (see below).

GetRelation

Getting resource relation object by relation name.

This method takes 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 additional options applied to the request.
  • return value - 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.
Examples of usage (given the presets):
// 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:

// 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, when the cache is enabled then by default true, false otherwise
})
  .subscribe((shop: Shop) => {
    // some logic        
  });

GetRelatedCollection

Getting related resource collection by relation name.

This method takes 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 additional options applied to the request.
  • return value - 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.
Examples of usage (given the presets):
// 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:

// 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, when the cache is enabled then by default true, false otherwise
})
  .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 parameter with it you can pass projection param (see below).

If do not pass pageParams with PagedGetOption then will be used default page options.

Method signature:

getRelatedPage<T extends PagedResourceCollection<BaseResource>>(relationName: string, options?: PagedGetOption): Observable<T>;
  • relationName - resource relation name used to get request URL.
  • options - PagedGetOption additional options applied to the request, if not passed pageParams then used default page params.
  • return value - 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.
Examples of usage (given the presets):
// 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:

// 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
})
  .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 contains request body and additional body options.
  • options - 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):
// 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:

// 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 contains request body and additional body options.
  • options - 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):
// 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:

// 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 contains request body and additional body options.
  • options - 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):
// 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:

// 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 is Resource class has a self link therefore it has an id property, EmbeddedResource has not. Resource classes are @Entity server-side classes. EmbeddedResource classes are @Embeddable entities.

Resource class extend BaseResource with additional resource relations methods that used only with Resource type.

IsResourceOf

Uses when resource has sub-types, and you want to know what subtype current resource has. Read more about sub-types here.

Each Resource has a private property is resourceName that calculated by the URL which resource was get. Suppose to get Cart resource used the next URL: http://localhost:8080/api/v1/carts/1. Then Cart.resourceName will be equals to carts because this part of the URL represents the resource name.

Method signature:

isResourceOf<T extends Resource>(typeOrName: (new () => T) | string): boolean;
  • typeOrName - resource type, or string that represent resource name. If you pass resource type for example someResource.isResourceOf(CartPayment) then class name will be used to compare with the resource name (ignoring letter case). If you pass resource name as a string then it will be used to compare with resource name with (ignoring letter case).
  • return value - true when resource name equals passed value, false otherwise.
Examples of usage (given the presets):
// Suppose was perform GET request to get the Cart resource by the URL: http://localhost:8080/api/v1/carts/1

cart.isResourceOf('carts'); // return TRUE
cart.isResourceOf('cart'); // return FALSE
cart.isResourceOf(Cart); // return FALSE because Cart class constructor name is 'cart'

AddRelation

Adding passed entities (they should exist on the server) to the resource collection behind the relation name.

Method signature:

addRelation<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):
/* 
 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]
*/
// Suppose product1 already exists with id = 1
const product1 = ...;
// Suppose product2 already exists with id = 2
const product2 = ...;

cart.addRelation('products', [product1, product2])
  .subscribe((result: HttpResponse<any>) => {
     // some logic            
  });

UpdateRelation

Updating an entity value by relation link URL.

Method signature:

updateRelation<T extends Resource>(relationName: string, entity: T): Observable<HttpResponse<any>>;
  • relationName - resource relation name used to get request URL.
  • entity - new entity value.
  • return value - Angular HttpResponse result.
Examples of usage (given the presets):
/* 
 Performing PATCH request by the URL: http://localhost:8080/api/v1/carts/1/shop
 Content-type: 'text/uri-list'
 Body: http://localhost:8080/api/v1/shops/2
*/
// Suppose newShop already exists with id = 2
const newShop = ...;
cart.updateRelation('shop', newShop)
  .subscribe((result: HttpResponse<any>) => {
     // some logic            
  });

BindRelation

Binding the passed entity to this resource by relation link URL.

Method signature:

bindRelation<T extends Resource>(relationName: string, entity: T): Observable<HttpResponse<any>>;
  • relationName - resource relation name used to get request URL.
  • entity - entity to bind.
  • return value - Angular HttpResponse result.
Examples of usage (given the presets):
/* 
 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
*/
// Suppose shopToBind already exists with id = 1
const shopToBind = ...;
cart.bindRelation('shop', shopToBind)
  .subscribe((result: HttpResponse<any>) => {
     // some logic            
  });

ClearCollectionRelation

Unbinding all resources from resource collection behind resource name.

Method signature:

clearCollectionRelation<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):
/* 
 Performing PUT request by the URL: http://localhost:8080/api/v1/carts/1/products
 Content-type: 'text/uri-list'
 Body: ''
*/
cart.clearCollectionRelation('products')
  .subscribe((result: HttpResponse<any>) => {
     // some logic            
  });

DeleteRelation

Unbinding resource relation entity by relation link URL.

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):
// 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. 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 methods.

ResourceCollection

This resource type represents collection of resources. You can get this type as result GetRelatedCollection, GetResourceCollection or perform CustomQuery/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, GetResourcePage or perform CustomQuery/CustomSearchQuery with passed return type as PagedResourceCollection.

PagedResourceCollection extends 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 then used default values: page = 0, size = 20.

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.

HasLast

Checks that PagedResourceCollection has the link to get the last page result.

Method signature:

hasLast(): boolean;
  • return value - true when the link to get the last page exists, false otherwise.

HasNext

Checks that PagedResourceCollection has the link to get the next page result.

Method signature:

hasNext(): boolean;
  • return value - true when the link to get the next page exists, false otherwise.

HasPrev

Checks that PagedResourceCollection has the link to get the previous page result.

Method signature:

hasPrev(): boolean;
  • return value - true when the link to get the prev page exists, false otherwise.

First

Performing a request to get the first-page result by the first-page link.

Method signature:

first(options?: {useCache: true;}): Observable<PagedResourceCollection<T>>;
  • options - additional options to manipulate the cache when getting a result (by default will be used the cache if it enabled in the configuration).
  • return value - PagedResourceCollection with resource types T.
  • throws error - when the link to get the first-page result is not exist.
Examples of usage:

Suppose products have 3 pages with 20 resources per page, and the previous request was to get products with a page number = 1.

To get the first products page, will perform request to page number = 0 with current or default page size (if before page size not passed).

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=0&size=20
const pagedProductCollection = ...;
pagedProductCollection.first()
  .subscribe((firstPageResult: PagedResourceCollection<Product>) => {
     // firstPageResult can be fetched from the cache if before was performing the same request
     // some logic        
  });

With options:

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=0&size=20
const pagedProductCollection = ...;
pagedProductCollection.first({useCache: false})
  .subscribe((firstPageResult: PagedResourceCollection<Product>) => {
     // firstPageResult always will be fetched from the server because the cache is disabled for this request
     // some logic        
  });

Last

Performing a request to get the last-page result by the last-page link.

Method signature:

last(options?: {useCache: true;}): Observable<PagedResourceCollection<T>>;
  • options - additional options to manipulate the cache when getting a result (by default will be used the cache if it enabled in the configuration).
  • return value - PagedResourceCollection with resource types T.
  • throws error - when the link to get the last-page result is not exist.
Examples of usage:

Suppose products have 3 pages with 20 resources per page, and the previous request was to get products with a page number = 1.

To get the last products page, will perform request to page number = 2 with current or default page size (if before page size not passed).

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=2&size=20
const pagedProductCollection = ...;
pagedProductCollection.last()
  .subscribe((lastPageResult: PagedResourceCollection<Product>) => {
     // lastPageResult can be fetched from the cache if before was performing the same request
     // some logic        
  });

With options:

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=2&size=20
const pagedProductCollection = ...;
pagedProductCollection.last({useCache: false})
  .subscribe((lastPageResult: PagedResourceCollection<Product>) => {
     // lastPageResult always will be fetched from the server because the cache is disabled for this request
     // some logic        
  });

Next

Performing a request to get the next-page result by the next-page link.

Method signature:

next(options?: {useCache: true;}): Observable<PagedResourceCollection<T>>;
  • options - additional options to manipulate the cache when getting a result (by default will be used the cache if it enabled in the configuration).
  • return value - PagedResourceCollection with resource types T.
  • throws error - when the link to get the next-page result is not exist.
Examples of usage:

Suppose products have 3 pages with 20 resources per page, and the previous request was to get products with a page number = 1.

To get the next products page, will perform request to page number = 2 with current or default page size (if before page size not passed).

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=1&size=20
const pagedProductCollection = ...;
pagedProductCollection.next()
  .subscribe((nextPageResult: PagedResourceCollection<Product>) => {
     // nextPageResult can be fetched from the cache if before was performing the same request
     // some logic        
  });

With options:

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=1&size=20
const pagedProductCollection = ...;
pagedProductCollection.next({useCache: false})
  .subscribe((nextPageResult: PagedResourceCollection<Product>) => {
     // nextPageResult always will be fetched from the server because the cache is disabled for this request
     // some logic        
  });

Prev

Performing a request to get the prev-page result by the prev-page link.

Method signature:

prev(options?: {useCache: true;}): Observable<PagedResourceCollection<T>>;
  • options - additional options to manipulate the cache when getting a result (by default will be used the cache if it enabled in the configuration).
  • return value - PagedResourceCollection with resource types T.
  • throws error - when the link to get the prev-page result is not exist.
Examples of usage:

Suppose products have 3 pages with 20 resources per page, and the previous request was to get products with a page number = 1.

To get the prev products page, will perform request to page number = 0 with current or default page size (if before page size not passed).

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=0&size=20
const pagedProductCollection = ...;
pagedProductCollection.prev()
  .subscribe((prevPageResult: PagedResourceCollection<Product>) => {
     // prevPageResult can be fetched from the cache if before was performing the same request
     // some logic        
  });

With options:

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=0&size=20
const pagedProductCollection = ...;
pagedProductCollection.prev({useCache: false})
  .subscribe((prevPageResult: PagedResourceCollection<Product>) => {
     // prevPageResult always will be fetched from the server because the cache is disabled for this request
     // some logic        
  });

Page

Performing a request to get the page with passed page number and current or default page size (if before page size not passed).

To pass page number, page size, sort params together use customPage method.

Method signature:

page(pageNumber: number, options?: {useCache: true;}): Observable<PagedResourceCollection<T>>;
  • pageNumber - number of the page to get.
  • options - additional options to manipulate the cache when getting a result (by default will be used the cache if it enabled in the configuration).
  • return value - PagedResourceCollection with resource types T.
  • throws error - when pageNumber greater than total pages.
Examples of usage:

Suppose products have 5 pages with 20 resources per page, and the previous request was to get products with a page number = 1.

To get the products page = 3, will perform request to page number = 3 with current or default page size (if before page size not passed).

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=3&size=20
const pagedProductCollection = ...;
pagedProductCollection.page(3)
  .subscribe((customPageResult: PagedResourceCollection<Product>) => {
     // customPageResult can be fetched from the cache if before was performing the same request
     // some logic        
  });

With options:

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=3&size=20
const pagedProductCollection = ...;
pagedProductCollection.page(3, {useCache: false})
  .subscribe((customPageResult: PagedResourceCollection<Product>) => {
     // customPageResult always will be fetched from the server because the cache is disabled for this request
     // some logic        
  });

Size

Performing a request to get the page with passed page size and current or default page number (if before page number not passed).

To pass page number, page size, sort params together use customPage method.

Method signature:

size(size: number, options?: {useCache: true;}): Observable<PagedResourceCollection<T>>;
  • size - count of resources to page.
  • options - additional options to manipulate the cache when getting a result (by default will be used the cache if it enabled in the configuration).
  • return value - PagedResourceCollection with resource types T.
  • throws error - when size greater than total count resources.
Examples of usage:

Suppose products have 5 pages with 20 resources per page, and the previous request was to get products with a page number = 1, size = 20.

To increase the current page size to 50, will perform a request to the current page number with page size = 50.

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=1&size=50
const pagedProductCollection = ...;
pagedProductCollection.size(50)
  .subscribe((customPageResult: PagedResourceCollection<Product>) => {
     // customPageResult can be fetched from the cache if before was performing the same request
     // some logic        
  });

With options:

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=1&size=50
const pagedProductCollection = ...;
pagedProductCollection.size(50, {useCache: false})
  .subscribe((customPageResult: PagedResourceCollection<Product>) => {
     // customPageResult always will be fetched from the server because the cache is disabled for this request
     // some logic        
  });

SortElements

Sorting the current page result.

To pass page number, page size, sort params together use customPage method.

Method signature:

sortElements(sortParam: Sort, options?: {useCache: true;}): Observable<PagedResourceCollection<T>>;
  • sortParam - Sort params.
  • options - additional options to manipulate the cache when getting a result (by default will be used the cache if it enabled in the configuration).
  • return value - PagedResourceCollection with resource types T.
Examples of usage:

Suppose products have 5 pages with 20 resources per page, and the previous request was to get products with a page number = 1, size = 20.

To sort the current page result, will perform a request to the current page number with the current page size and passed sort params.

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=1&size=20&sort=cost,ASC&sort=name,DESC
const pagedProductCollection = ...;
pagedProductCollection.sortElements({cost: 'ASC', name: 'DESC'})
  .subscribe((customPageResult: PagedResourceCollection<Product>) => {
     // customPageResult can be fetched from the cache if before was performing the same request
     // some logic        
  });

With options:

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=1&size=20&sort=cost,ASC&sort=name,DESC
const pagedProductCollection = ...;
pagedProductCollection.sortElements({cost: 'ASC', name: 'DESC'}, {useCache: false})
  .subscribe((customPageResult: PagedResourceCollection<Product>) => {
     // customPageResult always will be fetched from the server because the cache is disabled for this request
     // some logic        
  });

CustomPage

Performing a request to get the page with custom page number, page size, and sort params.

Method signature:

customPage(params: SortedPageParam, options?: {useCache: true;}): Observable<PagedResourceCollection<T>>;
  • params - SortedPageParam page and sort params.
  • options - additional options to manipulate the cache when getting a result (by default will be used the cache if it enabled in the configuration).
  • return value - PagedResourceCollection with resource types T.
  • throws error - when the page size, greater than total count resources or page number greater than total pages.

When pass only part of the params then used default page params for not passed ones.

Examples of usage:
// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=2&size=30&sort=name,ASC
const pagedProductCollection = ...;
pagedProductCollection.customPage({
    pageParams: {
      page: 2,
      size: 30
    },
    sort: {
      name: 'ASC'
    }
  })
  .subscribe(value1 => {
     // customPageResult can be fetched from the cache if before was performing the same request
     // some logic      
  });

With options:

// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=2&size=30&sort=name,ASC
const pagedProductCollection = ...;
pagedProductCollection.customPage({
    pageParams: {
      page: 2,
      size: 30
    },
    sort: {
      name: 'ASC'
    }
  },
  {useCache: true})
  .subscribe(value1 => {
     // customPageResult always will be fetched from the server because the cache is disabled for this request
     // some logic  
  });

Subtypes support

The library allows work with entities hierarchy.

Suppose exists the next resource's hierarchy:

 import { Resource } from '@lagoshny/ngx-hal-client';
  
  export class Cart extends Resource {
  
      public client: Client;
  
  }
  
  export class Client extends Resource {
  
    public address: string;
  
  }
  
  export class PhysicalClient extends Client {
  
    public fio: string;
  
  }
  
  export class JuridicalClient extends Client {
  
    public inn: string;
  
  }
 

With hal-json representation:

  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
      },
      "client": {
        "href": "http://localhost:8080/api/v1/carts/1/physicalClient"
      }
    }
  }
  
  PhysicalClient:
  {
    "fio": "Some fio",
    "_links": {
      "self": {
        "href": "http://localhost:8080/api/v1/physicalClients/1"
      },
      "physicalClient": {
        "href": "http://localhost:8080/api/v1/physicalClients/1"
      }
    }
  }

From the example, above can note that the Cart resource has the client property with type Client. In its turn, client can have one of the types PhysicalClient or JuridicalClient.

You can use Resource.isResourceOf method to know what client resource type you got.

Examples of usage:
// Suppose exists cart resource and after getting client relation need to know what is the client type
const cart = ...
cart.getRelation('client')
  .subscribe((client: Client) => {
    if (client.isResourceOf('physicalClients')) {
      const physicalClient = client as PhysicalClient;
    // some logic        
    } else if (client.isResourceOf('juridicalClients')) {
      const juridicalClient = client as JuridicalClient;
    // some logic        
    }
  });

Resource service

As described before to work with resources you can use built-in HateoasResourceService or create custom resource service.

Difference in methods signature between built-in HateoasResourceService and custom resource service is built-in service always has a resource name as the first method param but can use without creating custom resource service

Resource service presets

Examples of usage resource service methods rely on this presets.

  • Server root url = http://localhost:8080/api/v1
  • Resource class is

    import { Resource } from '@lagoshny/ngx-hal-client';
    
    export class Product extends Resource {
    
        public name: string;
    
        public cost: number;
    
        public description: string;
    
    }
  • Resource service as built-in HateoasResourceService is

    @Component({ ... })
    export class AppComponent {
        constructor(private productHateoasService: HateoasResourceService<Product>) {
        }
    }
  • Resource service as custom resource service is

    import { HalResourceOperation } from '@lagoshny/ngx-hal-client';
    import { Product } from '../model/product.model';
    
    @Injectable({providedIn: 'root'})
    export class ProductService extends HalResourceOperation<Product> {
      constructor() {
        super('products');
      }
    }
    
    @Component({ ... })
    export class AppComponent {
        constructor(private productService: ProductService) {
        }
    }

No matter which service used both have the same resource methods.

GetResource

Getting one resource Resource.

This method takes GetOption parameter with it you can pass projection param.

Method signature:

getResource(id: number | string, options?: GetOption): Observable<T>;
  • id - resource id to get.
  • options - GetOption additional options applied to the request.
  • return value - Resource with type T.
  • throws error when returned value is not Resource.
Example of usage (given the presets):
// Performing GET request by the URL: http://localhost:8080/api/v1/products/1
this.productService.getResource(1)
    .subscribe((product: Product) => {
        // some logic
    })

this.productHateoasService.getResource('products', 1)
    .subscribe((product: Product) => {
        // some logic
    })

With options:

// Performing GET request by the URL: http://localhost:8080/api/v1/products/1?testParam=test&projection=productProjection&sort=cost,ASC
this.productService.getResource(1, {
  params: {
    testParam: 'test',
    projection: 'productProjection',
  },
  sort: {
    cost: 'ASC'
  },
  // useCache: true | false, when the cache is enabled then by default true, false otherwise
}).subscribe((product: Product) => {
    // some logic
})

this.productHateoasService.getResource('products', 1, {
  params: {
    testParam: 'test',
    projection: 'productProjection',
  },
  sort: {
    cost: 'ASC'
  },
  // useCache: true | false, when the cache is enabled then by default true, false otherwise
}).subscribe((product: Product) => {
    // some logic
})

GetCollection

Getting collection of resources ResourceCollection. This method takes GetOption parameter with it you can pass projection param.

Method signature:

getCollection(options?: GetOption): Observable<ResourceCollection<T>>;
Example of usage (given the presets):
// Performing GET request by the URL: http://localhost:8080/api/v1/products
this.productService.getCollection()
    .subscribe((collection: ResourceCollection<Product>) => {
        const products: Array<Product> = collection.resources;
        // some logic
    })

this.productHateoasService.getCollection('products')
    .subscribe((collection: ResourceCollection<Product>) => {
        const products: Array<Product> = collection.resources;
        // some logic
    })

With options:

// Performing GET request by the URL: http://localhost:8080/api/v1/products?testParam=test&projection=productProjection&sort=cost,ASC
this.productService.getCollection({
  params: {
    testParam: 'test',
    projection: 'productProjection',
  },
  sort: {
    cost: 'ASC'
  },
  // useCache: true | false, when the cache is enabled then by default true, false otherwise
}).subscribe((collection: ResourceCollection<Product>) => {
    const products: Array<Product> = collection.resources;
    // some logic
})

this.productHateoasService.getCollection('products', {
  params: {
    testParam: 'test',
    projection: 'productProjection',
  },
  sort: {
    cost: 'ASC'
  },
  // useCache: true | false, when the cache is enabled then by default true, false otherwise
}).subscribe((collection: ResourceCollection<Product>) => {
    const products: Array<Product> = collection.resources;
    // some logic
})

GetPage

Getting paged collection of resources PagedResourceCollection.

This method takes 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.

Method signature:

getPage(options?: PagedGetOption): Observable<PagedResourceCollection<T>>;
Example of usage (given the presets):
// Performing GET request by the URL: http://localhost:8080/api/v1/products?page=0&size=20
this.productService.getPage()
    .subscribe((page: PagedResourceCollection<Product>) => {
        const products: Array<Product> = page.resources;
        /* can use page methods
           page.first();
           page.last();
           page.next();
           page.prev();
           page.customPage();
        */    
    });

this.productHateoasService.getPage('products')
    .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:

// Performing GET request by the URL: http://localhost:8080/api/v1/products?testParam=test&projection=productProjection&page=1&size=40&sort=cost,ASC
this.productService.getPage({
  pageParams: {
    page: 1,
    size: 40
  },
  params: {
    testParam: 'test',
    projection: 'productProjection',
  },
  sort: {
    cost: 'ASC'
  },
  // useCache: true | false, when the cache is enabled then by default true, false otherwise
}).subscribe((page: PagedResourceCollection<Product>) => {
    const products: Array<Product> = page.resources;
    /* can use page methods
       page.first();
       page.last();
       page.next();
       page.prev();
       page.customPage();
    */  
});

this.productHateoasService.getPage('products', {
  pageParams: {
    page: 1,
    size: 40
  },
  params: {
    testParam: 'test',
    projection: 'productProjection',
  },
  sort: {
    cost: 'ASC'
  },
  // useCache: true | false, when the cache is enabled then by default true, false otherwise
}).subscribe((page: PagedResourceCollection<Product>) => {
    const products: Array<Product> = page.resources;
    /* can use page methods
       page.first();
       page.last();
       page.next();
       page.prev();
       page.customPage();
    */  
});

CreateResource

Creating new resource Resource.

Method signature:

createResource(requestBody: RequestBody<T>): Observable<T | any>;
  • requestBody - RequestBody contains request body (in this case resource object) and additional body options.
  • return value - Resource with type T or raw response data when returned value is not resource object.
Example of usage (given the presets):
/*
Performing POST request by the URL: http://localhost:8080/api/v1/products
Request body:
{
  "name": "Apple",
  "cost": 100
}
Note: description is not passed because by default null values ignore. If you want pass description = null value then need to pass additional valuesOption params.
*/
const newProduct = new Product();
newProduct.cost = 100;
newProduct.name = 'Apple';
newProduct.description = null;
this.productService.createResource({
  body: newProduct
}).subscribe((createdProduct: Product) => {
    // some logic 
});

this.productHateoasService.createResource('products', {
  body: newProduct
}).subscribe((createdProduct: Product) => {
    // some logic 
});

With options:

/*
Performing POST request by the URL: http://localhost:8080/api/v1/products
Request body:
{
  "name": "Apple",
  "cost": 100,
  "description": null
}
Note: description is passed with null value because valuesOption = Include.NULL_VALUES was passed.
*/ 
const newProduct = new Product();
newProduct.cost = 100;
newProduct.name = 'Apple';
newProduct.description = null;
this.productService.createResource({
  body: newProduct,
  valuesOption: {
    include: Include.NULL_VALUES
  }
}).subscribe((createdProduct: Product) => {
    // some logic 
});

this.productHateoasService.createResource('products', {
  body: newProduct,
  valuesOption: {
    include: Include.NULL_VALUES
  }
}).subscribe((createdProduct: Product) => {
    // some logic 
});

UpdateResource

Updating all values of an existing resource at once by resource self link URL.

For not passed values of resource, null values will be used.

To update a resource performs PUT request by the URL equals to resource self link passed in entity param.

To update a resource by an id directly use UpdateResourceById.

To update part of the values of resource use PatchResource method.

Method signature:

updateResource(entity: T, requestBody?: RequestBody<any>): Observable<T | any>;
  • entity - resource to update.
  • requestBody - RequestBody contains request body (in this case new values for resource) and additional body options.
  • return value - Resource with type T or raw response data when returned value is not resource object.

When passed only entity param then values of entity will be used to update values of resource.

Example of usage (given the presets):
/*
Suppose exitsProduct has a self link = http://localhost:8080/api/v1/products/1
Performing PUT request by the URL: http://localhost:8080/api/v1/products/1
Request body:
{
  "name" = exitsProduct.name,
  "cost": 500
}
Note: 
1) Description is not passed because by default null values ignore. If you want pass description = null value then need to pass additional valuesOption params.
2) Since update resource updating all resource values at once and for description value is not passing then the server-side can overwrite description to null. 
*/
const exitsProduct = ...;
exitsProduct.cost = 500;
exitsProduct.description = null;
this.productService.updateResource(exitsProduct)
  .subscribe((updatedProduct: Product) => {
    // some logic 
});
// For productHateoasService this snippet is identical

With options:

/*
Suppose exitsProduct has a self link = http://localhost:8080/api/v1/products/1
Performing PUT request by the URL: http://localhost:8080/api/v1/products/1
Request body:
{
  "name": null,
  "cost": 500
}
Note: 
1) Name was passed with null value because valuesOption = Include.NULL_VALUES was passed.
2) Since update resource updating all resource values at once and for description value is not passing then the server-side can overwrite description to null. 
*/
const exitsProduct = ...;
this.productService.updateResource(exitsProduct, {
  body: {
    name: null,
    cost: 500
  },
  valuesOption: {
    include: Include.NULL_VALUES
  }
}).subscribe((updatedProduct: Product) => {
    // some logic 
});
// For productHateoasService this snippet is identical

UpdateResourceById

Updating all values of an existing resource at once by resource id.

To update a resource by resource self link URL use UpdateResource.

To update part of the values of resource use PatchResource method.

Method signature:

updateResourceById(id: number | string, requestBody: RequestBody<any>): Observable<T | any>;
  • id - resource id to update.
  • requestBody - RequestBody contains request body (in this case new values for resource) and additional body options.
  • return value - Resource with type T or raw response data when returned value is not resource object.
Example of usage (given the presets):
/*
Suppose exitsProduct has an id = 1
Performing PUT request by the URL: http://localhost:8080/api/v1/products/1
Request body:
{
  "name" = exitsProduct.name,
  "cost": 500
}
Note: 
1) Description is not passed because by default null values ignore. If you want pass description = null value then need to pass additional valuesOption params.
2) Since update resource updating all resource values at once and for description value is not passing then the server-side can overwrite description to null. 
*/
const exitsProduct = ...;
exitsProduct.cost = 500;
exitsProduct.description = null;
this.productService.updateResourceById(1, {
  body: {
    ...exitsProduct
  }
})
  .subscribe((updatedProduct: Product) => {
    // some logic 
});

this.productHateoasService.updateResourceById('products', 1, {
  body: {
    ...exitsProduct
  }
})
  .subscribe((updatedProduct: Product) => {
    // some logic 
});

With options:

/*
Suppose exitsProduct has an id = 1
Performing PUT request by the URL: http://localhost:8080/api/v1/products/1
Request body:
{
  "name": null,
  "cost": 500
}
Note: 
1) Name was passed with null value because valuesOption = Include.NULL_VALUES was passed.
2) Since update resource updating all resource values at once and for description value is not passing then the server-side can overwrite description to null.
*/
this.productService.updateResourceById(1, {
  body: {
    name: null,
    cost: 500
  },
  valuesOption: {
    include: Include.NULL_VALUES
  }
})
  .subscribe((updatedProduct: Product) => {
    // some logic 
});

this.productHateoasService.updateResourceById('products', 1, {
  body: {
    name: null,
    cost: 500
  },
  valuesOption: {
    include: Include.NULL_VALUES
  }
})
  .subscribe((updatedProduct: Product) => {
    // some logic 
});

PatchResource

Patching part values of an existing resource by resource self link URL.

To patch a resource performs PATCH request by the URL equals to resource self link passed in entity param.

To patch a resource by an id directly use PatchResourceById.

To update all values of the resource at once use UpdateResource method.

Method signature:

patchResource(entity: T, requestBody?: RequestBody<any>): Observable<T | any>;
  • entity - resource to patch.
  • requestBody - RequestBody contains request body (in this case new values for resource) and additional body options.
  • return value - Resource with type T or raw response data when returned value is not resource object.

When passed only entity param then values of entity will be used to patch values of resource.

Example of usage (given the presets):
/*
Suppose exitsProduct has a self link = http://localhost:8080/api/v1/products/1
Performing PATCH request by the URL: http://localhost:8080/api/v1/products/1
Request body:
{
  "name" = exitsProduct.name,
  "cost": 500
}
Note: 
1) Description is not passed because by default null values ignore. If you want pass description = null value then need to pass additional valuesOption params.
2) Since patch resource updating only part of resource values at once then all not passed values will have the old values.
*/
const exitsProduct = ...;
exitsProduct.cost = 500;
exitsProduct.description = null;
this.productService.patchResource(exitsProduct)
  .subscribe((patchedProduct: Product) => {
    // some logic 
});
// For productHateoasService this snippet is identical

With options:

/*
Suppose exitsProduct has a self link = http://localhost:8080/api/v1/products/1
Performing PATCH request by the URL: http://localhost:8080/api/v1/products/1
Request body:
{
  "name": null,
  "cost": 500
}
Note: 
1) Name was passed with null value because valuesOption = Include.NULL_VALUES was passed.
2) Since patch resource updating only part of resource values at once then all not passed values will have the old values.
*/
const exitsProduct = ...;
this.productService.patchResource(exitsProduct, {
  body: {
    name: null,
    cost: 500
  },
  valuesOption: {
    include: Include.NULL_VALUES
  }
}).subscribe((patchedProduct: Product) => {
    // some logic 
});
// For productHateoasService this snippet is identical

PatchResourceById

Patching part values of an existing resource by resource id.

To patch a resource by resource self link URL use UpdateResource.

To update all values of the resource at once use UpdateResource method.

Method signature:

patchResourceById(id: number | string, requestBody: RequestBody<any>): Observable<T | any>;
  • id - resource id to patch.
  • requestBody - RequestBody contains request body (in this case new values for reso
3.8.1

5 months ago

3.8.0

5 months ago

3.7.0

5 months ago

3.6.2

8 months ago

3.6.1

10 months ago

3.6.0

10 months ago

3.3.8

1 year ago

3.3.7

1 year ago

3.4.0

1 year ago

3.5.0

1 year ago

2.6.7

1 year ago

2.6.8

1 year ago

3.3.6

2 years ago

3.3.5

2 years ago

2.6.5

2 years ago

2.6.6

2 years ago

3.3.4

2 years ago

2.6.4

2 years ago

2.4.1

2 years ago

2.6.1

2 years ago

2.6.0

2 years ago

2.4.2

2 years ago

2.6.3

2 years ago

2.6.2

2 years ago

3.2.2

2 years ago

3.2.1

2 years ago

3.2.0

2 years ago

2.5.0

2 years ago

2.5.2

2 years ago

2.5.1

2 years ago

3.3.1

2 years ago

3.3.0

2 years ago

3.1.2

2 years ago

3.1.1

2 years ago

3.3.3

2 years ago

3.3.2

2 years ago

2.4.0

2 years ago

3.0.10

2 years ago

3.1.0

2 years ago

3.0.11

2 years ago

2.3.11

2 years ago

2.3.10

2 years ago

3.0.9

2 years ago

3.0.8

2 years ago

3.0.7

2 years ago

2.3.8

2 years ago

2.3.7

2 years ago

2.3.9

2 years ago

2.3.4

2 years ago

2.3.6

2 years ago

2.3.5

2 years ago

3.0.4

2 years ago

3.0.6

2 years ago

3.0.5

2 years ago

3.0.3

2 years ago

3.0.2

2 years ago

3.0.1

2 years ago

3.0.0

2 years ago

2.3.0

2 years ago

2.2.1

2 years ago

2.1.1

2 years ago

2.3.2

2 years ago

2.3.1

2 years ago

2.3.3

2 years ago

3.0.0-ivy

2 years ago

2.1.0

2 years ago

2.0.0

3 years ago

2.0.3-beta

3 years ago

2.0.0-beta

3 years ago

2.0.1-beta

3 years ago

2.0.2-beta

3 years ago

1.1.1

3 years ago

1.1.0

3 years ago

1.0.1

3 years ago

1.0.0

4 years ago

1.0.0-beta

4 years ago