Giter Club home page Giter Club logo

directus / v8-archive Goto Github PK

View Code? Open in Web Editor NEW
503.0 30.0 204.0 158.22 MB

Directus Database API — Wraps Custom SQL Databases with a REST/GraphQL API

Home Page: https://docs.directus.io/api/reference.html

PHP 47.83% Shell 0.40% Twig 0.05% JavaScript 1.42% TypeScript 3.18% HTML 0.02% CSS 0.36% Vue 10.12% SCSS 0.79% Dockerfile 0.02% C# 12.60% Go 2.02% Scala 0.01% Java 7.19% Ruby 6.45% Objective-C 6.81% Makefile 0.01% Python 0.28% Handlebars 0.33% Liquid 0.11%
php directus mysql database database-management api api-wrapper rest-api restful vue

v8-archive's Introduction

Directus Logo

 

This end-of-life version of Directus has been deprecated and is superseded by the latest version on directus/directus. Learn more about the updates and improvements of our new version at directus.io.

 

Our Core team is quite small and can not maintain legacy versions of Directus. We'll glady review any community pull-requests to this repository, but using this version is highly discouraged.

 

Usage

To build this archive to a ready to use bundle, run the build.sh file


Directus is released under the GPLv3 license. RANGER Studio LLC owns all Directus trademarks and logos on behalf of our project's community. Copyright © 2006-2020, RANGER Studio LLC.

v8-archive's People

Contributors

anticz avatar apiraino avatar assmith avatar benhaynes avatar binal-7span avatar coolov avatar dev7ch avatar freen avatar hemratna avatar itsmerhp avatar javiercf avatar jel-massih avatar jsguru-git avatar kinzi avatar lapsus avatar lasha avatar maxgsoulcycle avatar mickrip avatar moebrowne avatar nitwel avatar okayawright avatar parnas avatar rijkvanzanten avatar stevepatter avatar theharshin avatar urvashi-7span avatar vincentkempers avatar wellingguzman avatar wolfulus avatar worksdev 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

v8-archive's Issues

Update readme

Add installation and usage instructions and add section on code structure

Add separate fields for validation errors

The API can return more than one validation error on posting / updating items. For example: wrong datatype and too long.

In order to make the error more useful, we should change the error message structure to something like:

{
  "code": 4,
  "message": "There was a validation error on the fields",
  "info": [
    {
      "field": "name",
      "message": "This value should not be blank.",
      "code": 15
    },
    {
      "field": "age",
      "message": "The maximum length has been exceeded",
      "code": 20
    }
  ]
}

Item rollback

Add ability to rollback an activity.

Ex: Item updated has a mistake? rollback to previous information.

Protect global endpoints

We have two endpoints (/types and /server) that cannot be "protected" via ACL (Permissions).

Are these types of endpoints going to be admin-only, user-only or public?

Add support for M2MM / M2X

The API should be able to retrieve the nested (related) items based on primary key and mixed collection, meaning it can nest items from multiple different tables

Fetch Item by revision

Add ability to fetch a item by revision.

Ex: Get information of a particular item when it was created. (first revision).

Endpoint: /items/articles/revision/1

Add GROUP datatype & directus_fields.group column

The GROUP datatype will be similar to ALIAS, in that it doesn't actually store any data. It's just there so the application can know it's an interface meant to group other interfaces.

The directus_fields.group column will be a foreign key to the (same) directus_fields table. That way, we have a way of marking what interfaces go in what group.

Add installation flow

This can be done in either of two ways:

Create installation endpoints

The API has an endpoint that can be POST-ed to to create a (new) config file. This means the application can have an installation flow build in that can be used for any api.

Add GUI in the API

When trying to use the api from the browser when there is no config file setup redirects to the install wizard.

There are a few unanswered questions:

  • Do we only allow the install wizard for "fresh" installations? E.g. the first config file
  • How would the user create additional config files? Manually?
  • In case we allow the user to use the same wizard for multiple envs, how do we secure this flow?

POSTing to directus_fields fails

Posting a new item to /_/fields/<collection> results in

{
    "error": {
        "code": "42000",
        "message": "SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 2",
        "class": "PDOException",
        "file": "/var/www/directus-demo-api/vendor/zendframework/zend-db/src/Adapter/Driver/Pdo/Connection.php",
        "line": 379,
Full error
{
    "error": {
        "code": "42000",
        "message": "SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 2",
        "class": "PDOException",
        "file": "/var/www/directus-demo-api/vendor/zendframework/zend-db/src/Adapter/Driver/Pdo/Connection.php",
        "line": 379,
        "trace": [
            {
                "file": "/var/www/directus-demo-api/vendor/zendframework/zend-db/src/Adapter/Driver/Pdo/Connection.php",
                "line": 379,
                "function": "query",
                "class": "PDO",
                "type": "->",
                "args": [
                    "ALTER TABLE `projects`\n ADD COLUMN `testing` VARCHAR"
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/zendframework/zend-db/src/Adapter/Adapter.php",
                "line": 205,
                "function": "execute",
                "class": "Zend\\Db\\Adapter\\Driver\\Pdo\\Connection",
                "type": "->",
                "args": [
                    "ALTER TABLE `projects`\n ADD COLUMN `testing` VARCHAR"
                ]
            },
            {
                "file": "/var/www/directus-demo-api/src/core/Directus/Database/Schema/SchemaFactory.php",
                "line": 179,
                "function": "query",
                "class": "Zend\\Db\\Adapter\\Adapter",
                "type": "->",
                "args": [
                    "ALTER TABLE `projects`\n ADD COLUMN `testing` VARCHAR",
                    "execute"
                ]
            },
            {
                "file": "/var/www/directus-demo-api/src/core/Directus/Services/TablesService.php",
                "line": 733,
                "function": "buildTable",
                "class": "Directus\\Database\\Schema\\SchemaFactory",
                "type": "->",
                "args": [
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/src/core/Directus/Services/TablesService.php",
                "line": 367,
                "function": "updateTableSchema",
                "class": "Directus\\Services\\TablesService",
                "type": "->",
                "args": [
                    "projects",
                    {
                        "fields": [
                            {
                                "type": "TINYINT",
                                "interface": "toggle",
                                "field": "testing",
                                "collection": "projects"
                            }
                        ]
                    }
                ]
            },
            {
                "file": "/var/www/directus-demo-api/src/endpoints/Fields.php",
                "line": 56,
                "function": "addColumn",
                "class": "Directus\\Services\\TablesService",
                "type": "->",
                "args": [
                    "projects",
                    "testing",
                    {
                        "type": "TINYINT",
                        "interface": "toggle"
                    },
                    []
                ]
            },
            {
                "function": "create",
                "class": "Directus\\Api\\Routes\\Fields",
                "type": "->",
                "args": [
                    {},
                    {},
                    {
                        "env": "_",
                        "collection": "projects"
                    }
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/Handlers/Strategies/RequestResponse.php",
                "line": 41,
                "function": "call_user_func",
                "args": [
                    [
                        {},
                        "create"
                    ],
                    {},
                    {},
                    {
                        "env": "_",
                        "collection": "projects"
                    }
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/Route.php",
                "line": 335,
                "function": "__invoke",
                "class": "Slim\\Handlers\\Strategies\\RequestResponse",
                "type": "->",
                "args": [
                    [
                        {},
                        "create"
                    ],
                    {},
                    {},
                    {
                        "env": "_",
                        "collection": "projects"
                    }
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/MiddlewareAwareTrait.php",
                "line": 117,
                "function": "__invoke",
                "class": "Slim\\Route",
                "type": "->",
                "args": [
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/Route.php",
                "line": 313,
                "function": "callMiddlewareStack",
                "class": "Slim\\Route",
                "type": "->",
                "args": [
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/App.php",
                "line": 495,
                "function": "run",
                "class": "Slim\\Route",
                "type": "->",
                "args": [
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/akrabat/rka-ip-address-middleware/src/IpAddress.php",
                "line": 93,
                "function": "__invoke",
                "class": "Slim\\App",
                "type": "->",
                "args": [
                    {},
                    {}
                ]
            },
            {
                "function": "__invoke",
                "class": "RKA\\Middleware\\IpAddress",
                "type": "->",
                "args": [
                    {},
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/DeferredCallable.php",
                "line": 43,
                "function": "call_user_func_array",
                "args": [
                    {},
                    [
                        {},
                        {},
                        {}
                    ]
                ]
            },
            {
                "function": "__invoke",
                "class": "Slim\\DeferredCallable",
                "type": "->",
                "args": [
                    {},
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/MiddlewareAwareTrait.php",
                "line": 70,
                "function": "call_user_func",
                "args": [
                    {},
                    {},
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/src/core/Directus/Application/Http/Middlewares/AuthenticationMiddleware.php",
                "line": 113,
                "function": "Slim\\{closure}",
                "class": "Slim\\App",
                "type": "->",
                "args": [
                    {},
                    {}
                ]
            },
            {
                "function": "__invoke",
                "class": "Directus\\Application\\Http\\Middlewares\\AuthenticationMiddleware",
                "type": "->",
                "args": [
                    {},
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/DeferredCallable.php",
                "line": 43,
                "function": "call_user_func_array",
                "args": [
                    {},
                    [
                        {},
                        {},
                        {}
                    ]
                ]
            },
            {
                "function": "__invoke",
                "class": "Slim\\DeferredCallable",
                "type": "->",
                "args": [
                    {},
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/MiddlewareAwareTrait.php",
                "line": 70,
                "function": "call_user_func",
                "args": [
                    {},
                    {},
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/src/core/Directus/Application/Http/Middlewares/CorsMiddleware.php",
                "line": 20,
                "function": "Slim\\{closure}",
                "class": "Slim\\App",
                "type": "->",
                "args": [
                    {},
                    {}
                ]
            },
            {
                "function": "__invoke",
                "class": "Directus\\Application\\Http\\Middlewares\\CorsMiddleware",
                "type": "->",
                "args": [
                    {},
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/DeferredCallable.php",
                "line": 43,
                "function": "call_user_func_array",
                "args": [
                    {},
                    [
                        {},
                        {},
                        {}
                    ]
                ]
            },
            {
                "function": "__invoke",
                "class": "Slim\\DeferredCallable",
                "type": "->",
                "args": [
                    {},
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/MiddlewareAwareTrait.php",
                "line": 70,
                "function": "call_user_func",
                "args": [
                    {},
                    {},
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/MiddlewareAwareTrait.php",
                "line": 117,
                "function": "Slim\\{closure}",
                "class": "Slim\\App",
                "type": "->",
                "args": [
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/App.php",
                "line": 388,
                "function": "callMiddlewareStack",
                "class": "Slim\\App",
                "type": "->",
                "args": [
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/vendor/slim/slim/Slim/App.php",
                "line": 296,
                "function": "process",
                "class": "Slim\\App",
                "type": "->",
                "args": [
                    {},
                    {}
                ]
            },
            {
                "file": "/var/www/directus-demo-api/src/core/Directus/Application/Application.php",
                "line": 142,
                "function": "run",
                "class": "Slim\\App",
                "type": "->",
                "args": [
                    false
                ]
            },
            {
                "file": "/var/www/directus-demo-api/public/index.php",
                "line": 5,
                "function": "run",
                "class": "Directus\\Application\\Application",
                "type": "->",
                "args": []
            }
        ]
    }
}

DateUtils should use php default timezone

DateUtils methods should use the default timezone instead of UTC.


Additional discussion

Should Directus convert all times to UTC or not?

We have been asked several time to allow Directus to customized the system date time timezone.

Allowing this the user can set the timezone Directus must use for the core datetime value.

--- Edit:
This being configurable means if the user change the timezone, Directus won't take consideration of this change and will serve system time incorrectly. Is this a good idea?

Use SwiftMailer transports as adapters

I tried to find a better way to create multiple adapters, and created new interfaces that were used to create messages.

This was a bad idea in the middle of the process as I realized that I was creating the same swifter mailer transport.

Using the transport as an adapter is a better way because we let the mailer to take the transport message and use it as needed.

Add web-hooks system

This feels like it's an easy one to add, but i might be wrong.

The API will call a provided url for any given hook.

The questions that remain are:

  • Where do we keep this mapping? (DB vs config.php)
  • Do we support multiple webhooks per event?
  • Do we even need to bother designing a specific way of handling this, seeing how easy it would be to throw in a http request using the normal events?

Add FILE data type

File data type will represents a Many to one relationship to directus_files table.

Customizable emails template

Allow user to customize the Directus default email templates.

Users has requested a way to replace the current emails template with their own.

Adding a new directory in /customs will allow them to create a template with the same name, having the custom template priority over the default one.

Re-implement refresh tokens

Implement refresh tokens or a different way to refresh a token before or after it gets invalid.

Currently for development purpose this can be fetch using the access token and /auth/refresh endpoint.

Create basis (system) schema

Directus has a few system-records in the DB to operate properly. Think of records like system relations (files-folders).

We need to include an SQL file that can be used during installation so the DB is setup correctly.

We also need to make sure this file can be used when you already have data in your database. (When you move your existing dataset to directus)

This needs to wait a bit till we have finalized the api / app integration.

Rename table to collection

There are certain parts in the API and the API's API's (so meta) that still use the term table instead of collection. For example in hooks:

<?php
return [
    'filters' => [
        'table.insert:before' => new \Directus\Customs\Hooks\Products\BeforeInsertProducts()
    //  ^^^^^^^
    ]
];

Allow POSTing to /activity

Sending messages between users is done by posting an item to activity with the content in the message column. In order to do this, we need to be able to POST to /activity

Create endpoint to check logs

Is creating a endpoint to check the API logs useful?
a endpoint /server/logs to see a full list of the logs, a collection of log files. and /server/logs/<file> get an specific file log data.

Updating Aliases data type.

When updating a field from non-alias type to an alias type it should drop the column. losing all of its data.

  • Update an alias type without changing its type
    • Implementation
    • Test Case
  • Update an alias type changing its type to a non-alias
    • Implementation
    • Test Case
  • Update a non-alias type to a alias type
    • Implementation
    • Test Case

Add option to set API in read-only mode

Adding a option to put the API in read-only mode, we lock all actions except reading data from the API.

We can aggressively cache the result as there's not change to be expected.

Add (native) support for rate-limiting

Could be very nice to have an "official" rate-limiting system in place that can use a user-defined setting (either in the api config file or (preferably) directus_settings) to protect the api

Set email subjects

Create proper email subjects

Email subjects:

  • User invitation (currently: Invitation to Directus)
  • New Installation (currently: Your new Directus instance: <project-title>)
  • Forgot Password (currently: Reset password)
  • Reset Password (currently: New password)

Forgot password is when the user request they have forgotten their password, sending them a link to reset its password.

Reset password is when they received a temporary password.

Split Log Files into Days – Options for Pruning After X Days

Right now, there is no limit on the filesize of the logs. This can potentially result in huge huge file. We need to implement either a max filesize for the logs, or an "expiry date". (Either delete old records when filesize > threshold, or when logs.date < threshold)

Cache Route callable

Caching the router callable prevent the api for looking the callable on every request.

Add /export and /import endpoints

It would be extremely cool if Directus had a native way of exporting and importing (existing) data and whole Directus setups. Moving from a locally installed Directus to hosted should/could be as easy as hitting export, logging in to hosted, and hitting import.

Different "types" of import/export?:

  • Schema only
    Just import/export your table setup & corresponding records in directus_* tables
  • Data only
    Just import/export the data
  • Everything
    A full database dump. Useful for backups & migrating between servers / hosted-local

Thoughts @wellingguzman @benhaynes ?

Handle PHP errors/exceptions before the App starts

Before the application start, all exceptions are thrown into the stream. When the application is in production this may not be pretty to have.

Catching these exceptions/errors and convert it to json can help the expected json from the api. Having to dump any exceptions to the stream is helpful when you are developing otherwise, no good.

Fields parameter breaks on *.*

An asterisk (*) should fetch all fields, by that logic, *.* should fetch all fields and all related fields of 1 level deep (*.*.* = two levels deep etc). However, *.* only renders an empty array:

Example:

localhost/api/public/_/users

Works as expected

{
    "data": [
        {
            "id": 1,
            "status": 1,
            "first_name": "Admin",
            "last_name": "User",
            "email": "[email protected]",
            "email_notifications": true,
            "group": 1,
            "avatar": "2",
            "company": null,
            "title": null,
            "locale": "en-US",
            "locale_options": null,
            "timezone": "America/New_York",
            "last_ip": null,
            "last_login": null,
            "last_access": null,
            "last_page": "",
            "token": "JVl4J0inbW6IqjNR8QzCPZ2nvP6b4fHM",
            "invite_token": "",
            "invite_accepted": 0
        }
    ]
} 

localhost/api/public/_/users?fields=*

Works as expected

{
    "data": [
        {
            "id": 1,
            "status": 1,
            "first_name": "Admin",
            "last_name": "User",
            "email": "[email protected]",
            "email_notifications": true,
            "group": 1,
            "avatar": "2",
            "company": null,
            "title": null,
            "locale": "en-US",
            "locale_options": null,
            "timezone": "America/New_York",
            "last_ip": null,
            "last_login": null,
            "last_access": null,
            "last_page": "",
            "token": "JVl4J0inbW6IqjNR8QzCPZ2nvP6b4fHM",
            "invite_token": "",
            "invite_accepted": 0
        }
    ]
}

localhost/api/public/_/users?fields=*.*

Should fetch all fields and all related fields, but breaks completely

{
    "data": [
        []
    ]
}

localhost/api/public/_/users?fields=*,avatar.*

_ Works as expected _

{
    "data": [
        {
            "id": [],
            "status": 1,
            "first_name": "Admin",
            "last_name": "User",
            "email": "[email protected]",
            "email_notifications": true,
            "group": 1,
            "avatar": {
                "id": 2,
                "filename": "29386209_1216971415099541_8182455277803536384_n.jpg",
                "title": "29386209 1216971415099541 8182455277803536384 N",
                "description": "Mooie foto van richard",
                "location": "",
                "tags": "",
                "width": 320,
                "height": 320,
                "filesize": 11977,
                "duration": null,
                "metadata": null,
                "type": "image/jpeg",
                "charset": "binary",
                "embed": null,
                "folder": null,
                "upload_user": 1,
                "upload_date": "2018-04-06 21:01:59",
                "storage_adapter": "local"
            },
            "company": null,
            "title": null,
            "locale": "en-US",
            "locale_options": null,
            "timezone": "America/New_York",
            "last_ip": null,
            "last_login": null,
            "last_access": null,
            "last_page": "",
            "token": "JVl4J0inbW6IqjNR8QzCPZ2nvP6b4fHM",
            "invite_token": "",
            "invite_accepted": 0
        }
    ]
}

Add support for extension hooks / endpoints

Each extension should have the ability to add hooks and/or endpoints dynamically to the api.

For example, the slug interface has an option to mirror the content of another field. As of right now, this feature only works from the Directus app, seeing the interface doesn't have any say over the api.

This enhancement would mean that the interface has it's own entry point to the APIs hooks system.

.
├── Interface.js
├── Readonly.js
├── hooks.php  <<<<<<<<<<<<
├── meta.json
├── package-lock.json
└── package.json

that allows it to modify values in the API as well. This would mean that interface settings are both for the app and for the API. A very very powerful feature.

The extension hooks could / should look the same as "regular" hooks:

'hooks' => [
    'postInsert' => function ($TableGateway, $record, $db) {
        // ...
    },
]

It would be very handy if interfaces could get their options (as set in directus_fields.options) passed as well. BUT, seeing this is only valid for interfaces (and not all other extensions), it might not be worth it. (It can also be fetched from $db if needed).

Next to Hooks, it's also crucial for extensions to be able to add endpoints directly. Again, the slug interface would add a /slug endpoint that would sluggify the value, so the application can use that endpoint to dynamically show the to-be-saved slugged value.

Just like hooks, these endpoints would look the same as "regular" custom endpoints:

$app->post('/slug', function () {
    // ...
});

and would reside in the their own file:

.
├── Interface.js
├── Readonly.js
├── endpoints.php  <<<<<<<<<<<<
├── meta.json
├── package-lock.json
└── package.json

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.