mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
fix(issues): coerce anchor.createdAt to Date before postgres binding (PRO-3144) (#5220)
## Thinking Path > - Paperclip orchestrates AI agents in a control plane backed by Postgres + drizzle-orm > - `listComments` is the cursor-paginated comment listing on the issues service; the cursor branch uses Drizzle's `gt`/`lt`/`eq` against `issueComments.createdAt` > - On postgres.js v3.4.8, passing a `Date` instance through the comparison helpers triggers `TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument must be of type string or an instance of Buffer or ArrayBuffer. Received an instance of Date` > - The driver's binding path expects a Date constructed via the standard runtime, but drizzle's `select` returns instances that don't satisfy that check in this version > - This PR coerces `anchor.createdAt` through `toISOString()` → `new Date(...)` so the comparison helpers always receive a binding-safe Date, then folds in a follow-up that hoists the Date into a single allocation reused across all four `gt`/`lt`/`eq` call sites > - The benefit is `listComments` cursor pagination stops 500-ing on Pg v3.4.8 with one Date allocation per call instead of four, exercised by both ascending and descending cursor tests ## What Changed - `server/src/services/issues.ts` — coerce `anchor.createdAt` to a binding-safe `Date` once and reuse the same instance across all four cursor comparisons (`gt` / `lt` / `eq`) - `server/src/__tests__/issues-service.test.ts` — add an ascending-cursor sibling test so both `gt` and `lt` cursor paths are exercised; the existing descending test continues to pass ## Verification ```bash # Both cursor branches pnpm --filter @paperclipai/server exec vitest run \ src/__tests__/issues-service.test.ts -t "anchor comment" # → 2 passed, 41 skipped # Production smoke curl -s "$PAPERCLIP_API_URL/api/issues/<issueId>/comments?after=<commentId>&order=asc" \ -H "Authorization: Bearer $PAPERCLIP_API_KEY" # Expect: JSON array, no 500 TypeError ``` ## Risks - Low risk. Pure cursor-pagination internals in `listComments`; no schema, migration, or external contract changes - Drizzle's `gt`/`lt`/`eq` continue to receive a `Date` for the timestamp column, producing the same bound parameter as before - Behavioural surface is exercised by ascending + descending cursor tests against a real Postgres test database ## Model Used - Claude Opus 4.7 (`claude-opus-4-7`), no extended-thinking mode, used for the hoist+test follow-up commit ## Fixes Closes #2612, Closes #3661, Closes #3830 ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots — n/a, backend-only - [x] I have updated relevant documentation to reflect my changes — n/a, no docs touched - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Elena Voronova <elena@paperclip.ing> Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Devin Foley <devin@devinfoley.com>
This commit is contained in:
parent
edeab22c28
commit
d58a862549
2 changed files with 72 additions and 4 deletions
|
|
@ -1243,6 +1243,64 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
|||
expect(comments.map((comment) => comment.id)).toEqual([firstCommentId]);
|
||||
});
|
||||
|
||||
it("paginates later comments in ascending order from an anchor comment", async () => {
|
||||
const companyId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
const firstCommentId = randomUUID();
|
||||
const anchorCommentId = randomUUID();
|
||||
const latestCommentId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
|
||||
await db.insert(issues).values({
|
||||
id: issueId,
|
||||
companyId,
|
||||
title: "Paged comments issue",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
});
|
||||
|
||||
await db.insert(issueComments).values([
|
||||
{
|
||||
id: firstCommentId,
|
||||
companyId,
|
||||
issueId,
|
||||
body: "First comment",
|
||||
createdAt: new Date("2026-03-26T10:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-26T10:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: anchorCommentId,
|
||||
companyId,
|
||||
issueId,
|
||||
body: "Anchor comment",
|
||||
createdAt: new Date("2026-03-26T11:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-26T11:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: latestCommentId,
|
||||
companyId,
|
||||
issueId,
|
||||
body: "Latest comment",
|
||||
createdAt: new Date("2026-03-26T12:00:00.000Z"),
|
||||
updatedAt: new Date("2026-03-26T12:00:00.000Z"),
|
||||
},
|
||||
]);
|
||||
|
||||
const comments = await svc.listComments(issueId, {
|
||||
afterCommentId: anchorCommentId,
|
||||
order: "asc",
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
expect(comments.map((comment) => comment.id)).toEqual([latestCommentId]);
|
||||
});
|
||||
|
||||
it("lists user comments when derived run attribution scans a timestamp window", async () => {
|
||||
const companyId = randomUUID();
|
||||
const agentId = randomUUID();
|
||||
|
|
|
|||
|
|
@ -5589,15 +5589,25 @@ export function issueService(db: Db) {
|
|||
.then((rows) => rows[0] ?? null);
|
||||
|
||||
if (!anchor) return [];
|
||||
const anchorCreatedAt =
|
||||
anchor.createdAt instanceof Date
|
||||
? anchor.createdAt
|
||||
: new Date(String(anchor.createdAt));
|
||||
conditions.push(
|
||||
order === "asc"
|
||||
? or(
|
||||
gt(issueComments.createdAt, anchor.createdAt),
|
||||
and(eq(issueComments.createdAt, anchor.createdAt), gt(issueComments.id, anchor.id)),
|
||||
gt(issueComments.createdAt, anchorCreatedAt),
|
||||
and(
|
||||
eq(issueComments.createdAt, anchorCreatedAt),
|
||||
gt(issueComments.id, anchor.id),
|
||||
),
|
||||
)!
|
||||
: or(
|
||||
lt(issueComments.createdAt, anchor.createdAt),
|
||||
and(eq(issueComments.createdAt, anchor.createdAt), lt(issueComments.id, anchor.id)),
|
||||
lt(issueComments.createdAt, anchorCreatedAt),
|
||||
and(
|
||||
eq(issueComments.createdAt, anchorCreatedAt),
|
||||
lt(issueComments.id, anchor.id),
|
||||
),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue