feat: implement file and repository management hooks and APIs
This commit is contained in:
@@ -1,63 +1,37 @@
|
||||
import { supabaseBrowser } from "../supabase/client";
|
||||
import { throwIfError, requireData, getUserIdOrThrow } from "./_helpers";
|
||||
import type { Archivo, UUID } from "../types/domain";
|
||||
import { invokeEdge } from "../supabase/invokeEdge";
|
||||
import { throwIfError } from "./_helpers";
|
||||
import type { AppFile } from "./openaiFiles.api";
|
||||
|
||||
const DEFAULT_BUCKET = "archivos";
|
||||
const EDGE = {
|
||||
signedUrl: "files_signed_url", // Edge: recibe archivoId o ruta_storage y devuelve URL
|
||||
} as const;
|
||||
|
||||
export type UploadFileInput = {
|
||||
file: File;
|
||||
bucket?: string;
|
||||
pathPrefix?: string; // ej: "planes/<planId>" o "materias/<id>"
|
||||
export async function files_list(params?: {
|
||||
temporal?: boolean;
|
||||
notas?: string | null;
|
||||
};
|
||||
|
||||
export async function files_upload(input: UploadFileInput): Promise<Archivo> {
|
||||
search?: string;
|
||||
limit?: number;
|
||||
}): Promise<AppFile[]> {
|
||||
const supabase = supabaseBrowser();
|
||||
const userId = await getUserIdOrThrow(supabase);
|
||||
|
||||
const bucket = input.bucket ?? DEFAULT_BUCKET;
|
||||
const safeName = input.file.name.replace(/[^\w.\-() ]+/g, "_");
|
||||
const pathPrefix = (input.pathPrefix ?? `usuarios/${userId}`).replace(/\/+$/g, "");
|
||||
|
||||
const storagePath = `${pathPrefix}/${crypto.randomUUID()}-${safeName}`;
|
||||
|
||||
const { data: upData, error: upErr } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(storagePath, input.file, { upsert: false });
|
||||
|
||||
throwIfError(upErr);
|
||||
requireData(upData, "No se pudo subir archivo.");
|
||||
|
||||
const { data: row, error: insErr } = await supabase
|
||||
let q = supabase
|
||||
.from("archivos")
|
||||
.insert({
|
||||
ruta_storage: `${bucket}/${storagePath}`,
|
||||
nombre: input.file.name,
|
||||
mime_type: input.file.type || null,
|
||||
bytes: input.file.size,
|
||||
subido_por: userId as UUID,
|
||||
temporal: Boolean(input.temporal),
|
||||
notas: input.notas ?? null,
|
||||
})
|
||||
.select("id,ruta_storage,nombre,mime_type,bytes,subido_por,subido_en,temporal,openai_file_id,notas")
|
||||
.single();
|
||||
.select("id,openai_file_id,nombre,mime_type,bytes,ruta_storage,temporal,notas,subido_en")
|
||||
.order("subido_en", { ascending: false });
|
||||
|
||||
throwIfError(insErr);
|
||||
return requireData(row, "No se pudo registrar metadata del archivo.");
|
||||
}
|
||||
if (typeof params?.temporal === "boolean") q = q.eq("temporal", params.temporal);
|
||||
if (params?.search?.trim()) q = q.ilike("nombre", `%${params.search.trim()}%`);
|
||||
if (params?.limit) q = q.limit(params.limit);
|
||||
|
||||
export async function files_signed_url(params: {
|
||||
ruta_storage: string; // "bucket/path/to/file"
|
||||
expiresIn?: number; // segundos
|
||||
}): Promise<string> {
|
||||
const supabase = supabaseBrowser();
|
||||
const expires = params.expiresIn ?? 60 * 10;
|
||||
|
||||
const [bucket, ...rest] = params.ruta_storage.split("/");
|
||||
const path = rest.join("/");
|
||||
|
||||
const { data, error } = await supabase.storage.from(bucket).createSignedUrl(path, expires);
|
||||
const { data, error } = await q;
|
||||
throwIfError(error);
|
||||
return requireData(data?.signedUrl, "No se pudo generar URL firmada.");
|
||||
return (data ?? []) as AppFile[];
|
||||
}
|
||||
|
||||
/** Para preview/descarga desde espejo — SIN tocar storage directo en el cliente */
|
||||
export async function files_get_signed_url(payload: {
|
||||
archivoId: string; // id interno (tabla archivos)
|
||||
expiresIn?: number; // segundos
|
||||
}): Promise<{ signedUrl: string }> {
|
||||
return invokeEdge<{ signedUrl: string }>(EDGE.signedUrl, payload);
|
||||
}
|
||||
|
||||
64
src/data/api/openaiFiles.api.ts
Normal file
64
src/data/api/openaiFiles.api.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { invokeEdge } from "../supabase/invokeEdge";
|
||||
import type { UUID } from "../types/domain";
|
||||
|
||||
/**
|
||||
* Metadata “canónica” para UI (archivo OpenAI + espejo en Supabase)
|
||||
* Se apoya en tu tabla `archivos`.
|
||||
*/
|
||||
export type AppFile = {
|
||||
id: UUID; // id interno (tabla archivos)
|
||||
openai_file_id: string; // id OpenAI
|
||||
nombre: string;
|
||||
mime_type: string | null;
|
||||
bytes: number | null;
|
||||
|
||||
// espejo Supabase para preview/descarga
|
||||
ruta_storage: string | null; // "bucket/path"
|
||||
signed_url?: string | null;
|
||||
|
||||
// auditoría/evidencia
|
||||
temporal: boolean;
|
||||
notas?: string | null;
|
||||
|
||||
subido_en: string;
|
||||
};
|
||||
|
||||
const EDGE = {
|
||||
upload: "openai_files_upload",
|
||||
remove: "openai_files_delete",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Sube archivo a OpenAI y (opcional) crea espejo en Storage
|
||||
* - El frontend NO toca Storage.
|
||||
*/
|
||||
export async function openai_files_upload(payload: {
|
||||
/**
|
||||
* Si tu Edge soporta multipart: manda File/Blob directo.
|
||||
* Si no, manda base64/bytes (según tu implementación).
|
||||
*/
|
||||
file: File;
|
||||
|
||||
/** “temporal” = evidencia usada para generar plan/materia */
|
||||
temporal?: boolean;
|
||||
|
||||
/** contexto para auditoría */
|
||||
contexto?: {
|
||||
planId?: UUID;
|
||||
asignaturaId?: UUID;
|
||||
motivo?: "WIZARD_PLAN" | "WIZARD_MATERIA" | "ADHOC";
|
||||
};
|
||||
|
||||
/** si quieres forzar espejo para preview siempre */
|
||||
mirrorToSupabase?: boolean;
|
||||
}): Promise<AppFile> {
|
||||
return invokeEdge<AppFile>(EDGE.upload, payload);
|
||||
}
|
||||
|
||||
export async function openai_files_delete(payload: {
|
||||
openaiFileId: string;
|
||||
/** si quieres borrar también espejo y registro */
|
||||
hardDelete?: boolean;
|
||||
}): Promise<{ ok: true }> {
|
||||
return invokeEdge<{ ok: true }>(EDGE.remove, payload);
|
||||
}
|
||||
44
src/data/api/repositories.api.ts
Normal file
44
src/data/api/repositories.api.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { invokeEdge } from "../supabase/invokeEdge";
|
||||
|
||||
export type Repository = {
|
||||
id: string; // id del vector store (OpenAI) o tu id interno
|
||||
nombre: string;
|
||||
creado_en?: string;
|
||||
meta?: Record<string, any>;
|
||||
};
|
||||
|
||||
const EDGE = {
|
||||
create: "repos_create",
|
||||
remove: "repos_delete",
|
||||
add: "repos_add_files",
|
||||
detach: "repos_remove_files",
|
||||
} as const;
|
||||
|
||||
export async function repos_create(payload: {
|
||||
nombre: string;
|
||||
descripcion?: string;
|
||||
/** si tu implementación crea también registro DB */
|
||||
persist?: boolean;
|
||||
}): Promise<Repository> {
|
||||
return invokeEdge<Repository>(EDGE.create, payload);
|
||||
}
|
||||
|
||||
export async function repos_delete(payload: { repoId: string }): Promise<{ ok: true }> {
|
||||
return invokeEdge<{ ok: true }>(EDGE.remove, payload);
|
||||
}
|
||||
|
||||
/** Agrega archivos (OpenAI file ids) a un repositorio */
|
||||
export async function repos_add_files(payload: {
|
||||
repoId: string;
|
||||
openaiFileIds: string[];
|
||||
}): Promise<{ ok: true }> {
|
||||
return invokeEdge<{ ok: true }>(EDGE.add, payload);
|
||||
}
|
||||
|
||||
/** Quita archivos (OpenAI file ids) del repositorio */
|
||||
export async function repos_remove_files(payload: {
|
||||
repoId: string;
|
||||
openaiFileIds: string[];
|
||||
}): Promise<{ ok: true }> {
|
||||
return invokeEdge<{ ok: true }>(EDGE.detach, payload);
|
||||
}
|
||||
Reference in New Issue
Block a user