elodo

Communicate with any REST API in an elegant and object oriented way.

Usage no npm install needed!

<script type="module">
  import elodo from 'https://cdn.skypack.dev/elodo';
</script>

README

Elodo

Communicate with any REST API in an elegant and object oriented way.

TOC

Quick preview

Installation

Setup

Fetching resources

Persisting resources

Relationships

Cancel requests

Cast attributes

File uploads

Options

Quick preview

const post = Post.$create({
    title: 'What is Elodo?',
    body: 'Elodo is an easy way to use resource models in front-end applications!',
});

// POST request to "http://api.com/api/posts"
await post.$store();
post.title = 'What is Elodo? Amazing!';

// PUT request to "http://api.com/api/posts/1"
await post.$update();
// GET request to "http://api.com/api/posts?include=comments,author"
const posts = await Post
    .$request()
    .$include('comments', 'author')
    .$index();

// Each post is tranformed to an instance of Post
const post = posts[0];
await post.$update();

Installation

npm install elodo
npm install axios

Setup

Structure

/src
├── /api
|   ├── /resources
|   |   ├── post.js
|   |   └── comment.js
|   └── client.js
|   └── resource.js
|   └── router.js
├── ...
└── app.js

Resource

api/resource.js

import { createResource } from 'elodo';
import { client } from './client';
import { router } from './router';

export const Resource = createResource({
    client,
    router,
});

api/router.js

import { createRouter } from 'elodo';

export const router = createRouter();

router.prefix('http://api.com/api/v1/', function (router) {
    // Register each crud action
    router.index('posts', () => `posts`);
    router.store('posts', () => `posts`);
    router.show('posts', (post) => `posts/${post.id}`);
    router.update('posts', (post) => `posts/${post.id}`);
    router.destroy('posts', (post) => `posts/${post.id}`);

    // Or register all crud actions at once
    // $index GET /posts
    // $store POST /posts
    // $show GET /posts/{post.id}
    // $store POST /posts/{post.id}
    // $update PUT /posts/{post.id}
    // $destroy DELETE /posts/{post.id}
    router.resource('posts');
});

api/client.js

import Axios from 'axios';

export const client = function () {
    return Axios.create();
};

api/resources/post.js

import { Resource } from '../resource';

export class Post extends Resource {
    get _attributes () {
        return {
            id: null,
            title: null,
            body: null,
        };
    }

    get _route () {
        return 'posts'; // Used to create the route path
    }
}

Now you can use the post resource in app.js or any other file

import { Post } from './api/resources/post';

const posts = await Post.$index(); // GET request to "/posts"

const post = await Post.$find(1); // GET request to "/posts/1"

await post.$show();  // GET request to "/posts/1"

await post.$store();  // POST request to "/posts"

await post.$update();  // PUT request to "/posts/1"

await post.$destroy();  // DELETE request to "/posts/1"

Fetching resources

Fetch list of resources

Fetch single resource

Fetch filtered list of resources

Fetch list of resources sorted by an attribute

Fetch list of resources with relationships

Fetch list of resources with selected fields

Fetch list of resources with appended fields

Fetch list of resources with specific params

Fetch paginated list of resources

Fetch list of resources

// GET /posts
const posts = await Post.$index();

Fetch single resource

Find a resource by primary id

// GET /posts/1
const post = await Post.$find(1);

Show a resource by its attributes

// GET /posts/1
const post = Post.$create({ id: 1 });
await post.$show();

You can also use the $refresh alias

// GET /posts/1
const post = Post.$create({ id: 1 });
await post.$refresh();

Fetch filtered list of resources

// GET /posts?filter[title]=Hello
const posts = await Post
    .$request()
    .$filter('title', 'Hello')
    .$index();

You can also use the where alias

// GET /posts?filter[title]=Hello
const posts = await Post
    .$request()
    .$where('title', 'Hello')
    .$index();

Fetch list of resources sorted by an attribute

// GET /posts?sort=title
const posts = await Post
    .$request()
    .$sort('title')
    .$index();

Sort descending

// GET /posts?sort=-title
const posts = await Post
    .$request()
    .$sortDesc('title')
    .$index();

Combine multiple sorts

// GET /posts?sort=id,-name
const posts = await Post
    .$request()
    .$sort('id')
    .$sortDesc('name')
    .$index();

Fetch list of resources with relationships

// GET /posts?include=comments,author
const posts = await Post
    .$request()
    .$include('comments', 'author')
    .$index();

Fetch list of resources with selected fields

// GET /posts?fields[posts]=id,title
const posts = await Post
    .$request()
    .$fields({ 'posts': ['id', 'title'] })
    .$index();

Fetch list of resources with appended fields

// GET /posts?append=published_at
const posts = await Post
    .$request()
    .$append('published_at')
    .$index();

Fetch list of resources with limited resultes

// GET /posts?limit=15
const posts = await Post
    .$request()
    .$limit(15)
    .$index();

Fetch list of resources with specific params

// GET /posts?param=value
const posts = await Post
    .$request()
    .$param('param', 'value')
    .$index();

Fetch paginated list of resources

// GET /posts?page[size]=15&page[number]=1
const pagination = await Post
    .$request()
    .$page({
        size: 15,
        number: 1,
    })
    .$index();

// Pagination data is tranformed to instances of Post
const post = pagination.data[0];
await post.$update();

Set the page directly

// GET /posts?page=1
await Post
    .$request()
    .$page(1)
    .$index();

Use with the limit param

// GET /posts?page=1&limit=15
await Post
    .$request()
    .$page(1)
    .$limit(15)
    .$index();

Persisting resources

Store resource

Update resource

Delete resource

Store resource

// POST /posts
const post = Post.$create({ title: 'Hello' });
await post.$store();

Or use the $save alias

// POST /posts
const post = Post.$create({ title: 'Hello' });
await post.$save();

Update resource

// Put /posts/1
const post = await Post.$find(1);
post.title = 'Updated title';
await post.$update();

Delete resource

// DELETE /posts/1
const post = await Post.$find(1);
await post.$destroy();

Or use the $delete alias

// DELETE /posts/1
const post = await Post.$find(1);
await post.$delete();

Relationships

Start with defining your relationship routes

import { createRouter } from 'elodo';

export const router = createRouter();

router.prefix('http://api.com/api/v1/', function (router) {
    // $index GET /posts
    // $store POST /posts
    // $show GET /posts/{post.id}
    // $store POST /posts/{post.id}
    // $update PUT /posts/{post.id}
    // $destroy DELETE /posts/{post.id}
    router.resource('posts');

    // $index GET /posts/{post.id}/comments
    // $store POST /posts/{post.id}/comments
    // $show GET /posts/{post.id}/comments/{comment.id}
    // $store POST /posts/{post.id}/comments/{comment.id}
    // $update PUT /posts/{post.id}/comments/{comment.id}
    // $destroy DELETE /posts/{post.id}/comments/{comment.id}
    router.resource('posts.comments');
});
const post = Post.$find(1);
const comment = Comment.$create({ body: 'Hello' });

// POST /posts/1/comments
await comment.$parent(post).$store();

// POST /comments
await comment.$store();

Cancel requests

import { Post } from './resources/post';
import { createSource, isCancel } from 'elodo';

const source = createSource();

Post.$source(source)
    .$index()
    .then((posts) => {
        ...
    })
    .catch((error) => {
        if (isCancel(error)) {
            // Request was canceled
        }
    });

source.cancel();

Cancel any crud action

import { Post } from './resources/post';
import { createSource, isCancel } from 'elodo';

const source = createSource();
const post = Post.$create();

post.$source(source)
    .$store()
    .then((posts) => {
        // Render posts
    })
    .catch((error) => {
        if (isCancel(error)) {
            // Request was canceled
        }
    });

source.cancel();

Cast attributes

Cast nested properties

Custom cast

Cast to relationship

The cast property allows you to convert attributes coming from the server.

Build in cast types are: number, float, int, bigint, boolean, string, date, json, and json.parse.

import { Resource } from '../resource';

export class Post extends Resource {
    get _attributes () {
        return {
            id: null,
            title: null,
            published_at: null,
        };
    }

    get _casts () {
        return {
            id: 'int',
            published_at: 'date',
        };
    }

    get _route () {
        return 'posts';
    }
}

Cast nested properties

import { Resource } from '../resource';

export class Post extends Resource {
    get _attributes () {
        return {
            object: {
                prop: null,
            },
        };
    }

    get _casts () {
        return {
            'object.prop': 'boolean',
        };
    }

    get _route () {
        return 'posts';
    }
}

Custom cast

Add a function to that returns the transformed value.

import { Resource } from '../resource';

export class Post extends Resource {
    get _attributes () {
        return {
            id: null,
            title: null,
            published_at: null,
        };
    }

    get _casts () {
        return {
            published_at: (value) => new Date(value),
        };
    }

    get _route () {
        return 'posts';
    }
}

Cast to relationship

import { Resource } from '../resource';
import { Comment } from './comment';

export class Post extends Resource {
    get _attributes () {
        return {
            id: null,
            title: null,
            comments: null,
        };
    }

    get _casts () {
        return {
            comments: (value) => Comment.$create(value),
        };
    }

    get _route () {
        return 'posts';
    }
}

File uploads

Change the content type of the resource to formdata

./api/resources/post.js

import { Resource } from '../resource';

export class Post extends Resource {
    get _attributes () {
        return {
            id: null,
            thumbnail: null,
        };
    }

    get _contentType () {
        return 'formdata';
    }

    get _route () {
        return 'posts';
    }
}

In ./app.js or any other file

import { Post } from './api/resources/post';

const fileInput = document.querySelector('#fileInput');
const file = fileInput.files[0];

const post = Post.$create({ file });

// POST request to "/posts" with file in formdata
await post.$store();

Options

To add or override options you can create a base resource

import { createResource } from 'elodo';
import { client } from './client';
import { router } from './router';

const BaseResource = createResource({
    client,
    router,
});

export class Resource extends BaseResource {
    /**
     * Default attributes
     */
    get _attributes () {
        return {
            id: null,
        };
    }

    /**
     * Default route
     */
    get _route () {
        return 'default.route';
    }

    /**
     * Default primary key
     */
    get _primaryKey () {
        return 'id';
    }

    /**
     * Default casts
     */
    get _casts () {
        return {};
    }

    /**
     * Default content type
     */
    get _contentType () {
        return 'json';
    }

    /**
     * Custom $latest function
     */
    static $latest () {
        return this.$request()
            .$limit(5)
            .$sortDesc('created_at')
            .$index()
        ;
    }
}