@iceylan/restorm
Advanced tools
Comparing version 1.3.1 to 1.4.0
@@ -5,2 +5,15 @@ # Changelog | ||
## [1.4.0](https://github.com/ismailceylan/restorm/compare/v1.3.1...v1.4.0) (2024-05-05) | ||
### Features | ||
* add dynamic resource building ([ac3be37](https://github.com/ismailceylan/restorm/commit/ac3be3760d416aaff5e8d87a293eeef7c915613c)) | ||
### Bug Fixes | ||
* **model:** add hasInstance method to remove maximum call stack exceeded errors ([b0431cd](https://github.com/ismailceylan/restorm/commit/b0431cde61c525097832e23f522d4f98a73b8895)) | ||
* **query-builder:** temp resource occupation caused by the find method ([6bff726](https://github.com/ismailceylan/restorm/commit/6bff72663a0e176fd5e14958aa77ea9b2785c497)) | ||
### [1.3.1](https://github.com/ismailceylan/restorm/compare/v1.3.0...v1.3.1) (2024-03-29) | ||
@@ -7,0 +20,0 @@ |
{ | ||
"name": "@iceylan/restorm", | ||
"version": "1.3.1", | ||
"version": "1.4.0", | ||
"description": "Provides a robust interface for managing access to RESTful resources in JavaScript.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
251
README.md
@@ -12,3 +12,3 @@ # Introduction | ||
``` | ||
npm install restorm | ||
npm install @iceylan/restorm | ||
``` | ||
@@ -22,3 +22,3 @@ | ||
// base-model.js | ||
import { Model } from "restorm"; | ||
import { Model } from "@iceylan/restorm"; | ||
@@ -45,6 +45,14 @@ export default class BaseModel extends Model | ||
## Retreiving a List of Resources | ||
The `all` method returns a promise. | ||
There are two ways to get a list of resources. | ||
Upon the request being fulfilled, the list of resources sent by the server is instantiated with the `Post` model. These instances are then placed into a `collection`, which is filled into the awaiting promise. | ||
* `all` - Get all resources as collection under any circumstance. | ||
* `get` - Get all resources as collection or a single resource model. | ||
### Using all Method | ||
The `all` method creates a request to access all resources under the root directory of endpoint. | ||
It will make sure that even the response has only one resource, the resolved promise will be fulfilled with a collection and that collection will be filled with instance(s) of the model(s). | ||
If the response is empty, the promise will be fulfilled with an empty collection. | ||
```js | ||
@@ -58,7 +66,30 @@ const posts = await Post.all(); | ||
### Using get Method | ||
If we are sure that the endpoint is kind of a multiple resource returner or we are aware that we should have to deal with the results either as a collection or a single model then we can use `get` method. | ||
The method will detect the returned resource's type and returns a promise that will be fulfilled with an instance of `collection` of models or an instance of `model`. | ||
```js | ||
import { Collection } from "@iceylan/restorm"; | ||
const result = await Post.where( conditions ).get(); | ||
if( result instanceof Collection ) | ||
{ | ||
result.forEach( post => | ||
console.log( post.id ) | ||
); | ||
} | ||
``` | ||
``` | ||
GET /api/v1.0/posts | ||
``` | ||
## Retrieving a Single Resource | ||
There are two main methods that enable us to obtain a single resource. | ||
There are three methods that enable us to obtain a single resource. | ||
* `first` - Get the first resource in a resource collection. | ||
* `find` - Find a specific resource based on the primary key. | ||
* `get` - Get all resources as collection or a single resource model. | ||
@@ -89,2 +120,80 @@ ### Getting the First Resource | ||
### Using get Method | ||
The `get` method creates a request to access all resources under the root directory of restful endpoint with filtering criteria. The results may be a single resource or an array of resources. | ||
`get` will autodetect the returned resource type and return a promise that will be fulfilled with an instance of the `model` or `collection` of models. | ||
```js | ||
import { Model } from "@iceylan/restorm"; | ||
const result = await Post.where( conditions ).get(); | ||
if( result instanceof Model ) | ||
{ | ||
console.log( result.title ); | ||
} | ||
``` | ||
``` | ||
GET /api/v1.0/posts | ||
``` | ||
## Including Relationships | ||
The `with` method is used to include related resource to a model. This process is known as eager loading. We can provide as many relationship names as arguments to the method or as an array. | ||
We can directly include a related resource by its model: | ||
```js | ||
const posts = await Post | ||
.with( "comments", "author" ) | ||
// or | ||
.with([ "comments", "author" ]) | ||
.all(); | ||
``` | ||
``` | ||
GET /api/v1.0/posts?with=comments,author | ||
``` | ||
With that, the API endpoint will return the posts with their comments and authors. | ||
### Including Nested Relationships | ||
Even including nested relationships is possible. That means we can include the related resources of the related resources and so on. | ||
```js | ||
const posts = await Post.with( "author", "comments.author" ).all(); | ||
``` | ||
``` | ||
GET /api/v1.0/posts?with=author,comments.author | ||
``` | ||
The API endpoint should return the following response: | ||
```json | ||
[ | ||
{ | ||
"id": 1, | ||
"post": "lorem ipsum", | ||
"author": | ||
{ | ||
"id": 1, | ||
"name": "John Doe" | ||
}, | ||
"comments": | ||
[ | ||
{ | ||
"id": 1, | ||
"comment": "lorem ipsum", | ||
"author": | ||
{ | ||
"id": 1, | ||
"name": "Jane Doe" | ||
} | ||
} | ||
] | ||
} | ||
] | ||
``` | ||
## Filtering Resources | ||
@@ -96,3 +205,3 @@ To list specific resources, we add filters with `where` method to accomplish this. | ||
```js | ||
const posts = await Post.where( "type", "article" ).get(); | ||
const posts = await Post.where( "type", "article" ).all(); | ||
``` | ||
@@ -105,14 +214,12 @@ | ||
### Filtering Related Resource | ||
Restorm may request the inclusion of another resource related with the resources it will receive. We'll see this later. | ||
If the related resource that we included in by `with` method is kind of multiple (i.e., one-to-many), for example comments of a post, we can also indirectly add filters to these sub-resources (comments) to reduce the returned results. | ||
If the related resource is multiple (i.e., one-to-many), we can also indirectly add filters to these sub-resources. | ||
```js | ||
const posts = await Post | ||
.where( "type", "article" ) | ||
.with( "comments" ) | ||
.where( "type", "article" ) | ||
.where( "comments.state", "approved" ) | ||
// or | ||
.where([ "comments", "state" ], "approved" ) | ||
.get(); | ||
.all(); | ||
``` | ||
@@ -125,3 +232,3 @@ | ||
### Object Syntax | ||
We can perform these operations in a more organized and concise manner through object syntax, providing a cleaner and more streamlined usage. For example, if we are using something like vue.js or react.js, we can manage sorting operations on reactive objects and directly pass this object to the `where` method. | ||
We can perform these operations in a more organized and concise manner through object syntax, providing a cleaner and more streamlined usage. For example, if we are using something like vue.js or react.js, we can manage operations on reactive objects and directly pass this object to the `where` method. | ||
@@ -142,3 +249,3 @@ ```js | ||
const posts = await Post.with( "comments" ).where( conditions ).get(); | ||
const posts = await Post.with( "comments" ).where( conditions ).all(); | ||
``` | ||
@@ -242,60 +349,2 @@ | ||
## Including Relationships | ||
The `with` method is used to include related resource to a model. This process is known as eager loading. We can provide as many relationship names as arguments to the method or as an array. | ||
We can directly include a related resource by its model: | ||
```js | ||
const posts = await Post | ||
.with( "comments", "author" ) | ||
// or | ||
.with([ "comments", "author" ]) | ||
.all(); | ||
``` | ||
``` | ||
GET /api/v1.0/posts?with=comments,author | ||
``` | ||
With that, the API endpoint will return the posts with their comments and authors. | ||
### Including Nested Relationships | ||
Even including nested relationships is possible. That means we can include the related resources of the related resources and so on. | ||
```js | ||
const posts = await Post.with( "author", "comments.author" ).all(); | ||
``` | ||
``` | ||
GET /api/v1.0/posts?with=author,comments.author | ||
``` | ||
The API endpoint should return the following response: | ||
```json | ||
[ | ||
{ | ||
"id": 1, | ||
"post": "lorem ipsum", | ||
"author": | ||
{ | ||
"id": 1, | ||
"name": "John Doe" | ||
}, | ||
"comments": | ||
[ | ||
{ | ||
"id": 1, | ||
"comment": "lorem ipsum", | ||
"author": | ||
{ | ||
"id": 1, | ||
"name": "Jane Doe" | ||
} | ||
} | ||
] | ||
} | ||
] | ||
``` | ||
## Selecting Fields | ||
@@ -329,3 +378,3 @@ The `select` method is used to select specific fields from the model. | ||
### Object Syntax | ||
We can use object syntax to organize field selection operations. To select fields from the model, we should define an empty key and provide an array of fields as the value. | ||
We can use object syntax to organize field selection operations. To select fields directly from the model, we should define an empty key or relation name as a key and provide an array of fields to selected as the value. | ||
@@ -362,3 +411,3 @@ ```js | ||
```js | ||
const posts = await Post.page( 2 ).get(); | ||
const posts = await Post.page( 2 ).all(); | ||
``` | ||
@@ -374,3 +423,3 @@ | ||
```js | ||
const posts = await Post.limit( 10 ).get(); | ||
const posts = await Post.limit( 10 ).all(); | ||
``` | ||
@@ -387,6 +436,6 @@ | ||
{ | ||
limit = 10; | ||
itemsPerPage = 10; | ||
} | ||
const posts = await Post.get(); | ||
const posts = await Post.all(); | ||
``` | ||
@@ -398,8 +447,18 @@ | ||
This query would give us the first 10 posts. | ||
We defined a default limit on Post level. The query would give us the first 10 posts. | ||
```js | ||
const [ firstPage, secondPage ] = await Promise.all( | ||
[ | ||
Post.page( 1 ).all(), | ||
Post.page( 2 ).all() | ||
]); | ||
``` | ||
We requested the first and second page and got the first and second 10 posts simultaneously. | ||
### Advanced Pagination | ||
The `paginate` method can handle the pagination process that we have been doing manually and adds some extra features to it. | ||
This method returns a `LengthAwarePaginator` instance which it extends our `Collection` class. That makes it an advanced collection that can be used to get the total number of items, current page, items per page, total number of pages, request the next page with ease and of course get the items. We will see soon what collections can do. | ||
This method returns a `LengthAwarePaginator` instance which it extends our `Collection` class. We will see soon what collections can do but for now, that makes it an advanced collection that can be used to get the total number of items, current page, items per page, total number of pages, request the next page with ease and of course get the items. | ||
@@ -415,3 +474,3 @@ ```js | ||
#### Pagination Metadata | ||
The `LengthAwarePaginator` has a property named `page` which holds a `Page` instance. If the Rest API has included pagination metadata in its responses, this information is abstracted with the `Page` class, and we can access it through the paginator. | ||
The `LengthAwarePaginator` has a property named `page` which holds a `Page` instance. If the Rest API that we are requesting has included pagination metadata in its responses, this information is abstracted with the `Page` class, and we can access it through the paginator. | ||
@@ -469,8 +528,8 @@ Let's now explore what these useful pagination information are. | ||
#### Normalizing Metadata | ||
Rest APIs can provide various types of pagination metadata. The `Page` class normalizes this metadata into a consistent format, but we need to specify which information corresponds to which attribute and distribute them properly. | ||
#### Normalizing Pagination Metadata | ||
Restful APIs can provide various types of pagination metadata depending on the frameworks, libraries and developers. The `Page` class normalizes this metadata into a consistent format, but we need to specify which information corresponds to which attribute and distribute them properly. | ||
We achieve this by defining a static method called `$pluckPaginations` on the model. Restorm invokes this method by passing the body of the response sent by the Rest API and the `Page` instance through the argument tunnel. We should then use these objects to ensure the necessary distribution. | ||
We achieve this by defining a static method called `$pluckPaginations` on our models. Restorm invokes this method by passing the body of the response sent by the Restful API and the `Page` instance through the argument tunnel. We should then use these objects to ensure the necessary distribution. | ||
For example, while our post data may be provided through Django, our user data may be powered by Laravel. In those kind of cases, we can define the mentioned function separately in the `Post` model and the `User` model. Otherwise if all our data is being fed by the same framework, we can write it once in the `BaseModel`. | ||
For example, while our post data may be provided through Django and user data may be powered by Laravel. In those kind of cases, we can define the mentioned function individually in the `Post` model and the `User` model. Otherwise if all the data is being fed by the same framework, we can write it once in the `BaseModel`. | ||
@@ -503,8 +562,20 @@ ```js | ||
```js | ||
paginator.next(); | ||
const posts = await paginator.next(); | ||
paginator.forEach( post => | ||
console.log( post.title ) | ||
); | ||
``` | ||
``` | ||
GET /api/v1.0/posts?limit=10&page=2&paginate=length-aware | ||
``` | ||
We can use `posts` collection which is a basic (not paginated) collection to get the next page of posts or we keep going to use the same paginator instance to access same models placed on `posts` with extra pagination functionality. | ||
## Conditional Queries | ||
Sometimes, we might want to add a constraint to our query. To do this, we can use the `when` method. The first argument is a boolean expression, and the second is a callback function. The callback will receive query builder instance and the condition flag's value as arguments. | ||
The main advantage of using `when` method is that it allows us to keep querying in a single line without breaking the chain. Otherwise, we would have to use if-else statements and add more lines. | ||
```js | ||
@@ -527,3 +598,3 @@ function getUserId() | ||
) | ||
.get(); | ||
.all(); | ||
``` | ||
@@ -535,3 +606,3 @@ | ||
You have to be careful with falsy values. For example, if you pass `0` as an user ID, it will be considered as `false`, and the query will not be executed. | ||
You have to be careful with falsy values. For example, if you pass `0` as an user ID, it will be considered as `false`, and the constraint piece will not be executed even though when it should. | ||
@@ -598,9 +669,9 @@ ## Additional Params | ||
{ | ||
title: "Elon Musk went to the Moon instead Mars", | ||
title: "Elon Musk went to the Moon instead of Mars", | ||
content: "Yeah! You heard right, he did just like that! Unbelievable." | ||
}); | ||
newPost.post(); | ||
// or | ||
newPost.save(); | ||
// or | ||
newPost.post(); | ||
``` | ||
@@ -614,2 +685,2 @@ | ||
If the primary key is not set, it will send a `POST` request, otherwise it will send a `PUT` request. That means it can handle creation and updating of resources. | ||
If the primary key is not set on the model instance, it will send a `POST` request, otherwise it will send a `PUT` request. That means it can handle creation and updating of resources. |
import { Model } from "."; | ||
/** | ||
* Represents a list of data. | ||
*/ | ||
export default class Collection | ||
@@ -4,0 +7,0 @@ { |
@@ -34,2 +34,9 @@ import { Page, Collection, QueryBuilder } from "."; | ||
/** | ||
* Latest promise. | ||
* | ||
* @type {Promise<Collection>} | ||
*/ | ||
latestPromise = Promise.resolve( new Collection ); | ||
/** | ||
* Instantiate length aware paginator. | ||
@@ -57,3 +64,3 @@ * | ||
* | ||
* @return {LengthAwarePaginator} | ||
* @return {Promise<Collection>} | ||
*/ | ||
@@ -66,3 +73,3 @@ ping() | ||
this.builder | ||
return this.latestPromise = this.builder | ||
.on( "finished", () => this.isPending = false, once ) | ||
@@ -77,5 +84,3 @@ .on( 204, () => this.data = [], once ) | ||
}, once ) | ||
.get(); | ||
return this; | ||
.all(); | ||
} | ||
@@ -86,3 +91,3 @@ | ||
* | ||
* @return {LengthAwarePaginator} | ||
* @return {Promise<Collection>} | ||
*/ | ||
@@ -93,9 +98,8 @@ next() | ||
{ | ||
return this; | ||
return this.latestPromise; | ||
} | ||
this.builder.page( this.page.currentPage + 1 ); | ||
this.ping(); | ||
return this; | ||
return this.ping(); | ||
} | ||
@@ -102,0 +106,0 @@ |
@@ -546,3 +546,3 @@ import { QueryBuilder } from "."; | ||
const staticMethods = {} | ||
const ignored = [ "name", "prototype", "constructor", "length" ]; | ||
const ignoredMethods = [ "name", "prototype", "constructor", "length" ]; | ||
let current = this; | ||
@@ -560,3 +560,3 @@ | ||
.filter( name => | ||
! ignored.includes( name ) && this[ name ] instanceof Function | ||
! ignoredMethods.includes( name ) && this[ name ] instanceof Function | ||
) | ||
@@ -577,2 +577,13 @@ .forEach( name => | ||
/** | ||
* Checks if the given instance is an instance of the model. | ||
* | ||
* @param {any} instance | ||
* @return {boolean} | ||
*/ | ||
static [ Symbol.hasInstance ]( instance ) | ||
{ | ||
return this instanceof instance.constructor; | ||
} | ||
/** | ||
* Instantiates the model with the given data. | ||
@@ -579,0 +590,0 @@ * |
@@ -412,3 +412,3 @@ import { symOnce } from "./utils/symbols"; | ||
{ | ||
const result = await this.page( null ).limit( null ).get(); | ||
const result = await this.get(); | ||
@@ -479,3 +479,3 @@ if( result instanceof Model ) | ||
* Sends the given payload with `PUT` request to the represented | ||
* model's resource. | ||
* model's endpoint. | ||
* | ||
@@ -525,10 +525,40 @@ * @param {string|number|object} targetPrimaryKeyValueOrPayload a primary key | ||
/** | ||
* @async | ||
* @callback ActionHook | ||
* @return {Promise<AxiosResponse>} | ||
*/ | ||
/** | ||
* @callback HydrateHook | ||
* @param {AxiosResponse} response response object returned from restful endpoint | ||
* @return {object} hydrated resource | ||
*/ | ||
/** | ||
* @callback AfterHook | ||
*/ | ||
/** | ||
* @typedef RequestLifecycleHooks | ||
* @type {object} | ||
* @property {ActionHook=} action a method encapsulates requesting operations | ||
* @property {HydrateHook=} hydrate a method encapsulates hydrating operations | ||
* @property {AfterHook=} after a method encapsulates actions to be run after receiving response | ||
*/ | ||
/** | ||
* @typedef RestormResponse | ||
* @type {object} | ||
* @property {Model|Collection} hydrated the response body instantiated as a model or collection | ||
* @property {AxiosResponse} response response object | ||
* @property {object|array<{}>} data response data | ||
*/ | ||
/** | ||
* Makes requests, triggers events, runs hooks, converts received | ||
* resource to a Model or Collection, and resolves with it. | ||
* | ||
* @param {object} hooks lifecycle methods | ||
* @property {function} hooks.action request maker function | ||
* @property {function} hooks.hydrate hydrate hook | ||
* @property {function} hooks.after method to be run after receiving response | ||
* @return {Promise<{hydrated:Model|Collection,response:AxiosResponse,data:{}|[]}>} | ||
* @async | ||
* @param {RequestLifecycleHooks=} hooks lifecycle methods | ||
* @return {Promise<RestormResponse|Error>} | ||
* @emits QueryBuilder#waiting | ||
@@ -562,6 +592,6 @@ */ | ||
* | ||
* @param {AxiosResponse<any,any>} response response object | ||
* @param {function} hydrate hydrate hook | ||
* @param {function} after method to be run after receiving response | ||
* @return {{hydrated:Model|Collection,response:AxiosResponse,data:{}|[]} | ||
* @param {AxiosResponse} response response object | ||
* @param {HydrateHook=} hydrate hydrate hook | ||
* @param {AfterHook=} after method to be run after receiving response | ||
* @return {RestormResponse} | ||
* @emits QueryBuilder#[StatusCode] | ||
@@ -669,8 +699,14 @@ * @emits QueryBuilder#success | ||
{ | ||
// save current resource endpoint | ||
const resource = this.resource; | ||
const result = await this | ||
.resource( this.model.resource + "/" + primary ) | ||
.from( this.model.resource + "/" + primary ) | ||
.page( null ) | ||
.limit( null ) | ||
.get(); | ||
// we should restore latest resource | ||
this.resource = resource; | ||
if( result instanceof Model ) | ||
@@ -810,7 +846,7 @@ { | ||
* @param {string|object} fieldNameOrFieldsObj field name or fields object | ||
* @param {function} castHandle cast implementer | ||
* @param {function} castingHandler cast implementer | ||
* @param {array} payload additional arguments to push cast implementer's arguments | ||
* @return {QueryBuilder} | ||
*/ | ||
cast( fieldNameOrFieldsObj, castHandle, payload = []) | ||
cast( fieldNameOrFieldsObj, castingHandler, payload = []) | ||
{ | ||
@@ -822,3 +858,3 @@ if( arguments.length > 1 ) | ||
payload, | ||
handle: castHandle, | ||
handle: castingHandler, | ||
} | ||
@@ -825,0 +861,0 @@ } |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
77353
2268
665