Socket
Socket
Sign inDemoInstall

@iceylan/restorm

Package Overview
Dependencies
9
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    @iceylan/restorm

Provides a robust interface for managing access to RESTful resources in JavaScript.


Version published
Maintainers
1
Created

Readme

Source

Introduction

Restorm is a lightweight JavaScript library designed to streamline the process of handling RESTful services within client-side applications. With Restorm, we can easily model data on the client side and interact with RESTful APIs by adhering to predefined rules and conventions.

This library simplifies the communication process between the client and server, abstracting away the complexities of HTTP requests and allowing developers to focus on building robust, scalable applications. Whether you're fetching data, creating new resources, or updating existing ones, Restorm provides a well known interface for performing CRUD operations while ensuring compliance with RESTful principles.

With its intuitive design and flexible configuration options, Restorm empowers developers to efficiently integrate RESTful services into their JavaScript applications, enhancing productivity and promoting best practices in web development.

Installation

To install Restorm, you can use npm:

npm install @iceylan/restorm

Initialization

Set up on src/models.

// base-model.js
import { Model } from "@iceylan/restorm";

export default class BaseModel extends Model
{
	baseUrl = "https://yourdomain.com/api/v1.0";
}
// post.js
import { BaseModel } from ".";

export default class Post extends BaseModel
{
	resource = "posts";
}

Building The Query

After preparing our models, we can create RESTful requests with them.

Retreiving a List of Resources

There are two ways to get a list of resources.

  • 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.

const posts = await Post.all();
GET /api/v1.0/posts

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.

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 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.

Getting the First Resource

The first method creates a request to the root directory of resources with filtering criteria, and it requests the first item of the results found.

The returned promise is fulfilled with an instance created from the Post model.

const post = await Post.first();
GET /api/v1.0/posts?limit=1

Finding a Specific Resource

The find method creates a request to access a resource under the root directory of resources using a primary key.

const post = await Post.find( 1 );
GET /api/v1.0/posts/1

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.

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:

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.

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:

