Skip to content

Commit 500cae3

Browse files
committed
refactor: replace any types with generics and add Zod validation schemas
Eliminate `any` usage across the codebase by introducing proper generics, typed interfaces (StatementLike, DbLike, PromptRow, etc.), and helper conversion functions (toNumber, toString, parseVariables). Add comprehensive Zod validation schemas for API endpoint inputs to enforce runtime type safety alongside compile-time checks.
1 parent adc8fdf commit 500cae3

23 files changed

+2678
-245
lines changed

src/lib/db/encryption.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ export function decrypt(ciphertext: string | null | undefined): string | null |
114114

115115
/**
116116
* Encrypt sensitive fields in a connection object (mutates in-place).
117-
* Uses `any` because the DB layer returns untyped rows from rowToCamel/cleanNulls.
118117
*/
119-
export function encryptConnectionFields(conn: any): any {
118+
export function encryptConnectionFields<T extends ConnectionFields | null | undefined>(conn: T): T {
120119
if (!isEncryptionEnabled()) return conn;
120+
if (!conn) return conn;
121121

122122
if (conn.apiKey) conn.apiKey = encrypt(conn.apiKey);
123123
if (conn.accessToken) conn.accessToken = encrypt(conn.accessToken);
@@ -128,9 +128,8 @@ export function encryptConnectionFields(conn: any): any {
128128

129129
/**
130130
* Decrypt sensitive fields in a connection row (returns new object).
131-
* Uses `any` because the DB layer returns untyped rows from rowToCamel/cleanNulls.
132131
*/
133-
export function decryptConnectionFields(row: any): any {
132+
export function decryptConnectionFields<T extends ConnectionFields | null | undefined>(row: T): T {
134133
if (!row) return row;
135134
if (!isEncryptionEnabled()) return row;
136135

src/lib/db/prompts.ts

Lines changed: 98 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,61 @@
1212
import crypto from "node:crypto";
1313
import { getDbInstance } from "./core";
1414

15+
interface StatementLike<TRow = unknown> {
16+
all: (...params: unknown[]) => TRow[];
17+
get: (...params: unknown[]) => TRow | undefined;
18+
run: (...params: unknown[]) => { lastInsertRowid?: number | bigint; changes?: number };
19+
}
20+
21+
interface DbLike {
22+
prepare: <TRow = unknown>(sql: string) => StatementLike<TRow>;
23+
exec: (sql: string) => void;
24+
transaction: (fn: () => void) => () => void;
25+
}
26+
27+
interface PromptRow {
28+
id: unknown;
29+
slug: unknown;
30+
version: unknown;
31+
content: unknown;
32+
content_hash: unknown;
33+
variables: unknown;
34+
description: unknown;
35+
is_active: unknown;
36+
created_at: unknown;
37+
}
38+
39+
interface PromptListRow {
40+
slug: unknown;
41+
active_version: unknown;
42+
total_versions: unknown;
43+
}
44+
45+
function toNumber(value: unknown, fallback = 0): number {
46+
return typeof value === "number"
47+
? value
48+
: typeof value === "bigint"
49+
? Number(value)
50+
: typeof value === "string" && value.trim().length > 0
51+
? Number(value)
52+
: fallback;
53+
}
54+
55+
function toString(value: unknown, fallback = ""): string {
56+
return typeof value === "string" ? value : fallback;
57+
}
58+
59+
function parseVariables(value: unknown): string[] | null {
60+
if (typeof value !== "string") return null;
61+
try {
62+
const parsed = JSON.parse(value) as unknown;
63+
if (!Array.isArray(parsed)) return null;
64+
return parsed.filter((item): item is string => typeof item === "string");
65+
} catch {
66+
return null;
67+
}
68+
}
69+
1570
// ── Schema (auto-created on first access) ──
1671

1772
const PROMPT_SCHEMA = `
@@ -37,7 +92,7 @@ let _initialized = false;
3792
function ensureSchema(): void {
3893
if (_initialized) return;
3994
try {
40-
const db = getDbInstance();
95+
const db = getDbInstance() as unknown as DbLike;
4196
db.exec(PROMPT_SCHEMA);
4297
_initialized = true;
4398
} catch {
@@ -74,13 +129,13 @@ export function savePrompt(
74129
options: { variables?: string[]; description?: string } = {}
75130
): PromptTemplate {
76131
ensureSchema();
77-
const db = getDbInstance();
132+
const db = getDbInstance() as unknown as DbLike;
78133
const hash = hashContent(content);
79134

80135
// Check if identical content already exists for this slug
81136
const existing = db
82-
.prepare("SELECT * FROM prompt_templates WHERE slug = ? AND content_hash = ?")
83-
.get(slug, hash) as any;
137+
.prepare<PromptRow>("SELECT * FROM prompt_templates WHERE slug = ? AND content_hash = ?")
138+
.get(slug, hash);
84139

85140
if (existing) {
86141
return rowToPrompt(existing);
@@ -93,9 +148,11 @@ export function savePrompt(
93148

94149
// Get next version number
95150
const maxVersion = db
96-
.prepare("SELECT MAX(version) as max_v FROM prompt_templates WHERE slug = ?")
97-
.get(slug) as any;
98-
const nextVersion = (maxVersion?.max_v || 0) + 1;
151+
.prepare<{
152+
max_v: unknown;
153+
}>("SELECT MAX(version) as max_v FROM prompt_templates WHERE slug = ?")
154+
.get(slug);
155+
const nextVersion = toNumber(maxVersion?.max_v, 0) + 1;
99156

100157
// Insert new version
101158
const result = db
@@ -113,7 +170,7 @@ export function savePrompt(
113170
);
114171

115172
return {
116-
id: Number(result.lastInsertRowid),
173+
id: toNumber(result.lastInsertRowid, 0),
117174
slug,
118175
version: nextVersion,
119176
content,
@@ -130,10 +187,10 @@ export function savePrompt(
130187
*/
131188
export function getActivePrompt(slug: string): PromptTemplate | null {
132189
ensureSchema();
133-
const db = getDbInstance();
190+
const db = getDbInstance() as unknown as DbLike;
134191
const row = db
135-
.prepare("SELECT * FROM prompt_templates WHERE slug = ? AND is_active = 1")
136-
.get(slug) as any;
192+
.prepare<PromptRow>("SELECT * FROM prompt_templates WHERE slug = ? AND is_active = 1")
193+
.get(slug);
137194
return row ? rowToPrompt(row) : null;
138195
}
139196

@@ -142,10 +199,10 @@ export function getActivePrompt(slug: string): PromptTemplate | null {
142199
*/
143200
export function getPromptVersion(slug: string, version: number): PromptTemplate | null {
144201
ensureSchema();
145-
const db = getDbInstance();
202+
const db = getDbInstance() as unknown as DbLike;
146203
const row = db
147-
.prepare("SELECT * FROM prompt_templates WHERE slug = ? AND version = ?")
148-
.get(slug, version) as any;
204+
.prepare<PromptRow>("SELECT * FROM prompt_templates WHERE slug = ? AND version = ?")
205+
.get(slug, version);
149206
return row ? rowToPrompt(row) : null;
150207
}
151208

@@ -154,34 +211,38 @@ export function getPromptVersion(slug: string, version: number): PromptTemplate
154211
*/
155212
export function listPromptVersions(slug: string): PromptTemplate[] {
156213
ensureSchema();
157-
const db = getDbInstance();
214+
const db = getDbInstance() as unknown as DbLike;
158215
const rows = db
159-
.prepare("SELECT * FROM prompt_templates WHERE slug = ? ORDER BY version DESC")
160-
.all(slug) as any[];
216+
.prepare<PromptRow>("SELECT * FROM prompt_templates WHERE slug = ? ORDER BY version DESC")
217+
.all(slug);
161218
return rows.map(rowToPrompt);
162219
}
163220

164221
/**
165222
* List all prompt slugs with their active version info.
166223
*/
167-
export function listPrompts(): Array<{ slug: string; activeVersion: number; totalVersions: number }> {
224+
export function listPrompts(): Array<{
225+
slug: string;
226+
activeVersion: number;
227+
totalVersions: number;
228+
}> {
168229
ensureSchema();
169-
const db = getDbInstance();
230+
const db = getDbInstance() as unknown as DbLike;
170231
const rows = db
171-
.prepare(
232+
.prepare<PromptListRow>(
172233
`SELECT slug,
173234
MAX(CASE WHEN is_active = 1 THEN version ELSE 0 END) as active_version,
174235
COUNT(*) as total_versions
175236
FROM prompt_templates
176237
GROUP BY slug
177238
ORDER BY slug`
178239
)
179-
.all() as any[];
240+
.all();
180241

181242
return rows.map((r) => ({
182-
slug: r.slug,
183-
activeVersion: r.active_version,
184-
totalVersions: r.total_versions,
243+
slug: toString(r.slug),
244+
activeVersion: toNumber(r.active_version, 0),
245+
totalVersions: toNumber(r.total_versions, 0),
185246
}));
186247
}
187248

@@ -190,11 +251,11 @@ export function listPrompts(): Array<{ slug: string; activeVersion: number; tota
190251
*/
191252
export function rollbackPrompt(slug: string, version: number): PromptTemplate | null {
192253
ensureSchema();
193-
const db = getDbInstance();
254+
const db = getDbInstance() as unknown as DbLike;
194255

195256
const target = db
196-
.prepare("SELECT * FROM prompt_templates WHERE slug = ? AND version = ?")
197-
.get(slug, version) as any;
257+
.prepare<PromptRow>("SELECT * FROM prompt_templates WHERE slug = ? AND version = ?")
258+
.get(slug, version);
198259

199260
if (!target) return null;
200261

@@ -226,16 +287,16 @@ export function renderPrompt(slug: string, vars: Record<string, string> = {}): s
226287

227288
// ── Internal ──
228289

229-
function rowToPrompt(row: any): PromptTemplate {
290+
function rowToPrompt(row: PromptRow): PromptTemplate {
230291
return {
231-
id: row.id,
232-
slug: row.slug,
233-
version: row.version,
234-
content: row.content,
235-
contentHash: row.content_hash,
236-
variables: row.variables ? JSON.parse(row.variables) : null,
237-
description: row.description,
238-
isActive: row.is_active === 1,
239-
createdAt: row.created_at,
292+
id: toNumber(row.id, 0),
293+
slug: toString(row.slug),
294+
version: toNumber(row.version, 1),
295+
content: toString(row.content),
296+
contentHash: toString(row.content_hash),
297+
variables: parseVariables(row.variables),
298+
description: typeof row.description === "string" ? row.description : null,
299+
isActive: row.is_active === 1 || row.is_active === true,
300+
createdAt: toString(row.created_at),
240301
};
241302
}

0 commit comments

Comments
 (0)