Files
zopu-code/packages/backend/convex/projects.ts
2026-07-24 03:00:08 +05:30

229 lines
6.9 KiB
TypeScript

import {
preparePublicGitSource,
type ProjectImportOutcome,
type ProjectView,
type PublicGitImportResult,
type PreparedPublicGitSource,
} from "@code/primitives/project";
import { v } from "convex/values";
import { Effect } from "effect";
import type { Id } from "./_generated/dataModel";
import { action, internalMutation, query } from "./_generated/server";
import { createInitialArtifacts } from "./artifactModel";
import { requireCurrentOrganization } from "./authz";
import {
makeProjectMutationStore,
makeProjectQueryStore,
persistPublicGitImportTransaction,
} from "./projectStore";
// ---------------------------------------------------------------------------
// Internal: persist a public Git import in one transaction
// ---------------------------------------------------------------------------
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<ProjectImportOutcome> => {
const result = await persistPublicGitImportTransaction(
ctx,
args.userId,
args.source as PreparedPublicGitSource,
args.remote as PublicGitImportResult
);
// Seed operational artifacts on first creation (detected by checking
// existing artifacts)
const existingArtifacts = await ctx.db
.query("projectArtifacts")
.withIndex("by_project", (q) =>
q.eq("projectId", result.id as unknown as Id<"projects">)
)
.first();
if (!existingArtifacts) {
const now = Date.now();
const artifacts = createInitialArtifacts();
await Promise.all(
artifacts.map(({ content, path }) =>
ctx.db.insert("projectArtifacts", {
content,
createdAt: now,
path,
projectId: result.id as unknown as Id<"projects">,
revision: 1,
updatedAt: now,
})
)
);
await ctx.db.insert("projectEvents", {
createdAt: now,
data: { host: result.source.host, url: result.source.url },
kind: "project.connected",
projectId: result.id as unknown as Id<"projects">,
});
}
return result;
},
});
// ---------------------------------------------------------------------------
// Internal: put a context document
// ---------------------------------------------------------------------------
export const putContextDocument = internalMutation({
args: {
userId: v.string(),
projectId: v.id("projects"),
write: v.union(
v.object({
_tag: v.literal("Repository"),
content: v.string(),
kind: v.union(
v.literal("readme"),
v.literal("agents"),
v.literal("product"),
v.literal("business"),
v.literal("design"),
v.literal("tech")
),
sourceUrl: v.string(),
}),
v.object({
_tag: v.literal("PublicTextUrl"),
content: v.string(),
kind: v.union(
v.literal("readme"),
v.literal("agents"),
v.literal("product"),
v.literal("business"),
v.literal("design"),
v.literal("tech")
),
sourceUrl: v.string(),
}),
v.object({
_tag: v.literal("UserText"),
content: v.string(),
kind: v.union(
v.literal("readme"),
v.literal("agents"),
v.literal("product"),
v.literal("business"),
v.literal("design"),
v.literal("tech")
),
origin: v.union(v.literal("paste"), v.literal("upload")),
})
),
},
handler: async (ctx, args) => {
const store = makeProjectMutationStore(ctx);
return store.putContext(args.userId, args.projectId, args.write as never);
},
});
// ---------------------------------------------------------------------------
// Public query: list projects
// ---------------------------------------------------------------------------
export const list = query({
args: {},
handler: async (ctx): Promise<ProjectView[]> => {
const { userId } = await requireCurrentOrganization(ctx);
const store = makeProjectQueryStore(ctx);
return store.listProjects(userId);
},
});
// ---------------------------------------------------------------------------
// Public query: get a project
// ---------------------------------------------------------------------------
export const get = query({
args: { projectId: v.id("projects") },
handler: async (ctx, args): Promise<ProjectView | null> => {
const { userId } = await requireCurrentOrganization(ctx);
const store = makeProjectQueryStore(ctx);
return store.getProject(userId, args.projectId);
},
});
// ---------------------------------------------------------------------------
// Public action: import a public Git repository
// (daemon wiring added in the Daemon phase)
// ---------------------------------------------------------------------------
export const importPublicGit = action({
args: { repositoryUrl: v.string() },
handler: async (_ctx, args): Promise<ProjectImportOutcome> => {
// Normalize through the domain layer to validate the URL before the
// daemon adapter is wired.
await Effect.runPromise(preparePublicGitSource(args.repositoryUrl));
// The daemon PublicGit adapter is wired in the Daemon phase.
throw new Error("PublicGit import is not yet wired to the daemon adapter");
},
});
// ---------------------------------------------------------------------------
// Public mutation: put context text (paste/upload)
// ---------------------------------------------------------------------------
export const putText = internalMutation({
args: {
projectId: v.id("projects"),
kind: v.union(
v.literal("readme"),
v.literal("agents"),
v.literal("product"),
v.literal("business"),
v.literal("design"),
v.literal("tech")
),
content: v.string(),
origin: v.union(v.literal("paste"), v.literal("upload")),
},
handler: async (ctx, args) => {
const { userId } = await requireCurrentOrganization(ctx);
const store = makeProjectMutationStore(ctx);
return store.putContext(userId, args.projectId, {
_tag: "UserText" as const,
content: args.content,
kind: args.kind,
origin: args.origin,
});
},
});