fix(db): compute token statistics
This commit is contained in:
@@ -5,58 +5,15 @@ import * as schema from "@/schema";
|
|||||||
|
|
||||||
const isDevelopment = process.env.NODE_ENV === "development";
|
const isDevelopment = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
const connectionConfig = {
|
const pool = new Pool({
|
||||||
allowExitOnIdle: true,
|
allowExitOnIdle: true,
|
||||||
|
connectionString: process.env.BASANGO_DATABASE_URL!,
|
||||||
connectionTimeoutMillis: 15_000,
|
connectionTimeoutMillis: 15_000,
|
||||||
idleTimeoutMillis: isDevelopment ? 5_000 : 60_000,
|
idleTimeoutMillis: isDevelopment ? 5_000 : 60_000,
|
||||||
max: isDevelopment ? 8 : 12,
|
max: isDevelopment ? 8 : 12,
|
||||||
maxUses: isDevelopment ? 100 : 0,
|
maxUses: isDevelopment ? 100 : 0,
|
||||||
};
|
|
||||||
|
|
||||||
const pool = new Pool({
|
|
||||||
connectionString: process.env.BASANGO_DATABASE_URL!,
|
|
||||||
...connectionConfig,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves runtime statistics for the database connection pool.
|
|
||||||
*
|
|
||||||
* This function reads internal pool and connection configuration values and returns
|
|
||||||
* a snapshot describing pool usage, capacity and utilization. Values that are not
|
|
||||||
* available on the underlying pool or configuration are normalized to safe defaults
|
|
||||||
* (zeros or false) so the result is stable.
|
|
||||||
*
|
|
||||||
* @returns An object describing the current connection pool statistics and a small summary.
|
|
||||||
*/
|
|
||||||
export const getConnectionPoolStats = () => {
|
|
||||||
const stats = {
|
|
||||||
active: Math.max(0, (pool.totalCount ?? 0) - (pool.idleCount ?? 0)),
|
|
||||||
ended: pool.ended ?? false,
|
|
||||||
idle: pool.idleCount ?? 0,
|
|
||||||
name: "primary",
|
|
||||||
total: pool.options.max ?? 0,
|
|
||||||
waiting: pool.waitingCount ?? 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const totalConnections = connectionConfig.max;
|
|
||||||
const utilization =
|
|
||||||
totalConnections > 0 ? Math.round((stats.active / totalConnections) * 100) : 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
instance: "local",
|
|
||||||
pools: { primary: stats },
|
|
||||||
region: "unknown",
|
|
||||||
summary: {
|
|
||||||
hasExhaustedPools: stats.active >= totalConnections || (stats.waiting ?? 0) > 0,
|
|
||||||
totalActive: stats.active,
|
|
||||||
totalConnections,
|
|
||||||
totalWaiting: stats.waiting,
|
|
||||||
utilizationPercent: utilization,
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const db = drizzle(pool, {
|
export const db = drizzle(pool, {
|
||||||
casing: "snake_case",
|
casing: "snake_case",
|
||||||
schema,
|
schema,
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ export class Engine {
|
|||||||
this.target = new Pool({
|
this.target = new Pool({
|
||||||
allowExitOnIdle: true,
|
allowExitOnIdle: true,
|
||||||
connectionString: this.targetOptions.database,
|
connectionString: this.targetOptions.database,
|
||||||
max: 8,
|
max: 16,
|
||||||
});
|
});
|
||||||
this.ignore = { ...DEFAULT_IGNORE, ...(this.targetOptions.ignoreColumns ?? {}) };
|
this.ignore = { ...DEFAULT_IGNORE, ...(this.targetOptions.ignoreColumns ?? {}) };
|
||||||
this.pageSize = this.targetOptions.pageSize ?? 10_000;
|
this.pageSize = this.targetOptions.pageSize ?? 1000;
|
||||||
this.batchSize = Math.max(1, this.targetOptions.batchSize ?? 1000);
|
this.batchSize = Math.max(1, this.targetOptions.batchSize ?? 50);
|
||||||
console.log(
|
console.log(
|
||||||
`Engine initialized with pageSize=${this.pageSize} and batchSize=${this.batchSize}`,
|
`Engine initialized with pageSize=${this.pageSize} and batchSize=${this.batchSize}`,
|
||||||
);
|
);
|
||||||
@@ -162,9 +162,10 @@ export class Engine {
|
|||||||
try {
|
try {
|
||||||
await target.query(insertSql, params);
|
await target.query(insertSql, params);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
// Fallback: coerce all *_at params to now() and retry once
|
||||||
|
// This will never happen in production but anyway let's keep it safe
|
||||||
const msg = String((err as Error)?.message ?? "");
|
const msg = String((err as Error)?.message ?? "");
|
||||||
if (msg.includes("invalid input syntax for type timestamp")) {
|
if (msg.includes("invalid input syntax for type timestamp")) {
|
||||||
// Fallback: coerce all *_at params to now() and retry once
|
|
||||||
const fixed = columns!.map((c, i) => (c.endsWith("_at") ? new Date() : params[i]));
|
const fixed = columns!.map((c, i) => (c.endsWith("_at") ? new Date() : params[i]));
|
||||||
await target.query(insertSql, fixed);
|
await target.query(insertSql, fixed);
|
||||||
} else {
|
} else {
|
||||||
@@ -224,7 +225,6 @@ export class Engine {
|
|||||||
const t = this.normalizedName(table);
|
const t = this.normalizedName(table);
|
||||||
const clone: Record<string, unknown> = { ...row };
|
const clone: Record<string, unknown> = { ...row };
|
||||||
|
|
||||||
// Normalize UUIDs and timestamps and categories
|
|
||||||
for (const [key, val] of Object.entries(clone)) {
|
for (const [key, val] of Object.entries(clone)) {
|
||||||
if (val == null) continue;
|
if (val == null) continue;
|
||||||
|
|
||||||
@@ -233,7 +233,6 @@ export class Engine {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Robust timestamp normalization for *_at columns
|
|
||||||
if (key.endsWith("_at")) {
|
if (key.endsWith("_at")) {
|
||||||
clone[key] = this.normalizeTimestampValue(val);
|
clone[key] = this.normalizeTimestampValue(val);
|
||||||
continue;
|
continue;
|
||||||
@@ -263,14 +262,6 @@ export class Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t === "article" && key === "token_statistics") {
|
|
||||||
clone[key] = computeTokenStatistics({
|
|
||||||
body: String(clone.body ?? ""),
|
|
||||||
categories: Array.isArray(clone.categories) ? clone.categories : [],
|
|
||||||
title: String(clone.title ?? ""),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t === "article" && key === "reading_time") {
|
if (t === "article" && key === "reading_time") {
|
||||||
clone[key] = Math.max(1, computeReadingTime(String(clone.body ?? "")));
|
clone[key] = Math.max(1, computeReadingTime(String(clone.body ?? "")));
|
||||||
}
|
}
|
||||||
@@ -279,24 +270,9 @@ export class Engine {
|
|||||||
if (Array.isArray(val)) {
|
if (Array.isArray(val)) {
|
||||||
clone[key] = val;
|
clone[key] = val;
|
||||||
} else if (typeof val === "string") {
|
} else if (typeof val === "string") {
|
||||||
const raw = val.trim();
|
const parts = val
|
||||||
|
|
||||||
// If the value is a JSON array string like '["ROLE_USER","ROLE_ADMIN"]', parse it.
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
clone[key] = parsed;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// not JSON, fall back to CSV-like parsing below
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove surrounding brackets/quotes then split by comma and strip quotes/space
|
|
||||||
const parts = raw
|
|
||||||
.replace(/^\[|\]$/g, "")
|
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((s) => s.replace(/^["']|["']$/g, "").trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
clone[key] = parts.length ? parts : ["ROLE_USER"];
|
clone[key] = parts.length ? parts : ["ROLE_USER"];
|
||||||
@@ -304,7 +280,6 @@ export class Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compute credibility JSON if bias/reliability/transparency present
|
|
||||||
if (t === "article" || t === "source") {
|
if (t === "article" || t === "source") {
|
||||||
const bias = clone.bias ?? null;
|
const bias = clone.bias ?? null;
|
||||||
const reliability = clone.reliability ?? null;
|
const reliability = clone.reliability ?? null;
|
||||||
@@ -318,14 +293,10 @@ export class Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure article token_statistics exists (computed on the fly)
|
if (t === "article") {
|
||||||
if (
|
|
||||||
t === "article" &&
|
|
||||||
(clone.token_statistics == null || typeof clone.token_statistics !== "object")
|
|
||||||
) {
|
|
||||||
clone.token_statistics = computeTokenStatistics({
|
clone.token_statistics = computeTokenStatistics({
|
||||||
body: String(clone.body ?? ""),
|
body: String(clone.body ?? ""),
|
||||||
categories: Array.isArray(clone.categories) ? (clone.categories as string[]) : [],
|
categories: Array.isArray(clone.categories) ? clone.categories : [],
|
||||||
title: String(clone.title ?? ""),
|
title: String(clone.title ?? ""),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -352,7 +323,7 @@ export class Engine {
|
|||||||
return JSON.stringify(v);
|
return JSON.stringify(v);
|
||||||
}
|
}
|
||||||
if (col === "roles" && v) {
|
if (col === "roles" && v) {
|
||||||
return JSON.stringify(v);
|
return v;
|
||||||
}
|
}
|
||||||
if (col === "metadata" && v && typeof v === "object") {
|
if (col === "metadata" && v && typeof v === "object") {
|
||||||
return JSON.stringify(v);
|
return JSON.stringify(v);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const env = createEnvAccessor([
|
|||||||
"BASANGO_DATABASE_URL",
|
"BASANGO_DATABASE_URL",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
async function promptConfirm(question: string, def = false) {
|
async function askConfirmation(question: string, def = false) {
|
||||||
const rl = createInterface({ input, output });
|
const rl = createInterface({ input, output });
|
||||||
const suffix = def ? "[Y/n]" : "[y/N]";
|
const suffix = def ? "[Y/n]" : "[y/N]";
|
||||||
const answer = await rl.question(`${question} ${suffix} `);
|
const answer = await rl.question(`${question} ${suffix} `);
|
||||||
@@ -28,7 +28,7 @@ async function promptConfirm(question: string, def = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const ok = await promptConfirm("Do you want to continue?", false);
|
const ok = await askConfirmation("Do you want to continue?", false);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
console.warn("Process aborted");
|
console.warn("Process aborted");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -101,12 +101,6 @@ export type GeoLocation = {
|
|||||||
accuracyRadius?: number;
|
accuracyRadius?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClientProfile = {
|
|
||||||
userIp?: string;
|
|
||||||
userAgent?: string;
|
|
||||||
hints: unknown[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ArticleMetadata = {
|
export type ArticleMetadata = {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a new API key with the format mid_{random_string}
|
|
||||||
* @returns A new API key string
|
|
||||||
*/
|
|
||||||
export function generateApiKey(): string {
|
|
||||||
// Generate 32 random bytes and convert to hex
|
|
||||||
const randomString = randomBytes(32).toString("hex");
|
|
||||||
return `basango_${randomString}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates if a string is a valid API key format
|
|
||||||
* @param key The key to validate
|
|
||||||
* @returns True if the key starts with 'basango_' and has the correct length
|
|
||||||
*/
|
|
||||||
export function isValidApiKeyFormat(key: string): boolean {
|
|
||||||
return key.startsWith("basango_") && key.length === 68; // basango_ (8) + 64 hex chars
|
|
||||||
}
|
|
||||||
@@ -31,10 +31,12 @@ export const computeTokenStatistics = (data: {
|
|||||||
body: string;
|
body: string;
|
||||||
categories: string[];
|
categories: string[];
|
||||||
}): TokenStatistics => {
|
}): TokenStatistics => {
|
||||||
const title = computeTokenCount(data.title);
|
const [title, body, categories, excerpt] = [
|
||||||
const body = computeTokenCount(data.body);
|
computeTokenCount(data.title),
|
||||||
const categories = computeTokenCount(data.categories.join(","));
|
computeTokenCount(data.body),
|
||||||
const excerpt = computeTokenCount(data.body.substring(0, 200));
|
computeTokenCount(data.categories.join(",")),
|
||||||
|
computeTokenCount(data.body.substring(0, 200)),
|
||||||
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
body,
|
body,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { sql } from "drizzle-orm";
|
|
||||||
|
|
||||||
import { db } from "@/client";
|
|
||||||
|
|
||||||
export async function checkHealth() {
|
|
||||||
await db.execute(sql`SELECT 1`);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from "./api-keys";
|
export * from "./computed";
|
||||||
export * from "./health";
|
|
||||||
export * from "./pagination";
|
export * from "./pagination";
|
||||||
export * from "./search-query";
|
export * from "./search-query";
|
||||||
|
|||||||
Reference in New Issue
Block a user