187 lines
6.2 KiB
TypeScript
187 lines
6.2 KiB
TypeScript
import { mutation, query } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import { getAuthUserId } from "@convex-dev/auth/server";
|
|
|
|
export const getProjectDataSources = query({
|
|
args: { projectId: v.id("projects") },
|
|
handler: async (ctx, args) => {
|
|
const userId = await getAuthUserId(ctx);
|
|
if (!userId) return [];
|
|
|
|
// Verify project ownership
|
|
const project = await ctx.db.get(args.projectId);
|
|
if (!project || project.userId !== userId) return [];
|
|
|
|
return await ctx.db
|
|
.query("dataSources")
|
|
.filter((q) => q.eq(q.field("projectId"), args.projectId))
|
|
.collect();
|
|
},
|
|
});
|
|
|
|
export const getById = query({
|
|
args: { dataSourceId: v.id("dataSources") },
|
|
handler: async (ctx, args) => {
|
|
const userId = await getAuthUserId(ctx);
|
|
if (!userId) return null;
|
|
|
|
const dataSource = await ctx.db.get(args.dataSourceId);
|
|
if (!dataSource) return null;
|
|
|
|
const project = await ctx.db.get(dataSource.projectId);
|
|
if (!project || project.userId !== userId) return null;
|
|
|
|
return dataSource;
|
|
},
|
|
});
|
|
|
|
export const addDataSource = mutation({
|
|
args: {
|
|
projectId: v.optional(v.id("projects")), // Optional, if not provided, use default
|
|
url: v.string(),
|
|
name: v.string(),
|
|
type: v.literal("website"),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const userId = await getAuthUserId(ctx);
|
|
if (!userId) throw new Error("Unauthorized");
|
|
|
|
let normalizedUrl = args.url.trim();
|
|
if (normalizedUrl.startsWith("manual:")) {
|
|
// Keep manual sources as-is.
|
|
} else if (!normalizedUrl.startsWith("http")) {
|
|
normalizedUrl = `https://${normalizedUrl}`;
|
|
}
|
|
normalizedUrl = normalizedUrl.replace(/\/+$/, "");
|
|
|
|
let projectId = args.projectId;
|
|
|
|
// Use default project if not provided
|
|
if (!projectId) {
|
|
const defaultProject = await ctx.db
|
|
.query("projects")
|
|
.filter((q) => q.and(q.eq(q.field("userId"), userId), q.eq(q.field("isDefault"), true)))
|
|
.first();
|
|
|
|
if (!defaultProject) {
|
|
// Create a default project if none exists (First onboarding)
|
|
projectId = await ctx.db.insert("projects", {
|
|
userId,
|
|
name: "My Project",
|
|
isDefault: true,
|
|
dorkingConfig: { selectedSourceIds: [] },
|
|
});
|
|
} else {
|
|
projectId = defaultProject._id;
|
|
}
|
|
}
|
|
|
|
const existing = await ctx.db
|
|
.query("dataSources")
|
|
.withIndex("by_project_url", (q) =>
|
|
q.eq("projectId", projectId!).eq("url", normalizedUrl)
|
|
)
|
|
.first();
|
|
|
|
const sourceId = existing
|
|
? existing._id
|
|
: await ctx.db.insert("dataSources", {
|
|
projectId: projectId!, // Assert exists
|
|
type: args.type,
|
|
url: normalizedUrl,
|
|
name: args.name,
|
|
analysisStatus: "pending",
|
|
lastAnalyzedAt: undefined,
|
|
lastError: undefined,
|
|
// analysisResults not set initially
|
|
});
|
|
|
|
// Auto-select this source in the project config
|
|
const project = await ctx.db.get(projectId!);
|
|
if (project) {
|
|
const currentSelected = project.dorkingConfig.selectedSourceIds;
|
|
if (!currentSelected.includes(sourceId)) {
|
|
await ctx.db.patch(projectId!, {
|
|
dorkingConfig: { selectedSourceIds: [...currentSelected, sourceId] }
|
|
});
|
|
}
|
|
}
|
|
|
|
return { sourceId, projectId: projectId!, isExisting: Boolean(existing) };
|
|
},
|
|
});
|
|
|
|
export const updateDataSourceStatus = mutation({
|
|
args: {
|
|
dataSourceId: v.id("dataSources"),
|
|
analysisStatus: v.union(
|
|
v.literal("pending"),
|
|
v.literal("completed"),
|
|
v.literal("failed")
|
|
),
|
|
lastError: v.optional(v.string()),
|
|
lastAnalyzedAt: v.optional(v.number()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const userId = await getAuthUserId(ctx);
|
|
if (!userId) throw new Error("Unauthorized");
|
|
|
|
const dataSource = await ctx.db.get(args.dataSourceId);
|
|
if (!dataSource) throw new Error("Data source not found");
|
|
|
|
const project = await ctx.db.get(dataSource.projectId);
|
|
if (!project || project.userId !== userId) {
|
|
throw new Error("Project not found or unauthorized");
|
|
}
|
|
|
|
await ctx.db.patch(args.dataSourceId, {
|
|
analysisStatus: args.analysisStatus,
|
|
lastError: args.lastError,
|
|
lastAnalyzedAt: args.lastAnalyzedAt,
|
|
});
|
|
},
|
|
});
|
|
|
|
export const remove = mutation({
|
|
args: { dataSourceId: v.id("dataSources") },
|
|
handler: async (ctx, args) => {
|
|
const userId = await getAuthUserId(ctx);
|
|
if (!userId) throw new Error("Unauthorized");
|
|
|
|
const dataSource = await ctx.db.get(args.dataSourceId);
|
|
if (!dataSource) throw new Error("Data source not found");
|
|
|
|
const project = await ctx.db.get(dataSource.projectId);
|
|
if (!project || project.userId !== userId) {
|
|
throw new Error("Project not found or unauthorized");
|
|
}
|
|
|
|
const updatedSelected = project.dorkingConfig.selectedSourceIds.filter(
|
|
(id) => id !== args.dataSourceId
|
|
);
|
|
await ctx.db.patch(project._id, {
|
|
dorkingConfig: { selectedSourceIds: updatedSelected },
|
|
});
|
|
|
|
const analyses = await ctx.db
|
|
.query("analyses")
|
|
.withIndex("by_dataSource_createdAt", (q) =>
|
|
q.eq("dataSourceId", args.dataSourceId)
|
|
)
|
|
.collect();
|
|
for (const analysis of analyses) {
|
|
await ctx.db.delete(analysis._id);
|
|
}
|
|
|
|
const analysisJobs = await ctx.db
|
|
.query("analysisJobs")
|
|
.filter((q) => q.eq(q.field("dataSourceId"), args.dataSourceId))
|
|
.collect();
|
|
for (const job of analysisJobs) {
|
|
await ctx.db.delete(job._id);
|
|
}
|
|
|
|
await ctx.db.delete(args.dataSourceId);
|
|
},
|
|
});
|