Server Helpers
Use deterministic helpers for ordering, reindexing, moving, removing, and patching blocks.
The server-safe package entrypoint exports registry helpers and pure list/data helpers.
import {
appendBlock,
getNextBlockIndex,
moveBlock,
reindexBlocks,
removeBlock,
sortBlocks,
} from "@tulip-systems/blocks";Create
Use getNextBlockIndex when inserting at the end.
const lastBlock = await db
.select({ index: pageBlocksTable.index })
.from(pageBlocksTable)
.where(eq(pageBlocksTable.pageId, input.pageId))
.orderBy(desc(pageBlocksTable.index))
.limit(1);
await db.insert(pageBlocksTable).values({
pageId: input.pageId,
slug: input.block.slug,
data: input.block.data,
index: (lastBlock[0]?.index ?? -1) + 1,
});For in-memory create flows, appendBlock assigns the next index.
const nextBlocks = appendBlock(blocks, { id: "new", slug: "hero", data: {} });Delete
Use removeBlock when you need both the removed block and the remaining compacted list.
const result = removeBlock(blocks, { id: input.id });
if (!result) throw new Error("Block not found");
await persistIndexes(result.blocks);If deletion is already done by the database, fetch the remaining rows and use reindexBlocks.
const remaining = await tx.query.pageBlocks.findMany({ orderBy: asc(pageBlocksTable.index) });
const compacted = reindexBlocks(remaining);
await persistIndexes(compacted);Move
Use moveBlock for drag-and-drop reorder mutations.
const result = moveBlock(blocks, { id: input.id, toIndex: input.toIndex });
if (!result) {
throw new Error("Block not found");
}
await persistIndexes(result.blocks);moveBlock clamps toIndex into the valid range and reindexes the full list.
Patch Data
Use registry.patchData for inline update mutations.
const existing = await db.query.pageBlocks.findFirst({ where: eq(pageBlocksTable.id, input.id) });
if (!existing) throw new Error("Block not found");
const patched = pageBlocks.patchData({ block: existing, patch: input.data });
if (!patched.success) {
throw new Error("Unknown block type");
}
await db.update(pageBlocksTable).set({ data: patched.data });This keeps partial inline edits safe: callers send only changed fields, and the server validates the merged result against the block definition schema.