Files
arcadia-admin/app/lib/arcadia/digital-objects.ts
jules ab116f8465 refactor: rename @crema/arcadia-client → @crema/arcadia-core-client
Disambiguates the Phoenix/auth client lib from lib-arcadia-agents-client.
Dir lib-arcadia-client → lib-arcadia-core-client; alias updated in
tsconfig paths, vite config, app.css @source, imports, CI and docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:31:56 +10:00

131 lines
3.8 KiB
TypeScript

// Arcadia digital objects API — minimal client covering the upload flow
// used by the avatar uploader. The full digital-objects API is much
// larger; add endpoints here as we wire more features.
import type { ArcadiaClient } from "@crema/arcadia-core-client"
export interface DigitalObject {
id: string
tenant_id?: string
user_id?: string
storage_config_id?: string
filename?: string
original_filename?: string
content_type?: string
size_bytes?: number
key?: string
object_key?: string
status?: "active" | "archived" | "deleted"
inserted_at?: string
}
interface UploadSession {
id: string
upload_url: string
presigned_url?: string
expires_at?: string
}
interface CreateUploadSessionInput {
filename: string
content_type: string
size_bytes: number
storage_config_id?: string
metadata?: Record<string, unknown>
tags?: string[]
}
/**
* Three-step upload: open a session, PUT the bytes to the presigned URL
* the session returns, complete the session to land the digital_object.
*
* Returns the finalized DigitalObject record.
*/
export async function uploadFile(
arcadia: ArcadiaClient,
file: File,
opts: { storage_config_id?: string; tags?: string[] } = {},
): Promise<DigitalObject> {
const sessionInput: CreateUploadSessionInput = {
filename: file.name,
content_type: file.type || "application/octet-stream",
size_bytes: file.size,
storage_config_id: opts.storage_config_id,
tags: opts.tags,
}
const session = await arcadia.POST<{ data: UploadSession }>(
"/api/v1/digital_objects/upload_sessions",
{ body: { upload_session: sessionInput } },
)
const uploadUrl = session.data.upload_url || session.data.presigned_url
if (!uploadUrl) throw new Error("Upload session returned no upload URL")
const putRes = await fetch(uploadUrl, {
method: "PUT",
headers: { "Content-Type": sessionInput.content_type },
body: file,
})
if (!putRes.ok) {
throw new Error(
`Upload to storage failed: ${putRes.status} ${await putRes.text().catch(() => "")}`,
)
}
const completed = await arcadia.POST<{ data: DigitalObject }>(
`/api/v1/digital_objects/upload_sessions/${encodeURIComponent(session.data.id)}/complete`,
{ body: {} },
)
return completed.data
}
export async function deleteDigitalObject(
arcadia: ArcadiaClient,
id: string,
): Promise<void> {
await arcadia.DELETE(`/api/v1/digital_objects/${encodeURIComponent(id)}`)
}
/**
* Fetch the raw bytes of a digital object and return a browser blob URL
* suitable for `<img src>`. Used as an immediate-display fallback when
* the async variant URLs aren't ready yet (e.g. fresh avatar upload).
*
* Bypasses the arcadia-client because that client only parses JSON or
* text — for binary we need response.blob(). Auth is injected manually
* from sessionStorage to match the rest of the auth surface.
*
* The returned blob URL is per-page; it does NOT survive a reload.
* Caller should not persist it to localStorage — only render in memory
* until the persistent variant URLs come through (e.g. on next mount).
*/
export async function fetchDigitalObjectAsBlobUrl(
baseUrl: string,
id: string,
token: string,
tenantId?: string,
): Promise<string> {
const headers: Record<string, string> = {
Accept: "*/*",
Authorization: `Bearer ${token}`,
}
if (tenantId) headers["X-Tenant-ID"] = tenantId
const url = `${baseUrl.replace(/\/+$/, "")}/api/v1/digital_objects/${encodeURIComponent(
id,
)}/content`
const res = await fetch(url, { headers })
if (!res.ok) {
throw new Error(
`Failed to fetch digital object content: ${res.status} ${await res.text().catch(() => "")}`,
)
}
const blob = await res.blob()
// eslint-disable-next-line no-console
console.info(
`[digital-objects] fetched blob id=${id} type=${blob.type} size=${blob.size}B`,
)
return URL.createObjectURL(blob)
}