feat: add Convex project backend with generic storage and org-scoped authz
Schema:
- Cut projects table to {organizationId, name, description, createdAt, updatedAt}
- Add projectSources table with git source metadata and normalized URL index
- Add projectContextDocuments table with six canonical kinds, origin, revision
- Add by_organizationId index for project enumeration
- Remove all GitHub-specific fields (ownerId, provider, repoOwner, etc.)
Backend:
- Add requireCurrentOrganization and requireProjectMember authz helpers
- Migrate signals.ts authorizeProject to organizationId invariant check
- Update artifactModel: 8 operational artifacts (removed project/business/design.md)
- Add projectStore.ts with query/mutation/action store adapters
- Rewrite projects.ts to thin generic application calls
- Update projectIssues/projectArtifacts to use requireProjectMember
- Update projectEvents.test.ts and signals.test.ts for new project creation
All 33 backend tests pass.
This commit is contained in:
@@ -2,20 +2,7 @@ import { ConvexError, v } from "convex/values";
|
||||
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import type { MutationCtx } from "./_generated/server";
|
||||
import { requireAuthUserId } from "./authz"
|
||||
|
||||
const requireProjectOwner = async (
|
||||
ctx: MutationCtx,
|
||||
projectId: Id<"projects">,
|
||||
ownerId: string
|
||||
) => {
|
||||
const project = await ctx.db.get("projects", projectId);
|
||||
if (!project || project.ownerId !== ownerId) {
|
||||
throw new ConvexError("Project not found");
|
||||
}
|
||||
return project;
|
||||
};
|
||||
import { requireProjectMember } from "./authz";
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
@@ -24,8 +11,7 @@ export const create = mutation({
|
||||
title: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const ownerId = await requireAuthUserId(ctx);
|
||||
await requireProjectOwner(ctx, args.projectId, ownerId);
|
||||
await requireProjectMember(ctx, args.projectId);
|
||||
const title = args.title.trim();
|
||||
const body = args.body.trim();
|
||||
if (title.length < 3 || title.length > 160) {
|
||||
@@ -81,12 +67,11 @@ export const create = mutation({
|
||||
export const begin = mutation({
|
||||
args: { issueId: v.id("projectIssues") },
|
||||
handler: async (ctx, args) => {
|
||||
const ownerId = await requireAuthUserId(ctx);
|
||||
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||
if (!issue) {
|
||||
throw new ConvexError("Issue not found");
|
||||
}
|
||||
await requireProjectOwner(ctx, issue.projectId, ownerId);
|
||||
await requireProjectMember(ctx, issue.projectId);
|
||||
if (issue.status === "queued" || issue.status === "working") {
|
||||
return issue.status;
|
||||
}
|
||||
@@ -109,12 +94,11 @@ export const begin = mutation({
|
||||
export const markDispatchFailed = mutation({
|
||||
args: { error: v.string(), issueId: v.id("projectIssues") },
|
||||
handler: async (ctx, args) => {
|
||||
const ownerId = await requireAuthUserId(ctx);
|
||||
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||
if (!issue) {
|
||||
throw new ConvexError("Issue not found");
|
||||
}
|
||||
await requireProjectOwner(ctx, issue.projectId, ownerId);
|
||||
await requireProjectMember(ctx, issue.projectId);
|
||||
const timestamp = Date.now();
|
||||
await ctx.db.patch("projectIssues", issue._id, {
|
||||
status: "failed",
|
||||
@@ -133,9 +117,9 @@ export const markDispatchFailed = mutation({
|
||||
export const list = query({
|
||||
args: { projectId: v.id("projects") },
|
||||
handler: async (ctx, args) => {
|
||||
const ownerId = await requireAuthUserId(ctx);
|
||||
const project = await ctx.db.get("projects", args.projectId);
|
||||
if (!project || project.ownerId !== ownerId) {
|
||||
try {
|
||||
await requireProjectMember(ctx, args.projectId);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return await ctx.db
|
||||
|
||||
Reference in New Issue
Block a user