- Add a digital-objects client (uploadFile: open session → PUT to presigned URL → complete) and a profile client (getProfile, updateProfile, pickAvatarUrl variant resolver). - Wire profile.tsx avatar upload to use the real flow: validate image+size, upload to digital_objects tagged "avatar", PATCH /api/v1/profile with avatar_digital_object_id, mirror the resolved URL into local prefs so the existing <AvatarImage> binding keeps working. Show Uploading… state and an inline error banner. Clear detaches via avatar_digital_object_id: null. - Fix the storage form sending the wrong field name for the local backend — arcadia's StorageConfig changeset requires `base_path`, not `path`. The 422 was silent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
2.4 KiB
TypeScript
89 lines
2.4 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-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)}`)
|
|
}
|