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:
@@ -7,9 +7,6 @@ import { internal } from "./_generated/api";
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import schema from "./schema";
|
||||
|
||||
// `import.meta.glob` is provided by the Vitest/Vite test runner at runtime; it
|
||||
// has no type definition under the Convex tsconfig (which scopes `types` to
|
||||
// node), so declare the minimal shape the test relies on.
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
readonly glob: (pattern: string) => Record<string, () => Promise<unknown>>;
|
||||
@@ -19,26 +16,45 @@ declare global {
|
||||
const modules = import.meta.glob("./**/*.ts");
|
||||
const api = anyApi;
|
||||
|
||||
// `agentWorkspace` gates agent-controlled mutations on `env.FLUE_DB_TOKEN`.
|
||||
// Vitest loads the repo `.env`, so resolve the real value from the env module
|
||||
// rather than hardcoding a constant that would mismatch the loaded secret.
|
||||
const FLUE_DB_TOKEN = env.FLUE_DB_TOKEN;
|
||||
|
||||
const ID_A = "https://convex.test|user-a";
|
||||
const identityA = { tokenIdentifier: ID_A };
|
||||
|
||||
const repository = {
|
||||
defaultBranch: "main",
|
||||
id: 12345,
|
||||
name: "zopu",
|
||||
owner: "puter",
|
||||
const SOURCE = {
|
||||
host: "github.com",
|
||||
projectName: "zopu",
|
||||
repositoryPath: "puter/zopu",
|
||||
normalizedUrl: "https://github.com/puter/zopu",
|
||||
url: "https://github.com/puter/zopu",
|
||||
};
|
||||
|
||||
// Read every projectEvents row for a project via the same indexes the control
|
||||
// plane writes through, returning them oldest-first. Reads MUST go through the
|
||||
// same test instance that performed the writes; a fresh `convexTest()` is an
|
||||
// isolated in-memory backend with no shared state.
|
||||
const REMOTE = {
|
||||
defaultBranch: "main",
|
||||
documents: [
|
||||
{
|
||||
kind: "readme" as const,
|
||||
path: "README.md",
|
||||
content: "# zopu\n\nRepository: https://github.com/puter/zopu\n",
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
const createTestProject = async (t: TestConvex<typeof schema>): Promise<Id<"projects">> => {
|
||||
// Ensure personal organization for the user
|
||||
await t.withIdentity(identityA).mutation(api.organizations.ensurePersonalOrganization);
|
||||
// Persist the public Git import
|
||||
const outcome = await t
|
||||
.withIdentity(identityA)
|
||||
.mutation(internal.projects.persistPublicGitImport, {
|
||||
userId: ID_A,
|
||||
source: SOURCE,
|
||||
remote: REMOTE,
|
||||
});
|
||||
return outcome.id as unknown as Id<"projects">;
|
||||
};
|
||||
|
||||
const eventsForProject = async (
|
||||
t: TestConvex<typeof schema>,
|
||||
projectId: Id<"projects">
|
||||
@@ -56,10 +72,7 @@ const eventsForProject = async (
|
||||
describe("projectEvents", () => {
|
||||
test("project connect emits a project.connected event", async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const projectId = await t.mutation(internal.projects.storeGitHubProject, {
|
||||
ownerId: ID_A,
|
||||
repository,
|
||||
});
|
||||
const projectId = await createTestProject(t);
|
||||
|
||||
const events = await eventsForProject(t, projectId);
|
||||
expect(events).toHaveLength(1);
|
||||
@@ -67,18 +80,11 @@ describe("projectEvents", () => {
|
||||
expect(events[0].projectId).toBe(projectId);
|
||||
expect(events[0].issueId).toBeUndefined();
|
||||
expect(events[0].runId).toBeUndefined();
|
||||
expect(events[0].data).toMatchObject({
|
||||
provider: "github",
|
||||
repository: repository.url,
|
||||
});
|
||||
});
|
||||
|
||||
test("issue create and begin emit issue.created then issue.queued events", async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const projectId = await t.mutation(internal.projects.storeGitHubProject, {
|
||||
ownerId: ID_A,
|
||||
repository,
|
||||
});
|
||||
const projectId = await createTestProject(t);
|
||||
|
||||
const issueId = await t
|
||||
.withIdentity(identityA)
|
||||
@@ -119,10 +125,7 @@ describe("projectEvents", () => {
|
||||
|
||||
test("artifact update emits an artifact.updated event", async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const projectId = await t.mutation(internal.projects.storeGitHubProject, {
|
||||
ownerId: ID_A,
|
||||
repository,
|
||||
});
|
||||
const projectId = await createTestProject(t);
|
||||
const issueId = await t
|
||||
.withIdentity(identityA)
|
||||
.mutation(api.projectIssues.create, {
|
||||
@@ -149,10 +152,7 @@ describe("projectEvents", () => {
|
||||
|
||||
test("agent status emits an agent.<status> event linked to the run", async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const projectId = await t.mutation(internal.projects.storeGitHubProject, {
|
||||
ownerId: ID_A,
|
||||
repository,
|
||||
});
|
||||
const projectId = await createTestProject(t);
|
||||
const issueId = await t
|
||||
.withIdentity(identityA)
|
||||
.mutation(api.projectIssues.create, {
|
||||
@@ -192,10 +192,7 @@ describe("projectEvents", () => {
|
||||
|
||||
test("wrong agent token is rejected before any event is written", async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const projectId = await t.mutation(internal.projects.storeGitHubProject, {
|
||||
ownerId: ID_A,
|
||||
repository,
|
||||
});
|
||||
const projectId = await createTestProject(t);
|
||||
const issueId = await t
|
||||
.withIdentity(identityA)
|
||||
.mutation(api.projectIssues.create, {
|
||||
|
||||
Reference in New Issue
Block a user