release: WRNexusJS 0.7.0

This commit is contained in:
2026-08-01 10:04:42 +05:30
parent c54144f2e4
commit 87507edf59
207 changed files with 12607 additions and 679 deletions
+3
View File
@@ -38,3 +38,6 @@ export { paginate, loadRelated } from "./query.ts";
export type { Paginated, PageOptions, RelationOptions } from "./query.ts";
export { cursorPaginate, optimisticUpdate, tenantScope, softDeleteClause } from "./advanced.ts";
export type { CursorPage, CursorPageOptions } from "./advanced.ts";
export { instrumentDb, queryOperation } from "./performance.ts";
export type { QueryIssue, QueryPolicy, QueryRecord } from "./performance.ts";
+179
View File
@@ -0,0 +1,179 @@
import type { Db, ExecResult, Row } from "./driver.ts";
import type { Model } from "./schema.ts";
export interface QueryRecord {
sql: string;
paramsCount: number;
durationMs: number;
rowCount?: number;
operation: "all" | "one" | "exec";
duplicateCount: number;
}
export interface QueryIssue {
code: string;
severity: "error" | "warning" | "info";
message: string;
sql: string;
}
export interface QueryPolicy {
slowQueryMs?: number;
timeoutMs?: number;
maxRows?: number;
duplicateWarningCount?: number;
warnSelectStar?: boolean;
warnUnboundedSelect?: boolean;
onQuery?: (record: QueryRecord) => void | Promise<void>;
onIssue?: (issue: QueryIssue) => void | Promise<void>;
}
function normalizedSql(sql: string): string {
return sql
.replace(/--.*$/gm, " ")
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/\s+/g, " ")
.trim();
}
function operationName(sql: string): string {
return normalizedSql(sql).split(" ")[0]?.toUpperCase() ?? "UNKNOWN";
}
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, sql: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<T>((_resolve, reject) => {
timer = setTimeout(
() =>
reject(
new Error(`WRN-DB-TIMEOUT: query exceeded ${timeoutMs} ms: ${sql.slice(0, 120)}`),
),
timeoutMs,
);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
export function instrumentDb(db: Db, policy: QueryPolicy = {}): Db {
const counts = new Map<string, number>();
const slowQueryMs = policy.slowQueryMs ?? 100;
const timeoutMs = policy.timeoutMs ?? 30_000;
const maxRows = policy.maxRows ?? 10_000;
const duplicateWarningCount = policy.duplicateWarningCount ?? 5;
const inspect = async (sql: string): Promise<void> => {
const normalized = normalizedSql(sql);
if (policy.warnSelectStar !== false && /^SELECT\s+\*/i.test(normalized)) {
await policy.onIssue?.({
code: "WRN-DB-SELECT-STAR",
severity: "warning",
message: "Avoid SELECT * in production queries.",
sql,
});
}
if (
policy.warnUnboundedSelect !== false &&
/^SELECT\b/i.test(normalized) &&
!/\bLIMIT\b/i.test(normalized) &&
!/\bCOUNT\s*\(/i.test(normalized)
) {
await policy.onIssue?.({
code: "WRN-DB-UNBOUNDED-SELECT",
severity: "warning",
message: "SELECT query has no LIMIT. Prefer cursor pagination for large datasets.",
sql,
});
}
};
const record = async (
sql: string,
params: unknown[],
operation: QueryRecord["operation"],
durationMs: number,
rowCount?: number,
): Promise<void> => {
const key = `${operation}:${normalizedSql(sql)}:${JSON.stringify(params)}`;
const duplicateCount = (counts.get(key) ?? 0) + 1;
counts.set(key, duplicateCount);
const queryRecord: QueryRecord = {
sql,
paramsCount: params.length,
durationMs,
...(rowCount === undefined ? {} : { rowCount }),
operation,
duplicateCount,
};
await policy.onQuery?.(queryRecord);
if (durationMs >= slowQueryMs) {
await policy.onIssue?.({
code: "WRN-DB-SLOW-QUERY",
severity: durationMs >= slowQueryMs * 5 ? "error" : "warning",
message: `Query took ${durationMs.toFixed(2)} ms.`,
sql,
});
}
if (duplicateCount === duplicateWarningCount) {
await policy.onIssue?.({
code: "WRN-DB-DUPLICATE-QUERY",
severity: "warning",
message: `The same query ran ${duplicateCount} times in one request scope (possible N+1).`,
sql,
});
}
};
const wrapped: Db = {
driver: db.driver,
async all<T = Row>(sql: string, params: unknown[] = [], model?: Model<T>): Promise<T[]> {
await inspect(sql);
const started = performance.now();
const rows = await withTimeout(db.all(sql, params, model), timeoutMs, sql);
const duration = performance.now() - started;
if (rows.length > maxRows) {
await policy.onIssue?.({
code: "WRN-DB-MAX-ROWS",
severity: "error",
message: `Query returned ${rows.length} rows; maximum is ${maxRows}.`,
sql,
});
throw new Error(`WRN-DB-MAX-ROWS: query returned ${rows.length} rows.`);
}
await record(sql, params, "all", duration, rows.length);
return rows;
},
async one<T = Row>(sql: string, params: unknown[] = [], model?: Model<T>): Promise<T | null> {
await inspect(sql);
const started = performance.now();
const row = await withTimeout(db.one(sql, params, model), timeoutMs, sql);
await record(sql, params, "one", performance.now() - started, row ? 1 : 0);
return row;
},
async exec(sql: string, params: unknown[] = []): Promise<ExecResult> {
const started = performance.now();
const result = await withTimeout(db.exec(sql, params), timeoutMs, sql);
await record(sql, params, "exec", performance.now() - started, result.changes);
return result;
},
tx<T>(fn: (transaction: Db) => Promise<T>): Promise<T> {
return db.tx((transaction) => fn(instrumentDb(transaction, policy)));
},
createTable(model) {
return db.createTable(model);
},
close() {
return db.close();
},
};
return wrapped;
}
export function queryOperation(sql: string): string {
return operationName(sql);
}