feat: add POST /api/communities/:slug/shapes endpoint
Enables external apps (e.g. rNotes) to push shapes to a canvas via REST API. Shapes are added in a single Automerge change and broadcast to connected WebSocket clients for real-time sync. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
426e05d631
commit
d227fbff16
|
|
@ -24,6 +24,7 @@ export interface ShapeData {
|
|||
content?: string;
|
||||
sourceId?: string;
|
||||
targetId?: string;
|
||||
[key: string]: unknown; // Allow additional shape-specific properties
|
||||
}
|
||||
|
||||
export interface CommunityDoc {
|
||||
|
|
@ -348,6 +349,40 @@ export function getDocumentData(slug: string): CommunityDoc | null {
|
|||
return JSON.parse(JSON.stringify(doc));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple shapes in a single Automerge change (for external API calls)
|
||||
*/
|
||||
export function addShapes(
|
||||
slug: string,
|
||||
shapes: Record<string, unknown>[],
|
||||
): string[] {
|
||||
const doc = communities.get(slug);
|
||||
if (!doc) return [];
|
||||
|
||||
const ids: string[] = [];
|
||||
const newDoc = Automerge.change(doc, `Add ${shapes.length} shapes`, (d) => {
|
||||
if (!d.shapes) d.shapes = {};
|
||||
for (const shape of shapes) {
|
||||
const id = (shape.id as string) || `shape-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
ids.push(id);
|
||||
d.shapes[id] = {
|
||||
type: (shape.type as string) || 'geo',
|
||||
id,
|
||||
x: (shape.x as number) ?? 100,
|
||||
y: (shape.y as number) ?? 100,
|
||||
width: (shape.width as number) ?? 300,
|
||||
height: (shape.height as number) ?? 200,
|
||||
rotation: (shape.rotation as number) ?? 0,
|
||||
...shape,
|
||||
id, // ensure id override
|
||||
} as ShapeData;
|
||||
}
|
||||
});
|
||||
communities.set(slug, newDoc);
|
||||
saveCommunity(slug);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Legacy functions for backward compatibility
|
||||
|
||||
export function updateShape(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { resolve } from "node:path";
|
||||
import type { ServerWebSocket } from "bun";
|
||||
import {
|
||||
addShapes,
|
||||
communityExists,
|
||||
createCommunity,
|
||||
deleteShape,
|
||||
|
|
@ -478,6 +479,86 @@ async function handleAPI(req: Request, url: URL): Promise<Response> {
|
|||
);
|
||||
}
|
||||
|
||||
// POST /api/communities/:slug/shapes - Add shapes to a community canvas
|
||||
if (
|
||||
url.pathname.match(/^\/api\/communities\/[^/]+\/shapes$/) &&
|
||||
req.method === "POST"
|
||||
) {
|
||||
const slug = url.pathname.split("/")[3];
|
||||
|
||||
// Check space access (write required)
|
||||
const token = extractToken(req.headers);
|
||||
const access = await evaluateSpaceAccess(slug, token, req.method, {
|
||||
getSpaceConfig,
|
||||
});
|
||||
|
||||
if (!access.allowed) {
|
||||
return Response.json(
|
||||
{ error: access.reason },
|
||||
{ status: access.claims ? 403 : 401, headers: corsHeaders }
|
||||
);
|
||||
}
|
||||
|
||||
if (access.readOnly) {
|
||||
return Response.json(
|
||||
{ error: "Write access required to add shapes" },
|
||||
{ status: 403, headers: corsHeaders }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure community is loaded
|
||||
await loadCommunity(slug);
|
||||
const data = getDocumentData(slug);
|
||||
if (!data) {
|
||||
return Response.json(
|
||||
{ error: "Community not found" },
|
||||
{ status: 404, headers: corsHeaders }
|
||||
);
|
||||
}
|
||||
|
||||
const body = (await req.json()) as { shapes?: Record<string, unknown>[] };
|
||||
if (!body.shapes || !Array.isArray(body.shapes) || body.shapes.length === 0) {
|
||||
return Response.json(
|
||||
{ error: "shapes array is required and must not be empty" },
|
||||
{ status: 400, headers: corsHeaders }
|
||||
);
|
||||
}
|
||||
|
||||
// Add shapes to the Automerge document
|
||||
const ids = addShapes(slug, body.shapes);
|
||||
|
||||
// Broadcast sync messages to all connected WebSocket clients
|
||||
const clients = communityClients.get(slug);
|
||||
if (clients) {
|
||||
for (const [peerId, client] of clients) {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
const syncMsg = generateSyncMessageForPeer(slug, peerId);
|
||||
if (syncMsg) {
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
type: "sync",
|
||||
data: Array.from(syncMsg),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json(
|
||||
{ ok: true, ids },
|
||||
{ status: 201, headers: corsHeaders }
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Failed to add shapes:", e);
|
||||
return Response.json(
|
||||
{ error: "Failed to add shapes" },
|
||||
{ status: 500, headers: corsHeaders }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue