Convex-only Slice 1: clients talk to Convex, Flue is a private worker (#20)
This commit is contained in:
@@ -3,27 +3,54 @@ import {
|
||||
preparePublicGitSource,
|
||||
type ProjectImportOutcome,
|
||||
type ProjectView,
|
||||
type PublicGitImportResult,
|
||||
type PreparedPublicGitSource,
|
||||
} from "@code/primitives/project";
|
||||
import { v } from "convex/values";
|
||||
import { ConvexError, v } from "convex/values";
|
||||
import { Effect } from "effect";
|
||||
|
||||
import { internal } from "./_generated/api";
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import type { Doc, Id } from "./_generated/dataModel";
|
||||
import { action, internalMutation, query } from "./_generated/server";
|
||||
import { createInitialArtifacts } from "./artifactModel";
|
||||
import { requireAuthUserId, requireCurrentOrganization } from "./authz";
|
||||
import {
|
||||
makeProjectMutationStore,
|
||||
makeProjectQueryStore,
|
||||
persistPublicGitImportTransaction,
|
||||
} from "./projectStore";
|
||||
import { inspectPublicGit } from "./publicGit";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal: persist a public Git import in one transaction
|
||||
// ---------------------------------------------------------------------------
|
||||
const toProjectView = async (
|
||||
ctx: Parameters<typeof requireCurrentOrganization>[0],
|
||||
project: Doc<"projects">
|
||||
): Promise<ProjectView> => {
|
||||
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: {
|
||||
@@ -51,143 +78,106 @@ export const persistPublicGitImport = internalMutation({
|
||||
content: v.string(),
|
||||
})
|
||||
),
|
||||
warnings: v.array(
|
||||
v.object({
|
||||
path: v.string(),
|
||||
message: 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">)
|
||||
const organization = await ctx.db
|
||||
.query("organizations")
|
||||
.withIndex("by_createdBy_and_kind", (q) =>
|
||||
q.eq("createdBy", args.userId).eq("kind", "personal")
|
||||
)
|
||||
.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">,
|
||||
.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,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
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]! };
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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);
|
||||
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))
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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);
|
||||
const { organizationId } = await requireCurrentOrganization(ctx);
|
||||
const project = await ctx.db.get(args.projectId);
|
||||
return project?.organizationId === organizationId
|
||||
? await toProjectView(ctx, project)
|
||||
: null;
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public action: import a supported public Git repository
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const importPublicGit = action({
|
||||
args: { repositoryUrl: v.string() },
|
||||
handler: async (ctx, args): Promise<ProjectImportOutcome> => {
|
||||
@@ -195,50 +185,17 @@ export const importPublicGit = action({
|
||||
const source = await Effect.runPromise(
|
||||
preparePublicGitSource(args.repositoryUrl)
|
||||
);
|
||||
const remote = await inspectPublicGit(source);
|
||||
const validatedRemote = await Effect.runPromise(
|
||||
decodePublicGitImportResult(remote)
|
||||
const remote = await Effect.runPromise(
|
||||
decodePublicGitImportResult(await inspectPublicGit(source))
|
||||
);
|
||||
return await ctx.runMutation(internal.projects.persistPublicGitImport, {
|
||||
remote: {
|
||||
defaultBranch: validatedRemote.defaultBranch,
|
||||
documents: validatedRemote.documents.map((document) => ({
|
||||
...document,
|
||||
})),
|
||||
warnings: validatedRemote.warnings.map((warning) => ({ ...warning })),
|
||||
defaultBranch: remote.defaultBranch,
|
||||
documents: remote.documents.map((document) => ({ ...document })),
|
||||
warnings: remote.warnings.map((warning) => ({ ...warning })),
|
||||
},
|
||||
source,
|
||||
userId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user