Giter VIP home page Giter VIP logo

libsql-js's People

Contributors

aqrln avatar avinassh avatar benmccann avatar bentleylong avatar fehnomenal avatar giovannibenussi avatar glommer avatar haaawk avatar jkonowitch avatar luciofranco avatar marinpostma avatar notrab avatar penberg avatar psarna avatar sevinf avatar sivukhin 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

libsql-js's Issues

Support Database.inTransaction

The following should print false / true / false, but instead it always prints false:

    console.log(`db.inTransaction = ${db.inTransaction}`)
    db.transaction(() => {
        console.log(`db.inTransaction = ${db.inTransaction}`)
    })()
    console.log(`db.inTransaction = ${db.inTransaction}`)

Integration tests don't run locally

If you attempt to run integration tests, you will see:

  beforeEach hook for Open in-memory database

  Rejected promise returned by test. Reason:

  Error {
    code: 'MODULE_NOT_FOUND',
    requireStack: [
      '/Users/penberg/src/tursodatabase/libsql-js/index.js',
    ],
    message: `Cannot find module '@libsql/darwin-arm64'␊
    Require stack:␊
    - /Users/penberg/src/tursodatabase/libsql-js/index.js`,
  }

  › - /Users/penberg/src/tursodatabase/libsql-js/index.js
  › Object.<anonymous> (/Users/penberg/src/tursodatabase/libsql-js/index.js:42:5)

The issue is that we broke the tests in a bundler fix 3ba82a3.

We can fix the issue with the following, but that will break bunding again:

diff --git a/index.js b/index.js
index f92abb8..df0db81 100644
--- a/index.js
+++ b/index.js
@@ -39,7 +39,7 @@ const {
   statementColumns,
   statementSafeIntegers,
   rowsNext,
-} = require(`@libsql/${target}`);
+} = load(__dirname) || require(`@libsql/${target}`);

 const SqliteError = require("./sqlite-error");

