SLICE-6: export & the round-trip lock — canonical serializer + streamed export (SD-0002 §7.2) #29

Merged
ben.stull merged 13 commits from worktree-slice-6-export-roundtrip into main 2026-06-12 05:23:24 +00:00
2 changed files with 28 additions and 0 deletions
Showing only changes of commit 6f213e1f02 - Show all commits
+12
View File
@@ -0,0 +1,12 @@
import { describe, expect, it } from "vitest";
import { EXPORT_STATUSES, exportUrl } from "./productsApi";
describe("export url", () => {
it("lists the four status filters with 'all' first", () => {
expect(EXPORT_STATUSES.map((s) => s.value)).toEqual(["all", "active", "draft", "archived"]);
});
it("builds the endpoint url with the status query", () => {
expect(exportUrl("all")).toBe("/api/products/export?status=all");
expect(exportUrl("archived")).toBe("/api/products/export?status=archived");
});
});
+16
View File
@@ -47,6 +47,22 @@ export function dialectLabel(d: string): string {
return d === "canonical" ? "Canonical format" : d;
}
export type ExportStatus = "all" | "active" | "draft" | "archived";
// PUC-9 status filter, 'all' first (the default). Labels drive the export menu.
export const EXPORT_STATUSES: { value: ExportStatus; label: string }[] = [
{ value: "all", label: "All products" },
{ value: "active", label: "Active" },
{ value: "draft", label: "Draft" },
{ value: "archived", label: "Archived" },
];
// The export is a browser download (native save dialog + streaming), not a fetch
// wrapper — so the API module contributes a URL builder, not a request().
export function exportUrl(status: ExportStatus): string {
return `/api/products/export?status=${status}`;
}
async function request<T>(path: string, init?: RequestInit): Promise<Result<T>> {
const resp = await fetch(path, { credentials: "include", ...init });
if (!resp.ok) return { ok: false, error: await errorOf(resp), status: resp.status };