Files
openmaic/OpenMAIC/lib/bundle/client-upload.ts
2026-08-16 14:58:47 +08:00

104 lines
2.9 KiB
TypeScript

// Client-side publish upload — the ops workbench → server publish route.
//
// Pure fetch wrapper so the wire contract (multipart fields, optional bearer token,
// response shape) is testable without a browser. The server route
// (`app/api/coursewares/route.ts`) validates the bundle and rejects
// tampered/incomplete uploads with a 4xx and an error body.
export interface PublishedRecord {
coursewareId: string;
version: number;
title: string;
language?: string;
status: string;
publishedAt: string;
contentHash: string;
sceneCount: number;
quizSceneCount: number;
bundleUrl: string;
}
export interface UploadCoursewareResult {
ok: boolean;
record?: PublishedRecord;
/** Error code from the server body, e.g. INVALID_REQUEST. */
errorCode?: string;
error?: string;
details?: string;
/** HTTP status for diagnostics (0 = network failure). */
status: number;
}
export interface UploadCoursewareOptions {
/** The frozen bundle ZIP produced by `packageCourseware`. */
zip: Blob;
coursewareId: string;
/** Used for ops→server publishing. Same-origin ops sessions omit it. */
token?: string;
/**
* Explicit immutable version. It must already match the ZIP's internal
* `bundle.json`; omit it for browser-built bundles so the server can allocate
* and stamp the next monotonic version under its publish lock.
*/
version?: number;
status?: 'published' | 'unpublished';
/** Defaults to the same-origin publish route. */
endpoint?: string;
fetchImpl?: typeof fetch;
}
export async function uploadCoursewareZip(
options: UploadCoursewareOptions,
): Promise<UploadCoursewareResult> {
const {
zip,
coursewareId,
token,
version,
status = 'published',
endpoint = '/api/coursewares',
fetchImpl = fetch,
} = options;
const form = new FormData();
form.append('zip', zip, 'bundle.zip');
form.append('coursewareId', coursewareId);
if (version !== undefined) form.append('version', String(version));
if (status !== 'published') form.append('status', status);
let response: Response;
try {
response = await fetchImpl(endpoint, {
method: 'POST',
...(token ? { headers: { authorization: `Bearer ${token}` } } : {}),
body: form,
});
} catch {
return { ok: false, status: 0, error: 'Network error during publish upload' };
}
let body: {
success?: boolean;
record?: PublishedRecord;
errorCode?: string;
error?: string;
details?: string;
} = {};
try {
body = (await response.json()) as typeof body;
} catch {
// Non-JSON failure (proxy error page, etc.)
}
if (!response.ok || !body.success) {
return {
ok: false,
status: response.status,
errorCode: body.errorCode,
error: body.error ?? `Publish failed (HTTP ${response.status})`,
...(body.details ? { details: body.details } : {}),
};
}
return { ok: true, status: response.status, record: body.record };
}