Hacker News new | ask | show | jobs
by theogravity 1045 days ago
I'm the core maintainer of the npm sqlite package that your library uses. I recommend that you don't make it a direct dependency, but use dependency injection or an adapter pattern that way your library isn't dependent on a specific package version of sqlite/sqlite3 (the npm sqlite package API is pretty static at this point though!).

The npm sqlite package used to have sqlite3 as a direct dependency in older major versions and most of the support issues were against sqlite3 instead of sqlite. Taking that dependency out and having the user inject it into sqlite instead removed 99% of the support issues. It's also really nice that sqlite has no dependencies at all.

If you go the adapter pattern route, you can support other sqlite libraries like better-sqlite3. Sometimes sqlite/sqlite3 doesn't fit a user's use-case and alternative libraries do.

Same deal with the pub/sub mechanism. You have the Typescript types defined to create abstractions. Would be nice to see adapters for Redis streams / kafka / etc, as in-memory pub/sub may not cut it after a certain point.

Great start on your library!

2 comments

Hey! Thanks for the feedback. I'd just be worried people might not be using the correct libraries since I think they still provide different functionality.

So exporting one Database that allows people to pick which driver they want to use might be a simple and user-friendly solution (incl. better-sqlite3) which people have asked for.

Interesting project! I have lately been trying out these cool and perhaps in some way similar sqlite libraries:

- https://github.com/haxtra/kvstore-sqlite (Basic key-value store for SQLite databases.)

- https://github.com/haxtra/super-sqlite3 (Fast SQLite library with optional full db encryption, simple query builder, and a host of utility features, all in one neat package.)

- https://github.com/haxtra/live-object (Standard javascript object with built-in JSON serialization to file. Dreams do come true sometimes.)

All from github user: https://github.com/haxtra

I think the super-sqlite3 source might also be an inspiration for the 'driver' topic: "super-sqlite3 is a thin wrapper around better-sqlite3-multiple-ciphers, which extends better-sqlite3 (the fastest SQLite library for node.js) with full database encryption using SQLite3MultipleCiphers. super-sqlite3 then adds its own query builder and other convenience features."

And do check out this user's XRay (JavaScript object browser component) library for your preferred component framework.

In my bookmarks I also found these other related and interesting links: - https://dgl.cx/2020/06/sqlite-json-support (An article about SQLite as a document database, using the relatively new 'genrated columns' feature of sqlite 3.31.0, which you seem to be using)

- https://www.npmjs.com/package/best.db (easy and quick storage)

- https://tinybase.org (This project seems to be an even more similar idea to Doculite) https://github.com/tinyplex/tinybase (The reactive data store for local-first apps.)

Good luck with your project!

> I recommend that you don't make it a direct dependency, but use dependency injection or an adapter pattern that way your library isn't dependent on a specific package version of sqlite/sqlite3

Do you have examples of what you mean?

Do you just mean in the code or is this something about how it is imported as a dependency in package.json?

They would have to install sqlite and sqlite3 separately and feed the sqlite instance to your library when creating a new instance of your library. It would no longer be a dependency in package.json (maybe a devDependency when you write unit tests using it). Look at how the npm sqlite source code does it as an example.

https://github.com/kriasoft/node-sqlite

In the src/index.ts file, the `open()` function takes in an instance of sqlite3, vs the file importing it from the sqlite3 package itself.

An adapter / driver pattern would extend this where you can interchange usage of either the sqlite package or an alternative.

For example, let's say you have two SQLite drivers. They both allow you to execute a statement, but their methods and maybe parameters are named differently. One might expose a `exec()`, while the other exposes `run()` to do the same thing.

Your codebase probably is coded for one or the other, which means you can't freely interchange the drivers.

So what you do to support this is create a an interface with methods that describe what you want to do. In the above case, you might describe a method called `executeStatement()`.

Then you create two classes, one for each driver. They both implement `executeStatement()`, but under the hood, they'll run `exec()` and `run()` in their implementations.

So your main code now will accept an instance of anything that implements your interface, and instead of calling like `exec()`, you'll be calling `executeStatement()`, which under the hood calls `exec()`.

So it goes like this:

- Define common interface for interacting with different database libraries (the abstraction)

- Implementation class using that interface (the drivers)

- Your constructor takes in anything that conforms to that interface

- The user creates an instance of your implementation (eg SqliteDriver) and feeds in the instance of the driver (eg `new SqliteDriver(<output of npm sqlite open()>)`

- Your code calls the interface methods in place of the actual database calls instead

You had something like:

  db.collection('users').set(..)
So rather than calling SQLite `run()`, it'd be calling your interface method `executeStatment()` (which calls `run()`) instead in that `set()` call.

It looks like the "Bridge" pattern is what you want here:

https://www.phind.com/agent?cache=cll1zw1n60010la08mn89dd8g

The generated code is Java, but the idea and concept is the exact same that I've described above.