Code Monkey home page Code Monkey logo

laravel-orion's Introduction

Latest Version on Packagist Build Status

Introduction

Laravel Orion allows you to build a fully featured REST API based on your Eloquent models and relationships with simplicity of Laravel as you love it.

Documentation

Documentation for Laravel Orion can be found on the website.

Supported By

License

The Laravel Orion is open-source software licensed under the MIT license.

laravel-orion's People

Contributors

a21ns1g4ts avatar alberto-bottarini avatar alexzarbn avatar alkin avatar chadonsom avatar gautierdele avatar gc-keba avatar grebbekevin avatar ibrahim-mubarak avatar iksaku avatar jasper-ter-veen avatar josecl avatar kennethtrecy avatar marcvdm avatar odparraj avatar rossity avatar sachinganesh avatar vradev-ph avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laravel-orion's Issues

[Feature Request] Support for nested relation routes

What and Why

Laravel supports restful-nested-resources, which are configured by chaining them with dots.

Their behaviour is already partially supported with all those great relation-routes, but they only support one level of nesting/relation.

As I need another nesting level I tried using laravels dot notation, which did work partially, which is even worse than failing all together, as the results look correct, but are simply wrong.

Adding Routes works fine

I am adding new routes like:

Orion::belongsToManyResource('as.bs','cs', \App\Http\Controllers\Api\BCController::class);

which creates the expected routes like:

Method URI Name Action
GET/HEAD api/as/{a}/bs/{b}/cs as.bs.cs.index App\Http\Controllers\Api\BCController@index
POST api/as/{a}/bs/{b}/cs as.bs.cs.store App\Http\Controllers\Api\BCController@store
... ... ... ....
GET/HEAD api/as/{a}/bs/{b}/cs/{c?} as.bs.cs.show App\Http\Controllers\Api\BCController@show
PUT/PATCH api/as/{a}/bs/{b}/cs/{c?} as.bs.cs.update App\Http\Controllers\Api\BCController@update
... ... ... ...

(I am not sure why the c is marked optional, but thats not the point)

Controller doesn't work as expected

GET api/as/1/bs/2/cs

Expected Result

Listing of all Cs of B2 (given B2 is related to A1)

Real Result

Listing of all Cs of B1 (even if B1 is not related to any A)

GET api/as/1/bs/2/cs/3

Expected Result

C3 (given C3 is related to B2 and B2 is related to A1)

Current Result

C2 (with C2 being related to B1)

Further expectations

I'd expect the APolicy to be checked too, but it doesn't seem to be.

Cheers

I am a bit sad for the controller not working out of the box and me having to try getting it to work as expected or writing it myselfe by hand again -- but thats just because of how awesome orion is and how much time it did save me at all the other controllers, which did work just out of the box. Great work - so much appreciated!

Support nested relations

Would love to be able to include nested relations.
Something like this:

protected function includes(): array
{
  return ['business.manager'];
}

would be able to save a few calls to the API!

Thanks a million for this package, I absolutely love it. So beautifully written ๐Ÿ˜ƒ

Soft Deletes seem to return the entire model for the key

When using soft deletes, I'm getting the following error:

"message": "No query results for model [App\\User].",
"exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
"file": "/var/www/api/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
"line": 222

I have a clue as to why.

In HandlesStandardOperations.php the method restore seems to be returning the whole model for the key instead of just the id. Haven't managed to track down why that is yet, need to do a little more source diving!

To test my theory, I added the following to the top of the restore method and it worked:

HandlesStandardOperations.php

   ...

    public function restore(Request $request, $key)
    {
        $key = $key->id; // <<<<<
        $requestedRelations = $this->relationsResolver->requestedRelations($request);

        $query = $this->buildRestoreFetchQuery($request, $requestedRelations);
        $entity = $this->runRestoreFetchQuery($request, $query, $key);

      ...
   }

Need Help

Hello,

