feat: read only publish of drawings
This commit is contained in:
@@ -205,4 +205,210 @@ describe("api", () => {
|
||||
expect(stored.elements).toEqual([{ id: "server" }]);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("returns disabled and enabled publication shapes", async () => {
|
||||
const { api, cleanup } = withApi();
|
||||
const created = await api.createDrawing().json();
|
||||
|
||||
const disabled = await api.getDrawingPublication(
|
||||
Object.assign(new Request(`http://local/api/drawings/${created.id}/publication`), { params: { id: created.id } }),
|
||||
);
|
||||
expect(disabled.status).toBe(200);
|
||||
expect(await disabled.json()).toEqual({ enabled: false });
|
||||
|
||||
const publish = await api.updateDrawingPublication(
|
||||
Object.assign(
|
||||
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
slug: "Flow Chart",
|
||||
}),
|
||||
}),
|
||||
{ params: { id: created.id } },
|
||||
),
|
||||
);
|
||||
|
||||
expect(publish.status).toBe(200);
|
||||
expect(await publish.json()).toEqual({
|
||||
enabled: true,
|
||||
slug: "flow-chart",
|
||||
});
|
||||
|
||||
const enabled = await api.getDrawingPublication(
|
||||
Object.assign(new Request(`http://local/api/drawings/${created.id}/publication`), { params: { id: created.id } }),
|
||||
);
|
||||
expect(await enabled.json()).toEqual({
|
||||
enabled: true,
|
||||
slug: "flow-chart",
|
||||
});
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("publishes, renames, and disables publications", async () => {
|
||||
const { api, cleanup } = withApi();
|
||||
const created = await api.createDrawing().json();
|
||||
|
||||
await api.updateDrawingPublication(
|
||||
Object.assign(
|
||||
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
slug: "first",
|
||||
}),
|
||||
}),
|
||||
{ params: { id: created.id } },
|
||||
),
|
||||
);
|
||||
|
||||
const renamed = await api.updateDrawingPublication(
|
||||
Object.assign(
|
||||
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
slug: "second",
|
||||
}),
|
||||
}),
|
||||
{ params: { id: created.id } },
|
||||
),
|
||||
);
|
||||
|
||||
expect(renamed.status).toBe(200);
|
||||
expect(await renamed.json()).toEqual({
|
||||
enabled: true,
|
||||
slug: "second",
|
||||
});
|
||||
|
||||
const disable = await api.updateDrawingPublication(
|
||||
Object.assign(
|
||||
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ enabled: false }),
|
||||
}),
|
||||
{ params: { id: created.id } },
|
||||
),
|
||||
);
|
||||
|
||||
expect(disable.status).toBe(200);
|
||||
expect(await disable.json()).toEqual({ enabled: false });
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("returns 400 for invalid publication slugs", async () => {
|
||||
const { api, cleanup } = withApi();
|
||||
const created = await api.createDrawing().json();
|
||||
|
||||
const response = await api.updateDrawingPublication(
|
||||
Object.assign(
|
||||
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
slug: "API",
|
||||
}),
|
||||
}),
|
||||
{ params: { id: created.id } },
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.json()).toEqual({
|
||||
ok: false,
|
||||
error: "slug is reserved",
|
||||
});
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("returns 409 for publication slug collisions", async () => {
|
||||
const { api, cleanup } = withApi();
|
||||
const first = await api.createDrawing().json();
|
||||
const second = await api.createDrawing().json();
|
||||
|
||||
await api.updateDrawingPublication(
|
||||
Object.assign(
|
||||
new Request(`http://local/api/drawings/${first.id}/publication`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
slug: "shared",
|
||||
}),
|
||||
}),
|
||||
{ params: { id: first.id } },
|
||||
),
|
||||
);
|
||||
|
||||
const response = await api.updateDrawingPublication(
|
||||
Object.assign(
|
||||
new Request(`http://local/api/drawings/${second.id}/publication`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
slug: "shared",
|
||||
}),
|
||||
}),
|
||||
{ params: { id: second.id } },
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(409);
|
||||
expect(await response.json()).toEqual({
|
||||
ok: false,
|
||||
error: "Published slug already exists",
|
||||
});
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("returns public scene data for enabled slugs and 404 otherwise", async () => {
|
||||
const { api, cleanup } = withApi();
|
||||
const created = await api.createDrawing().json();
|
||||
|
||||
await api.updateDrawing(
|
||||
Object.assign(
|
||||
new Request(`http://local/api/drawings/${created.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
title: "Public Flow",
|
||||
elements: [{ id: "shape" }],
|
||||
appState: { theme: "dark" },
|
||||
files: {},
|
||||
expectedRevision: 0,
|
||||
}),
|
||||
}),
|
||||
{ params: { id: created.id } },
|
||||
),
|
||||
);
|
||||
|
||||
await api.updateDrawingPublication(
|
||||
Object.assign(
|
||||
new Request(`http://local/api/drawings/${created.id}/publication`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
slug: "public-flow",
|
||||
}),
|
||||
}),
|
||||
{ params: { id: created.id } },
|
||||
),
|
||||
);
|
||||
|
||||
const enabled = await api.getPublicDrawing(
|
||||
Object.assign(new Request("http://local/api/public/drawings/public-flow"), { params: { slug: "public-flow" } }),
|
||||
);
|
||||
|
||||
expect(enabled.status).toBe(200);
|
||||
expect(await enabled.json()).toEqual({
|
||||
title: "Public Flow",
|
||||
elements: [{ id: "shape" }],
|
||||
appState: { theme: "dark" },
|
||||
files: {},
|
||||
});
|
||||
|
||||
const missing = await api.getPublicDrawing(
|
||||
Object.assign(new Request("http://local/api/public/drawings/missing"), { params: { slug: "missing" } }),
|
||||
);
|
||||
expect(missing.status).toBe(404);
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
+86
-1
@@ -1,5 +1,6 @@
|
||||
import { type DrawingStore } from "./db";
|
||||
import { type DrawingMeta } from "../core/shared";
|
||||
import { normalizePublicationSlug } from "../core/publication";
|
||||
import { type DrawingMeta, type DrawingPublication } from "../core/shared";
|
||||
import { HttpError, normalizeTitle, parseJsonBody, parseSceneText } from "../core/scene";
|
||||
|
||||
type RouteRequest = Request & {
|
||||
@@ -46,6 +47,10 @@ function parseExpectedRevision(raw: Record<string, unknown>): number | undefined
|
||||
return value;
|
||||
}
|
||||
|
||||
function jsonPublication(publication: DrawingPublication): Response {
|
||||
return json(publication);
|
||||
}
|
||||
|
||||
export function createApi(store: DrawingStore) {
|
||||
return {
|
||||
health() {
|
||||
@@ -85,6 +90,24 @@ export function createApi(store: DrawingStore) {
|
||||
}
|
||||
},
|
||||
|
||||
getPublicDrawing(request: RouteRequest) {
|
||||
try {
|
||||
const slug = request.params?.slug;
|
||||
if (!slug) {
|
||||
throw new HttpError(400, "Missing published slug");
|
||||
}
|
||||
|
||||
const drawing = store.getPublicDrawing(slug);
|
||||
if (!drawing) {
|
||||
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return json(drawing);
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
},
|
||||
|
||||
async updateDrawing(request: RouteRequest) {
|
||||
try {
|
||||
const id = request.params?.id;
|
||||
@@ -150,5 +173,67 @@ export function createApi(store: DrawingStore) {
|
||||
|
||||
return json({ ok: true, nextId: next?.id ?? null });
|
||||
},
|
||||
|
||||
getDrawingPublication(request: RouteRequest) {
|
||||
try {
|
||||
const id = request.params?.id;
|
||||
if (!id) {
|
||||
throw new HttpError(400, "Missing drawing id");
|
||||
}
|
||||
|
||||
const publication = store.getDrawingPublication(id);
|
||||
if (!publication) {
|
||||
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return jsonPublication(publication);
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
},
|
||||
|
||||
async updateDrawingPublication(request: RouteRequest) {
|
||||
try {
|
||||
const id = request.params?.id;
|
||||
if (!id) {
|
||||
throw new HttpError(400, "Missing drawing id");
|
||||
}
|
||||
|
||||
const raw = parseJsonBody<Record<string, unknown>>(await request.text());
|
||||
if (raw.enabled === false) {
|
||||
const result = store.disableDrawingPublication(id);
|
||||
if (!result.ok) {
|
||||
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return jsonPublication(result.publication);
|
||||
}
|
||||
|
||||
if (raw.enabled !== true) {
|
||||
throw new HttpError(400, "enabled must be a boolean");
|
||||
}
|
||||
|
||||
const drawing = store.getDrawing(id);
|
||||
if (!drawing) {
|
||||
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const result = store.publishDrawing(id, {
|
||||
slug: normalizePublicationSlug(raw.slug),
|
||||
});
|
||||
|
||||
if (!result.ok && result.reason === "not_found") {
|
||||
return json({ ok: false, error: "Drawing not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!result.ok && result.reason === "slug_conflict") {
|
||||
return json({ ok: false, error: "Published slug already exists" }, { status: 409 });
|
||||
}
|
||||
|
||||
return jsonPublication(result.publication);
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -135,4 +135,98 @@ describe("drawing store", () => {
|
||||
expect(renamed.ok ? renamed.drawing.title : null).toBe("Renamed");
|
||||
expect(renamed.ok ? renamed.drawing.revision : null).toBe(2);
|
||||
});
|
||||
|
||||
test("persists publication fields and exposes public drawings by slug", () => {
|
||||
const store = createTempStore();
|
||||
const created = store.createDrawing("Flow");
|
||||
store.updateDrawing(created.id, {
|
||||
scene: {
|
||||
elements: [{ id: "one" }],
|
||||
appState: { theme: "dark" },
|
||||
files: {},
|
||||
},
|
||||
});
|
||||
|
||||
const published = store.publishDrawing(created.id, { slug: "flow" });
|
||||
|
||||
expect(published.ok).toBe(true);
|
||||
expect(store.getDrawingPublication(created.id)).toEqual({
|
||||
enabled: true,
|
||||
slug: "flow",
|
||||
});
|
||||
expect(store.getPublicDrawing("flow")).toEqual({
|
||||
title: "Flow",
|
||||
elements: [{ id: "one" }],
|
||||
appState: { theme: "dark" },
|
||||
files: {},
|
||||
});
|
||||
});
|
||||
|
||||
test("enforces unique active publication slugs", () => {
|
||||
const store = createTempStore();
|
||||
const first = store.createDrawing("First");
|
||||
const second = store.createDrawing("Second");
|
||||
|
||||
expect(
|
||||
store.publishDrawing(first.id, {
|
||||
slug: "shared",
|
||||
}),
|
||||
).toEqual({
|
||||
ok: true,
|
||||
publication: {
|
||||
enabled: true,
|
||||
slug: "shared",
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
store.publishDrawing(second.id, {
|
||||
slug: "shared",
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
reason: "slug_conflict",
|
||||
});
|
||||
|
||||
expect(store.disableDrawingPublication(first.id)).toEqual({
|
||||
ok: true,
|
||||
publication: { enabled: false },
|
||||
});
|
||||
|
||||
expect(
|
||||
store.publishDrawing(second.id, {
|
||||
slug: "shared",
|
||||
}),
|
||||
).toEqual({
|
||||
ok: true,
|
||||
publication: {
|
||||
enabled: true,
|
||||
slug: "shared",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("clears public fields on disable", () => {
|
||||
const store = createTempStore();
|
||||
const created = store.createDrawing();
|
||||
|
||||
expect(
|
||||
store.publishDrawing(created.id, {
|
||||
slug: "note",
|
||||
}),
|
||||
).toEqual({
|
||||
ok: true,
|
||||
publication: {
|
||||
enabled: true,
|
||||
slug: "note",
|
||||
},
|
||||
});
|
||||
|
||||
expect(store.disableDrawingPublication(created.id)).toEqual({
|
||||
ok: true,
|
||||
publication: { enabled: false },
|
||||
});
|
||||
expect(store.getDrawingPublication(created.id)).toEqual({ enabled: false });
|
||||
expect(store.getPublicDrawing("note")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
+155
-2
@@ -2,7 +2,7 @@ import { Database } from "bun:sqlite";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { DEFAULT_TITLE, type DrawingMeta, type DrawingRecord, type ScenePayload } from "../core/shared";
|
||||
import { DEFAULT_TITLE, type DrawingMeta, type DrawingPublication, type DrawingRecord, type ScenePayload } from "../core/shared";
|
||||
import { emptyScene, normalizeScene, normalizeTitle } from "../core/scene";
|
||||
|
||||
type DrawingRow = {
|
||||
@@ -16,11 +16,26 @@ type DrawingRow = {
|
||||
|
||||
type DrawingMetaRow = Omit<DrawingRow, "data">;
|
||||
|
||||
type DrawingPublicationRow = {
|
||||
publicEnabled: number;
|
||||
publicSlug: string | null;
|
||||
};
|
||||
|
||||
type PublicDrawingRow = {
|
||||
title: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export type DrawingUpdateResult =
|
||||
| { ok: true; drawing: DrawingRecord }
|
||||
| { ok: false; reason: "not_found" }
|
||||
| { ok: false; reason: "conflict"; drawing: DrawingRecord };
|
||||
|
||||
export type DrawingPublicationUpdateResult =
|
||||
| { ok: true; publication: DrawingPublication }
|
||||
| { ok: false; reason: "not_found" }
|
||||
| { ok: false; reason: "slug_conflict" };
|
||||
|
||||
export type DrawingStore = ReturnType<typeof createDrawingStore>;
|
||||
|
||||
function createId(): string {
|
||||
@@ -54,6 +69,40 @@ function readMeta(row: DrawingMetaRow | null | undefined): DrawingMeta | null {
|
||||
};
|
||||
}
|
||||
|
||||
function readPublication(row: DrawingPublicationRow | null | undefined): DrawingPublication | null {
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (row.publicEnabled !== 1 || !row.publicSlug) {
|
||||
return { enabled: false };
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
slug: row.publicSlug,
|
||||
};
|
||||
}
|
||||
|
||||
function readPublicDrawing(row: PublicDrawingRow | null | undefined) {
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let scene: ScenePayload;
|
||||
try {
|
||||
scene = normalizeScene(JSON.parse(row.data));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown scene parsing error";
|
||||
throw new Error(`Invalid stored public scene: ${message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
title: row.title,
|
||||
...scene,
|
||||
};
|
||||
}
|
||||
|
||||
function readRow(row: DrawingRow | null | undefined): DrawingRecord | null {
|
||||
if (!row) {
|
||||
return null;
|
||||
@@ -73,6 +122,25 @@ function readRow(row: DrawingRow | null | undefined): DrawingRecord | null {
|
||||
};
|
||||
}
|
||||
|
||||
function ensurePublicationColumns(db: Database) {
|
||||
const columns = db
|
||||
.query("PRAGMA table_info(drawings)")
|
||||
.all() as Array<{ name: string }>;
|
||||
const names = new Set(columns.map((column) => column.name));
|
||||
|
||||
if (!names.has("public_enabled")) {
|
||||
db.exec("ALTER TABLE drawings ADD COLUMN public_enabled INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
|
||||
if (!names.has("public_slug")) {
|
||||
db.exec("ALTER TABLE drawings ADD COLUMN public_slug TEXT");
|
||||
}
|
||||
}
|
||||
|
||||
function isSlugConflictError(error: unknown): boolean {
|
||||
return error instanceof Error && error.message.includes("UNIQUE constraint failed: drawings.public_slug");
|
||||
}
|
||||
|
||||
export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
||||
mkdirSync(dirname(databasePath), { recursive: true });
|
||||
|
||||
@@ -86,13 +154,22 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
||||
data TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
revision INTEGER NOT NULL DEFAULT 0
|
||||
revision INTEGER NOT NULL DEFAULT 0,
|
||||
public_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
public_slug TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS drawings_updated_at_idx
|
||||
ON drawings(updated_at DESC, created_at DESC);
|
||||
`);
|
||||
|
||||
ensurePublicationColumns(db);
|
||||
db.exec(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS drawings_public_slug_active_idx
|
||||
ON drawings(public_slug)
|
||||
WHERE public_enabled = 1 AND public_slug IS NOT NULL;
|
||||
`);
|
||||
|
||||
const listQuery = db.query(`
|
||||
SELECT
|
||||
id,
|
||||
@@ -162,6 +239,38 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
||||
WHERE id = ?1
|
||||
`);
|
||||
|
||||
const getPublicationQuery = db.query(`
|
||||
SELECT
|
||||
public_enabled AS publicEnabled,
|
||||
public_slug AS publicSlug
|
||||
FROM drawings
|
||||
WHERE id = ?1
|
||||
`);
|
||||
|
||||
const publishQuery = db.query(`
|
||||
UPDATE drawings
|
||||
SET
|
||||
public_enabled = 1,
|
||||
public_slug = ?2
|
||||
WHERE id = ?1
|
||||
`);
|
||||
|
||||
const disablePublicationQuery = db.query(`
|
||||
UPDATE drawings
|
||||
SET
|
||||
public_enabled = 0,
|
||||
public_slug = NULL
|
||||
WHERE id = ?1
|
||||
`);
|
||||
|
||||
const getPublicDrawingQuery = db.query(`
|
||||
SELECT
|
||||
title,
|
||||
data
|
||||
FROM drawings
|
||||
WHERE public_enabled = 1 AND public_slug = ?1
|
||||
`);
|
||||
|
||||
function listDrawings(): DrawingMeta[] {
|
||||
return (listQuery.all() as DrawingMetaRow[]).map((row) => readMeta(row)!);
|
||||
}
|
||||
@@ -256,6 +365,46 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
||||
};
|
||||
}
|
||||
|
||||
function getDrawingPublication(id: string): DrawingPublication | null {
|
||||
return readPublication(getPublicationQuery.get(id) as DrawingPublicationRow | null | undefined);
|
||||
}
|
||||
|
||||
function publishDrawing(id: string, publication: { slug: string }): DrawingPublicationUpdateResult {
|
||||
try {
|
||||
const changes = Number(publishQuery.run(id, publication.slug).changes);
|
||||
if (changes === 0) {
|
||||
return { ok: false, reason: "not_found" };
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSlugConflictError(error)) {
|
||||
return { ok: false, reason: "slug_conflict" };
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
publication: getDrawingPublication(id)!,
|
||||
};
|
||||
}
|
||||
|
||||
function disableDrawingPublication(id: string): DrawingPublicationUpdateResult {
|
||||
const changes = Number(disablePublicationQuery.run(id).changes);
|
||||
if (changes === 0) {
|
||||
return { ok: false, reason: "not_found" };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
publication: { enabled: false },
|
||||
};
|
||||
}
|
||||
|
||||
function getPublicDrawing(slug: string) {
|
||||
return readPublicDrawing(getPublicDrawingQuery.get(slug) as PublicDrawingRow | null | undefined);
|
||||
}
|
||||
|
||||
function close() {
|
||||
db.close(false);
|
||||
}
|
||||
@@ -270,6 +419,10 @@ export function createDrawingStore(databasePath = resolveDatabasePath()) {
|
||||
ensureInitialDrawing,
|
||||
updateDrawing,
|
||||
deleteDrawing,
|
||||
getDrawingPublication,
|
||||
publishDrawing,
|
||||
disableDrawingPublication,
|
||||
getPublicDrawing,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createDevServer } from "./dev-server";
|
||||
|
||||
describe("createDevServer", () => {
|
||||
test("serves the spa for private and public drawing routes", () => {
|
||||
const server = createDevServer();
|
||||
|
||||
expect(Object.hasOwn(server.routes, "/d/:id")).toBe(true);
|
||||
expect(Object.hasOwn(server.routes, "/p/:slug")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ export function createDevServer() {
|
||||
routes: {
|
||||
...server.routes,
|
||||
"/d/:id": index,
|
||||
"/p/:slug": index,
|
||||
},
|
||||
} satisfies Parameters<typeof Bun.serve>[0];
|
||||
}
|
||||
|
||||
@@ -58,6 +58,37 @@ describe("createServer", () => {
|
||||
const { server } = createServerFixture();
|
||||
|
||||
expect(Object.hasOwn(server.routes, "/d/:id")).toBe(false);
|
||||
expect(Object.hasOwn(server.routes, "/p/:slug")).toBe(false);
|
||||
});
|
||||
|
||||
test("serves public drawing data through the public api", async () => {
|
||||
const { server, store } = createServerFixture();
|
||||
const drawing = store.createDrawing("Published");
|
||||
store.publishDrawing(drawing.id, { slug: "published-note" });
|
||||
|
||||
const response = await server.routes["/api/public/drawings/:slug"]?.GET?.(
|
||||
Object.assign(new Request("http://local/api/public/drawings/published-note"), {
|
||||
params: { slug: "published-note" },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response?.status).toBe(200);
|
||||
expect(await response?.json()).toEqual({
|
||||
title: "Published",
|
||||
elements: [],
|
||||
appState: { theme: "dark" },
|
||||
files: {},
|
||||
});
|
||||
});
|
||||
|
||||
test("returns 404 for unknown or unpublished public slugs", async () => {
|
||||
const { server } = createServerFixture();
|
||||
|
||||
const response = await server.routes["/api/public/drawings/:slug"]?.GET?.(
|
||||
Object.assign(new Request("http://local/api/public/drawings/missing"), { params: { slug: "missing" } }),
|
||||
);
|
||||
|
||||
expect(response?.status).toBe(404);
|
||||
});
|
||||
|
||||
test("returns JSON 404 for unmatched requests", async () => {
|
||||
|
||||
@@ -29,6 +29,13 @@ export function createServer(options: CreateServerOptions = {}) {
|
||||
PUT: (request: Request & { params: Record<string, string> }) => api.updateDrawing(request),
|
||||
DELETE: (request: Request & { params: Record<string, string> }) => api.deleteDrawing(request),
|
||||
},
|
||||
"/api/public/drawings/:slug": {
|
||||
GET: (request: Request & { params: Record<string, string> }) => api.getPublicDrawing(request),
|
||||
},
|
||||
"/api/drawings/:id/publication": {
|
||||
GET: (request: Request & { params: Record<string, string> }) => api.getDrawingPublication(request),
|
||||
PUT: (request: Request & { params: Record<string, string> }) => api.updateDrawingPublication(request),
|
||||
},
|
||||
},
|
||||
fetch: (_request: Request) => Response.json({ ok: false, error: "Not found" }, { status: 404 }),
|
||||
} satisfies Parameters<typeof Bun.serve>[0];
|
||||
|
||||
Reference in New Issue
Block a user