[
	{
		"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

To list specific resources, we add filters with where method to accomplish this.

We can directly filter represented resources by models:

const articles = await Post.where( "type", "article" ).all();
GET /api/v1.0/posts?filter[type]=article

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.

const articles = await Post
	.where( "type", "article" )
	.with( "comments" )
	.where( "comments.state", "approved" )
	// or
	.where([ "comments", "state" ], "approved" )
	.all();
GET /api/v1.0/posts?with=comments&filter[type]=article&filter[comments.state]=approved

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 operations on reactive objects and directly pass this object to the where method.

const conditions =
{
	type: "article",
	"comments.state": "approved"
	// or
	[ relationName + "." + fieldName ]: "approved",
	// or
	comments:
	{
		state: "approved"
	},
}

const articles = await Post.with( "comments" ).where( conditions ).all();
GET /api/v1.0/posts?with=comments&filter[type]=article&filter[comments.state]=approved

Multiple Values

We can also add multiple values for a filter.

const posts = await Post.where( "type", [ "article", "news" ]).get();

This request will get all the posts of the article and news types.

GET /api/v1.0/posts?filter[type]=article,news

We didn't add a separate whereIn method because the where method is flexible enough to handle it on its own.

const posts = await Post.where( "id", [ 4, 8, 15 ]).get();
GET /api/v1.0/posts?filter[id]=4,8,15

Sorting Resources

The orderBy method is used to obtain results sorted by a specific field of resource.

We can directly subject a model to sorting like this:

const posts = await Post.orderBy( "updated_at", "desc" ).get();
GET /api/v1.0/posts?sort[updated_at]=desc

We may also want to sort on two different fields simultaneously.

const posts = await Post
	.orderBy( "updated_at", "asc" )
	.orderBy( "created_at", "desc" )
	.get();
GET /api/v1.0/posts?sort[updated_at]=asc&sort[created_at]=desc

We can also subject the related model to the sorting process.

const posts = await Post
	.with( "comments.replies" )
	.orderBy( "comments.id", "desc" );
	// or with array syntax
	.orderBy([ "comments", "id" ], "desc" );
	// or we can go further and sort on nested relationships
	.orderBy( "comments.replies.created_at", "desc" );
	.all();
GET /api/v1.0/posts?with=comments.replies&sort[comments.id]=desc&sort[comments.replies.created_at]=desc

Object Syntax

We can use object syntax to organize sorting operations.

const sorting =
{
	updated_at: "desc",
	created_at: "asc",
	"comments.id": "desc",
	// or
	[ relationName + "." + fieldName ]: "desc",
	// or
	comments:
	{
		id: "desc",
		replies:
		{
			created_at: "desc"
		}
	}
}

const posts = await Post.with( "comments" ).orderBy( sorting ).get();
GET /api/v1.0/posts?with=comments&sort[updated_at]=desc&sort[created_at]=asc&sort[comments.id]=desc&sort[comments.replies.created_at]=desc

Selecting Fields

The select method is used to select specific fields from the model.

const posts = await Post.select([ "id", "title", "author_id" ]).get();
GET /api/v1.0/posts?field[]=id,title,author_id

We can also select fields from the related resources. First argument of the select method is the name of the related resource. We can provide relation name as a string with dot notation or as an array and the field names should be provided as an array.

const posts = Post
	.select( "comments", [ "id", "author_id", "comment" ])
	.select([ "comments", "author" ], [ "id", "username" ])
	// or
	.select( "comments.author", [ "id", "username" ])
	.get();
GET /api/v1.0/posts?field[comments]=id,author_id,comment&field[comments.author]=id,username

Object Syntax

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.

const selections =
{
	// resource fields
	"": [ "id", "title", "author_id" ],
	// related resource fields
	comments: [ "id", "author_id", "comment" ],
	// deeply related resource fields
	"comments.author": [ "id", "username" ],
	// or
	comments:
	{
		author: [ "id", "username" ]
	}
}

const posts = Post.select( selections ).all();
GET /api/v1.0/posts?field[]=id,title,author_id&field[comments]=id,author_id,comment&field[comments.author]=id,username

Pagination

Pagination is a process of dividing a large set of data into smaller and more manageable chunks. Restorm provides basic and advanced pagination support.

Basic Pagination

We can accomplish this process manually by using the page and limit methods. With two of these methods, we can get the desired paginated results.

So, let's see how we can use these methods.

Page

The page method is used to specify the page number.

const posts = await Post.page( 2 ).all();
GET /api/v1.0/posts?page=2
Limit

The limit method is used to specify the number of items per page.

const posts = await Post.limit( 10 ).all();
GET /api/v1.0/posts?limit=10

If the limit value we are going to use is the same most of the time, we can define it in the model instead of using the limit method every time.

class Post extends BaseModel
{
	itemsPerPage = 10;
}

const posts = await Post.all();
GET /api/v1.0/posts?limit=10

We defined a default limit on Post level. The query would give us the first 10 posts.

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. 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.

const paginator = await Post.paginate();
GET /api/v1.0/posts?limit=10&page=1&paginate=length-aware
Pagination Metadata

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.

Let's now explore what these useful pagination information are.

Current Page

The currentPage property holds the current page number.

const { currentPage } = paginator.page;
From

The from property holds the starting item number.

const { from } = paginator.page;
Last Page

The lastPage property holds the last page number.

const { lastPage } = paginator.page;
Per Page

The perPage property holds the number of items per page.

const { perPage } = paginator.page;
To

The to property holds the ending item number.

const { to } = paginator.page;
Total

The total property holds the total number of items.

const { total } = paginator.page;
End

The end property is a flag that indicates if the pagination is at the end and there are no more items to be fetched. It's a read-only property. The value of this property is calculated from the currentPage and lastPage properties.

const { end } = paginator.page;
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 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 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.

// models/base-model.js
import { Model } from "restorm";

class BaseModel extends Model
{
	static $pluckPaginations( page, responseBody )
	{
		page.currentPage = responseBody.current_page;
		page.lastPage = responseBody.last_page;
		page.perPage = responseBody.per_page;
		page.total = responseBody.total;
		page.from = responseBody.from;
		page.to = responseBody.to;
	}
}
Querying Next Page

Paginators allows us to query the next page of resources that we are currently working on. We can do this by calling the next method on the paginator instance.

There is no need to track the current page number, increase it etc. as Restorm will handle this for us.

Additionally, Restorm keeps track of whether requests have been completed or not. When the next method is called, if there is still a pending request awaiting response, the call is ignored. We don't need to deal with such matters.

await paginator.next();

paginator.forEach( post =>
	console.log( post.title )
);
GET /api/v1.0/posts?limit=10&page=2&paginate=length-aware

We used same paginator instance to access fetched resources as models placed on it.

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.

const posts = await Post
	.when( getUserId(), ( query, userId ) =>
		query.where( "author_id", userId )
	)
	.all();

function getUserId()
{
	if( something )
	{
		return 481516;
	}
	else
	{
		return null;
	}
}
GET /api/v1.0/posts?filter[author_id]=481516

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 block won't be executed, even though it should by your perspective.

Additional Params

Sometimes, we might want to pass additional parameters to the query that restorm doesn't handle explicitly.

To do this, we can use the params method. It accepts an object of additional parameters. We should pass all the parameters that we want to pass to the query at once.

const params =
{
	foo: "bar",
	do: true,
	some: [ "good", "bad", "ugly" ]
}

const posts = await Post.params( params ).get();
GET /api/v1.0/posts?foo=bar&do=true&some=good,bad,ugly

Custom Resource

If we want to use a custom resource, we can do it by using the from method.

With this method, we can bypass the current resource defined on the model and create requests to a custom resource temporarily.

Static Resource

We can specify the resource name directly as a string.

const posts = await Post.from( "timeline" ).all();
GET /api/v1.0/timeline

All the items in the returned collection will be instance of Post model.

Model Aware Custom Resource

Sometimes, we might want to build dynamic resource URIs depending on some model instances that we have already.

const post = new Post({ id: 48 });
const comment = new Comment({ id: 4815, post_id: post.id });

const reactions = await Reaction.from( post, comment, "reactions" ).all();
GET /api/v1.0/posts/48/comments/4815/reactions

CRUD Operations

Restorm provides a set of methods that allow us to perform CRUD operations on RESTful resources, and we have seen above how to handle the (R)ead side by get, all, find, first methods.

So let's see how to perform (C)reate, (U)pdate and (D)elete operations.

Create

On HTTP protocol, a resource creation should be performed by sending a POST request to the RESTful api endpoints.

We designed a few alternative ways to create resources. First, we can create resources as model instances and send them.

const title = "Elon Musk went to the Moon instead of Mars";
const content = "Yeah! You heard right, he did just like that! Unbelievable.";

const draft = new Article({ title });

// and we can assign missed properties later to the model
draft.content = content;

const article = await draft.post();
// or
const article = await draft.save();

if( article instanceof AxiosError )
{
	console.log( "network or server errors", article );
}

The post method returns a promise that will be fulfilled with an instance of Article model or an Error if there is an issue. Errors won't be thrown and you can't catch them with async-await & try-catch or then-catch mechanism. Restorm will supress throwing all the HTTP errors with 400 and 500 status codes and network errors as well.

If you want to handle errors then you can add event listeners to your queries or models, or you can handle it manually when they're returned by post method.

If Restorm detects issues with the usage of its methods, it will throws an error and stops your application. You shouldn't catch and handle that kind of errors manually and suppress them, just have to solve and disappear them.

We can also statically use the post method.

const newArticle = await Article.post(
{
	title: "Elon Musk went to the Moon instead of Mars",
	content: "Yeah! You heard right, he did just like that! Unbelievable."
});

console.log( newArticle.id );
// 1
POST /api/v1.0/articles

The result should look like this:

{
	"id": 1,
	"title": "Elon Musk went to the Moon instead of Mars",
	"content": "Yeah! You heard right, he did just like that! Unbelievable."
}

The post method is very self-explanatory, it just sends a POST request to the api endpoint but the save method has something magical behind it.

If the primary key (id in this case) is not set on the model instance like the example above, it will send a POST request, otherwise it will send a PATCH request. We will see that in the next sections.

Update

We can update an existing resource by sending a PATCH or PUT request to api endpoints.

As you know already the PUT is used to update an existing resource as a whole. That means, missing properties should convert to null or default values on the database.

The PATCH on the other hand, is used to update only some of the properties of the resource and missing properties will be stay as they are. This is what makes PATCH lightweight compared to PUT.

Restorm smart enough to know which properties of the model you modified and that gives us an opportunity to send always a lightweight PATCH request instead of a PUT.

const article = await Article.find( 1 );

article.title = "Jeff Bezos went to the Moon instead of Mars";

await article.save();
// or more explicitly
await article.patch();
PATCH /api/v1.0/articles/1

And the resource will be like:

{
	"id": 1,
	"title": "Jeff Bezos went to the Moon instead of Mars",
	"content": "Yeah! You heard right, he did just like that! Unbelievable."
}

The save method always sends a PATCH request if the case is about updating an existing resource. However, sometimes we really want to send a PUT request. We can do that by using the put method of Restorm.

const state =
{
	title: "Jeff Bezos went to the Moon instead of Mars"
}

// we can get the resource as a model from the endpoint
const article = await Article.find( 1 );

// or we can bake the model manually without `GET` request
// if we sure that the resource exists remotely
const article = new Article({ id: 1 });

// and we can send a `PUT` request for a model
await article.put( state );

// or shortly we can use static syntax
await Article.put( 1, state );
PUT /api/v1.0/articles/1

And the resource should be like:

{
	"id": 1,
	"title": "Jeff Bezos went to the Moon instead of Mars",
	"content": null
}

Event Management

Restorm provides an event management system to handle network errors, server errors and internal events.

We can add event listeners to our queries, models or model instances. Events are kind of hooks that will be triggered when certain events happen.

Since Restorm have this mechanism, it doesn't need to throw network and server errors. Restorm only throws runtime errors. So that means network errors and 400-500 errors will be suppressed and won't stop the application. We should grab and handle them manually.

We can add event listeners to any query builder, model or model instance. The event listeners will be triggered when the query is executed.

We can register our listeners with on method.

QueryBuilder Event Binding

Note that if the on method on the models called statically, it will create a QueryBuilder instance internally, adds event listener we passed to it and returns it.

The event listeners that binded to a QueryBuilder will be valid only for that specific query builder instance.

const query = Post.on( "failed", gettingPostFailed );
const post = await query.find( 1 );

// or shortly
const post = await Post.on( "failed", gettingPostFailed ).find( 1 );

const another = await Post.find( 2 );

function gettingPostFailed( err )
{
	console.log( err )
}

The first query will print the error object in the console when the api endpoint returns an 400, 500 http status code or a network error happens. But the second query won't log anything even if it has an error because it's running on a different query builder instance and we didn't bind any event listener to it.

Model Instance Event Binding

As you know already, Restorm transforms remote resources into Model instances. We also can add event listeners to any model instance.

const post = await Post.find( 1 );

post.id = 12;
post.on( 304, postNotModified );

await post.patch();

// or shortly
await post.on( 304, postNotModified ).patch();

function postNotModified( post, response, data )
{
	console.log ( post );

	// this word refers to QueryBuilder instance
	console.log( this );
}

This time, the on method is not static. It operates on the context of the model that represents the resource and the model has its own query builder instance. It will bind the event listener we provided to this query builder and return the model's itself. Also, patching will happen over the same QueryBuilder instance.

Global Event Binding

Sometimes we might want to add event listeners to all queries to track events globally (app level).

We can define global event listeners as static methods on the models prefixed with on keyword like onFailed or onSuccess.

import { Model } from "@iceylan/restorm";

class BaseModel extends Model
{
	static onFailed( err )
	{
		console.log( err );

		// this word refers to QueryBuilder instance
		console.log( this );
	}
}

const post = await Post.find( 1 );

From now on, every child model of the BaseModel will inherit a listener for the failed event.

We can overwrite the event listeners in child models.

class Post extends BaseModel
{
	static onFailed( err )
	{
		alert( err.message );
	}
}

Now all errors of Post model will be displayed in alert dialog instead of console.

If we want, we can override parent model's event listeners instead of overwriting them.

class Post extends BaseModel
{
	static onFailed( err )
	{
		super.onFailed( err );

		alert( err.message );
	}
}

Now, first, the errors that related to Post model will be logged to the console and then an alert dialog will be displayed.

Keywords

FAQs

Last updated on 14 May 2024

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc