feat(admin) : persist drafts and add constellation conditions
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { adminDraftKey, parseLocalDraft, serializeLocalDraft } from "./local-draft";
|
||||
|
||||
describe("local admin drafts", () => {
|
||||
it("isolates drafts by guide and section", () => {
|
||||
expect(adminDraftKey("guide-1", "team")).toBe("buzz-sheet:admin-draft:guide-1:team");
|
||||
expect(adminDraftKey("guide-1", "weapon")).not.toBe(adminDraftKey("guide-1", "team"));
|
||||
expect(adminDraftKey("guide-2", "team")).not.toBe(adminDraftKey("guide-1", "team"));
|
||||
});
|
||||
|
||||
it("round trips a versioned draft", () => {
|
||||
const serialized = serializeLocalDraft({ title: "Recovered team" }, 7);
|
||||
const parsed = parseLocalDraft(serialized, (value) => {
|
||||
if (!value || typeof value !== "object" || typeof (value as { title?: unknown }).title !== "string") return null;
|
||||
return value as { title: string };
|
||||
});
|
||||
expect(parsed).toMatchObject({ schemaVersion: 1, serverVersion: 7, draft: { title: "Recovered team" } });
|
||||
});
|
||||
|
||||
it("rejects corrupt, incompatible, and invalid drafts", () => {
|
||||
const parse = (value: unknown) => typeof value === "string" ? value : null;
|
||||
expect(parseLocalDraft("not-json", parse)).toBeNull();
|
||||
expect(parseLocalDraft(JSON.stringify({ schemaVersion: 2, serverVersion: 1, updatedAt: "now", draft: "x" }), parse)).toBeNull();
|
||||
expect(parseLocalDraft(JSON.stringify({ schemaVersion: 1, serverVersion: 1, updatedAt: "now", draft: 5 }), parse)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
export const LOCAL_DRAFT_SCHEMA_VERSION = 1;
|
||||
|
||||
export interface LocalDraftEnvelope<Draft> {
|
||||
schemaVersion: typeof LOCAL_DRAFT_SCHEMA_VERSION;
|
||||
serverVersion: number;
|
||||
updatedAt: string;
|
||||
draft: Draft;
|
||||
}
|
||||
|
||||
export function adminDraftKey(guideId: string, section: string): string {
|
||||
return `buzz-sheet:admin-draft:${guideId}:${section}`;
|
||||
}
|
||||
|
||||
export function serializeLocalDraft<Draft>(draft: Draft, serverVersion: number): string {
|
||||
return JSON.stringify({
|
||||
schemaVersion: LOCAL_DRAFT_SCHEMA_VERSION,
|
||||
serverVersion,
|
||||
updatedAt: new Date().toISOString(),
|
||||
draft,
|
||||
} satisfies LocalDraftEnvelope<Draft>);
|
||||
}
|
||||
|
||||
export function parseLocalDraft<Draft>(serialized: string, parseDraft: (value: unknown) => Draft | null): LocalDraftEnvelope<Draft> | null {
|
||||
try {
|
||||
const value: unknown = JSON.parse(serialized);
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const envelope = value as Record<string, unknown>;
|
||||
if (envelope.schemaVersion !== LOCAL_DRAFT_SCHEMA_VERSION || typeof envelope.serverVersion !== "number" || typeof envelope.updatedAt !== "string") return null;
|
||||
const draft = parseDraft(envelope.draft);
|
||||
return draft === null ? null : {
|
||||
schemaVersion: LOCAL_DRAFT_SCHEMA_VERSION,
|
||||
serverVersion: envelope.serverVersion,
|
||||
updatedAt: envelope.updatedAt,
|
||||
draft,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user