mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-19 12:10:37 +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]);
|
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 () => {
|
it("lists user comments when derived run attribution scans a timestamp window", async () => {
|
||||||
const companyId = randomUUID();
|
const companyId = randomUUID();
|
||||||
const agentId = randomUUID();
|
const agentId = randomUUID();
|
||||||
|
|
|
||||||
|
|
@ -5589,15 +5589,25 @@ export function issueService(db: Db) {
|
||||||
.then((rows) => rows[0] ?? null);
|
.then((rows) => rows[0] ?? null);
|
||||||
|
|
||||||
if (!anchor) return [];
|
if (!anchor) return [];
|
||||||
|
const anchorCreatedAt =
|
||||||
|
anchor.createdAt instanceof Date
|
||||||
|
? anchor.createdAt
|
||||||
|
: new Date(String(anchor.createdAt));
|
||||||
conditions.push(
|
conditions.push(
|
||||||
order === "asc"
|
order === "asc"
|
||||||
? or(
|
? or(
|
||||||
gt(issueComments.createdAt, anchor.createdAt),
|
gt(issueComments.createdAt, anchorCreatedAt),
|
||||||
and(eq(issueComments.createdAt, anchor.createdAt), gt(issueComments.id, anchor.id)),
|
and(
|
||||||
|
eq(issueComments.createdAt, anchorCreatedAt),
|
||||||
|
gt(issueComments.id, anchor.id),
|
||||||
|
),
|
||||||
)!
|
)!
|
||||||
: or(
|
: or(
|
||||||
lt(issueComments.createdAt, anchor.createdAt),
|
lt(issueComments.createdAt, anchorCreatedAt),
|
||||||
and(eq(issueComments.createdAt, anchor.createdAt), lt(issueComments.id, anchor.id)),
|
and(
|
||||||
|
eq(issueComments.createdAt, anchorCreatedAt),
|
||||||
|
lt(issueComments.id, anchor.id),
|
||||||
|
),
|
||||||
)!,
|
)!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue