Spaces:
Runtime error
Runtime error
| 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; | |
| } | |