UNPKG

sq3-kv-data-store

Version:

Node.js key/value store for SQLITE3 that includes data search features

265 lines (184 loc) 8.75 kB
# Key/Value store for SQLite3, with data search features The `sq3-kv-data-store` package is primarily a key/value store built on top of SQLITE3. While it is meant to be used in-memory (`:memory:`), it can be used with any SQLITE3 storage location. Its tables can be stored alongside other tables in an application using SQLITE3. # Features * Like other KVS packages it has `put`, `get` `update`, and `delete` methods that take a simple text _key_ to add/retrieve/update/delete a _value_. The _value_ is treated as, and stored as, JSON. * It also supports `keys` and `exists` methods. * It supports a powerful `find` operating for performing Mango/Mongo-like queries on the JSON values stored in the `value` field. This relies on SQLITE3's built-in JSON capabilities. Selector objects are converted, on the fly, into SQL WHERE clauses testing JSON fields. # Comparison to related packages * KeyV is a typical key/value store. * Its API supports `get`, `set` and `delete` methods. * It supports multiple back-end storage engines. * It is an excellent choice if your needs are limited to set/get/delete values using a key. * PouchDB is a light-weight database that can serve as a key/value store and it can run in apps on mobile devices. * It has the `get`, `set`, and `delete` methods of a key/value store. * It supports synchronization to CouchDB for data persistence. * It supports a MongoDB-like `find` method, if you install the `pouchdb-find` plugin. * But, there are many strage quirks to the API, and on Node.js it brings in a lot of stale LevelDown-related packages that generate security warnings. # Install and usage Install: ```shell $ npm install sq3-kv-data-store --save ``` Usage: ```js import sqlite3 from 'sqlite3'; import * as sqlite_regex from "sqlite-regex"; import { SQ3DataStore } from 'sq3-kv-data-store'; ``` At the moment this package only supports use in ESM contexts. To use in CommonJS code, use the `import()` function. The `SQ3DataStore` class wraps around an SQLite3 instance. It has only been tested against the `sqlite3` package on Node.js. The `sqlite-regex` package is used for implementing regular expression comparisons. TODO: Support this against the SQLITE3 due to be implemented in Node.js 24. TODO: Support this against the SQLITE3 that exists in Bun. TODO: Determine what to do on Deno. TODO: Support this against better-sqlite3 # API ```js new SQ3DataStore( DB: sqlite3.Database | string, tablenm: string) ``` In `sq3-kv-data-store` we can store multiple pools of key/value data. Each pool corresponds to a dynamically created SQLITE3 table. Instantiating an instance of the `SQ3DataStore` class generates a simple table in the SQLITE3 instance specified in the `DB` parameter. The `tablenm` parameter gives the name for the table. One may share an SQLITE3 instance between `sq3-kv-data-store` and other code that is also storing tables. Passing a `Database` instance in the `DB` parameter allows using an existing SQLITE3 instance. Passing a _string_ instead generates a new Database object connecting to the connection URL in the string. No attempt is made by `sq3-kv-data-store` to avoid conflicting tables. It's up to your application to not step on other tables in your SQLITE3 instance. ```js const table1 = new SQ3DataStore(':memory:', 'table1'); const table2 = new SQ3DataStore(table1.DB, 'table2'); const table3 = new SQ3DataStore(table1.DB, 'table3'); ``` This shows creating three key/value pools in the same `:memory:` instance. One can create many data tables. ```js SQ3DataStore#DB ``` As just explained, the `DB` getter retrieves the `Database` instance being used in the table. It means your code can go behind the scenes and directly invoke SQLITE3 API methods. ```js SQ3DataStore#put( key: string, value: any ): Promise<void> ``` This stores a value in the data table. The value is stringified as JSON before storage. ```js SQ3DataStore#update( key: string, value: any ): Promise<void> ``` Replaces the _value_ for an existing _key_. ```js SQ3DataStore#get(key: string) : Promise<any | undefined> ``` Retrieves the value for the provided key. ```js SQ3DataStore#exists(key: string) : Promise<boolean> ``` Determines whether the database table contains an item with the given key. The return value is `true` if such an item exists, and `false` otherwise. ```js SQ3DataStore#keys(pattern?: string) : Promise<string[]> ``` Retrieves the keys currently existing in the database table. The `pattern` parameter allows specifying a LIKE pattern. If specified the returned keys will match the pattern. ```js SQ3DataStore#find(selectors: Array<any>) : Promise<Array<any> | undefined> ``` Searches the fields in the _value_ objects. This is discussed later. ```js SQ3DataStore#findAll(): Promise<Array<any>> ``` Retrieves all entries in the data table. ```js SQ3DataStore#delete(key: string): Promise<void> ``` Deletes an item from the data table. ```js SQ3DataStore#drop(key: string): Promise<void> ``` Deletes the data table from the SQLITE3 instance. # Searching with the `find` method The `find` method allows one to search against the JSON value similarly to MongoDB queries. This relies on the `json_extract` function from the JSON extension. This extension is generally bundled with SQLITE3. Simple example: ```js const found = await table.find({ '$.vpath': 'index.html' }); ``` This searches for items where the `vpath` item in the JSON is precisely equal to `index.html`. The `$.vpath` string is in the format required by the JSON extension. That documentation reads as so: > A well-formed PATH is a text value that begins with exactly one '$' character followed by zero or more instances of ".objectlabel" or "[arrayindex]". The simple example is equivalent to: ```js const found = await table.find({ '$.vpath': { $eq: 'index.html' } }); ``` The first example shows the implicit equality comparison. This is an explicit equality comparison using the `$eq` operator. The full list of operators of this sort are: * **`$eq`** - equality * **`$lt`** - less than * **`$lte`** - less than or equal * **`$gt`** - greater than * **`$gte`** - greater than or equal * **`$ne`** - not equal * **`$like`** - matches using an SQL LIKE clause - e.g. `'%Smith'` * **`$glob`** - matches using an filesystem path match - e.g. `'**/*.html'` * **`$regexp`** - matches using an regular expression - e.g. `'^Smith.*$'` **`$null` or `$notnull`** These operators test if the value is `null` (using `IS NULL`) or not null (using `IS NOT NULL`). ```js const found = await table.find({ $notnull: '$.vpath' }); ``` The `$null` operator matches both fields which are `undefined` or contain the value `null`. **`$exists`** Attempts to determine if the item exists using the `EXISTS` operator. However, KNOWN BUG, this produces a syntax error from SQLITE3. See https://github.com/robogeek/sqlite3-key-value-data-store/issues/1 **`$or` or `$and`** These take an array of sub-clauses. The `$or` form matches if one of the sub-clauses matches. The `$and` form matches if all the sub-clauses matches. ```js const found = await table.find({ $or: [ { '$.dirname': 'subdir' }, { $and: [ { '$.dirname': { $like: 'hier-broke%' }}, { '$.index': { $eq: 3 } } ]} ] }); ``` # Installing the REGEXP extension The `find` method supports matching on regular expressions. To use this feature one must install the `sqlite-regex` package, then enable it in the SQLITE3 database. ```shell $ npm install sqlite-regex --save ``` This package has `sqlite-regex` as a peer dependency. In your code do something like this: ```js import * as sqlite_regex from "sqlite-regex"; ... const db = new sqlite3.Database(URL); const regexp_loadable_path = sqlite_regex.getLoadablePath(); db.loadExtension(regexp_loadable_path, (err) => { // handle error }); ``` ## Using REGEXP where there might be missing data A collection of JSON documents are unlikely to all have the same shape or content. You can easily end up with a situation of where a `{ $regexp: 'regular-expression' }` operator is matched against a field which does not exist in all documents. In such a case an error is thrown, `SQLITE_ERROR: Unexpected null value`. The solution is to add the `$notnull` operator like so: ```js const rows = await table.find({ $notnull: '$.name', '$.name': { $regexp: '.*Smith.*' } }); ``` The generated SQL will be: ``` WHERE json_extract(value, '$.name') IS NOT NULL AND json_extract(value, '$.name') regexp '.*Smith.*' ``` This ensures the field exists before running the regular expression.