Tulip Logo IconTulip
Blocks

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.

On this page