ai_api / src /lib /db /files.ts
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
5.03 kB
import { getDbInstance, rowToCamel, objToSnake } from "./core";
import { v4 as uuidv4 } from "uuid";
import { DEFAULT_BATCH_EXPIRATION_SECONDS } from "@/shared/constants/batch";
export interface FileRecord {
id: string;
bytes: number;
createdAt: number;
filename: string;
purpose: string;
content?: Buffer | null;
mimeType?: string | null;
apiKeyId?: string | null;
expiresAt?: number | null;
deletedAt?: number | null;
}
const FILE_METADATA_COLUMNS =
"id, bytes, created_at, filename, purpose, mime_type, api_key_id, expires_at, deleted_at";
export function createFile(file: Omit<FileRecord, "id" | "createdAt">): FileRecord {
const db = getDbInstance();
const id = "file-" + uuidv4().replaceAll("-", "").substring(0, 24);
const createdAt = Math.floor(Date.now() / 1000);
let expiresAt = file.expiresAt;
if (expiresAt === undefined && file.purpose === "batch") {
// Default: batch files expire after 30 days
expiresAt = createdAt + DEFAULT_BATCH_EXPIRATION_SECONDS;
}
const record: FileRecord = {
id,
bytes: file.bytes,
createdAt,
filename: file.filename,
purpose: file.purpose,
content: file.content ?? null,
mimeType: file.mimeType ?? null,
apiKeyId: file.apiKeyId ?? null,
expiresAt: expiresAt ?? null,
deletedAt: null,
};
db.prepare(
`
INSERT INTO files (id, bytes, created_at, filename, purpose, content, mime_type, api_key_id, expires_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
).run(
record.id,
record.bytes,
record.createdAt,
record.filename,
record.purpose,
record.content,
record.mimeType,
record.apiKeyId,
record.expiresAt,
record.deletedAt
);
return record;
}
export function getFile(id: string): FileRecord | null {
const db = getDbInstance();
const row = db
.prepare(`SELECT ${FILE_METADATA_COLUMNS} FROM files WHERE id = ? AND deleted_at IS NULL`)
.get(id);
return row ? (rowToCamel(row) as unknown as FileRecord) : null;
}
export function getFileContent(id: string): Buffer | null {
const db = getDbInstance();
const row = db
.prepare("SELECT content FROM files WHERE id = ? AND deleted_at IS NULL")
.get(id) as { content: Buffer | Uint8Array | string | null } | undefined;
if (!row?.content) return null;
return Buffer.isBuffer(row.content) ? row.content : Buffer.from(row.content);
}
export function listFiles(
options: {
apiKeyId?: string;
purpose?: string;
limit?: number;
after?: string;
order?: "asc" | "desc";
} = {}
): FileRecord[] {
const db = getDbInstance();
const { apiKeyId, purpose, limit = 20, after, order = "desc" } = options;
let query = `SELECT ${FILE_METADATA_COLUMNS} FROM files WHERE deleted_at IS NULL`;
const params: any[] = [];
if (apiKeyId) {
query += " AND api_key_id = ?";
params.push(apiKeyId);
}
if (purpose) {
query += " AND purpose = ?";
params.push(purpose);
}
if (after) {
// Get the creation time of the 'after' file to use for pagination
const afterFile = getFile(after);
if (afterFile) {
if (order === "desc") {
query += " AND (created_at < ? OR (created_at = ? AND id < ?))";
} else {
query += " AND (created_at > ? OR (created_at = ? AND id > ?))";
}
params.push(afterFile.createdAt, afterFile.createdAt, after);
}
}
query += ` ORDER BY created_at ${order === "asc" ? "ASC" : "DESC"}, id ${order === "asc" ? "ASC" : "DESC"}`;
query += " LIMIT ?";
params.push(limit);
const rows = db.prepare(query).all(...params);
return rows.map((row) => rowToCamel(row) as unknown as FileRecord);
}
export function countFiles(options: { apiKeyId?: string; purpose?: string } = {}): number {
const db = getDbInstance();
const { apiKeyId, purpose } = options;
let query = "SELECT COUNT(*) as c FROM files WHERE deleted_at IS NULL";
const params: any[] = [];
if (apiKeyId) {
query += " AND api_key_id = ?";
params.push(apiKeyId);
}
if (purpose) {
query += " AND purpose = ?";
params.push(purpose);
}
const row = db.prepare(query).get(...params) as { c: number } | undefined;
return row ? Number(row.c) : 0;
}
export function formatFileResponse(file: FileRecord) {
// Ensure numeric date fields are valid numbers to avoid NaN in API responses
const createdAt =
typeof file.createdAt === "number" && Number.isFinite(file.createdAt) ? file.createdAt : 0;
const expiresAt =
typeof file.expiresAt === "number" && Number.isFinite(file.expiresAt) ? file.expiresAt : null;
return {
id: file.id,
bytes: file.bytes,
created_at: createdAt,
filename: file.filename,
object: "file",
purpose: file.purpose,
expires_at: expiresAt,
};
}
export function deleteFile(id: string): boolean {
const db = getDbInstance();
const result = db
.prepare("UPDATE files SET deleted_at = ?, content = NULL WHERE id = ?")
.run(Math.floor(Date.now() / 1000), id);
return result.changes > 0;
}