163 lines
4.7 KiB
TypeScript
163 lines
4.7 KiB
TypeScript
import {
|
|
decodeProjectIssueRequest,
|
|
ProjectIssueDispatchInput,
|
|
ProjectIssueRequestError,
|
|
ProjectIssueRequestResult,
|
|
ProjectIssueValidationError,
|
|
} from "@code/primitives/project-issue";
|
|
import type { ProjectIssueRequest } from "@code/primitives/project-issue";
|
|
import { dispatch } from "@flue/runtime";
|
|
import type { ConvexHttpClient } from "convex/browser";
|
|
import { makeFunctionReference } from "convex/server";
|
|
import { Effect, Schema } from "effect";
|
|
import type { Context } from "hono";
|
|
|
|
import { createAuthenticatedClient, extractBearerToken } from "./auth";
|
|
|
|
interface ProjectIssueCreateArgs extends Record<string, unknown> {
|
|
readonly body: string;
|
|
readonly projectId: string;
|
|
readonly title: string;
|
|
}
|
|
|
|
const createIssue = makeFunctionReference<
|
|
"mutation",
|
|
ProjectIssueCreateArgs,
|
|
string
|
|
>("projectIssues:create");
|
|
|
|
const createIssueFromSignal = makeFunctionReference<
|
|
"mutation",
|
|
{ readonly signalId: string },
|
|
{ readonly issueId: string; readonly projectId: string }
|
|
>("projectIssues:createFromSignal");
|
|
|
|
const beginIssue = makeFunctionReference<
|
|
"mutation",
|
|
{ readonly issueId: string },
|
|
"queued" | "working"
|
|
>("projectIssues:begin");
|
|
|
|
const markDispatchFailed = makeFunctionReference<
|
|
"mutation",
|
|
{ readonly error: string; readonly issueId: string },
|
|
null
|
|
>("projectIssues:markDispatchFailed");
|
|
|
|
const invalidRequest = (message: string) =>
|
|
Response.json({ error: message }, { status: 400 });
|
|
|
|
const knownAuthorizationFailure = (message: string): boolean =>
|
|
/authentication required|membership required|project not found|signal not found|not project-scoped/iu.test(
|
|
message
|
|
);
|
|
|
|
const errorMessage = (error: unknown): string =>
|
|
error instanceof Error ? error.message : String(error);
|
|
|
|
const decodeRequest = async (input: unknown): Promise<ProjectIssueRequest> => {
|
|
try {
|
|
return await Effect.runPromise(decodeProjectIssueRequest(input));
|
|
} catch (error) {
|
|
if (
|
|
error instanceof ProjectIssueRequestError ||
|
|
error instanceof ProjectIssueValidationError
|
|
) {
|
|
throw invalidRequest(
|
|
error instanceof Error ? error.message : "Invalid project request"
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const createIssueForRequest = async (
|
|
client: ConvexHttpClient,
|
|
request: Awaited<ReturnType<typeof decodeRequest>>
|
|
): Promise<{ readonly issueId: string; readonly projectId: string }> => {
|
|
if (request.kind === "signal") {
|
|
return client.mutation(createIssueFromSignal, {
|
|
signalId: request.signalId,
|
|
});
|
|
}
|
|
const issueId = await client.mutation(createIssue, {
|
|
body: request.body,
|
|
projectId: request.projectId,
|
|
title: request.title,
|
|
});
|
|
return { issueId, projectId: request.projectId };
|
|
};
|
|
|
|
export const projectRequestRoute = async (c: Context): Promise<Response> => {
|
|
const accessToken = extractBearerToken(c.req.raw);
|
|
if (!accessToken) {
|
|
return c.json({ error: "Unauthorized" }, 401);
|
|
}
|
|
|
|
let input: unknown;
|
|
try {
|
|
input = await c.req.json();
|
|
} catch {
|
|
return invalidRequest("Request body must be valid JSON");
|
|
}
|
|
|
|
let request: Awaited<ReturnType<typeof decodeRequest>>;
|
|
try {
|
|
request = await decodeRequest(input);
|
|
} catch (error) {
|
|
if (error instanceof Response) {
|
|
return error;
|
|
}
|
|
return c.json({ error: "Invalid project request" }, 400);
|
|
}
|
|
|
|
const client = createAuthenticatedClient(accessToken);
|
|
let issue:
|
|
| { readonly issueId: string; readonly projectId: string }
|
|
| undefined;
|
|
try {
|
|
issue = await createIssueForRequest(client, request);
|
|
const status = await client.mutation(beginIssue, {
|
|
issueId: issue.issueId,
|
|
});
|
|
const dispatchInput = Schema.decodeUnknownSync(ProjectIssueDispatchInput)({
|
|
issueId: issue.issueId,
|
|
kind: "project.issue.started",
|
|
projectId: issue.projectId,
|
|
});
|
|
const receipt = await dispatch({
|
|
agent: "project-manager",
|
|
id: issue.issueId,
|
|
input: dispatchInput,
|
|
});
|
|
const result = Schema.decodeUnknownSync(ProjectIssueRequestResult)({
|
|
acceptedAt: receipt.acceptedAt,
|
|
dispatchId: receipt.dispatchId,
|
|
issueId: issue.issueId,
|
|
projectId: issue.projectId,
|
|
status,
|
|
});
|
|
return c.json(result, 202);
|
|
} catch (error) {
|
|
const message = errorMessage(error);
|
|
if (issue) {
|
|
try {
|
|
await client.mutation(markDispatchFailed, {
|
|
error: message,
|
|
issueId: issue.issueId,
|
|
});
|
|
} catch {
|
|
// Preserve the original request failure; the issue remains inspectable.
|
|
}
|
|
}
|
|
return c.json(
|
|
{
|
|
error: knownAuthorizationFailure(message)
|
|
? "Project request is not authorized"
|
|
: "Project request could not be dispatched",
|
|
},
|
|
knownAuthorizationFailure(message) ? 403 : 502
|
|
);
|
|
}
|
|
};
|