Could you please provide us an example for how to do a custom search.
How to to pass parameters via axios for example. (I'm using Nuxt Js)

Thanks

[Feature Request] setAppends

This may be something more suited to userland, yet I thought it was worth asking!

Sometimes, we want to include attributes in our request. Laravel allows us to automatically append these attributes with appends

protected $appends = ['is_admin'];

However, often we only want those attributes on occassion. So My feature request is this.
Allow the developer to have an appends array on the Controller, similar to how we currently use includes

  protected function appends() : array
  {
      return ['is_admin'];
  }

Then we can use the following query to only append what we need:

(GET) https://myapp.com/api/users?appends=is_admin

Thoughts? ๐Ÿ˜ƒ

Question: how to best design authentication?

I've got a question, I'm using the Orion happily locally with Insomnia to build my endpoints. Works perfect, very happy with this well-thought-through and structured approach.

But now I need to setup authentication. Login, register, reset password etc.
I normally use Fortify for this, but this leverages the X-XSRF-TOKEN approach (web), instead of Bearer (API). I tried setting up Sanctum now too, but I'm getting confused on the best-practice approach.

Any thoughts or experiences that can help me move forward?

Thanks.

API response returns "This action is unauthorized." by default

Hello, I'm new to Laravel Orion.

I'm trying to get the forum index or a specific forum model by id, but I'm struggling for hours now because he's not accepting or ignoring my policy.

I'm getting the error "This action is unauthorized."

When adding the Orion\Concerns\DisableAuthorization trait it works fine.

AuthServiceProvider.php

<?php

namespace App\Providers;

use App\Models\Forum;
use App\Policies\ForumPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        //'App\Models\Forum' => 'App\Policies\ForumPolicy', <-- This didn't work
        Forum::class => ForumPolicy::class,
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        // This didn't work
        // Gate::guessPolicyNamesUsing(function ($modelClass) {
        //     return 'App\\Policies\\' . class_basename($modelClass) . 'Policy';
        // });
    }
}

I have tried php artisan config:clear, php artisan cache:clear and composer dump to make sure he's using the right policies.

ForumController.php

<?php

namespace App\Http\Controllers\Api;

use App\Models\Forum;
use Orion\Http\Controllers\Controller;

class ForumController extends Controller
{
    protected $model = Forum::class;
}

Forum.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Forum extends Model
{
    use HasFactory;
}

ForumPolicy.php

<?php

namespace App\Policies;

use App\Models\Forum;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class ForumPolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can view any models.
     *
     * @param User $user
     * @return mixed
     */
    public function viewAny(User $user): bool
    {
        return true;
    }

    /**
     * Determine whether the user can view the model.
     *
     * @param User $user
     * @param Forum $forum
     * @return mixed
     */
    public function view(User $user, Forum $forum): bool
    {
        return true;
    }

    /**
     * Determine whether the user can create models.
     *
     * @param User $user
     * @return mixed
     */
    public function create(User $user): bool
    {
        return true;
    }

    /**
     * Determine whether the user can update the model.
     *
     * @param User $user
     * @param Forum $forum
     * @return mixed
     */
    public function update(User $user, Forum $forum): bool
    {
        return true;
    }

    /**
     * Determine whether the user can delete the model.
     *
     * @param User $user
     * @param Forum $forum
     * @return mixed
     */
    public function delete(User $user, Forum $forum): bool
    {
        return true;
    }

    /**
     * Determine whether the user can restore the model.
     *
     * @param User $user
     * @param Forum $forum
     * @return mixed
     */
    public function restore(User $user, Forum $forum): bool
    {
        return true;
    }

    /**
     * Determine whether the user can permanently delete the model.
     *
     * @param User $user
     * @param Forum $forum
     * @return mixed
     */
    public function forceDelete(User $user, Forum $forum): bool
    {
        return false;
    }
}

api.php

<?php

use App\Http\Controllers\Api\ForumController;
use Illuminate\Support\Facades\Route;
use Orion\Facades\Orion;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::group(['as' => 'api.'], function () {
    Orion::resource('forums', ForumController::class)->withSoftDeletes()->withoutBatch();
});

What am I doing wrong, did I miss a part in the documentation or is this maybe a bugg?

Kind regards,
Pascal

[Help] - ManyToMany inverse relationship

Hello guys,

I have a problem with the belongsToManyResources routes, I need the bidirectional mapping to work together.
Orion::belongsToManyResource('users', 'customers', \App\Http\Controllers\API\UserCustomerController::class);
Orion::belongsToManyResource('customers', 'users', \App\Http\Controllers\API\CustomerUserController::class);

When the attach method of endpoint /users/{user}/customers/attach is called, the record is inserted correctly. However when the endpoint /customers/{customer}/users/attach method is called orion tries to insert a new user, for some reason it does not have the same behavior as the first endpoint.

