init
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -136,3 +136,5 @@ dist
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
bun.lock
|
||||
data/
|
||||
|
||||
70
bun.lock
70
bun.lock
@@ -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=="],
|
||||
}
|
||||
}
|
||||
BIN
data/db.sqlite
BIN
data/db.sqlite
Binary file not shown.
Binary file not shown.
Binary file not shown.
10
index.js
10
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"oceanic.js": "^1.15.0"
|
||||
"oceanic.js": "^1.15.0",
|
||||
"pg": "^8.23.0"
|
||||
}
|
||||
}
|
||||
176
utils/kvdb.js
176
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<void>}
|
||||
*/
|
||||
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<any>}
|
||||
@@ -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<boolean>}
|
||||
@@ -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<void>}
|
||||
@@ -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<boolean>} 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 {
|
||||
|
||||
Reference in New Issue
Block a user