import { decodePublicGitImportResult, preparePublicGitSource, type ProjectImportOutcome, type ProjectView, } from "@code/primitives/project"; import { ConvexError, v } from "convex/values"; import { Effect } from "effect"; import { internal } from "./_generated/api"; import type { Doc, Id } from "./_generated/dataModel"; import { action, internalMutation, query } from "./_generated/server"; import { requireAuthUserId, requireCurrentOrganization } from "./authz"; import { inspectPublicGit } from "./publicGit"; const toProjectView = async ( ctx: Parameters[0], project: Doc<"projects"> ): Promise => { const documents = await ctx.db .query("projectContextDocuments") .withIndex("by_projectId_and_path", (q) => q.eq("projectId", project._id)) .collect(); const view = { contextDocuments: documents.map((document) => ({ content: document.content, kind: document.kind, origin: document.origin, path: document.path, revision: 1, sourceUrl: document.sourceUrl, })), createdAt: project.createdAt, id: String(project._id), name: project.name, organizationId: String(project.organizationId), sources: [ { createdAt: project.createdAt, defaultBranch: project.defaultBranch, host: project.sourceHost, kind: "git", normalizedUrl: project.normalizedSourceUrl, projectId: String(project._id), repositoryPath: project.repositoryPath, updatedAt: project.updatedAt, url: project.sourceUrl, }, ], updatedAt: project.updatedAt, }; return view as unknown as ProjectView; }; export const persistPublicGitImport = internalMutation({ args: { userId: v.string(), source: v.object({ host: v.string(), projectName: v.string(), repositoryPath: v.string(), normalizedUrl: v.string(), url: v.string(), }), remote: v.object({ defaultBranch: v.optional(v.string()), documents: v.array( v.object({ kind: v.union( v.literal("readme"), v.literal("agents"), v.literal("product"), v.literal("business"), v.literal("design"), v.literal("tech") ), path: v.string(), content: v.string(), }) ), warnings: v.array(v.object({ path: v.string(), message: v.string() })), }), }, handler: async (ctx, args): Promise => { const organization = await ctx.db .query("organizations") .withIndex("by_createdBy_and_kind", (q) => q.eq("createdBy", args.userId).eq("kind", "personal") ) .unique(); if (!organization) { throw new ConvexError("Organization not found"); } const timestamp = Date.now(); const existing = await ctx.db .query("projects") .withIndex("by_organizationId_and_normalizedSourceUrl", (q) => q .eq("organizationId", organization._id) .eq("normalizedSourceUrl", args.source.normalizedUrl) ) .unique(); let projectId: Id<"projects">; if (existing) { projectId = existing._id; await ctx.db.patch(projectId, { defaultBranch: args.remote.defaultBranch, name: args.source.projectName, sourceHost: args.source.host, sourceUrl: args.source.url, repositoryPath: args.source.repositoryPath, updatedAt: timestamp, }); const oldDocuments = await ctx.db .query("projectContextDocuments") .withIndex("by_projectId_and_path", (q) => q.eq("projectId", projectId)) .collect(); for (const document of oldDocuments) { await ctx.db.delete(document._id); } } else { projectId = await ctx.db.insert("projects", { createdAt: timestamp, defaultBranch: args.remote.defaultBranch, name: args.source.projectName, normalizedSourceUrl: args.source.normalizedUrl, organizationId: organization._id, repositoryPath: args.source.repositoryPath, sourceHost: args.source.host, sourceUrl: args.source.url, updatedAt: timestamp, }); } for (const document of args.remote.documents) { await ctx.db.insert("projectContextDocuments", { ...document, createdAt: timestamp, origin: "repository", projectId, sourceUrl: args.source.url, updatedAt: timestamp, }); } const project = await ctx.db.get(projectId); if (!project) { throw new Error("Project could not be read after import"); } const view = await toProjectView(ctx, project); return { ...view, source: view.sources[0]! }; }, }); export const list = query({ args: {}, handler: async (ctx): Promise => { const { organizationId } = await requireCurrentOrganization(ctx); const projects = await ctx.db .query("projects") .withIndex("by_organizationId_and_createdAt", (q) => q.eq("organizationId", organizationId) ) .order("desc") .take(50); return await Promise.all( projects.map((project) => toProjectView(ctx, project)) ); }, }); export const get = query({ args: { projectId: v.id("projects") }, handler: async (ctx, args): Promise => { const { organizationId } = await requireCurrentOrganization(ctx); const project = await ctx.db.get(args.projectId); return project?.organizationId === organizationId ? await toProjectView(ctx, project) : null; }, }); export const importPublicGit = action({ args: { repositoryUrl: v.string() }, handler: async (ctx, args): Promise => { const userId = await requireAuthUserId(ctx); const source = await Effect.runPromise( preparePublicGitSource(args.repositoryUrl) ); const remote = await Effect.runPromise( decodePublicGitImportResult(await inspectPublicGit(source)) ); return await ctx.runMutation(internal.projects.persistPublicGitImport, { remote: { defaultBranch: remote.defaultBranch, documents: remote.documents.map((document) => ({ ...document })), warnings: remote.warnings.map((warning) => ({ ...warning })), }, source, userId, }); }, });