The mappings are correct according to the laravel documentation. The table is also mapped in both relationships.

Can someone help me?

Thanks

ErrorHandler

Hi,
I have been using your package for the last 24hs and I love it already. It completely changes the way of building APIs.

I am having trouble when trying to GET, PUT|PATCH or DELETE an either soft|forced deleted record.
I get a 404 response with NotFoundHttpException error page.
How can I prevent this and send a specific json response with whatever error message in it?

Thank you very much for your help and awesome package!!

Unexpected behaviour in Search Query

I have a search query with filter and sort applied as shown below. This endpoint was working fine before and I started getting an empty response even though there are valid records in the DB. Even the total count in the response was correct.
This issue was appearing only when using the sort filter. I updated one of the record and I started getting the results again. I'm not able to reproduce this issue.

What might be the issue here?

Orion search

Convention for JSON response

First off: so happy with this package. Exactly what the doctor ordered.

The one thing I'm uncertain of (happy to hear your thoughts) is the default JSON response. I'm used to it container a status, message, and data object. And for errors showing status, message and errors object (through a custom exception). So far, though that might be because I drew it into an existing project, I'm getting non-JSON exceptions. And successful calls do not contain status or message (e.g. 200, resource update, data{}).

So I'd be curious to learn what conventions are followed in this project.

Keep it up!

Eager Loading is not working

I have two models posts and comments with relationship oneToMany. A post hasManyComments and a Comment Belongs to a Post.
I have defined in the Post Model, protected $with = 'comments'., but when I execute, the Eager loading is not working. Do we have a fix around?

SearchableBy with nested relations

Hey,

is there a way to archive searching on related models (nested)?

I have a User, Location and a Address Model and Iam trying to make the users name and the postalcode searchable:

protected function alwaysIncludes() : array
{
    return ['user', 'user.location', 'user.location.address'];
}

protected function searchableBy() : array
{
    return ['user.name', 'user.location.address.postalcode'];
}

But this will result in an Error:
Call to undefined method App\Models\User::postalcode()

whats the correct way to search on nested models?

How to override the default api guard

Anyone who want to use web guard instead of API.

Add the following trait on your controller, It will override the default API guard.

trait UserResolverTrait
{
    public function resolveUser(): ?\Illuminate\Contracts\Auth\Authenticatable
    {
        return Auth::guard('web')->user();
    }
}

Does "Request" have to be an instance of "Orion\Http\Requests\Request"?

When hitting an Orion index route, I'm getting the error:

Argument 1 passed to Orion\\Http\\Controllers\\Controller::index() must be an instance of Orion\\Http\\Requests\\Request

Does this mean that we are unable to use laravels Illuminate\Foundation\Http\FormRequest class for requests?

If not, that's totally fine! Might just need to add that to the docs here:
https://tailflow.github.io/laravel-orion-docs/guide/security.html#authentication

How to disable policy for a specific endpoint

Is it currently possible to disable policy checks for a given endoint?

My use case is for indexing users. When I hit api/users

  • an admin will get all users
  • a manager will get users they manage
  • a plain user will only get themself

However, I don't want to set my viewAny policy to return true; because that feels a bit risky.

...

I may be asking for too much... Honestly, this extension is an absolute dream. I've spent the last 4 days converting our entire API to orion because the benefits are insane!

Modify model query builder

I want to modify the model query in a controller before a request is executed. This is required if I want to filter the data and show only owned models by a user in the index method,

Currently, I'm overriding newModelQuery in the controller to achieve the above

//in PostController.php

public function newModelQuery(): Builder
{
    $query = $this->getModel()::query();
    if(auth()->user()->hasRole('user')){
        $query = $query->where('user_id', auth()->user()->id);
    }
    return $query;
}

is there any better way to do this?

Can the hook itself pass the query builder as shown below?

protected function beforeIndex(Request $request, $query)
{
    if(auth()->user()->hasRole('user')){
        $query = $query->where('user_id', auth()->user()->id);
    }
}

Create post on current authenticated user

Firstly ,thank for provide such a helpful package!
How can I create post on current authenticated user? Or maybe I must use a relation controller like UserPostsController?

[Feature Request] Endpoint caching

It will be awesome if there is a default cache or customizable cache.

I was thinking do add it, but I'm not sure if using middleware is gonna be the best for this package or creating custom controllers, configs.

Also if there is a cache option it should be enabled as default, and disable as DisableCache trait.

