Add beads: Skill v4 epic (1bg), Quality gates (n1h), Brainstorm epics (jq5, 2e6), memory nodes
This commit is contained in:
parent
80c3d06284
commit
835018c183
6 changed files with 2189 additions and 136 deletions
File diff suppressed because it is too large
Load diff
125
src/app/api/beads/[id]/comments/[commentId]/comment-mutation.ts
Normal file
125
src/app/api/beads/[id]/comments/[commentId]/comment-mutation.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { deleteCommentViaDolt, updateCommentViaDolt } from '../../../../../../lib/read-interactions';
|
||||||
|
|
||||||
|
export interface RouteParams {
|
||||||
|
id: string;
|
||||||
|
commentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PatchBody {
|
||||||
|
projectRoot?: unknown;
|
||||||
|
text?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommentMutationDeps {
|
||||||
|
updateComment: (projectRoot: string, commentId: number, text: string) => Promise<boolean>;
|
||||||
|
deleteComment: (projectRoot: string, commentId: number) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultDeps: CommentMutationDeps = {
|
||||||
|
updateComment: updateCommentViaDolt,
|
||||||
|
deleteComment: deleteCommentViaDolt,
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseCommentId(raw: string): number {
|
||||||
|
const parsed = Number(raw);
|
||||||
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||||
|
throw new Error('commentId must be a positive integer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProjectRoot(raw: unknown): string {
|
||||||
|
if (typeof raw !== 'string' || !raw.trim()) {
|
||||||
|
throw new Error('projectRoot is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCommentText(raw: unknown): string {
|
||||||
|
if (typeof raw !== 'string' || !raw.trim()) {
|
||||||
|
throw new Error('text is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function badRequest(message: string): NextResponse {
|
||||||
|
return NextResponse.json({ ok: false, error: { message } }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function notFound(message: string): NextResponse {
|
||||||
|
return NextResponse.json({ ok: false, error: { message } }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function serverError(message: string): NextResponse {
|
||||||
|
return NextResponse.json({ ok: false, error: { message } }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePatchCommentRequest(
|
||||||
|
request: Request,
|
||||||
|
params: RouteParams,
|
||||||
|
deps: CommentMutationDeps = defaultDeps,
|
||||||
|
): Promise<NextResponse> {
|
||||||
|
let body: PatchBody;
|
||||||
|
try {
|
||||||
|
body = (await request.json()) as PatchBody;
|
||||||
|
} catch {
|
||||||
|
return badRequest('Invalid JSON body.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let projectRoot: string;
|
||||||
|
let commentId: number;
|
||||||
|
let text: string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
projectRoot = parseProjectRoot(body.projectRoot);
|
||||||
|
commentId = parseCommentId(params.commentId);
|
||||||
|
text = parseCommentText(body.text);
|
||||||
|
} catch (error) {
|
||||||
|
return badRequest(error instanceof Error ? error.message : 'Invalid request.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await deps.updateComment(projectRoot, commentId, text);
|
||||||
|
if (!updated) {
|
||||||
|
return notFound('Comment not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true, id: params.id, commentId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API] Failed to update comment:', error);
|
||||||
|
return serverError('Failed to update comment.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleDeleteCommentRequest(
|
||||||
|
request: Request,
|
||||||
|
params: RouteParams,
|
||||||
|
deps: CommentMutationDeps = defaultDeps,
|
||||||
|
): Promise<NextResponse> {
|
||||||
|
let projectRoot: string;
|
||||||
|
let commentId: number;
|
||||||
|
|
||||||
|
try {
|
||||||
|
projectRoot = parseProjectRoot(new URL(request.url).searchParams.get('projectRoot'));
|
||||||
|
commentId = parseCommentId(params.commentId);
|
||||||
|
} catch (error) {
|
||||||
|
return badRequest(error instanceof Error ? error.message : 'Invalid request.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleted = await deps.deleteComment(projectRoot, commentId);
|
||||||
|
if (!deleted) {
|
||||||
|
return notFound('Comment not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true, id: params.id, commentId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API] Failed to delete comment:', error);
|
||||||
|
return serverError('Failed to delete comment.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,141 +1,18 @@
|
||||||
import { NextResponse } from 'next/server';
|
import type { NextResponse } from 'next/server';
|
||||||
|
import { handleDeleteCommentRequest, handlePatchCommentRequest, type RouteParams } from './comment-mutation';
|
||||||
import { deleteCommentViaDolt, updateCommentViaDolt } from '../../../../../../lib/read-interactions';
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
interface RouteParams {
|
|
||||||
id: string;
|
|
||||||
commentId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PatchBody {
|
|
||||||
projectRoot?: unknown;
|
|
||||||
text?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CommentMutationDeps {
|
|
||||||
updateComment: (projectRoot: string, commentId: number, text: string) => Promise<boolean>;
|
|
||||||
deleteComment: (projectRoot: string, commentId: number) => Promise<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultDeps: CommentMutationDeps = {
|
|
||||||
updateComment: updateCommentViaDolt,
|
|
||||||
deleteComment: deleteCommentViaDolt,
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseCommentId(raw: string): number {
|
|
||||||
const parsed = Number(raw);
|
|
||||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
||||||
throw new Error('commentId must be a positive integer.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseProjectRoot(raw: unknown): string {
|
|
||||||
if (typeof raw !== 'string' || !raw.trim()) {
|
|
||||||
throw new Error('projectRoot is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return raw.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCommentText(raw: unknown): string {
|
|
||||||
if (typeof raw !== 'string' || !raw.trim()) {
|
|
||||||
throw new Error('text is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return raw.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function badRequest(message: string): NextResponse {
|
|
||||||
return NextResponse.json({ ok: false, error: { message } }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function notFound(message: string): NextResponse {
|
|
||||||
return NextResponse.json({ ok: false, error: { message } }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
function serverError(message: string): NextResponse {
|
|
||||||
return NextResponse.json({ ok: false, error: { message } }, { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handlePatchCommentRequest(
|
|
||||||
request: Request,
|
|
||||||
params: RouteParams,
|
|
||||||
deps: CommentMutationDeps,
|
|
||||||
): Promise<NextResponse> {
|
|
||||||
let body: PatchBody;
|
|
||||||
try {
|
|
||||||
body = (await request.json()) as PatchBody;
|
|
||||||
} catch {
|
|
||||||
return badRequest('Invalid JSON body.');
|
|
||||||
}
|
|
||||||
|
|
||||||
let projectRoot: string;
|
|
||||||
let commentId: number;
|
|
||||||
let text: string;
|
|
||||||
|
|
||||||
try {
|
|
||||||
projectRoot = parseProjectRoot(body.projectRoot);
|
|
||||||
commentId = parseCommentId(params.commentId);
|
|
||||||
text = parseCommentText(body.text);
|
|
||||||
} catch (error) {
|
|
||||||
return badRequest(error instanceof Error ? error.message : 'Invalid request.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updated = await deps.updateComment(projectRoot, commentId, text);
|
|
||||||
if (!updated) {
|
|
||||||
return notFound('Comment not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ ok: true, id: params.id, commentId });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[API] Failed to update comment:', error);
|
|
||||||
return serverError('Failed to update comment.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleDeleteCommentRequest(
|
|
||||||
request: Request,
|
|
||||||
params: RouteParams,
|
|
||||||
deps: CommentMutationDeps,
|
|
||||||
): Promise<NextResponse> {
|
|
||||||
let projectRoot: string;
|
|
||||||
let commentId: number;
|
|
||||||
|
|
||||||
try {
|
|
||||||
projectRoot = parseProjectRoot(new URL(request.url).searchParams.get('projectRoot'));
|
|
||||||
commentId = parseCommentId(params.commentId);
|
|
||||||
} catch (error) {
|
|
||||||
return badRequest(error instanceof Error ? error.message : 'Invalid request.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const deleted = await deps.deleteComment(projectRoot, commentId);
|
|
||||||
if (!deleted) {
|
|
||||||
return notFound('Comment not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ ok: true, id: params.id, commentId });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[API] Failed to delete comment:', error);
|
|
||||||
return serverError('Failed to delete comment.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PATCH(
|
export async function PATCH(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<RouteParams> },
|
{ params }: { params: Promise<RouteParams> },
|
||||||
): Promise<NextResponse> {
|
): Promise<NextResponse> {
|
||||||
return handlePatchCommentRequest(request, await params, defaultDeps);
|
return handlePatchCommentRequest(request, await params);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<RouteParams> },
|
{ params }: { params: Promise<RouteParams> },
|
||||||
): Promise<NextResponse> {
|
): Promise<NextResponse> {
|
||||||
return handleDeleteCommentRequest(request, await params, defaultDeps);
|
return handleDeleteCommentRequest(request, await params);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Background,
|
Background,
|
||||||
MarkerType,
|
MarkerType,
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import dagre from 'dagre';
|
import dagre from 'dagre';
|
||||||
|
import { Maximize2 } from 'lucide-react';
|
||||||
|
|
||||||
import type { BeadIssue } from '../../lib/types';
|
import type { BeadIssue } from '../../lib/types';
|
||||||
import type { AgentArchetype } from '../../lib/types-swarm';
|
import type { AgentArchetype } from '../../lib/types-swarm';
|
||||||
|
|
@ -37,10 +38,22 @@ export interface WorkflowGraphProps {
|
||||||
const NODE_WIDTH = 320;
|
const NODE_WIDTH = 320;
|
||||||
const NODE_HEIGHT = 150;
|
const NODE_HEIGHT = 150;
|
||||||
|
|
||||||
function layoutDagre(nodes: Node<GraphNodeData>[], edges: Edge[]): Node<GraphNodeData>[] {
|
type LayoutDirection = 'LR' | 'TB';
|
||||||
|
type LayoutDensity = 'normal' | 'compact';
|
||||||
|
|
||||||
|
function layoutDagre(
|
||||||
|
nodes: Node<GraphNodeData>[],
|
||||||
|
edges: Edge[],
|
||||||
|
direction: LayoutDirection,
|
||||||
|
density: LayoutDensity,
|
||||||
|
): Node<GraphNodeData>[] {
|
||||||
const dagreGraph = new dagre.graphlib.Graph();
|
const dagreGraph = new dagre.graphlib.Graph();
|
||||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||||
dagreGraph.setGraph({ rankdir: 'LR' });
|
dagreGraph.setGraph({
|
||||||
|
rankdir: direction,
|
||||||
|
ranksep: density === 'compact' ? 70 : 120,
|
||||||
|
nodesep: density === 'compact' ? 35 : 70,
|
||||||
|
});
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||||
|
|
@ -77,6 +90,8 @@ function WorkflowGraphInner({
|
||||||
assignMode = false,
|
assignMode = false,
|
||||||
}: WorkflowGraphProps) {
|
}: WorkflowGraphProps) {
|
||||||
const { fitView } = useReactFlow();
|
const { fitView } = useReactFlow();
|
||||||
|
const [layoutDirection, setLayoutDirection] = useState<LayoutDirection>('LR');
|
||||||
|
const [layoutDensity, setLayoutDensity] = useState<LayoutDensity>('normal');
|
||||||
|
|
||||||
// Use the extracted hook for all graph analysis
|
// Use the extracted hook for all graph analysis
|
||||||
const {
|
const {
|
||||||
|
|
@ -95,6 +110,9 @@ function WorkflowGraphInner({
|
||||||
return { nodes: [] as Node<GraphNodeData>[], edges: [] as Edge[] };
|
return { nodes: [] as Node<GraphNodeData>[], edges: [] as Edge[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sourcePosition = layoutDirection === 'TB' ? Position.Bottom : Position.Right;
|
||||||
|
const targetPosition = layoutDirection === 'TB' ? Position.Top : Position.Left;
|
||||||
|
|
||||||
const baseNodes: Node<GraphNodeData>[] = visibleBeads.map((issue) => {
|
const baseNodes: Node<GraphNodeData>[] = visibleBeads.map((issue) => {
|
||||||
let matchedArchetype: AgentArchetype | undefined;
|
let matchedArchetype: AgentArchetype | undefined;
|
||||||
if (archetypes && issue.assignee) {
|
if (archetypes && issue.assignee) {
|
||||||
|
|
@ -130,8 +148,8 @@ function WorkflowGraphInner({
|
||||||
onViewTelemetry: onViewTelemetry,
|
onViewTelemetry: onViewTelemetry,
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
sourcePosition: Position.Right,
|
sourcePosition,
|
||||||
targetPosition: Position.Left,
|
targetPosition,
|
||||||
type: 'flowNode',
|
type: 'flowNode',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -187,10 +205,10 @@ function WorkflowGraphInner({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes: layoutDagre(baseNodes, graphEdges),
|
nodes: layoutDagre(baseNodes, graphEdges, layoutDirection, layoutDensity),
|
||||||
edges: graphEdges,
|
edges: graphEdges,
|
||||||
};
|
};
|
||||||
}, [beads, hideClosed, selectedId, signalById, actionableNodeIds, cycleNodeIdSet, chainNodeIds, blockerTooltipMap, archetypes, assignMode, onSelect, onViewInSocial, onAssignMode, onViewTelemetry]);
|
}, [beads, hideClosed, selectedId, signalById, actionableNodeIds, cycleNodeIdSet, chainNodeIds, blockerTooltipMap, archetypes, assignMode, onSelect, onViewInSocial, onAssignMode, onViewTelemetry, layoutDirection, layoutDensity]);
|
||||||
|
|
||||||
const nodeTypes: NodeTypes = useMemo(
|
const nodeTypes: NodeTypes = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -220,7 +238,11 @@ function WorkflowGraphInner({
|
||||||
fitView({ padding: 0.3, duration: 200 });
|
fitView({ padding: 0.3, duration: 200 });
|
||||||
}, 50);
|
}, 50);
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, [fitView, flowModel.nodes.length]);
|
}, [fitView, flowModel.nodes.length, layoutDirection, layoutDensity]);
|
||||||
|
|
||||||
|
const handleFitToScreen = useCallback(() => {
|
||||||
|
fitView({ padding: 0.24, duration: 240 });
|
||||||
|
}, [fitView]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative h-full min-h-[24rem] overflow-hidden rounded-2xl border border-white/5 bg-[radial-gradient(circle_at_50%_50%,rgba(15,23,42,0.4),rgba(5,8,15,0.8))] shadow-inner ${className}`}>
|
<div className={`relative h-full min-h-[24rem] overflow-hidden rounded-2xl border border-white/5 bg-[radial-gradient(circle_at_50%_50%,rgba(15,23,42,0.4),rgba(5,8,15,0.8))] shadow-inner ${className}`}>
|
||||||
|
|
@ -245,6 +267,66 @@ function WorkflowGraphInner({
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute right-3 top-3 z-10 flex flex-wrap items-center gap-2">
|
||||||
|
<div className="inline-flex items-center gap-1 rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-tertiary)] p-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLayoutDirection('LR')}
|
||||||
|
className={`rounded-md px-2 py-1 text-[11px] font-semibold transition-colors ${
|
||||||
|
layoutDirection === 'LR'
|
||||||
|
? 'bg-[var(--surface-hover)] text-[var(--text-primary)]'
|
||||||
|
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Horizontal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLayoutDirection('TB')}
|
||||||
|
className={`rounded-md px-2 py-1 text-[11px] font-semibold transition-colors ${
|
||||||
|
layoutDirection === 'TB'
|
||||||
|
? 'bg-[var(--surface-hover)] text-[var(--text-primary)]'
|
||||||
|
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Vertical
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex items-center gap-1 rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-tertiary)] p-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLayoutDensity('compact')}
|
||||||
|
className={`rounded-md px-2 py-1 text-[11px] font-semibold transition-colors ${
|
||||||
|
layoutDensity === 'compact'
|
||||||
|
? 'bg-[var(--surface-hover)] text-[var(--text-primary)]'
|
||||||
|
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Compact
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLayoutDensity('normal')}
|
||||||
|
className={`rounded-md px-2 py-1 text-[11px] font-semibold transition-colors ${
|
||||||
|
layoutDensity === 'normal'
|
||||||
|
? 'bg-[var(--surface-hover)] text-[var(--text-primary)]'
|
||||||
|
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Normal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleFitToScreen}
|
||||||
|
className="inline-flex items-center gap-2 rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-tertiary)] px-3 py-1.5 text-xs font-semibold text-[var(--text-secondary)] transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)]"
|
||||||
|
aria-label="Fit graph to screen"
|
||||||
|
title="Fit graph to screen"
|
||||||
|
>
|
||||||
|
<Maximize2 className="h-3.5 w-3.5" />
|
||||||
|
Fit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
className="workflow-graph-flow"
|
className="workflow-graph-flow"
|
||||||
defaultEdgeOptions={defaultEdgeOptions}
|
defaultEdgeOptions={defaultEdgeOptions}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { runBdCommand } from './bridge';
|
import { runBdCommand } from './bridge';
|
||||||
|
import { getDoltConnection } from './dolt-client';
|
||||||
|
import type { ResultSetHeader } from 'mysql2';
|
||||||
|
|
||||||
export interface BeadInteraction {
|
export interface BeadInteraction {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -36,3 +38,54 @@ export async function readInteractionsViaBd(projectRoot: string, beadId: string)
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function asNonEmptyProjectRoot(projectRoot: string): string {
|
||||||
|
if (typeof projectRoot !== 'string' || !projectRoot.trim()) {
|
||||||
|
throw new Error('projectRoot is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectRoot.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function asValidCommentId(commentId: number): number {
|
||||||
|
if (!Number.isInteger(commentId) || commentId <= 0) {
|
||||||
|
throw new Error('commentId must be a positive integer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return commentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asNonEmptyCommentText(text: string): string {
|
||||||
|
if (typeof text !== 'string' || !text.trim()) {
|
||||||
|
throw new Error('text is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCommentViaDolt(projectRoot: string, commentId: number, text: string): Promise<boolean> {
|
||||||
|
const root = asNonEmptyProjectRoot(projectRoot);
|
||||||
|
const id = asValidCommentId(commentId);
|
||||||
|
const nextText = asNonEmptyCommentText(text);
|
||||||
|
|
||||||
|
const pool = await getDoltConnection(root);
|
||||||
|
const [result] = await pool.execute<ResultSetHeader>(
|
||||||
|
'UPDATE comments SET text = ? WHERE id = ?',
|
||||||
|
[nextText, id],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.affectedRows > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCommentViaDolt(projectRoot: string, commentId: number): Promise<boolean> {
|
||||||
|
const root = asNonEmptyProjectRoot(projectRoot);
|
||||||
|
const id = asValidCommentId(commentId);
|
||||||
|
|
||||||
|
const pool = await getDoltConnection(root);
|
||||||
|
const [result] = await pool.execute<ResultSetHeader>(
|
||||||
|
'DELETE FROM comments WHERE id = ?',
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.affectedRows > 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import assert from 'node:assert/strict';
|
||||||
import {
|
import {
|
||||||
handleDeleteCommentRequest,
|
handleDeleteCommentRequest,
|
||||||
handlePatchCommentRequest,
|
handlePatchCommentRequest,
|
||||||
} from '../../src/app/api/beads/[id]/comments/[commentId]/route';
|
} from '../../src/app/api/beads/[id]/comments/[commentId]/comment-mutation';
|
||||||
|
|
||||||
async function readJson(response: Response): Promise<any> {
|
async function readJson(response: Response): Promise<any> {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue