Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 137 additions & 4 deletions convex/importData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const importProject = internalMutation({
args: {
oldId: v.string(), // Original PostgreSQL UUID
name: v.string(),
userId: v.id("users"), // Changed from v.string() to v.id("users")
userId: v.id("users"), // References users table
framework: v.union(
v.literal("NEXTJS"),
v.literal("ANGULAR"),
Expand Down Expand Up @@ -202,7 +202,7 @@ export const importAttachment = internalMutation({
export const importUsage = internalMutation({
args: {
key: v.string(), // Original key like "rlflx:user_XXX"
userId: v.id("users"), // Changed from v.string() to v.id("users")
userId: v.id("users"), // References users table
points: v.number(),
expire: v.optional(v.string()), // ISO date string
},
Expand Down Expand Up @@ -275,12 +275,125 @@ export const clearAllData = internalMutation({
},
});

/**
* Clean up orphaned projects and related data (projects with invalid userId references)
* This fixes schema validation errors when users table is missing references
*/
export const cleanupOrphanedProjects = internalMutation({
args: {},
handler: async (ctx) => {
const allUsers = await ctx.db.query("users").collect();
const validUserIds = new Set(allUsers.map((u) => u._id as string));

let cleanedProjectsCount = 0;
let cleanedUsageCount = 0;
let cleanedOAuthCount = 0;
let cleanedImportsCount = 0;
const orphanedProjectIds: string[] = [];

// 1. Find all orphaned projects
const allProjects = await ctx.db.query("projects").collect();
for (const project of allProjects) {
if (!validUserIds.has(project.userId as any)) {
orphanedProjectIds.push(project._id);
}
}

// Delete all data related to orphaned projects in reverse dependency order
for (const projectId of orphanedProjectIds) {
// Delete attachments for messages in this project
const messages = await ctx.db
.query("messages")
.withIndex("by_projectId", (q) => q.eq("projectId", projectId as any))
.collect();

for (const message of messages) {
const attachments = await ctx.db
.query("attachments")
.withIndex("by_messageId", (q) => q.eq("messageId", message._id))
.collect();

for (const attachment of attachments) {
await ctx.db.delete(attachment._id);
}

// Delete fragments for this message
const fragments = await ctx.db
.query("fragments")
.withIndex("by_messageId", (q) => q.eq("messageId", message._id))
.collect();

for (const fragment of fragments) {
await ctx.db.delete(fragment._id);
}

// Delete the message itself
await ctx.db.delete(message._id);
}

// Delete fragment drafts for this project
const drafts = await ctx.db
.query("fragmentDrafts")
.withIndex("by_projectId", (q) => q.eq("projectId", projectId as any))
.collect();

for (const draft of drafts) {
await ctx.db.delete(draft._id);
}

// Delete the project itself
await ctx.db.delete(projectId as any);
cleanedProjectsCount++;
}

// 2. Clean up orphaned usage records
const allUsage = await ctx.db.query("usage").collect();
for (const usage of allUsage) {
if (!validUserIds.has(usage.userId as any)) {
await ctx.db.delete(usage._id);
cleanedUsageCount++;
}
}

// 3. Clean up orphaned oauthConnections
const allOAuth = await ctx.db.query("oauthConnections").collect();
for (const oauth of allOAuth) {
if (!validUserIds.has(oauth.userId as any)) {
await ctx.db.delete(oauth._id);
cleanedOAuthCount++;
}
}

// 4. Clean up orphaned imports
const allImports = await ctx.db.query("imports").collect();
for (const importRecord of allImports) {
if (!validUserIds.has(importRecord.userId as any)) {
await ctx.db.delete(importRecord._id);
cleanedImportsCount++;
}
}

const totalCleaned = cleanedProjectsCount + cleanedUsageCount + cleanedOAuthCount + cleanedImportsCount;

return {
success: true,
message: `Cleaned up ${totalCleaned} orphaned records (${cleanedProjectsCount} projects, ${cleanedUsageCount} usage, ${cleanedOAuthCount} oauth, ${cleanedImportsCount} imports)`,
cleanedProjectCount: cleanedProjectsCount,
cleanedUsageCount,
cleanedOAuthCount,
cleanedImportsCount,
totalCleaned,
orphanedProjectIds,
};
},
});

// Public action wrappers for HTTP client access
export const importProjectAction = action({
args: {
oldId: v.string(),
name: v.string(),
userId: v.id("users"), // Changed from v.string() to v.id("users")
userId: v.id("users"), // References users table
framework: v.union(
v.literal("NEXTJS"),
v.literal("ANGULAR"),
Expand Down Expand Up @@ -382,11 +495,31 @@ export const importAttachmentAction = action({
export const importUsageAction = action({
args: {
key: v.string(),
userId: v.id("users"), // Changed from v.string() to v.id("users")
userId: v.id("users"), // References users table
points: v.number(),
expire: v.optional(v.string()),
},
handler: async (ctx, args): Promise<{ userId: string; newId: any }> => {
return await ctx.runMutation(internal.importData.importUsage, args);
},
});

/**
* Public action to clean up ALL orphaned data (admin function)
* Removes all projects, usage, oauthConnections, imports with invalid userId references
*/
export const cleanupOrphanedProjectsAction = action({
args: {},
handler: async (ctx): Promise<{
success: boolean;
message: string;
cleanedProjectCount: number;
cleanedUsageCount: number;
cleanedOAuthCount: number;
cleanedImportsCount: number;
totalCleaned: number;
orphanedProjectIds: string[];
}> => {
return await ctx.runMutation(internal.importData.cleanupOrphanedProjects);
},
});
8 changes: 4 additions & 4 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default defineSchema({
// Projects table
projects: defineTable({
name: v.string(),
userId: v.id("users"), // Changed to reference users table
userId: v.id("users"), // References users table
framework: frameworkEnum,
modelPreference: v.optional(v.string()), // User's preferred AI model (e.g., "auto", "anthropic/claude-haiku-4.5", "openai/gpt-4o")
createdAt: v.optional(v.number()), // timestamp
Expand Down Expand Up @@ -175,7 +175,7 @@ export default defineSchema({

// OAuth Connections table - for storing encrypted OAuth tokens
oauthConnections: defineTable({
userId: v.id("users"), // Changed to reference users table
userId: v.id("users"), // References users table
provider: oauthProviderEnum,
accessToken: v.string(), // Encrypted token
refreshToken: v.optional(v.string()),
Expand All @@ -190,7 +190,7 @@ export default defineSchema({

// Imports table - tracking import history and status
imports: defineTable({
userId: v.id("users"), // Changed to reference users table
userId: v.id("users"), // References users table
projectId: v.id("projects"),
messageId: v.optional(v.id("messages")),
source: importSourceEnum,
Expand All @@ -209,7 +209,7 @@ export default defineSchema({

// Usage table - rate limiting and credit tracking
usage: defineTable({
userId: v.id("users"), // Changed to reference users table
userId: v.id("users"), // References users table
points: v.number(), // Remaining credits
expire: v.optional(v.number()), // Expiration timestamp
planType: v.optional(v.union(v.literal("free"), v.literal("pro"))), // Track plan type
Expand Down
2 changes: 1 addition & 1 deletion env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ CONVEX_DEPLOYMENT=""

# Better Auth
BETTER_AUTH_SECRET="" # Generate with: openssl rand -base64 32
BETTER_AUTH_URL="http://localhost:3000" # Use production URL outside of local dev
BETTER_AUTH_URL="http://localhost:3000" # IMPORTANT: Must match your deployment domain (e.g., https://zapdev.link)
SESSION_COOKIE_PREFIX="zapdev" # Optional Better Auth cookie prefix override
SESSION_COOKIE_NAME="" # Optional full cookie name override (defaults to "<prefix>.session_token")

Expand Down
Loading
Loading