UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

120 lines (119 loc) 4.6 kB
import { StringSchema } from "../../schema/StringSchema.js"; import { SQLProvider } from "./SQLProvider.js"; /** * Abstract SQLite provider with JSON1 function support for nested keys, array containment, and array mutations. * * Note the following compatibility caveats: * - For `with` and `omit` updates this does not preserve ordering of the original array. * - For `with` and `omit` updates this does not guarantee equality for de-duplication when working with nested objects or arrays. */ export class SQLiteProvider extends SQLProvider { // Override `addItem` to support string (UUID) IDs in SQLite. // SQLite has no native UUID generation, so when the collection uses a `StringSchema` ID // we generate the UUID client-side and delegate to `setItem`. // Note: `as II` is required here because TypeScript cannot narrow the generic `II` from `instanceof StringSchema`. async addItem(collection, data) { if (collection.id instanceof StringSchema) { const id = crypto.randomUUID(); // `as II` needed: TypeScript can't narrow II from instanceof check. await this.setItem(collection, id, data); return id; } return super.addItem(collection, data); } /** Get the SQLite JSON path for the nested segments of a key (everything after the column name), e.g. `$.b.c` */ sqlPath(key) { return this.sqlConcat(key.slice(1).map(k => this.sqlIdentifier(k)), ".", "$."); } /** Get the SQLite JSON extract syntax, e.g. `json_extract("a", $.b.c)` */ sqlExtract(key) { const column = this.sqlIdentifier(key[0]); if (key.length > 1) { const path = this.sqlPath(key); return this.sql `json_extract(${column}, ${path})`; } return column; } // Override to implement `with` and `omit` updates and deeply nested JSONB values. sqlUpdate(update) { const { action, key, value } = update; const column = this.sqlIdentifier(key[0]); // Implement all updates for deeply nested paths. if (key.length > 1) { const path = this.sqlPath(key); if (action === "set") { return this.sql `${column} = json_set(${column}, ${path}, ${value})`; } if (action === "sum") { return this.sql `${column} = json_set(${column}, ${path}, json_extract(${column}, ${path}) + ${value})`; } const source = this.sql `json_extract(${column}, ${path})`; if (action === "with") { return this.sql `${column} = json_set(${column}, ${path}, ( SELECT COALESCE(json_group_array(v), '[]') FROM ( SELECT s.value AS v FROM json_each(${source}) AS s UNION SELECT v.value AS v FROM json_each(${value}) AS v ) ))`; } if (action === "omit") { return this.sql `${column} = json_set(${column}, ${path}, ( SELECT COALESCE(json_group_array(s.value), '[]') FROM json_each(${source}) AS s WHERE NOT EXISTS ( SELECT 1 FROM json_each(${value}) AS v WHERE v.value = s.value ) ))`; } return action; // Never happens. } // Implement `with` and `omit` on flat values. if (action === "with") { return this.sql `${column} = ( SELECT COALESCE(json_group_array(v), '[]') FROM ( SELECT value AS v FROM json_each(${column}) UNION SELECT value AS v FROM json_each(${value}) ) )`; } if (action === "omit") { return this.sql `${column} = ( SELECT COALESCE(json_group_array(s.value), '[]') FROM json_each(${column}) AS s WHERE NOT EXISTS ( SELECT 1 FROM json_each(${value}) AS v WHERE v.value = s.value ) )`; } return super.sqlUpdate(update); } // Override to implement `contains` queries and deeply-nested JSONB queries. sqlFilter(filter) { const { key, operator, value } = filter; // Implement `contains` filters. if (operator === "contains") { const column = this.sqlIdentifier(key[0]); if (key.length > 1) { const path = this.sqlPath(key); return this.sql `EXISTS ( SELECT 1 FROM json_each(${column}, ${path}) as v WHERE v.value = ${value} )`; } return this.sql `EXISTS ( SELECT 1 FROM json_each(${column}) as v WHERE v.value = ${value} )`; } return super.sqlFilter(filter); } }