Using slug instead of model ID

is it possible to make use of slug istead of id?

/**
* Get the route key for the model.
*
* @return string
*/
 public function getRouteKeyName()
{
    return 'slug';
}

so far using slug gave me SQLSTATE[22P02]: Invalid text representation: 7 ERROR: invalid input syntax for type bigint: error

Model resolution for Relationships fail when using explicit binding

When using relationship controllers, the model resolution fails when using explicit binding as mentioned here
https://laravel.com/docs/8.x/routing#explicit-binding

The following implementation works around it and uses laravel's internal route resolution logic and should theoretically work with slugs as well. This is only an example and I'm aware we need to update this in a lot of places

    protected function runParentFetchQuery(Request $request, Builder $query, $parentKey): Model
    {
        if ($parentKey instanceof $this->model) {
            return $parentKey;
        }

        /** @var Model $modelClass */
        $modelClass = $this->getModel();
        return (new $modelClass())->resolveRouteBinding($parentKey);
    }

Action classes

Hi!

I'm new to APIs, but this package seems pretty cool and structured in its setup.
I have a question though.

Currently, in my LabelController in the store() method, I call a StoreLabelAction. This dedicated single action class invokes the store and returns the Label model. I do this so that whenever I create a label, I have a single class that I can call. Like this:

    public function store(LabelRequest $request, StoreLabelAction $action)
    {
        $label = $action->execute($request->validated());

        return response($label, 200);
    }

My StoreLabelAction looks like this (simplified):

public function execute(array $data) : Label
	{
		if(Label::where('title', $data['title'])->first()) 
		{
			throw new LabelNotUniqueException("Label should be unique.");
		}

		$label = new Label;
		$label->title = $data['title'];
		$label->save();

		return $label;
	}

I feel this a clean way of doing so, but following the docs with the performStore() I seem to be lost on how to implement this logic. If at all. What am I missing?

route model binding has a 32 character limit

This is not an issue with orion. However, orion might be able to implement a solution to this error.

When using route model binding (as orion does behind the scenes), the name of the model cannot be more than 32 characters long.

The following will not work when creating a laravel route:

