mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
refactor(db): extrai row-parsers + tipos compartilhados de db/apiKeys.ts (#4943)
Integrated into release/v3.8.36
This commit is contained in:
committed by
GitHub
parent
f1589801ee
commit
ca0fdddebf
@@ -27,6 +27,24 @@ import {
|
||||
hasClaudeCodeWildcardPermission,
|
||||
matchesWildcardPattern,
|
||||
} from "./apiKeys/modelPermissions";
|
||||
import {
|
||||
parseAllowedModels,
|
||||
parseAllowedCombos,
|
||||
parseNoLog,
|
||||
parseAutoResolve,
|
||||
parseDisableNonPublicModels,
|
||||
parseAllowUsageCommand,
|
||||
parseIsActive,
|
||||
parseAccessSchedule,
|
||||
parseRateLimits,
|
||||
parseAllowedConnections,
|
||||
parseAllowedQuotas,
|
||||
parseStringList,
|
||||
parseNullableTimestamp,
|
||||
parseIsBanned,
|
||||
parseStreamDefaultMode,
|
||||
} from "./apiKeys/rowParsers";
|
||||
import type { AccessSchedule, RateLimitRule } from "./apiKeys/types";
|
||||
|
||||
// ──────────────── Performance Optimizations ────────────────
|
||||
|
||||
@@ -40,18 +58,8 @@ interface CacheEntry<TValue> {
|
||||
value: TValue;
|
||||
}
|
||||
|
||||
export interface RateLimitRule {
|
||||
limit: number;
|
||||
window: number;
|
||||
}
|
||||
|
||||
export interface AccessSchedule {
|
||||
enabled: boolean;
|
||||
from: string;
|
||||
until: string;
|
||||
days: number[];
|
||||
tz: string;
|
||||
}
|
||||
// Re-exported for the historical public surface (moved to ./apiKeys/types).
|
||||
export type { AccessSchedule, RateLimitRule } from "./apiKeys/types";
|
||||
|
||||
interface ApiKeyMetadata {
|
||||
id: string;
|
||||
@@ -277,11 +285,6 @@ function evictIfNeeded<TKey, TValue>(cache: Map<TKey, TValue>) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function getModelPermissionCandidates(modelId: string): Promise<string[]> {
|
||||
const candidates = new Set<string>();
|
||||
addModelCandidate(candidates, modelId);
|
||||
@@ -312,8 +315,6 @@ async function getModelPermissionCandidates(modelId: string): Promise<string[]>
|
||||
return Array.from(candidates);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function getPublishedModelLookupTarget(
|
||||
modelId: string
|
||||
): Promise<{ providerId: string; modelId: string } | null> {
|
||||
@@ -341,8 +342,6 @@ async function getPublishedModelLookupTarget(
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function ensureApiKeyColumn(
|
||||
db: ApiKeysDbLike,
|
||||
columnNames: Set<string>,
|
||||
@@ -486,156 +485,6 @@ export async function getApiKeyById(id: string) {
|
||||
return camelRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to safely parse allowed_models JSON
|
||||
*/
|
||||
function parseAllowedModels(value: unknown): string[] {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((entry): entry is string => typeof entry === "string")
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseAllowedCombos(value: unknown): string[] {
|
||||
return parseStringList(value);
|
||||
}
|
||||
|
||||
function parseNoLog(value: unknown): boolean {
|
||||
return value === true || value === 1 || value === "1";
|
||||
}
|
||||
|
||||
function parseAutoResolve(value: unknown): boolean {
|
||||
return value === true || value === 1 || value === "1";
|
||||
}
|
||||
|
||||
function parseDisableNonPublicModels(value: unknown): boolean {
|
||||
return value === true || value === 1 || value === "1";
|
||||
}
|
||||
|
||||
function parseAllowUsageCommand(value: unknown): boolean {
|
||||
return value === true || value === 1 || value === "1";
|
||||
}
|
||||
|
||||
function parseIsActive(value: unknown): boolean {
|
||||
// DEFAULT 1 — active unless explicitly set to 0
|
||||
if (value === 0 || value === "0" || value === false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseAccessSchedule(value: unknown): AccessSchedule | null {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") return null;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
if (
|
||||
typeof obj["enabled"] !== "boolean" ||
|
||||
typeof obj["from"] !== "string" ||
|
||||
typeof obj["until"] !== "string" ||
|
||||
!Array.isArray(obj["days"]) ||
|
||||
typeof obj["tz"] !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const days = (obj["days"] as unknown[]).filter(
|
||||
(d): d is number => typeof d === "number" && Number.isInteger(d) && d >= 0 && d <= 6
|
||||
);
|
||||
return {
|
||||
enabled: obj["enabled"],
|
||||
from: obj["from"],
|
||||
until: obj["until"],
|
||||
days,
|
||||
tz: obj["tz"],
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseRateLimits(value: unknown): RateLimitRule[] | null {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") return null;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (!Array.isArray(parsed)) return null;
|
||||
return parsed.filter(
|
||||
(rule: RateLimitRule) =>
|
||||
typeof rule === "object" &&
|
||||
rule !== null &&
|
||||
typeof rule.limit === "number" &&
|
||||
typeof rule.window === "number"
|
||||
) as RateLimitRule[];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to safely parse allowed_connections JSON
|
||||
*/
|
||||
function parseAllowedConnections(value: unknown): string[] {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((entry): entry is string => typeof entry === "string")
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to safely parse allowed_quotas JSON
|
||||
*/
|
||||
function parseAllowedQuotas(value: unknown): string[] {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((entry): entry is string => typeof entry === "string")
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseStringList(value: unknown): string[] {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") return [];
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((entry): entry is string => typeof entry === "string")
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseNullableTimestamp(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed === "" ? null : trimmed;
|
||||
}
|
||||
|
||||
function parseIsBanned(value: unknown): boolean {
|
||||
return value === 1 || value === "1" || value === true;
|
||||
}
|
||||
|
||||
function parseStreamDefaultMode(value: unknown): "legacy" | "json" {
|
||||
return value === "json" ? "json" : "legacy";
|
||||
}
|
||||
|
||||
async function hashKey(key: string): Promise<string> {
|
||||
if (!key || typeof key !== "string") return "";
|
||||
// CodeQL: This is intentionally SHA-256, NOT password hashing. API keys are
|
||||
|
||||
161
src/lib/db/apiKeys/rowParsers.ts
Normal file
161
src/lib/db/apiKeys/rowParsers.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* db/apiKeys/rowParsers.ts — pure column parsers for persisted api_keys rows.
|
||||
*
|
||||
* Extracted verbatim from db/apiKeys.ts (god-file decomposition): the family of
|
||||
* functions that coerce raw SQLite column values (JSON strings, 0/1 flags, nullable
|
||||
* timestamps) into the typed shapes the host hydrates rows with. Pure — no DB handle,
|
||||
* no caches, no side effects — so they live as a co-located leaf. Behavior-preserving
|
||||
* move; apiKeys.ts imports them back for getApiKeys/getApiKeyById/getApiKeyMetadata.
|
||||
*/
|
||||
|
||||
import type { AccessSchedule, RateLimitRule } from "./types";
|
||||
|
||||
/**
|
||||
* Helper function to safely parse allowed_models JSON
|
||||
*/
|
||||
export function parseAllowedModels(value: unknown): string[] {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((entry): entry is string => typeof entry === "string")
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAllowedCombos(value: unknown): string[] {
|
||||
return parseStringList(value);
|
||||
}
|
||||
|
||||
export function parseNoLog(value: unknown): boolean {
|
||||
return value === true || value === 1 || value === "1";
|
||||
}
|
||||
|
||||
export function parseAutoResolve(value: unknown): boolean {
|
||||
return value === true || value === 1 || value === "1";
|
||||
}
|
||||
|
||||
export function parseDisableNonPublicModels(value: unknown): boolean {
|
||||
return value === true || value === 1 || value === "1";
|
||||
}
|
||||
|
||||
export function parseAllowUsageCommand(value: unknown): boolean {
|
||||
return value === true || value === 1 || value === "1";
|
||||
}
|
||||
|
||||
export function parseIsActive(value: unknown): boolean {
|
||||
// DEFAULT 1 — active unless explicitly set to 0
|
||||
if (value === 0 || value === "0" || value === false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function parseAccessSchedule(value: unknown): AccessSchedule | null {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") return null;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
if (
|
||||
typeof obj["enabled"] !== "boolean" ||
|
||||
typeof obj["from"] !== "string" ||
|
||||
typeof obj["until"] !== "string" ||
|
||||
!Array.isArray(obj["days"]) ||
|
||||
typeof obj["tz"] !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const days = (obj["days"] as unknown[]).filter(
|
||||
(d): d is number => typeof d === "number" && Number.isInteger(d) && d >= 0 && d <= 6
|
||||
);
|
||||
return {
|
||||
enabled: obj["enabled"],
|
||||
from: obj["from"],
|
||||
until: obj["until"],
|
||||
days,
|
||||
tz: obj["tz"],
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRateLimits(value: unknown): RateLimitRule[] | null {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") return null;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (!Array.isArray(parsed)) return null;
|
||||
return parsed.filter(
|
||||
(rule: RateLimitRule) =>
|
||||
typeof rule === "object" &&
|
||||
rule !== null &&
|
||||
typeof rule.limit === "number" &&
|
||||
typeof rule.window === "number"
|
||||
) as RateLimitRule[];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to safely parse allowed_connections JSON
|
||||
*/
|
||||
export function parseAllowedConnections(value: unknown): string[] {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((entry): entry is string => typeof entry === "string")
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to safely parse allowed_quotas JSON
|
||||
*/
|
||||
export function parseAllowedQuotas(value: unknown): string[] {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((entry): entry is string => typeof entry === "string")
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function parseStringList(value: unknown): string[] {
|
||||
if (!value || typeof value !== "string" || value.trim() === "") return [];
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((entry): entry is string => typeof entry === "string")
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function parseNullableTimestamp(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed === "" ? null : trimmed;
|
||||
}
|
||||
|
||||
export function parseIsBanned(value: unknown): boolean {
|
||||
return value === 1 || value === "1" || value === true;
|
||||
}
|
||||
|
||||
export function parseStreamDefaultMode(value: unknown): "legacy" | "json" {
|
||||
return value === "json" ? "json" : "legacy";
|
||||
}
|
||||
21
src/lib/db/apiKeys/types.ts
Normal file
21
src/lib/db/apiKeys/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* db/apiKeys/types.ts — shared API-key value types.
|
||||
*
|
||||
* Extracted from db/apiKeys.ts (god-file decomposition): the persisted-row shapes
|
||||
* that both the host module and the row-parser leaf need. Kept as a neutral leaf so
|
||||
* apiKeys.ts and apiKeys/rowParsers.ts can import them without a cycle. apiKeys.ts
|
||||
* re-exports both interfaces to preserve its historical public surface.
|
||||
*/
|
||||
|
||||
export interface RateLimitRule {
|
||||
limit: number;
|
||||
window: number;
|
||||
}
|
||||
|
||||
export interface AccessSchedule {
|
||||
enabled: boolean;
|
||||
from: string;
|
||||
until: string;
|
||||
days: number[];
|
||||
tz: string;
|
||||
}
|
||||
90
tests/unit/apikeys-row-parsers-split.test.ts
Normal file
90
tests/unit/apikeys-row-parsers-split.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// Characterization of the db/apiKeys.ts row-parsers split (god-file decomposition): the pure column
|
||||
// parsers that coerce raw SQLite values into typed shapes moved into db/apiKeys/rowParsers.ts, and the
|
||||
// two shared row types (AccessSchedule / RateLimitRule) into db/apiKeys/types.ts. Behavior-preserving
|
||||
// move — these locks pin the coercion semantics (JSON lists, 0/1 flags, nullable timestamps, schedule
|
||||
// validation) and that the host still re-exports the two public types. DB-backed hydration stays
|
||||
// covered by api-key-policy / combo-provider-wildcard.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const P = await import("../../src/lib/db/apiKeys/rowParsers.ts");
|
||||
|
||||
test("module exposes all fifteen parsers", () => {
|
||||
for (const name of [
|
||||
"parseAllowedModels",
|
||||
"parseAllowedCombos",
|
||||
"parseNoLog",
|
||||
"parseAutoResolve",
|
||||
"parseDisableNonPublicModels",
|
||||
"parseAllowUsageCommand",
|
||||
"parseIsActive",
|
||||
"parseAccessSchedule",
|
||||
"parseRateLimits",
|
||||
"parseAllowedConnections",
|
||||
"parseAllowedQuotas",
|
||||
"parseStringList",
|
||||
"parseNullableTimestamp",
|
||||
"parseIsBanned",
|
||||
"parseStreamDefaultMode",
|
||||
]) {
|
||||
assert.equal(typeof (P as Record<string, unknown>)[name], "function", `missing ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseAllowedModels keeps only string entries, tolerates junk", () => {
|
||||
assert.deepEqual(P.parseAllowedModels('["a","b",1,null]'), ["a", "b"]);
|
||||
assert.deepEqual(P.parseAllowedModels(""), []);
|
||||
assert.deepEqual(P.parseAllowedModels("not json"), []);
|
||||
assert.deepEqual(P.parseAllowedModels(null), []);
|
||||
});
|
||||
|
||||
test("flag parsers honor the 0/1/true/false matrix", () => {
|
||||
assert.equal(P.parseNoLog(1), true);
|
||||
assert.equal(P.parseNoLog("1"), true);
|
||||
assert.equal(P.parseNoLog(0), false);
|
||||
// isActive defaults to active unless explicitly falsy
|
||||
assert.equal(P.parseIsActive(undefined), true);
|
||||
assert.equal(P.parseIsActive(0), false);
|
||||
assert.equal(P.parseIsActive("0"), false);
|
||||
assert.equal(P.parseIsBanned(1), true);
|
||||
assert.equal(P.parseIsBanned(0), false);
|
||||
});
|
||||
|
||||
test("parseStreamDefaultMode collapses to legacy unless json", () => {
|
||||
assert.equal(P.parseStreamDefaultMode("json"), "json");
|
||||
assert.equal(P.parseStreamDefaultMode("legacy"), "legacy");
|
||||
assert.equal(P.parseStreamDefaultMode("anything"), "legacy");
|
||||
});
|
||||
|
||||
test("parseNullableTimestamp trims and nulls empties", () => {
|
||||
assert.equal(P.parseNullableTimestamp(" 2026-01-01 "), "2026-01-01");
|
||||
assert.equal(P.parseNullableTimestamp(" "), null);
|
||||
assert.equal(P.parseNullableTimestamp(42), null);
|
||||
});
|
||||
|
||||
test("parseAccessSchedule validates shape + clamps days, else null", () => {
|
||||
const ok = P.parseAccessSchedule(
|
||||
JSON.stringify({ enabled: true, from: "08:00", until: "18:00", days: [0, 3, 9], tz: "UTC" })
|
||||
);
|
||||
assert.ok(ok);
|
||||
assert.deepEqual(ok?.days, [0, 3]); // 9 dropped (out of 0..6)
|
||||
assert.equal(P.parseAccessSchedule('{"enabled":true}'), null);
|
||||
assert.equal(P.parseAccessSchedule("[]"), null);
|
||||
assert.equal(P.parseAccessSchedule(""), null);
|
||||
});
|
||||
|
||||
test("parseRateLimits keeps well-formed numeric rules only", () => {
|
||||
const out = P.parseRateLimits(
|
||||
JSON.stringify([{ limit: 10, window: 60 }, { limit: "x", window: 1 }, null])
|
||||
);
|
||||
assert.deepEqual(out, [{ limit: 10, window: 60 }]);
|
||||
assert.equal(P.parseRateLimits("not array"), null);
|
||||
assert.equal(P.parseRateLimits(""), null);
|
||||
});
|
||||
|
||||
test("host re-exports the two public row types (compile-time) and the parsers stay wired", async () => {
|
||||
// type-only re-export can't be probed at runtime; assert the host module still loads + exposes its API
|
||||
const HOST = await import("../../src/lib/db/apiKeys.ts");
|
||||
assert.equal(typeof (HOST as Record<string, unknown>).getApiKeys, "function");
|
||||
assert.equal(typeof (HOST as Record<string, unknown>).isModelAllowedForKey, "function");
|
||||
});
|
||||
Reference in New Issue
Block a user