xiaohongshu
xiaohongshu
cascade-deletes
Normalized schema cascade delete patterns. Handles junction tables, parent-child ordering, nullify vs delete strategies. Triggers on "cascade", "delete", "cleanup", "junction", "related records", "deleteConversation", "deleteUser".
Full skill instructions
Normalized schema requires explicit cascade deletes. Convex has no foreign key constraints or automatic cascades - must handle manually.
Normalized schema separates related data into multiple tables. Deleting a conversation must also delete:
Leaving orphaned records wastes storage, breaks queries, violates data integrity.
Promise.allFrom packages/backend/convex/lib/utils/cascade.ts:
export async function cascadeDeleteConversation(
ctx: MutationCtx,
conversationId: Id<"conversations">,
options?: { deleteMessages?: boolean; deleteConversation?: boolean },
): Promise<void> {
const deleteMessages = options?.deleteMessages ?? true;
const deleteConversation = options?.deleteConversation ?? true;
// Step 1: Parallel queries for all related records
const [
bookmarks,
shares,
files,
memories,
junctions,
participants,
tokenUsage,
attachments,
toolCalls,
sources,
canvasDocs,
] = await Promise.all([
ctx.db
.query("bookmarks")
.withIndex("by_conversation", (q) =>
q.eq("conversationId", conversationId),
)
.collect(),
ctx.db
.query("shares")
.withIndex("by_conversation", (q) =>
q.eq("conversationId", conversationId),
)
.collect(),
// ... more queries
]);
// Step 2: Parallel deletions for independent tables
await Promise.all([
...bookmarks.map((b) => ctx.db.delete(b._id)),
...shares.map((s) => ctx.db.delete(s._id)),
...junctions.map((j) => ctx.db.delete(j._id)),
...participants.map((p) => ctx.db.delete(p._id)),
...tokenUsage.map((t) => ctx.db.delete(t._id)),
...attachments.map((a) => ctx.db.delete(a._id)),
...toolCalls.map((tc) => ctx.db.delete(tc._id)),
...sources.map((src) => ctx.db.delete(src._id)),
]);
// Step 3: Nullify files/memories (can exist independently)
await Promise.all([
...files.map((f) => ctx.db.patch(f._id, { conversationId: undefined })),
...memories.map((m) => ctx.db.patch(m._id, { conversationId: undefined })),
]);
// Step 4: Handle parent-child relationships (history before documents)
for (const doc of canvasDocs) {
const history = await ctx.db
.query("canvasHistory")
.withIndex("by_document", (q) => q.eq("documentId", doc._id))
.collect();
await Promise.all(history.map((h) => ctx.db.delete(h._id)));
await ctx.db.delete(doc._id);
}
// Step 5: Delete messages (optional)
if (deleteMessages) {
const messages = await ctx.db
.query("messages")
.withIndex("by_conversation", (q) =>
q.eq("conversationId", conversationId),
)
.collect();
await Promise.all(messages.map((msg) => ctx.db.delete(msg._id)));
}
// Step 6: Delete conversation itself (optional)
if (deleteConversation) await ctx.db.delete(conversationId);
}
Usage in mutations:
export const deleteConversation = mutation({
args: { conversationId: v.id("conversations") },
handler: async (ctx, args) => {
const user = await getCurrentUserOrCreate(ctx);
const conv = await ctx.db.get(args.conversationId);
if (!conv || conv.userId !== user._id) throw new Error("Not found");
await cascadeDeleteConversation(ctx, args.conversationId);
},
});
Junction tables represent many-to-many relationships. Must delete before parent entities.
// ✅ CORRECT: Delete junction first
const junctions = await ctx.db
.query("projectConversations")
.withIndex("by_conversation", (q) =>
q.eq("conversationId", conversationId),
)
.collect();
await Promise.all(junctions.map((j) => ctx.db.delete(j._id)));
Junction tables to handle:
projectConversations - projects ↔ conversationsprojectNotes - projects ↔ notesprojectFiles - projects ↔ filesconversationParticipants - users ↔ conversationsbookmarkTags, snippetTags, noteTags, taskTags - tags ↔ entitiesDelete - Entity only exists for this parent:
attachments - message-specific filestoolCalls - execution results tied to messagesources - citations for specific messagebookmarks - reference to message in conversationshares - share link for conversationNullify - Entity can exist independently:
files - user uploads, can be reused across conversationsmemories - extracted facts, retain even after conversation deleted// Nullify pattern - preserve entity, remove association
await Promise.all([
...files.map((f) => ctx.db.patch(f._id, { conversationId: undefined })),
...memories.map((m) => ctx.db.patch(m._id, { conversationId: undefined })),
]);
When parent-child relationship exists, delete children first to avoid orphans.
Example: canvasDocuments (parent) → canvasHistory (child)
// ❌ WRONG: Delete parent first, orphans children
await ctx.db.delete(doc._id);
const history = await ctx.db
.query("canvasHistory")
.withIndex("by_document", (q) => q.eq("documentId", doc._id))
.collect();
await Promise.all(history.map((h) => ctx.db.delete(h._id))); // Already orphaned
// ✅ CORRECT: Delete children first
for (const doc of canvasDocs) {
const history = await ctx.db
.query("canvasHistory")
.withIndex("by_document", (q) => q.eq("documentId", doc._id))
.collect();
await Promise.all(history.map((h) => ctx.db.delete(h._id)));
await ctx.db.delete(doc._id);
}
Other parent-child relationships:
messages (parent) → attachments, toolCalls, sources (children)knowledgeSources (parent) → knowledgeChunks (children)files (parent) → fileChunks (children)From packages/backend/convex/lib/utils/cascade.ts, 5-phase approach:
export async function cascadeDeleteUserData(
ctx: MutationCtx,
userId: Id<"users">,
): Promise<void> {
// Phase 1: Delete junction tables (many-to-many)
const [bookmarkTags, snippetTags, noteTags, taskTags, ...] = await Promise.all([
ctx.db.query("bookmarkTags").withIndex("by_user", (q) => q.eq("userId", userId)).collect(),
// ...
]);
await Promise.all([
...bookmarkTags.map((r) => ctx.db.delete(r._id)),
// ...
]);
// Phase 2: Delete child records (have FKs to parents deleted later)
const [toolCalls, sources, attachments, ...] = await Promise.all([
ctx.db.query("toolCalls").withIndex("by_user", (q) => q.eq("userId", userId)).collect(),
// ...
]);
await Promise.all([...toolCalls.map((r) => ctx.db.delete(r._id)), ...]);
// Phase 3: Delete parent records (messages, canvasDocuments, knowledgeSources)
const [messages, canvasDocuments, knowledgeSources] = await Promise.all([
ctx.db.query("messages").withIndex("by_user", (q) => q.eq("userId", userId)).collect(),
// ...
]);
await Promise.all([...messages.map((r) => ctx.db.delete(r._id)), ...]);
// Phase 4: Delete main content entities
const [conversations, bookmarks, snippets, notes, tasks, ...] = await Promise.all([
ctx.db.query("conversations").withIndex("by_user", (q) => q.eq("userId", userId)).collect(),
// ...
]);
await Promise.all([...conversations.map((r) => ctx.db.delete(r._id)), ...]);
// Phase 5: Delete user config/metadata
const [userPreferences, userOnboarding, usageRecords, ...] = await Promise.all([
ctx.db.query("userPreferences").withIndex("by_user", (q) => q.eq("userId", userId)).collect(),
// ...
]);
await Promise.all([...userPreferences.map((r) => ctx.db.delete(r._id)), ...]);
}
Usage:
export const deleteMyData = mutation({
args: { confirmationText: v.string() },
handler: async (ctx, { confirmationText }) => {
if (confirmationText !== "DELETE MY DATA") {
throw new Error('Please type "DELETE MY DATA" to confirm');
}
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
const user = await ctx.db
.query("users")
.withIndex("by_clerk_id", (q) => q.eq("clerkId", identity.subject))
.first();
if (!user) throw new Error("User not found");
await cascadeDeleteUserData(ctx, user._id);
return { success: true };
},
});
Parallel queries - Fetch all related records at once:
// ✅ GOOD: Single round-trip, parallel execution
const [bookmarks, shares, files] = await Promise.all([
ctx.db.query("bookmarks").collect(),
ctx.db.query("shares").collect(),
ctx.db.query("files").collect(),
]);
// ❌ BAD: Sequential, 3x latency
const bookmarks = await ctx.db.query("bookmarks").collect();
const shares = await ctx.db.query("shares").collect();
const files = await ctx.db.query("files").collect();
Parallel deletions - Delete independent records simultaneously:
// ✅ GOOD: All deletions in parallel
await Promise.all([
...bookmarks.map((b) => ctx.db.delete(b._id)),
...shares.map((s) => ctx.db.delete(s._id)),
...attachments.map((a) => ctx.db.delete(a._id)),
]);
// ❌ BAD: Sequential deletions
for (const b of bookmarks) await ctx.db.delete(b._id);
for (const s of shares) await ctx.db.delete(s._id);
Index usage - Always use indexed queries for related records:
// ✅ GOOD: Uses index, fast lookup
ctx.db
.query("messages")
.withIndex("by_conversation", (q) => q.eq("conversationId", conversationId))
.collect()
// ❌ BAD: Full table scan
ctx.db
.query("messages")
.filter((q) => q.eq(q.field("conversationId"), conversationId))
.collect()
packages/backend/convex/lib/utils/cascade.ts - Cascade delete utilitiespackages/backend/convex/conversations.ts - Conversation deletionpackages/backend/convex/users.ts - GDPR user data deletionFull cascade delete:
await cascadeDeleteConversation(ctx, conversationId);
Preserve conversation, delete messages only:
await cascadeDeleteConversation(ctx, conversationId, {
deleteMessages: true,
deleteConversation: false,
});
GDPR compliance:
await cascadeDeleteUserData(ctx, userId); // Keeps user account
await ctx.db.delete(userId); // Full account deletion
Don't use nested promises:
// ❌ BAD: Nested promises, hard to reason about
await Promise.all(
bookmarks.map(async (b) => {
const tags = await ctx.db.query("tags").collect();
await ctx.db.delete(b._id);
})
);
// ✅ GOOD: Flat structure, clear dependencies
const bookmarks = await ctx.db.query("bookmarks").collect();
const tags = await ctx.db.query("tags").collect();
await Promise.all([
...bookmarks.map((b) => ctx.db.delete(b._id)),
...tags.map((t) => ctx.db.delete(t._id)),
]);
Don't skip indexes:
// ❌ BAD: Filter scans entire table
const messages = await ctx.db
.query("messages")
.filter((q) => q.eq(q.field("userId"), userId))
.collect();
// ✅ GOOD: Index lookup, O(log n)
const messages = await ctx.db
.query("messages")
.withIndex("by_user", (q) => q.eq("userId", userId))
.collect();
Don't forget junction tables:
// ❌ BAD: Orphans projectConversations records
await ctx.db.delete(conversationId);
// ✅ GOOD: Clean up junction first
const junctions = await ctx.db
.query("projectConversations")
.withIndex("by_conversation", (q) => q.eq("conversationId", conversationId))
.collect();
await Promise.all(junctions.map((j) => ctx.db.delete(j._id)));
await ctx.db.delete(conversationId);
Don't delete parents before children:
// ❌ BAD: Orphans canvasHistory
await ctx.db.delete(doc._id);
// ✅ GOOD: Delete history first
const history = await ctx.db
.query("canvasHistory")
.withIndex("by_document", (q) => q.eq("documentId", doc._id))
.collect();
await Promise.all(history.map((h) => ctx.db.delete(h._id)));
await ctx.db.delete(doc._id);
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience