1.2.0 • Published 5 years ago

@industry/apikit v1.2.0

Weekly downloads
-
License
UNLICENSED
Repository
github
Last release
5 years ago

ApiKit

REST endpoints for any Laravel Model made simple.

Table of contents

Installation

Backend: composer require weka/apikit

Frontend: yarn add @industry/apikit

Usage in your backend

Adding a model

Resolve Weka\ApiKit\ResourceRegistry via Laravel's DI-Container and add your model with a compatible policy:

MyModel.php

class MyModel extends \Illuminate\Database\Eloquent\Model
{
    //
}

MyPolicy.php

use Illuminate\Contracts\Auth\Access\Authorizable;

class MyPolicy
{
    public function create(Authorizable $user = null)
    {
        return $user !== null;
    }

    //...
}

Any Service Provider, eg. AppServiceProvider.php

$registry = resolve(Weka\ApiKit\ResourceRegistry::class);
$resourceHandler = $registry->add(MyModel::class, MyPolicy::class);

// Use the following statement to get all your model's routes:
// $resourceHandler->getRoutes();

Adding a resource instead of a model

If you need more control over the behaviour add a resource instead of a model:

MyModel.php

class MyModel extends \Illuminate\Database\Eloquent\Model
{
    //
}

MyPolicy.php

use Illuminate\Contracts\Auth\Access\Authorizable;

class MyPolicy
{
    public function create(Authorizable $user = null)
    {
        return $user !== null;
    }

    //...
}

MyResource.php

use Illuminate\Http\Request;

class MyResource extends \Weka\ApiKit\ResourceRegistry\AbstractResource
{
    /**
     * Returns the fully qualified class name of the model
     *
     * @return string
     */
    public function getModelClassName() : string
    {
        // This is the model you want to access.
        return MyModel::class;
    }

    /**
     * Returns the resource's routeParameterName. This is also used as the route's parameter name.
     *
     * @return string
     */
    public function public function getRouteParameterName() : string
    {
        // This is the route parameter name which will be used for generating the routes.
        return 'foo';
    }

    /**
     * Returns the fully qualified class name of a policy associated with the model.
     *
     * @return string|null
     */
    public function getPolicy() : string
    {
        return MyPolicy::class;
    }

    /**
     * Returns the attributes which are used for creating the model.
     *
     * @param Request $request
     * @return array
     */
    public function getCreateAttributes(Request $request) : array
    {
        return array_merge($request->all(), [
            'user_id' => auth()->user->id,
        ];
    }

    /**
     * Returns the attributes which are used to update the model.
     *
     * @param Request $request
     * @return array
     */
    public function getUpdateAttributes(Request $request) : array
    {
        return array_merge($request->all(), [
            'user_id' => auth()->user->id,
        ];
    }
}

Any Service Provider, eg. AppServiceProvider.php

$resource = new MyResource();

$registry = resolve(Weka\ApiKit\ResourceRegistry::class);
$resourceHandler = $registry->add($resource);

Available policy methods

Define the following methods in your Policy. If you're new to the concept of Policies, review the Laravel's Policy documentation.

use Illuminate\Contracts\Auth\Access\Authorizable;

public function index(Authorizable $user) : bool
{
    // Return true or false
}

public function create(Authorizable $user) : bool
{
    // Return true or false
}

public function read(Authorizable $user, Model $model) : bool
{
    // Return true or false
}

public function update(Authorizable $user, Model $model) : bool
{
    // Return true or false
}

public function delete(Authorizable $user, Model $model) : bool
{
    // Return true or false
}

If guest users shoould be authorized, typehint the above $user with null:

use Illuminate\Contracts\Auth\Access\Authorizable;

public function index(Authorizable $user = null) : bool
{
    // Return true or false
}

Displaying errors to users

In case you need to send and display errors to users, throw anywhere in the stack a subclass of \Weka\ApiKit\ResourceRegistry\RequestException.

All subclasses of \Weka\ApiKit\ResourceRegistry\RequestException will be catched and a response with your specified message will be sent.

Create an exception with your message first:

PriceMissingException.php

use \Weka\ApiKit\ResourceRegistry\RequestException;

class PriceMissingException extends RequestException
{
    public function getUserMessage() : string
    {
        return 'You need to specify a price!';
    }
}

Throw the previosuly created exception anywhere in the stack.

MyModel.php

class MyModel extends \Illuminate\Database\Eloquent\Model
{
    protected static function boot()
    {
        parent::boot();

        static::creating(function($model) {
            if ($model->price === null) {
                throw new PriceMissingException();
            }
        });
    }
}

Usage within a resource:

MyResource.php

use Illuminate\Http\Request;

class MyResource extends \Weka\ApiKit\ResourceRegistry\AbstractResource
{
    // ...

    public function getCreateAttributes(Request $request) : array
    {
        if (!$request->has('price')) {
            throw new PriceMissingException();
        }

        return array_merge($request->all(), [
            'user_id' => auth()->user->id,
        ];
    }

    // ...
}

Events

You can subscribe to different events in a request lifecycle.

Just overload the proper methdos of \Weka\ApiKit\ResourceRegistry\AbstractResource in your custom Resource.

Available methods / events are:

  • public function onIndex(Builder $builder, Request $request) : Builder;
  • public function onBeforeCreate(Model $model, Request $request) : Model;
  • public function onAfterCreate(Model $model, Request $request) : Model;
  • public function onRead(Model $model, Request $request) : Model;
  • public function onBeforeUpdate(Model $model, Request $request) : Model;
  • public function onAfterUpdate(Model $model, Request $request) : Model;
  • public function onBeforeDelete(Model $model, Request $request) : Model;
  • public function onAfterDelete(Model $model, Request $request) : Model;

Verifying your resource was added

To verify if your resource was added successfully run php artisan apikit:routes in parent's app console. Your resource with all its associated routes should be displayed in the console output.

Usage in Frontend

Webpack configuration

The base Model class is published in pure ES6+ and is not transpiled. You need to instruct webpack to transpile it when imported. Add the following lines to your webpack's configuration.

import path from 'path';
import fs from 'fs';

const APIKIT_DIR = path.resolve(fs.realpathSync('node_modules/@industry/apikit'));

export const config = {
    entry: getEntryPoints(),
    // ...
    module : {
        rules : [
            {
                test: /\.js$/,
                include: path => {
                    return path.indexOf(APIKIT_DIR) > -1;
                },
                use: {
                    loader: 'babel-loader',
                    options: {
                        "presets": [
                            "@babel/preset-env",
                        ]
                    },
                }
            },
            // ...
        ]
    },
    // ...
};

Create your ES6 model class

import Model from '@industry/apikit';

class MyModel extends Model {
    static getEndpoint() {
        // Set this to the index route of your previously defined models.
        return '/apikit/my-model';
    }
}

That's it! You can now query the model as you wish.

  • MyModel.find(1) fetches Model associated with primary key 1
  • MyModel.query('foo') queries models using an apiKitSearch scope on server's model -- you need to implement that scope. See ApiKitSearchableTestModel for an example.
  • MyModel.column('title', 'foo') queries models using WHERE title = 'foo'
  • MyModel.column('title', 'foo*') queries models using WHERE title LIKE 'foo%'
  • MyModel.scope('myScope', 'foo') queries models using a myScope scope on server's model -- you need to implement that scope. See ApiKitTestModel for an example.

To review all available methods, checkout the base Model class.

Custom setters & custom getters

// Your model

setFooAttribute(value) {
  return value * value;
}

// model.attr().foo = 5; -> { foo: 25 }

getFirstNameAttribute(value) {
    return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
}

// model.attr().first_name = 'nancy';
// console.log(model.attr().first_name) --> "Nancy".

You can cast your values with setters:

setIdAttribute(value) {
    value = Number(value);

    if (isNaN(value)) {
        return null;
    }

    return value;
}