Route::get('very_long_route_name/{very_long_model_name_will_not_work_with_model_binding}', [SomeController::class, 'show');

I remember looking into this issue in the past, and it looks like Laravel doesn't intend on fixing this... It's actually because of a limitation in symfony.

Would it be possible to add something like modelBindingName('short_model_name') so that we can avoid this "32 character" limit?

Thanks for all your hard work!

Search in relationship with multiple databases.

Hi!

As far as I understand, there's no way to search in relations in different databases. When the QueryBuilder class applies the search to the query builder it uses the whereHas query builder function, but there's no way to specify another connection for the relation if needed.
I stand corrected if I'm wrong :)

Illuminate/Foundation dependency is missing

Hi,

I'm using this package in combination with Laravel Lumen.
I managed to get it working using very few additions to my own project.

I'm running into an issue where dependency 'illuminate/foundation' is not part of the tailflow/laravel-orion composer.json definition.

If you want I can submit a pull request?

In addition I can create a second pull request on how to use this awesome package with Laravel Lumen? Perhaps update the readme?

Thanks in advance!

Fallback to guarded when fillable is empty

The store and update methods rely solely on $model->getFillable()
However this returns empty when the models use $guarded instead of $fillable
We should hence gracefully fallback to $model->getGuarded() in such situations

Support for null filters

How would we go about implementing null filters

{
	"filters": [{
			"field": "created_at",
			"operator": ">=",
			"value": "2021-02-01"
		},
		{
			"field": "created_at",
			"operator": "<=",
			"value": "2021-02-28"
		},
		{
			"field": "category_id",
			"operator": "=",
			"value": null
		}
	]
}

This throws the following error
"The filters.2.value field is required when filters is present."

Could not check compatibility between

Am trying to override the buildIndexFetchQuery method on the controller and I got the following error message
'

Could not check compatibility between App\Http\Controllers\PostController::buildIndexFetchQuery(Illuminate\Http\Request $request, array $requestedRelations): App\Http\Controllers\Builder and Orion\Http\Controllers\Controller::buildIndexFetchQuery(Orion\Http\Requests\Request $request, array $requestedRelations): Illuminate\Database\Eloquent\Builder, because class App\Http\Controllers\Builder is not available

Any help?

ILIKE operator

Hi,

Is it possible to include in the list of operators ILIKE.
The key word ILIKE can be used instead of LIKE to make the match case-insensitive. This is not in the SQL standard but is a PostgreSQL extension.

How to filter null value

How to filters null value, alternative for the following query.

$users = User::where('active','=', null)->get()
$users = User::whereNotNull('active')->get();

Problem with model dependency injection in performStore hook

Declaration of App\\Http\\Controllers\\Orion\\AdminAuthorisedIpsController::performStore(Illuminate\\Http\\Request $request, App\\Models\\AdminAuthorisedIp $adminAuthorisedIp, array $attributes): void should be compatible with Orion\\Http\\Controllers\\Controller::performStore(Orion\\Http\\Requests\\Request $request, Illuminate\\Database\\Eloquent\\Model $entity, array $attributes): void"

Argument 3 passed to Orion\\Http\\Controllers\\Controller::performUpdate() must be of the type array, null given

Hello, am trying to use the laravel orion batch update functionality. it works fine on default but when i use change the keyName to slug it gives error "Argument 3 passed to Orion\Http\Controllers\Controller::performUpdate() must be of the type array, null given" what am i doing wrong?

am using postman for testing, bellow is my postman setup

Body -> row -> json

{
"resources": {
"nigeria": {
"description": "new description"
},
"gambia": {
"description": "new description"
}
}
}

URL: Patch -> http://elo.test/api/countries/batch

[Feature Request] Fully disable pagination

In some cases you don't want to use pagination, please add a concern to avoid/disable using pagination.

Such as

<?php

namespace App\Http\Controllers\Api;

use App\Models\Post;
use Orion\Concerns\DisablePagination;
use Orion\Http\Controllers\Controller;

class PostController extends Controller
{
    use DisablePagination;

    protected $model = Post::class;
}

Automatic Laravel Nova Integration

When looking at AWS amplify / Google firebase & how they have automatic API support with an admin panel..

Makes me imagine Nova & a fully featured API.. and how this package could maybe fill in that gap.

Like, this package could loop over Nova's resources, relationship fields & construct routes & controllers.

foreach(Nova::resources() as $resource) {

    $controller->model = $resource->model

    foreach($resource->fields() as $field) {
        if($field is hasMany etc) {
            $controller->relation = $field->attribute;
        }
    }
}

php artisan nova:resource Post, and you have a backend & API

Next all we need is some sort of open api / swagger automagic and we can all quit our jobs.


Just posing the idea. Great package btw :)

Call to undefined method HasManyThrough::save(), in Many-to-Many relation controller

Hi,

I'm using this package for the first time, and its amazing, btw.

But I'm running into a problem (at the moment, I can't figure it out, if is something on my side, or something I'm missing).

I have a Company model, a CompanyAddress model, and an Address model.
That means, Company has many Addresses through my CompanyAddress model.

I've set a controller for CompanyAddressController, that uses the $model = Company::class, and relation 'addresses' (which is my hasManyThrough relation).

The routes are set like this

Orion::hasManyThroughResource('companies', 'addresses', CompanyAddressController::class);

And the error below, is what I'm getting from the request. (using UUID as ids, not in the URL)

image

show method returns status code 422

I have follwed the documentation however when I visit api.consultation-category.show, supplying the id. returns status code 422 axios.get('api/consultation-category/' + cat_id)
.then((response)=>{
console.log(response)
})
controller
I placed my Api controllers inside a folder called Api

How to validate the parameter passed in the path uri?

Hello guys, I need to validate the parameter passed in the path.
The route is defined as follows:
api/customers/{customer}/address

In the validation rules the variable $this->request->customer is null.

Captura de Tela 2020-10-12 aฬ€s 07 25 54

And that is the request for the API.

Captura de Tela 2020-10-12 aฬ€s 07 28 14

[Feature Request] Add eloquent withCount() function

In Laravel you can use the following code to gather the amount of relationship items without loading the full model/data:

$threads = Thread::withCount(['posts'])->get();

We currently have:

    protected function includes(): array
    {
        return [
                'posts',
        ];
    }

    protected function alwaysIncludes(): array
    {
        return [
                'posts',
        ];
    }

Please add something like:

    protected function includesCount(): array
    {
        return [
                'posts',
        ];
    }

    protected function alwaysIncludesCount(): array
    {
        return [
                'posts',
        ];
    }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.