From aa1d60b19e415c097cbcbb668f008482ef437158 Mon Sep 17 00:00:00 2001 From: marek Date: Wed, 23 Sep 2026 18:46:15 +0200 Subject: [PATCH] init --- .gitignore | 2 + bun.lock | 70 ------------------ data/db.sqlite | Bin 4096 -> 0 bytes data/db.sqlite-shm | Bin 32768 -> 0 bytes data/db.sqlite-wal | Bin 201912 -> 0 bytes index.js | 10 ++- package.json | 3 +- utils/kvdb.js | 176 +++++++++++++++++++++------------------------ 8 files changed, 95 insertions(+), 166 deletions(-) delete mode 100644 bun.lock delete mode 100644 data/db.sqlite delete mode 100644 data/db.sqlite-shm delete mode 100644 data/db.sqlite-wal diff --git a/.gitignore b/.gitignore index 2309cc8..37819f5 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,5 @@ dist .yarn/install-state.gz .pnp.* +bun.lock +data/ diff --git a/bun.lock b/bun.lock deleted file mode 100644 index d2d9d8a..0000000 --- a/bun.lock +++ /dev/null @@ -1,70 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "dependencies": { - "oceanic.js": "^1.15.0", - }, - }, - }, - "packages": { - "@discordjs/voice": ["@discordjs/voice@0.19.2", "", { "dependencies": { "@snazzah/davey": "^0.1.9", "@types/ws": "^8.18.1", "discord-api-types": "^0.38.41", "prism-media": "^1.3.5", "tslib": "^2.8.1", "ws": "^8.19.0" } }, "sha512-3yJ255e4ag3wfZu/DSxeOZK1UtnqNxnspmLaQetGT0pDkThNZoHs+Zg6dgZZ19JEVomXygvfHn9lNpICZuYtEA=="], - - "@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" } }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], - - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], - - "@snazzah/davey": ["@snazzah/davey@0.1.12", "", { "optionalDependencies": { "@snazzah/davey-android-arm-eabi": "0.1.12", "@snazzah/davey-android-arm64": "0.1.12", "@snazzah/davey-darwin-arm64": "0.1.12", "@snazzah/davey-darwin-x64": "0.1.12", "@snazzah/davey-freebsd-x64": "0.1.12", "@snazzah/davey-linux-arm-gnueabihf": "0.1.12", "@snazzah/davey-linux-arm64-gnu": "0.1.12", "@snazzah/davey-linux-arm64-musl": "0.1.12", "@snazzah/davey-linux-x64-gnu": "0.1.12", "@snazzah/davey-linux-x64-musl": "0.1.12", "@snazzah/davey-wasm32-wasi": "0.1.12", "@snazzah/davey-win32-arm64-msvc": "0.1.12", "@snazzah/davey-win32-ia32-msvc": "0.1.12", "@snazzah/davey-win32-x64-msvc": "0.1.12" } }, "sha512-V+NlX5931RwVamZhhEfZekMdcvXDKdMAmHW1AuGaykVQsNyBOq3bpmGpoKRBDCYgFWKIufJ0Dcg3m4cYhvUy6g=="], - - "@snazzah/davey-android-arm-eabi": ["@snazzah/davey-android-arm-eabi@0.1.12", "", { "os": "android", "cpu": "arm" }, "sha512-6VC/an+Sx5dI5skb+90rYcIB1jhm48Rl0nDaw0UNT4bz1rMjpVfmmZqeocYXMq96IdbBMlE6OTKGcBm2C3gkQg=="], - - "@snazzah/davey-android-arm64": ["@snazzah/davey-android-arm64@0.1.12", "", { "os": "android", "cpu": "arm64" }, "sha512-0Bwd03/JsTFhlPhF4q/LW0RxSzntFpQdhz+TBdFljYSg8IEyA38saPJeTNjpIgDfhAumPzvhCdfS6O5qT8yXDw=="], - - "@snazzah/davey-darwin-arm64": ["@snazzah/davey-darwin-arm64@0.1.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lKMV6ITi9BQLt0fx/pAT7M8xcojVK7bryVJGdaW3bq8gABFslS3ti/KzrWabQvhpEV71FZe5mV0UcKHFVaTsZw=="], - - "@snazzah/davey-darwin-x64": ["@snazzah/davey-darwin-x64@0.1.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-vXXc/eW/e3TQeb7VsdtrPqs3/22j0aSqiP1ZXmZtDjQRBwgSxwItWYa6sh5MELP2EHB2igNlGzB6Hc0XlbGi4g=="], - - "@snazzah/davey-freebsd-x64": ["@snazzah/davey-freebsd-x64@0.1.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-G1gas5HrC4Xp3mRY0+OeAqXS6fG2tRgBEc8gQh69Hw4YK9RV9mzQKcmoKMkBM72U1+2C+2u57dolnKKzwozByQ=="], - - "@snazzah/davey-linux-arm-gnueabihf": ["@snazzah/davey-linux-arm-gnueabihf@0.1.12", "", { "os": "linux", "cpu": "arm" }, "sha512-97Fujh82r2Ll7dPZeNuoZ3yKfsqycf3c93OWXOo/ThNL/18Onl2Ht4SIvpX6VHhfeS9bDpHJ1lHCaz1b/i5ocw=="], - - "@snazzah/davey-linux-arm64-gnu": ["@snazzah/davey-linux-arm64-gnu@0.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-FWyAOv52cHKDM4BOsmImKKogHFvqNFoXmZcicNJbX3XpVl8Mas88ZoXQ+IA5V+qc9pNtAl1MbWwEZ9JrqAQtbg=="], - - "@snazzah/davey-linux-arm64-musl": ["@snazzah/davey-linux-arm64-musl@0.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-ptRbLSQxtV6EjXppS5z7qaPDI0NRKhrkJYsTlAjEghmOvlAObozSCYYnMO6nbkt6Ab3+lWqyahClzcRNcD2ouw=="], - - "@snazzah/davey-linux-x64-gnu": ["@snazzah/davey-linux-x64-gnu@0.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-w86fZvhJn0ErOoAQHt2UbQ95V/cgwvfvQ4GlTPQLCzt58nn+rLlXLgPn90qYlSQrZxFW38rKXwqVOMbm9p+pwQ=="], - - "@snazzah/davey-linux-x64-musl": ["@snazzah/davey-linux-x64-musl@0.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-LLNnO+hfG41ymeI+O1YHo5/0h3aKaetUNLdkBwpdJsjoyKMXZaeCnB+aHNkkupJCMPmWS0g6iPMCHUOqZSBcTg=="], - - "@snazzah/davey-wasm32-wasi": ["@snazzah/davey-wasm32-wasi@0.1.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.5" }, "cpu": "none" }, "sha512-MPKFuqYVkDFXheR7qmtEY4FWxQ/ADfgsCojQWHi13sibUqCTR9q2F1LqNn2i9IVh3sh1sxeg87fdFMCH63pl7g=="], - - "@snazzah/davey-win32-arm64-msvc": ["@snazzah/davey-win32-arm64-msvc@0.1.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-Uf4OYHyfbXpzyaOqIV8/h6kv166Qni5+Bxmc1E/ov4uhhKO8qXbOky8zbVOzu0U4cv5ll3s4IQ8jDAJkx/K75Q=="], - - "@snazzah/davey-win32-ia32-msvc": ["@snazzah/davey-win32-ia32-msvc@0.1.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-nRVbKTsb2ldcPI8D4BDA7P/UeiMEMvR+wYuUMp7H1pRD/3dF2hKo+MzUU+Pn78EhuYA3CWHItiAcS/GsPld67A=="], - - "@snazzah/davey-win32-x64-msvc": ["@snazzah/davey-win32-x64-msvc@0.1.12", "", { "os": "win32", "cpu": "x64" }, "sha512-AgUA3itPDVkxQq7RIkgE1thCiWePwWjyfOZefBBxGIlMWpRUOSwF4vY9kNLnrScyJcFULdR+Zm8PwiwV8/RKnw=="], - - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - - "@types/node": ["@types/node@22.20.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw=="], - - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - - "discord-api-types": ["discord-api-types@0.38.55", "", {}, "sha512-ytuaRTzdnHUCXJ6KjL9MrItQX0xKncBKEeYI1Bst4+ud47eejH3cG6gaesYakjpPcUhh68XYS2YwGq3mmujFsA=="], - - "oceanic.js": ["oceanic.js@1.15.0", "", { "dependencies": { "tslib": "^2.8.1", "ws": "^8.21.3" }, "optionalDependencies": { "@discordjs/voice": "^0.19.2" }, "peerDependencies": { "erlpack": "^0.1.4", "pako": "^2.1.0", "zlib-sync": "^0.1.10", "zstd-napi": "^0.0.12" }, "optionalPeers": ["erlpack", "pako", "zlib-sync", "zstd-napi"] }, "sha512-RV1ezHnMzxDxGCkzKpbhHxYOWo6ZHOjTFYOXhxCyIU1mI3Xj01NT+djwJkOiUCekd8k9ciFnpKRs7ckXuLbzwg=="], - - "prism-media": ["prism-media@1.3.5", "", { "peerDependencies": { "@discordjs/opus": ">=0.8.0 <1.0.0", "ffmpeg-static": "^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0", "node-opus": "^0.3.3", "opusscript": "^0.0.8" }, "optionalPeers": ["@discordjs/opus", "ffmpeg-static", "node-opus", "opusscript"] }, "sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], - } -} diff --git a/data/db.sqlite b/data/db.sqlite deleted file mode 100644 index 09e3110e7c163d9073f90dd93af4fd4108f2292c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYC*>f)}J+fNV2HHI9bB nXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDP#6LL6MF}4 diff --git a/data/db.sqlite-shm b/data/db.sqlite-shm deleted file mode 100644 index 8ffbaf5542a5779e1ab33ac71d390c99393f9e86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)w@pMr6a~$M3b;cXE~Xga82o1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZpe=!} z<~)hEesP$<9}%eLtq6htDNs!s5(58IpqBm&W+=lM$!Nwho{3CmD$|+CY?%f&pL!Ou zSmtvrXCD8(D?#=8R+~bbx42!0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oPur`~W|; BEMouw diff --git a/data/db.sqlite-wal b/data/db.sqlite-wal deleted file mode 100644 index c0b5a68a19b22b482c39613445b4b061c1686674..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 201912 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 diff --git a/index.js b/index.js index 808efee..4d6b913 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,15 @@ import { ApplicationCommandTypes, Client, ComponentTypes, InteractionTypes } fro import { readdir } from "node:fs/promises"; import { KVDB } from "./utils/kvdb"; -const db = new KVDB("data/db.sqlite"); +const db = new KVDB({ + user: process.env.POSTGRESQL_USERNAME, + password: process.env.POSTGRESQL_PASSWORD, + host: process.env.POSTGRESQL_HOST, + port: process.env.POSTGRESQL_PORT, + database: process.env.POSTGRESQL_DATABASE +}); + +await db.init(); const client = new Client({ token: process.env.TOKEN diff --git a/package.json b/package.json index be61961..2559ea7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { - "oceanic.js": "^1.15.0" + "oceanic.js": "^1.15.0", + "pg": "^8.23.0" } } \ No newline at end of file diff --git a/utils/kvdb.js b/utils/kvdb.js index 3a7080c..77fb4b7 100644 --- a/utils/kvdb.js +++ b/utils/kvdb.js @@ -2,35 +2,40 @@ vibecoded :sob: */ -import { Database } from "bun:sqlite"; +import { Pool } from "pg"; export class KVDB { /** - * @param {string} [filename="db.sqlite"] - The SQLite database file (e.g., "db.sqlite" or ":memory:") + * @param {Pool | object} poolOrConfig - A pg.Pool instance or pg connection config object * @param {string} [table="main_kv"] - The table/context name (defaults to "main_kv") */ - constructor(filename = "db.sqlite", table = "main_kv") { - this.db = new Database(filename); - this.db.run("PRAGMA journal_mode = WAL;"); + constructor(poolOrConfig, table = "main_kv") { + this.pool = poolOrConfig instanceof Pool ? poolOrConfig : new Pool(poolOrConfig); // Sanitize table name to prevent SQL injection in DDL this.tableName = table.replace(/[^a-zA-Z0-9_]/g, ""); + } - this.db.run(` + /** + * Initialize the database table. Call this once after instantiating the class. + * @returns {Promise} + */ + async init() { + await this.pool.query(` CREATE TABLE IF NOT EXISTS "${this.tableName}" ( key TEXT PRIMARY KEY, - value TEXT + value JSONB ) `); } /** - * Switch database table/context without recreating database connections + * Switch database table/context sharing the same connection pool * @param {string} newTableName * @returns {KVDB} */ table(newTableName) { - return new KVDB(this.db.filename, newTableName); + return new KVDB(this.pool, newTableName); } /** @@ -45,37 +50,35 @@ export class KVDB { async set(keyPath, value) { const parts = keyPath.split("."); const rootKey = parts[0]; + const jsonValue = JSON.stringify(value); if (parts.length === 1) { - this.db.run( - `INSERT INTO "${this.tableName}" (key, value) VALUES (?, ?) - ON CONFLICT(key) DO UPDATE SET value = excluded.value`, - [rootKey, JSON.stringify(value)] + await this.pool.query( + `INSERT INTO "${this.tableName}" (key, value) VALUES ($1, $2::jsonb) + ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`, + [rootKey, jsonValue] ); return; } - const jsonPath = `$.${parts.slice(1).join(".")}`; + const pathArr = parts.slice(1); - this.db.run( - `INSERT INTO "${this.tableName}" (key, value) VALUES (?, '{}') + await this.pool.query( + `INSERT INTO "${this.tableName}" (key, value) VALUES ($1, '{}'::jsonb) ON CONFLICT(key) DO NOTHING`, [rootKey] ); - this.db.run( + await this.pool.query( `UPDATE "${this.tableName}" - SET value = json_set(value, ?, json(?)) - WHERE key = ?`, - [jsonPath, JSON.stringify(value), rootKey] + SET value = jsonb_set(value, $1::text[], $2::jsonb, true) + WHERE key = $3`, + [pathArr, jsonValue, rootKey] ); } /** * Get a key/nested property, OR find an element inside an array using a predicate function. - * Examples: - * await db.get("user.roles") -> returns whole array - * await db.get("user.roles", (r) => r.id === "admin") -> returns matching role object * @param {string} keyPath * @param {Function} [predicate] - Optional matching function for array items * @returns {Promise} @@ -86,29 +89,19 @@ export class KVDB { let data = null; if (parts.length === 1) { - const row = this.db - .query(`SELECT value FROM "${this.tableName}" WHERE key = ?`) - .get(rootKey); - data = row ? JSON.parse(row.value) : null; + const { rows } = await this.pool.query( + `SELECT value FROM "${this.tableName}" WHERE key = $1`, + [rootKey] + ); + // pg parses JSONB automatically + data = rows.length > 0 ? rows[0].value : null; } else { - const jsonPath = `$.${parts.slice(1).join(".")}`; - const row = this.db - .query( - `SELECT json_extract(value, ?) AS result FROM "${this.tableName}" WHERE key = ?` - ) - .get(jsonPath, rootKey); - - if (row && row.result !== null) { - if (typeof row.result === "string") { - try { - data = JSON.parse(row.result); - } catch { - data = row.result; - } - } else { - data = row.result; - } - } + const pathArr = parts.slice(1); + const { rows } = await this.pool.query( + `SELECT value #> $1::text[] AS result FROM "${this.tableName}" WHERE key = $2`, + [pathArr, rootKey] + ); + data = (rows.length > 0 && rows[0].result !== undefined) ? rows[0].result : null; } if (typeof predicate === "function") { @@ -121,10 +114,6 @@ export class KVDB { /** * Check if a key/property exists, OR check if an array contains a specific item / predicate match. - * Examples: - * await db.has("user.roles") -> true/false - * await db.has("user.roles", (r) => r.id === "admin") -> true/false - * await db.has("user.roles", "admin_role_id") -> true/false * @param {string} keyPath * @param {any | Function} [itemOrPredicate] * @returns {Promise} @@ -150,27 +139,25 @@ export class KVDB { const rootKey = parts[0]; if (parts.length === 1) { - const row = this.db - .query(`SELECT 1 FROM "${this.tableName}" WHERE key = ?`) - .get(rootKey); - return Boolean(row); + const { rows } = await this.pool.query( + `SELECT 1 FROM "${this.tableName}" WHERE key = $1`, + [rootKey] + ); + return rows.length > 0; } - const jsonPath = `$.${parts.slice(1).join(".")}`; - const row = this.db - .query( - `SELECT json_type(value, ?) AS type FROM "${this.tableName}" WHERE key = ?` - ) - .get(jsonPath, rootKey); + const pathArr = parts.slice(1); + const { rows } = await this.pool.query( + `SELECT 1 FROM "${this.tableName}" + WHERE key = $1 AND value #> $2::text[] IS NOT NULL`, + [rootKey, pathArr] + ); - return Boolean(row && row.type !== null); + return rows.length > 0; } /** * Delete a key, property, OR remove item(s) from an array using a predicate or value. - * Examples: - * await db.delete("user.roles") -> deletes the roles key/property entirely - * await db.delete("user.roles", (r) => r.id === "admin") -> pulls matching item from array * @param {string} keyPath * @param {any | Function} [itemOrPredicate] * @returns {Promise} @@ -198,16 +185,16 @@ export class KVDB { const rootKey = parts[0]; if (parts.length === 1) { - this.db.run(`DELETE FROM "${this.tableName}" WHERE key = ?`, [rootKey]); + await this.pool.query(`DELETE FROM "${this.tableName}" WHERE key = $1`, [rootKey]); return; } - const jsonPath = `$.${parts.slice(1).join(".")}`; - this.db.run( + const pathArr = parts.slice(1); + await this.pool.query( `UPDATE "${this.tableName}" - SET value = json_remove(value, ?) - WHERE key = ?`, - [jsonPath, rootKey] + SET value = value #- $1::text[] + WHERE key = $2`, + [pathArr, rootKey] ); } @@ -220,35 +207,40 @@ export class KVDB { async push(keyPath, value) { const parts = keyPath.split("."); const rootKey = parts[0]; - const jsonPath = parts.length === 1 ? "$" : `$.${parts.slice(1).join(".")}`; + const wrappedValue = JSON.stringify([value]); if (parts.length === 1) { - this.db.run( - `INSERT INTO "${this.tableName}" (key, value) VALUES (?, '[]') - ON CONFLICT(key) DO NOTHING`, - [rootKey] + await this.pool.query( + `INSERT INTO "${this.tableName}" (key, value) VALUES ($1, $2::jsonb) + ON CONFLICT(key) DO UPDATE + SET value = CASE + WHEN jsonb_typeof("${this.tableName}".value) = 'array' + THEN "${this.tableName}".value || $2::jsonb + ELSE $2::jsonb + END`, + [rootKey, wrappedValue] ); } else { - this.db.run( - `INSERT INTO "${this.tableName}" (key, value) VALUES (?, '{}') + const pathArr = parts.slice(1); + + await this.pool.query( + `INSERT INTO "${this.tableName}" (key, value) VALUES ($1, '{}'::jsonb) ON CONFLICT(key) DO NOTHING`, [rootKey] ); - this.db.run( + + await this.pool.query( `UPDATE "${this.tableName}" - SET value = json_set(value, ?, json('[]')) - WHERE key = ? AND json_type(value, ?) IS NULL`, - [jsonPath, rootKey, jsonPath] + SET value = jsonb_set( + value, + $1::text[], + COALESCE(value #> $1::text[], '[]'::jsonb) || $2::jsonb, + true + ) + WHERE key = $3`, + [pathArr, wrappedValue, rootKey] ); } - - const appendPath = `${jsonPath}[#]`; - this.db.run( - `UPDATE "${this.tableName}" - SET value = json_insert(value, ?, json(?)) - WHERE key = ?`, - [appendPath, JSON.stringify(value), rootKey] - ); } /** @@ -290,10 +282,6 @@ export class KVDB { /** * Renames a key or nested property path without changing its value. - * Examples: - * await db.rename("user.roles", "user.rolesOld") - * await db.rename("user.roles", "rolesOld") - * await db.rename("user", "userOld") * @param {string} oldKeyPath * @param {string} newKeyPath * @returns {Promise} True if target key existed and was renamed @@ -316,8 +304,8 @@ export class KVDB { const newRoot = newParts[0]; if (oldParts.length === 1 && newParts.length === 1) { - this.db.run( - `UPDATE "${this.tableName}" SET key = ? WHERE key = ?`, + await this.pool.query( + `UPDATE "${this.tableName}" SET key = $1 WHERE key = $2`, [newRoot, oldRoot] ); } else {