diff --git a/promise.js b/promise.js
index 503cc61..ba0a26b 100644
--- a/promise.js
+++ b/promise.js
@@ -26,7 +26,7 @@ const {
   statementColumns,
   statementSafeIntegers,
   rowsNext,
-} = require(`@libsql/${currentTarget()}`);
+} = load(__dirname) || require(`@libsql/${currentTarget()}`);

 /**
  * Database represents a connection that can prepare and execute SQL statements.
  * ```

Disk image is malformed on `sync()` with fresh database

With the following knex dialect that runs db.sync() when a connection is opened:

const BaseClient = require("knex/lib/dialects/better-sqlite3");

class Client_Libsql extends BaseClient {
    static dialect = "libsql";

    _driver() {
        return require("libsql");
    }

    async acquireRawConnection() {
      const options = this.connectionSettings.options || {};

      console.log("Connecting with Turso")

      const db = new this.driver(this.connectionSettings.filename, options);
      db.sync();

      return db;
    }
}

Object.assign(Client_Libsql.prototype, {
    dialect: "libsql",
    driverName: "libsql",
});

module.exports = Client_Libsql;

An user reports that they get the following error when no database exists:

/tmp/testing/node_modules/libsql/index.js:90
    databaseSyncSync.call(this.db);
                     ^

Error: replication error: Injector error: SQLite error: database disk image is malformed
    at Database.sync (/tmp/testing/node_modules/libsql/index.js:90:22)
    at Client_Libsql.acquireRawConnection (/tmp/testing/src/db/turso-knex-client.js:17:10)
    at create (/tmp/testing/node_modules/knex/lib/client.js:262:39) {
  code: ''
}

But if they run the app again after the failure, it starts working fine.

Consider changing `sync()` to something more accurate (e.g. `pull()`)

Synchronization often suggests a two-way sync between two copies of data. What seems to be implemented here is a one-way "pull" of data missing in the embedded replica. Clients periodically "pull" new data into the database (similar to a git pull), and there is no concept of local changes to be pushed up to the service, since those are always delegated to the service.

libSQL Bundling Error with Expo Router v3 API Routes

Hello everyone,
I'm currently in the process of building an Expo app utilizing the Expo Router v3 API routes. Previously, I had been using Planetscale without any issues. However, upon attempting to migrate to Turso, I've encountered a bundling error:

Metro error: Unable to resolve module ./.targets from /Users/fefranco/code/libsql-expo/node_modules/libsql/index.js: 

None of these files exist:
  * node_modules/libsql/.targets(.web.ts|.ts|.web.tsx|.tsx|.web.js|.js|.web.jsx|.jsx|.web.json|.json|.web.cjs|.cjs|.web.mjs|.mjs|.web.scss|.scss|.web.sass|.sass|.web.css|.css)
  * node_modules/libsql/.targets/index(.web.ts|.ts|.web.tsx|.tsx|.web.js|.js|.web.jsx|.jsx|.web.json|.json|.web.cjs|.cjs|.web.mjs|.mjs|.web.scss|.scss|.web.sass|.sass|.web.css|.css)
   6 | // Static requires for bundlers.
   7 | if (0) {
>  8 |   require("./.targets");
     |            ^
   9 | }
  10 |
  11 | let target = currentTarget();


Call Stack
  Object.requireFileContentsWithMetro (node_modules/@expo/cli/src/start/server/getStaticRenderFunctions.ts:190:13)
  processTicksAndRejections (node:internal/process/task_queues)
  bundleAsync (node_modules/@expo/cli/src/start/server/metro/bundleApiRoutes.ts:39:26)
  getApiRoute (node_modules/@expo/cli/src/start/server/metro/createServerRouteMiddleware.ts:138:36)
  handler (node_modules/@expo/server/src/index.ts:178:20)
  <unknown> (node_modules/@expo/server/src/vendor/http.ts:36:24)
TypeError: Cannot read properties of null (reading 'GET')
    at handler (/Users/fefranco/code/libsql-expo/node_modules/@expo/server/src/index.ts:184:32)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at /Users/fefranco/code/libsql-expo/node_modules/@expo/server/src/vendor/http.ts:36:24
Unable to resolve "./.targets" from "node_modules/libsql/index.js"

I've created a minimal reproduction of the issue in CodeSandbox

Compliation fails with latest libsql

   Compiling libsql_replication v0.0.2 (https://github.com/libsql/libsql/?rev=59c4fd2c4d262a91879800903e3bae4180fdf6f4#59c4fd2c)
error[E0433]: failed to resolve: could not find `tokio_stream` in `codegen`
   --> /Users/penberg/.cargo/git/checkouts/libsql-00c15cfa15b9f13b/59c4fd2/crates/replication/src/generated/wal_log.rs:228:48
    |
228 |         type LogEntriesStream: tonic::codegen::tokio_stream::Stream<
    |                                                ^^^^^^^^^^^^ could not find `tokio_stream` in `codegen`

error[E0433]: failed to resolve: could not find `tokio_stream` in `codegen`
   --> /Users/penberg/.cargo/git/checkouts/libsql-00c15cfa15b9f13b/59c4fd2/crates/replication/src/generated/wal_log.rs:242:46
    |
242 |         type SnapshotStream: tonic::codegen::tokio_stream::Stream<
    |                                              ^^^^^^^^^^^^ could not find `tokio_stream` in `codegen`

For more information about this error, try `rustc --explain E0433`.
error: could not compile `libsql_replication` (lib) due to 2 previous errors

Error relocating @libsql/linux-x64-musl/index.node: fcntl64: symbol not found

I use fastify, turso and drizzle-orm.
It works fine locally on my windows machine.

I'm trying to deploy my app to koyeb.com (they have AMD EPYC CPU).

Dockerfile

FROM node:18-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable
RUN corepack prepare pnpm@latest --activate
RUN pnpm i --frozen-lockfile

FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /usr/app
COPY --from=builder /app/dist ./dist
COPY package.json ./
RUN npm i --omit=dev
USER node
ENV NODE_ENV="production"
CMD ["npm", "start"]

tsconfig.json
https://github.com/fastify/tsconfig/blob/master/tsconfig.json

After running npm start I get this error:

Error: Error relocating /usr/app/node_modules/@libsql/linux-x64-musl/index.node: fcntl64: symbol not found
    at Module._extensions..node (node:internal/modules/cjs/loader:1452:18)
    at Module.load (node:internal/modules/cjs/loader:1197:32)
    at Module._load (node:internal/modules/cjs/loader:1013:12)
    at Module.require (node:internal/modules/cjs/loader:1225:19)
    at require (node:internal/modules/helpers:177:18)
    at Object.<anonymous> (/usr/app/node_modules/libsql/index.js:42:5)
    at Module._compile (node:internal/modules/cjs/loader:1356:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1414:10)
    at Module.load (node:internal/modules/cjs/loader:1197:32)
    at Module._load (node:internal/modules/cjs/loader:1013:12) {
  code: 'ERR_DLOPEN_FAILED'
}

Same error with 18-alpine, 20-alpine, 21-alpine and npm/pnpm

UPD. Downgrading @libsql/client to 0.4.3 worked for me
Related issue: https://discord.com/channels/933071162680958986/1214203500939186186

Remote example is not working

Looks like missing execute_batch() in the libSQL remote protocol implementation:

penberg@vonneumann libsql-js % node examples/remote/example.js
thread '<unnamed>' panicked at 'not yet implemented', /Users/penberg/.cargo/git/checkouts/libsql-00c15cfa15b9f13b/c032b9f/libsql/src/hrana/mod.rs:277:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
zsh: abort      node examples/remote/example.js

exec() blindly splits statements on semicolons, fails on valid SQLite statements

The following fails due to an attempt to split statements on ";", even though it's valid SQL :

    db.exec("INSERT INTO t (value) VALUES ('x;y')`)

Similarly, SQL that attempts to create a trigger will fail because the trigger statements each need to end with a semicolon that's part of the greater SQL statement that creates it.

create trigger tr
after insert on tbl
for each row
begin
    update some stuff;  // this semicolon causes problems
end;

This failure is similar to the way the libsql shell previously failed on creating triggers when blindly splitting on semis: tursodatabase/libsql-shell-go#131

See functions js_exec_sync and js_exec_async, which both do the same thing.

Likely the only way to correctly handle semis in these situations is to use a tokenizing SQL parser that better understands SQLite syntax. It would be even better if this was a part of the common libsql core so all future client SDKs could use it without having to bring their own parser.

TypeScript type definitions missing since 0.25

While things work OK for JavaScript, now TypeScript types are missing.

import Libsql from 'libsql-experimental'

Error:

error TS7016: Could not find a declaration file for module 'libsql-experimental'. 'path/to/node_modules/libsql-experimental/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/libsql-experimental` if it exists or add a new declaration (.d.ts) file containing `declare module 'libsql-experimental';`

Something changed with the addition of the promise API.

Batch support?

Gustavo asked the following

What to do about scripts (IOW "multiple statements"), such as when you want to load some DB dump?

LLRT support?

LLRT is still in very very early stages, so it's not surprising that it's not working with turso/libsql.

It seems like it should be possible, and the main hurdle seems to be somehow bundling the correct binaries. I keep running into Cannot find module '@libsql/linux-arm64-gnu' and some minor syntax issues like awslabs/llrt#296

Bun install support

Installing libsql-experimental fails with Bun for some reason:

penberg@vonneumann foo % bun install libsql-experimental
bun add v0.7.0 (aa1ad7f0)
  🔍 @libsql-experimental/darwin-arm64 [1/2]
error: package "@libsql-experimental/darwin-arm64" not found registry.npmjs.org/@libsql-experimental/darwin-arm64 404
  🔍 @libsql-experimental/linux-x64-gnu [2/2]
error: package "@libsql-experimental/linux-x64-gnu" not found registry.npmjs.org/@libsql-experimental/linux-x64-gnu 404
error: @libsql-experimental/[email protected] failed to resolve
error: @libsql-experimental/[email protected] failed to resolve

Bundler support is broken

I used the package in a lambda built via sst & esbuild. It seems the dependency is not included inside the output bundle.

Error: Cannot find module '@libsql/darwin-arm64'
image

I see that there is a special line to force bundlers to include the dependency, but the file ./targets seems to be missing, maybe it's related?

libsql-js/index.js

Lines 6 to 9 in 6e705ac

// Static requires for bundlers.
if (0) {
require("./.targets");
}

Neon toolchain does not work with Rust 1.79.0

The build breaks with:

> cargo build --message-format=json | npm exec neon dist

   Compiling libsql-js v0.3.12 (/home/haaawk/libsql-js)
error: No artifacts were generated for crate libsqlibsql-js                                  
error: Broken pipe (os error 32)
warning: build failed, waiting for other jobs to finish..

This is due to the fact that 1.79.0-nightly (80d5b607d 2024-04-19) version of cargo does not have the following lines in JSON that were present in previous versions of cargo:

{"reason":"compiler-message","package_id":"path+file:///Users/haaawk/work/libsql-js-perf#[email protected]","manifest_path":"/Users/haaawk/work/libsql-js-perf/Cargo.toml","target":{"kind":["cdylib"],"crate_types":["cdylib"],"name":"libsql-js","src_path":"/Users/haaawk/work/libsql-js-perf/src/lib.rs","edition":"2021","doc":true,"doctest":false,"test":true},"message":{"rendered":"warning: unused variable:cx\n --> src/database.rs:290:24\n |\n290 | ...&self, cx: &mut FunctionContext) -> Option<Arc<Mutex<libsql::Connection>>> {\n | ^^ help: if this is intentional, prefix it with an underscore: _cx\n |\n = note: #[warn(unused_variables)] on by default\n\n","$message_type":"diagnostic","children":[{"children":[],"code":null,"level":"note","message":"#[warn(unused_variables)]on by default","rendered":null,"spans":[]},{"children":[],"code":null,"level":"help","message":"if this is intentional, prefix it with an underscore","rendered":null,"spans":[{"byte_end":11024,"byte_start":11022,"column_end":26,"column_start":24,"expansion":null,"file_name":"src/database.rs","is_primary":true,"label":null,"line_end":290,"line_start":290,"suggested_replacement":"_cx","suggestion_applicability":"MaybeIncorrect","text":[{"highlight_end":26,"highlight_start":24,"text":" fn get_conn(&self, cx: &mut FunctionContext) -> Option<Arc<Mutex<libsql::Connection>>> {"}]}]}],"code":{"code":"unused_variables","explanation":null},"level":"warning","message":"unused variable:cx","spans":[{"byte_end":11024,"byte_start":11022,"column_end":26,"column_start":24,"expansion":null,"file_name":"src/database.rs","is_primary":true,"label":null,"line_end":290,"line_start":290,"suggested_replacement":null,"suggestion_applicability":null,"text":[{"highlight_end":26,"highlight_start":24,"text":" fn get_conn(&self, cx: &mut FunctionContext) -> Option<Arc<Mutex<libsql::Connection>>> {"}]}]}}

{"reason":"compiler-message","package_id":"path+file:///Users/haaawk/work/libsql-js-perf#[email protected]","manifest_path":"/Users/haaawk/work/libsql-js-perf/Cargo.toml","target":{"kind":["cdylib"],"crate_types":["cdylib"],"name":"libsql-js","src_path":"/Users/haaawk/work/libsql-js-perf/src/lib.rs","edition":"2021","doc":true,"doctest":false,"test":true},"message":{"rendered":"warning: 2 warnings emitted\n\n","$message_type":"diagnostic","children":[],"code":null,"level":"warning","message":"2 warnings emitted","spans":[]}}

{"reason":"compiler-artifact","package_id":"path+file:///Users/haaawk/work/libsql-js-perf#[email protected]","manifest_path":"/Users/haaawk/work/libsql-js-perf/Cargo.toml","target":{"kind":["cdylib"],"crate_types":["cdylib"],"name":"libsql-js","src_path":"/Users/haaawk/work/libsql-js-perf/src/lib.rs","edition":"2021","doc":true,"doctest":false,"test":true},"profile":{"opt_level":"z","debuginfo":0,"debug_assertions":false,"overflow_checks":false,"test":false},"features":[],"filenames":["/Users/haaawk/work/libsql-js-perf/target/release/liblibsql_js.dylib"],"executable":null,"fresh":true}

Support multiple statements using `exec`

Currently, it only executes the first statement. e.g. db.exec("create table foo (x); create table bar (x);") only creates the foo table and ignores everything after the first semicolon.

Knex compatibility

I'm getting the following errors when using with knex.

Im using the database export from the pormise.js file

  1. statement.run is undefined. i noticed this when running knex.raw()
  2. types are not exported of promise.js so in a ts file import Database from 'libsql/promise'

stmt.run().lastInsertRowid is wrong with RANDOM ROWID

Hi, I am new to JS so it is possible I am missing something.

  • I am using the RANDOM ROWID feature with libsql. When this is enabled I cannot retrieve items using the lastInsertRowid property from stmt.run() commands after insertion. The lastInsertRowid is slightly off from the actual rowid. However using stmt.all() after a select * ... statement I can get the correct rowids.
  • Looking at the tests they do not seem to cover the BigInt/SafeIntegers use case.

Here is example code to reproduce the issue:

import Database from 'libsql';

const db = new Database(':memory:');
db.defaultSafeIntegers(true);
db.exec(`
  DROP TABLE IF EXISTS users;
  CREATE TABLE users (rowid INTEGER PRIMARY KEY, name TEXT) RANDOM ROWID;`);

const stmt_insert = db.prepare("INSERT INTO users(name) VALUES (?)");
const info_insert = stmt_insert.run('alma');
console.log([typeof info_insert.lastInsertRowid, info_insert.lastInsertRowid]);
const stmt_list = db.prepare("SELECT * from users");
const info_list = stmt_list.all()
console.log([typeof info_list[0].rowid ,info_list]);

and sample output:

$ node ex.js
[ 'number', 4504423795271622000 ]
[ 'bigint', [ { rowid: 4504423795271622254n, name: 'alma' } ] ]

Database constructor performs unwanted blocking / erroring operations

Running new Database with a configuration that wants to use a remote sqld will cause the program to block on disk or network I/O for some amount of time, potentially for very long amounts of time. It can also cause the constructor to fail. There are two problems with this off the top of my head:

  • It's highly unexpected for a constructor to block in JS, especially for setups that want to eagerly create objects for later use, or expect that their DI never blocks or fails, and always provides an object.
  • It prevents the program from working at all if something is wrong establishing a connection to the server at the time the constructor is called (unless the requirement is that the caller should catch and retry errors with the constructor, which I don't like).

It would be more clean if the constructor only failed on configuration or programming errors (or I suppose whatever might make the underlying better-sqlite3 constructor fail, but it might be reasonable to delay that as well). This would allow the program to work if initially offline or without connection, and make error recovery easier to implement. As an alternative:

  • The request to pull new data could lazily contact the server and yield an error message at that point.
  • The SDK could eagerly contact the server off the main thread so that it's quickly ready for the first sync.

prepare function doesn't throw an error for invalid SQL

Example:

import Database from "libsql";
const opts = {
    authToken: process.env.TURSO_AUTH_TOKEN || ''
} as any;

const db = new Database(process.env.TURSO_DATABASE_URL || '', opts)
console.log(db.prepare('select invalidcolumn')); //doesn't throw error;
console.log(db.prepare('select * as')); //throw error

Using the 'better-sqlite' driver it throws an error in both situations.

Sync example fails with latest libSQL

With #54, the sync example fails as follows:

penberg@vonneumann sync % node example.js
2023-11-01T09:20:21.706640Z  INFO libsql_replication::replicator: Attempting to perform handshake with primary.
2023-11-01T09:20:21.706790Z  INFO libsql::replication::remote_client: Attempting to perform handshake with primary.
2023-11-01T09:20:21.972033Z ERROR libsql_replication::replicator: error connecting to primary. retrying. error: Replicator client error: invalid length: expected length 32 for simple format, found 0

Problem using this package (as transition dependency of @libsql/client) because of `__dirname`

Hello,
I am using @sveltejs/kit with @sveltejs/adapter-node and that kinda forces me to go ESM. And ESM does not like __dirname and similar stuff.

This line causes me troubles - https://github.com/libsql/libsql-js/blob/main/index.js#L33

What is to solution to this? Create ESM index.js?
I might be also very wrong and this issues is completely unrelated.

I've been stuck with this error for some time and it feels like I am getting out of options. Any help and ideas appreciated.
Thank you 🙏

Error handling

We have bunch of unwrap()calls in the code, let's fix them up...

Improve promise API

There's preliminary support for promise API, but we still need to do asyncify the following:

[ ] Database.open()
[x] Database.sync()
[ ] Statement.run()
[ ] Make Statement.iterate() return an async iterator?

Support all variants of bound parameters

SQLite supports a variety of styles for bound parameters:

  • ?
  • ?NNN
  • :VVV
  • @ VVV
  • $VVV

With 0.18, this library only supports the first (positional) and fourth (:) styles, while better-sqlite3 supports them all.

One thing that took me by surprise is the way the second (positional/indexed) style works with better-sqlite3. You pass the values in an object as if they were named, like this:

const insert = db.prepare('INSERT INTO users VALUES (?3, ?2, ?1)')
insert.run({
    2: 'Alice',
    1: '[email protected]',
    3: 2,
})

Specifying them as varargs to run() or an array just doesn't work at all.

Promise API needs TS types

import LibsqlP from 'libsql-experimental/promise'
src/both.ts:3:21 - error TS7016: Could not find a declaration file for module 'libsql-experimental/promise'. 'path/to/node_modules/libsql-experimental/promise.js' implicitly has an 'any' type.
  If the 'libsql-experimental' package actually exposes this module, try adding a new declaration (.d.ts) file containing `declare module 'libsql-experimental/promise';`

Support readonly option

With better-sqlite3, the readonly option behaves as such:

  • Constructor fails when combined with an in-memory database

TypeError: In-memory/temporary databases cannot be readonly

  • Queries that mutate fail

SqliteError: attempt to write a readonly database

  • db.readonly set to true

This library does none of the above yet.

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.