Implement artifact grouping API

This commit is contained in:
Dotta committed 2026-06-06 13:38:54 +00:00
1 parent a4fa0eaf5e
commit fa4360fe69
8 files changed
+538 -24

No files matched your search

+4
View File
@@ -449,6 +449,8 @@ export type {
IssueWorkProductReviewState,
CompanyArtifact,
CompanyArtifactAgentSummary,
CompanyArtifactGroup,
CompanyArtifactGroupBy,
CompanyArtifactIssueSummary,
CompanyArtifactMediaKind,
CompanyArtifactProjectSummary,
@@ -993,6 +995,8 @@ export {
COMPANY_ARTIFACTS_DEFAULT_LIMIT,
COMPANY_ARTIFACTS_MAX_LIMIT,
COMPANY_ARTIFACTS_MAX_QUERY_LENGTH,
companyArtifactGroupBySchema,
companyArtifactGroupSchema,
companyArtifactMediaKindSchema,
companyArtifactSchema,
companyArtifactSourceSchema,
+16
View File
@@ -2,6 +2,8 @@ export type CompanyArtifactSource = "document" | "attachment" | "work_product";
export type CompanyArtifactMediaKind = "image" | "video" | "text" | "document" | "file" | "empty";
export type CompanyArtifactGroupBy = "none" | "task" | "parent_task";
export interface CompanyArtifactIssueSummary {
id: string;
identifier: string;
@@ -35,7 +37,21 @@ export interface CompanyArtifact {
href: string;
}
export interface CompanyArtifactGroup {
id: string;
groupBy: Exclude<CompanyArtifactGroupBy, "none">;
issue: CompanyArtifactIssueSummary;
title: string;
count: number;
mediaKinds: CompanyArtifactMediaKind[];
previewArtifacts: CompanyArtifact[];
updatedAt: string;
href: string;
}
export interface CompanyArtifactsResponse {
artifacts: CompanyArtifact[];
groups?: CompanyArtifactGroup[];
selectedGroup?: CompanyArtifactGroup | null;
nextCursor: string | null;
}
+2
View File
@@ -217,6 +217,8 @@ export type {
export type {
CompanyArtifact,
CompanyArtifactAgentSummary,
CompanyArtifactGroup,
CompanyArtifactGroupBy,
CompanyArtifactIssueSummary,
CompanyArtifactMediaKind,
CompanyArtifactProjectSummary,
@@ -0,0 +1,86 @@
import { describe, expect, it } from "vitest";
import { companyArtifactsQuerySchema, companyArtifactsResponseSchema } from "./artifact.js";
const issue = {
id: "11111111-1111-4111-8111-111111111111",
identifier: "PAP-1",
title: "Build artifacts",
};
const artifact = {
id: "document:22222222-2222-4222-8222-222222222222",
source: "document",
mediaKind: "document",
title: "Plan",
previewText: "Artifact preview",
contentType: "text/markdown",
contentPath: null,
openPath: null,
downloadPath: null,
issue,
project: null,
createdByAgent: null,
updatedAt: "2026-06-06T12:00:00.000Z",
href: "/PAP/issues/PAP-1#document-plan",
};
describe("companyArtifactsQuerySchema", () => {
it("defaults to the existing flat artifact query", () => {
expect(companyArtifactsQuerySchema.parse({})).toMatchObject({
kind: "all",
groupBy: "none",
limit: 30,
});
});
it("accepts grouped artifact query parameters", () => {
expect(
companyArtifactsQuerySchema.parse({
groupBy: "parent_task",
groupIssueId: issue.id,
kind: "video",
q: "render",
}),
).toMatchObject({
groupBy: "parent_task",
groupIssueId: issue.id,
kind: "video",
q: "render",
});
});
it("rejects invalid grouped artifact query parameters", () => {
expect(() => companyArtifactsQuerySchema.parse({ groupBy: "agent" })).toThrow();
expect(() => companyArtifactsQuerySchema.parse({ groupIssueId: "PAP-1" })).toThrow();
});
});
describe("companyArtifactsResponseSchema", () => {
it("accepts grouped artifact responses with selected group metadata", () => {
const group = {
id: `task:${issue.id}`,
groupBy: "task",
issue,
title: issue.title,
count: 1,
mediaKinds: ["document"],
previewArtifacts: [artifact],
updatedAt: "2026-06-06T12:00:00.000Z",
href: `/PAP/artifacts?groupBy=task&groupIssueId=${issue.id}`,
};
expect(
companyArtifactsResponseSchema.parse({
artifacts: [artifact],
groups: [group],
selectedGroup: group,
nextCursor: null,
}),
).toMatchObject({
artifacts: [artifact],
groups: [group],
selectedGroup: group,
nextCursor: null,
});
});
});
@@ -8,10 +8,14 @@ export const companyArtifactSourceSchema = z.enum(["document", "attachment", "wo
export const companyArtifactMediaKindSchema = z.enum(["image", "video", "text", "document", "file", "empty"]);
export const companyArtifactGroupBySchema = z.enum(["none", "task", "parent_task"]);
export const companyArtifactsQuerySchema = z.object({
kind: z.enum(["image", "video", "text", "document", "file", "all"]).optional().default("all"),
projectId: z.string().uuid().optional(),
q: z.string().trim().max(COMPANY_ARTIFACTS_MAX_QUERY_LENGTH).optional(),
groupBy: companyArtifactGroupBySchema.optional().default("none"),
groupIssueId: z.string().uuid().optional(),
limit: z.coerce
.number()
.int()
@@ -49,8 +53,26 @@ export const companyArtifactSchema = z.object({
href: z.string().min(1),
});
export const companyArtifactGroupSchema = z.object({
id: z.string().min(1),
groupBy: companyArtifactGroupBySchema.exclude(["none"]),
issue: z.object({
id: z.string().uuid(),
identifier: z.string(),
title: z.string(),
}),
title: z.string(),
count: z.number().int().min(0),
mediaKinds: z.array(companyArtifactMediaKindSchema),
previewArtifacts: z.array(companyArtifactSchema),
updatedAt: z.string().datetime(),
href: z.string().min(1),
});
export const companyArtifactsResponseSchema = z.object({
artifacts: z.array(companyArtifactSchema),
groups: z.array(companyArtifactGroupSchema).optional(),
selectedGroup: companyArtifactGroupSchema.nullable().optional(),
nextCursor: z.string().nullable(),
});
+2
View File
@@ -330,6 +330,8 @@ export {
COMPANY_ARTIFACTS_DEFAULT_LIMIT,
COMPANY_ARTIFACTS_MAX_LIMIT,
COMPANY_ARTIFACTS_MAX_QUERY_LENGTH,
companyArtifactGroupBySchema,
companyArtifactGroupSchema,
companyArtifactMediaKindSchema,
companyArtifactSchema,
companyArtifactSourceSchema,
@@ -1,5 +1,6 @@
import { Readable } from "node:stream";
import express from "express";
import { eq } from "drizzle-orm";
import request from "supertest";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import {
@@ -329,7 +330,7 @@ describeEmbeddedPostgres("companyArtifactsService", () => {
updatedAt: new Date("2026-01-02T12:00:00.000Z"),
});
return { companyId, projectId, issueId, otherRunId };
return { companyId, projectId, issueId, secondIssueId, otherIssueId, otherRunId };
}
it("projects agent-created documents, direct attachments, and work products while excluding noisy sources", async () => {
@@ -495,6 +496,189 @@ describeEmbeddedPostgres("companyArtifactsService", () => {
expect(forged?.createdByAgent).toBeNull();
expect(result.artifacts.some((artifact) => artifact.createdByAgent?.name === "Other")).toBe(false);
});
it("groups artifacts by task after applying media, project, and search filters", async () => {
const { companyId, projectId, issueId } = await seedArtifacts();
const storage = createStorageService({ "notes.txt": Buffer.from("Searchable notes preview") });
const grouped = await companyArtifactsService(db, storage).list(companyId, {
groupBy: "task",
limit: 10,
});
expect(grouped.artifacts).toEqual([]);
expect(grouped.nextCursor).toBeNull();
expect(grouped.groups?.map((group) => ({
issue: group.issue.identifier,
count: group.count,
mediaKinds: group.mediaKinds,
href: group.href,
}))).toEqual([
{
issue: "PAP-2",
count: 1,
mediaKinds: ["document"],
href: "/PAP/artifacts?groupBy=task&groupIssueId=77777777-7777-4777-8777-777777777777",
},
{
issue: "PAP-1",
count: 3,
mediaKinds: ["video", "text"],
href: "/PAP/artifacts?groupBy=task&groupIssueId=66666666-6666-4666-8666-666666666666",
},
]);
expect(grouped.groups?.find((group) => group.issue.id === issueId)?.previewArtifacts.map((artifact) => artifact.title))
.toEqual(["direct-video.mp4", "Primary Cut", "notes.txt"]);
const projectVideos = await companyArtifactsService(db, storage).list(companyId, {
groupBy: "task",
projectId,
kind: "video",
limit: 10,
});
expect(projectVideos.groups?.map((group) => ({
issue: group.issue.identifier,
count: group.count,
href: group.href,
}))).toEqual([
{
issue: "PAP-1",
count: 2,
href:
"/PAP/artifacts?groupBy=task&groupIssueId=66666666-6666-4666-8666-666666666666&kind=video&projectId=55555555-5555-4555-8555-555555555555",
},
]);
const search = await companyArtifactsService(db, storage).list(companyId, {
groupBy: "task",
q: "review document",
limit: 10,
});
expect(search.groups?.map((group) => ({ issue: group.issue.identifier, count: group.count }))).toEqual([
{ issue: "PAP-2", count: 1 },
]);
});
it("paginates grouped task lists with the active group cursor", async () => {
const { companyId } = await seedArtifacts();
const firstPage = await companyArtifactsService(db, createStorageService()).list(companyId, {
groupBy: "task",
limit: 1,
});
expect(firstPage.groups?.map((group) => group.issue.identifier)).toEqual(["PAP-2"]);
expect(firstPage.nextCursor).toEqual(expect.any(String));
const secondPage = await companyArtifactsService(db, createStorageService()).list(companyId, {
groupBy: "task",
limit: 10,
cursor: firstPage.nextCursor ?? undefined,
});
expect(secondPage.groups?.map((group) => group.issue.identifier)).toEqual(["PAP-1"]);
expect(secondPage.nextCursor).toBeNull();
});
it("groups parent-task artifacts under the topmost same-company ancestor", async () => {
const { companyId, issueId, secondIssueId } = await seedArtifacts();
const grandchildIssueId = "21212121-2121-4212-8121-212121212121";
const grandchildAttachmentId = "23232323-2323-4232-8232-232323232323";
await db.update(issues).set({ parentId: issueId }).where(eq(issues.id, secondIssueId));
await db.insert(issues).values({
id: grandchildIssueId,
companyId,
parentId: secondIssueId,
identifier: "PAP-3",
title: "Grandchild render",
status: "done",
priority: "medium",
});
await db.insert(assets).values({
id: "24242424-2424-4242-8242-242424242424",
companyId,
provider: "local_disk",
objectKey: "grandchild.txt",
contentType: "text/plain",
byteSize: 48,
sha256: "sha256-grandchild",
originalFilename: "grandchild.txt",
createdByAgentId: "33333333-3333-4333-8333-333333333333",
});
await db.insert(issueAttachments).values({
id: grandchildAttachmentId,
companyId,
issueId: grandchildIssueId,
assetId: "24242424-2424-4242-8242-242424242424",
updatedAt: new Date("2026-01-05T00:00:00.000Z"),
});
const grouped = await companyArtifactsService(db, createStorageService()).list(companyId, {
groupBy: "parent_task",
limit: 10,
});
expect(grouped.artifacts).toEqual([]);
expect(grouped.groups?.map((group) => ({
issue: group.issue.identifier,
count: group.count,
previewTitles: group.previewArtifacts.map((artifact) => artifact.title),
}))).toEqual([
{
issue: "PAP-1",
count: 5,
previewTitles: ["grandchild.txt", "Review Notes", "direct-video.mp4"],
},
]);
});
it("returns selected group artifact pages and metadata without leaking foreign group issues", async () => {
const { companyId, issueId, otherIssueId } = await seedArtifacts();
const selected = await companyArtifactsService(db, createStorageService()).list(companyId, {
groupBy: "task",
groupIssueId: issueId,
limit: 2,
});
expect(selected.groups).toBeUndefined();
expect(selected.selectedGroup).toMatchObject({
id: `task:${issueId}`,
groupBy: "task",
issue: { identifier: "PAP-1" },
count: 3,
});
expect(selected.artifacts.map((artifact) => artifact.title)).toEqual(["direct-video.mp4", "Primary Cut"]);
expect(selected.nextCursor).toEqual(expect.any(String));
const selectedSecondPage = await companyArtifactsService(db, createStorageService()).list(companyId, {
groupBy: "task",
groupIssueId: issueId,
limit: 10,
cursor: selected.nextCursor ?? undefined,
});
expect(selectedSecondPage.artifacts.map((artifact) => artifact.title)).toEqual(["notes.txt"]);
const selectedEmptyByFilter = await companyArtifactsService(db, createStorageService()).list(companyId, {
groupBy: "task",
groupIssueId: issueId,
q: "does-not-match-this-stack",
limit: 10,
});
expect(selectedEmptyByFilter.selectedGroup).toMatchObject({
id: `task:${issueId}`,
count: 0,
});
expect(selectedEmptyByFilter.artifacts).toEqual([]);
const foreignSelected = await companyArtifactsService(db, createStorageService()).list(companyId, {
groupBy: "task",
groupIssueId: otherIssueId,
limit: 10,
});
expect(foreignSelected).toEqual({
artifacts: [],
selectedGroup: null,
nextCursor: null,
});
});
});
describe("company artifacts route authorization", () => {
+221 -23
View File
@@ -20,6 +20,8 @@ import {
companyArtifactsQuerySchema,
SYSTEM_ISSUE_DOCUMENT_KEYS,
type CompanyArtifact,
type CompanyArtifactGroup,
type CompanyArtifactGroupBy,
type CompanyArtifactMediaKind,
type CompanyArtifactsQuery,
type CompanyArtifactsResponse,
@@ -29,12 +31,23 @@ import type { StorageService } from "../storage/types.js";
const TEXT_PREVIEW_BYTES = 4096;
const PREVIEW_TEXT_MAX_LENGTH = 280;
const GROUP_PREVIEW_ARTIFACT_LIMIT = 3;
type ArtifactCursor = {
updatedAt: string;
id: string;
};
type ArtifactGroupBy = Exclude<CompanyArtifactGroupBy, "none">;
type IssueGroupingRow = {
id: string;
parentId: string | null;
identifier: string | null;
title: string;
updatedAt: Date;
};
function encodeCursor(cursor: ArtifactCursor) {
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
}
@@ -61,6 +74,12 @@ function cursorCondition(updatedAt: SQL<Date>, artifactId: SQL<string>, cursor:
return sql`(${updatedAt} < ${cursor.updatedAt}::timestamptz OR (${updatedAt} = ${cursor.updatedAt}::timestamptz AND ${artifactId} < ${cursor.id}))`;
}
function isAfterCursor(item: { updatedAt: string; id: string }, cursor: ArtifactCursor | null) {
if (!cursor) return true;
const dateDiff = Date.parse(item.updatedAt) - Date.parse(cursor.updatedAt);
return dateDiff < 0 || (dateDiff === 0 && item.id < cursor.id);
}
function escapeLikePattern(value: string) {
return value.replace(/[\\%_]/g, (match) => `\\${match}`);
}
@@ -116,6 +135,21 @@ function buildIssueHref(companyPrefix: string, identifier: string, anchor: strin
return `/${encodeURIComponent(companyPrefix)}/issues/${encodeURIComponent(identifier)}#${anchor}`;
}
function buildArtifactsGroupHref(
companyPrefix: string,
query: CompanyArtifactsQuery,
groupBy: ArtifactGroupBy,
groupIssueId: string,
) {
const params = new URLSearchParams();
params.set("groupBy", groupBy);
params.set("groupIssueId", groupIssueId);
if (query.kind !== "all") params.set("kind", query.kind);
if (query.projectId) params.set("projectId", query.projectId);
if (query.q) params.set("q", query.q);
return `/${encodeURIComponent(companyPrefix)}/artifacts?${params.toString()}`;
}
function attachmentContentPath(attachmentId: string) {
return `/api/attachments/${attachmentId}/content`;
}
@@ -136,11 +170,141 @@ async function readTextAttachmentPreview(
}
}
function sortArtifacts(artifacts: CompanyArtifact[]) {
return artifacts.sort((a, b) => {
const dateDiff = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
if (dateDiff !== 0) return dateDiff;
return b.id.localeCompare(a.id);
});
}
function pageByCursor<T extends { id: string; updatedAt: string }>(
items: T[],
limit: number,
cursor: ArtifactCursor | null,
) {
const filtered = items.filter((item) => isAfterCursor(item, cursor));
const page = filtered.slice(0, limit);
const nextCursor = filtered.length > limit
? encodeCursor({ id: page[page.length - 1]?.id ?? "", updatedAt: page[page.length - 1]?.updatedAt ?? new Date(0).toISOString() })
: null;
return { page, nextCursor };
}
async function loadIssueGroupingRows(db: Db, companyId: string) {
const rows = await db
.select({
id: issues.id,
parentId: issues.parentId,
identifier: issues.identifier,
title: issues.title,
updatedAt: issues.updatedAt,
})
.from(issues)
.where(eq(issues.companyId, companyId));
return new Map(rows.map((row): [string, IssueGroupingRow] => [row.id, row]));
}
function getIssueSummary(issue: IssueGroupingRow) {
return {
id: issue.id,
identifier: issue.identifier ?? issue.id,
title: issue.title,
};
}
function resolveRootIssueId(issueId: string, issueRows: Map<string, IssueGroupingRow>) {
let current = issueRows.get(issueId);
if (!current) return issueId;
const seen = new Set<string>();
while (current.parentId && !seen.has(current.id)) {
seen.add(current.id);
const parent = issueRows.get(current.parentId);
if (!parent) break;
current = parent;
}
return current.id;
}
function resolveGroupIssueId(groupBy: ArtifactGroupBy, issueId: string, issueRows: Map<string, IssueGroupingRow>) {
return groupBy === "task" ? issueId : resolveRootIssueId(issueId, issueRows);
}
function emptyGroup(input: {
companyPrefix: string;
query: CompanyArtifactsQuery;
groupBy: ArtifactGroupBy;
issue: IssueGroupingRow;
}): CompanyArtifactGroup {
const summary = getIssueSummary(input.issue);
return {
id: `${input.groupBy}:${input.issue.id}`,
groupBy: input.groupBy,
issue: summary,
title: summary.title,
count: 0,
mediaKinds: [],
previewArtifacts: [],
updatedAt: input.issue.updatedAt.toISOString(),
href: buildArtifactsGroupHref(input.companyPrefix, input.query, input.groupBy, input.issue.id),
};
}
function buildArtifactGroups(input: {
artifacts: CompanyArtifact[];
companyPrefix: string;
query: CompanyArtifactsQuery;
groupBy: ArtifactGroupBy;
issueRows: Map<string, IssueGroupingRow>;
}) {
const groups = new Map<string, CompanyArtifactGroup>();
for (const artifact of input.artifacts) {
const groupIssueId = resolveGroupIssueId(input.groupBy, artifact.issue.id, input.issueRows);
const groupIssue = input.issueRows.get(groupIssueId) ?? {
id: artifact.issue.id,
parentId: null,
identifier: artifact.issue.identifier,
title: artifact.issue.title,
updatedAt: new Date(artifact.updatedAt),
};
const groupId = `${input.groupBy}:${groupIssueId}`;
const existing = groups.get(groupId);
const group = existing ?? emptyGroup({
companyPrefix: input.companyPrefix,
query: input.query,
groupBy: input.groupBy,
issue: groupIssue,
});
if (!existing) groups.set(groupId, group);
group.count += 1;
if (!group.mediaKinds.includes(artifact.mediaKind)) {
group.mediaKinds.push(artifact.mediaKind);
}
if (group.previewArtifacts.length < GROUP_PREVIEW_ARTIFACT_LIMIT) {
group.previewArtifacts.push(artifact);
}
const dateDiff = Date.parse(artifact.updatedAt) - Date.parse(group.updatedAt);
if (dateDiff > 0 || (dateDiff === 0 && artifact.id > group.id)) {
group.updatedAt = artifact.updatedAt;
}
}
return [...groups.values()].sort((a, b) => {
const dateDiff = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
if (dateDiff !== 0) return dateDiff;
return b.id.localeCompare(a.id);
});
}
export function companyArtifactsService(db: Db, storage?: StorageService) {
return {
list: async (companyId: string, rawQuery: Partial<CompanyArtifactsQuery> = {}): Promise<CompanyArtifactsResponse> => {
const query = companyArtifactsQuerySchema.parse(rawQuery);
const cursor = decodeCursor(query.cursor);
const groupBy = query.groupBy === "none" ? null : query.groupBy;
const company = await db
.select({ id: companies.id, issuePrefix: companies.issuePrefix })
.from(companies)
@@ -162,7 +326,7 @@ export function companyArtifactsService(db: Db, storage?: StorageService) {
or(isNotNull(documents.createdByAgentId), isNotNull(documents.updatedByAgentId))!,
notInArray(issueDocuments.key, [...SYSTEM_ISSUE_DOCUMENT_KEYS]),
];
const documentCursor = cursorCondition(sql<Date>`${documents.updatedAt}`, documentArtifactId, cursor);
const documentCursor = groupBy ? undefined : cursorCondition(sql<Date>`${documents.updatedAt}`, documentArtifactId, cursor);
if (documentCursor) documentConditions.push(documentCursor);
if (query.projectId) documentConditions.push(eq(issues.projectId, query.projectId));
if (q) {
@@ -174,7 +338,7 @@ export function companyArtifactsService(db: Db, storage?: StorageService) {
)`);
}
const documentRows = await db
const documentRowsQuery = db
.select({
artifactId: documentArtifactId,
documentId: documents.id,
@@ -197,8 +361,8 @@ export function companyArtifactsService(db: Db, storage?: StorageService) {
.leftJoin(createdAgent, eq(documents.createdByAgentId, createdAgent.id))
.leftJoin(updatedAgent, eq(documents.updatedByAgentId, updatedAgent.id))
.where(and(...documentConditions))
.orderBy(desc(documents.updatedAt), desc(documentArtifactId))
.limit(fetchLimit);
.orderBy(desc(documents.updatedAt), desc(documentArtifactId));
const documentRows = groupBy ? await documentRowsQuery : await documentRowsQuery.limit(fetchLimit);
for (const row of documentRows) {
const identifier = row.issueIdentifier ?? row.issueId;
@@ -233,7 +397,9 @@ export function companyArtifactsService(db: Db, storage?: StorageService) {
eq(issueWorkProducts.provider, "paperclip"),
];
const workProductConditions: SQL[] = [...workProductBaseConditions];
const workProductCursor = cursorCondition(sql<Date>`${issueWorkProducts.updatedAt}`, workProductArtifactId, cursor);
const workProductCursor = groupBy
? undefined
: cursorCondition(sql<Date>`${issueWorkProducts.updatedAt}`, workProductArtifactId, cursor);
const workProductKind = contentTypeKindCondition(workProductContentType, query.kind);
if (workProductCursor) workProductConditions.push(workProductCursor);
if (workProductKind) {
@@ -256,7 +422,7 @@ export function companyArtifactsService(db: Db, storage?: StorageService) {
workProductConditions.push(searchCondition);
}
const workProductRows = await db
const workProductRowsQuery = db
.select({
artifactId: workProductArtifactId,
workProductId: issueWorkProducts.id,
@@ -290,8 +456,8 @@ export function companyArtifactsService(db: Db, storage?: StorageService) {
),
)
.where(and(...workProductConditions))
.orderBy(desc(issueWorkProducts.updatedAt), desc(workProductArtifactId))
.limit(fetchLimit);
.orderBy(desc(issueWorkProducts.updatedAt), desc(workProductArtifactId));
const workProductRows = groupBy ? await workProductRowsQuery : await workProductRowsQuery.limit(fetchLimit);
const workProductAttachmentRows = await db
.select({
@@ -342,7 +508,9 @@ export function companyArtifactsService(db: Db, storage?: StorageService) {
isNull(issueAttachments.issueCommentId),
isNotNull(assets.createdByAgentId),
];
const attachmentCursor = cursorCondition(sql<Date>`${issueAttachments.updatedAt}`, attachmentArtifactId, cursor);
const attachmentCursor = groupBy
? undefined
: cursorCondition(sql<Date>`${issueAttachments.updatedAt}`, attachmentArtifactId, cursor);
const attachmentKind = contentTypeKindCondition(sql<string>`${assets.contentType}`, query.kind);
if (attachmentCursor) attachmentConditions.push(attachmentCursor);
if (attachmentKind) attachmentConditions.push(attachmentKind);
@@ -355,7 +523,7 @@ export function companyArtifactsService(db: Db, storage?: StorageService) {
)`);
}
const attachmentRows = await db
const attachmentRowsQuery = db
.select({
artifactId: attachmentArtifactId,
attachmentId: issueAttachments.id,
@@ -379,8 +547,8 @@ export function companyArtifactsService(db: Db, storage?: StorageService) {
.leftJoin(projects, eq(issues.projectId, projects.id))
.leftJoin(attachmentAgent, eq(assets.createdByAgentId, attachmentAgent.id))
.where(and(...attachmentConditions))
.orderBy(desc(issueAttachments.updatedAt), desc(attachmentArtifactId))
.limit(fetchLimit);
.orderBy(desc(issueAttachments.updatedAt), desc(attachmentArtifactId));
const attachmentRows = groupBy ? await attachmentRowsQuery : await attachmentRowsQuery.limit(fetchLimit);
const attachmentArtifacts = await Promise.all(attachmentRows.map(async (row): Promise<CompanyArtifact | null> => {
if (workProductAttachmentIds.has(row.attachmentId)) return null;
@@ -416,18 +584,48 @@ export function companyArtifactsService(db: Db, storage?: StorageService) {
artifacts.push(...attachmentArtifacts.filter((artifact): artifact is CompanyArtifact => artifact !== null));
}
const sorted = artifacts
.sort((a, b) => {
const dateDiff = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
if (dateDiff !== 0) return dateDiff;
return b.id.localeCompare(a.id);
});
const page = sorted.slice(0, query.limit);
const nextCursor = sorted.length > query.limit
? encodeCursor({ id: page[page.length - 1]?.id ?? "", updatedAt: page[page.length - 1]?.updatedAt ?? new Date(0).toISOString() })
: null;
const sorted = sortArtifacts(artifacts);
if (!groupBy) {
const page = sorted.slice(0, query.limit);
const nextCursor = sorted.length > query.limit
? encodeCursor({ id: page[page.length - 1]?.id ?? "", updatedAt: page[page.length - 1]?.updatedAt ?? new Date(0).toISOString() })
: null;
return { artifacts: page, nextCursor };
return { artifacts: page, nextCursor };
}
const issueRows = await loadIssueGroupingRows(db, companyId);
const groups = buildArtifactGroups({
artifacts: sorted,
companyPrefix: company.issuePrefix,
query,
groupBy,
issueRows,
});
if (query.groupIssueId) {
const selectedIssue = issueRows.get(query.groupIssueId);
if (!selectedIssue) {
return { artifacts: [], selectedGroup: null, nextCursor: null };
}
const selectedGroupIssueId = resolveGroupIssueId(groupBy, selectedIssue.id, issueRows);
const selectedGroup = groups.find((group) => group.issue.id === selectedGroupIssueId)
?? emptyGroup({
companyPrefix: company.issuePrefix,
query,
groupBy,
issue: issueRows.get(selectedGroupIssueId) ?? selectedIssue,
});
const selectedArtifacts = sorted.filter((artifact) =>
resolveGroupIssueId(groupBy, artifact.issue.id, issueRows) === selectedGroupIssueId
);
const { page, nextCursor } = pageByCursor(selectedArtifacts, query.limit, cursor);
return { artifacts: page, selectedGroup, nextCursor };
}
const { page, nextCursor } = pageByCursor(groups, query.limit, cursor);
return { artifacts: [], groups: page, nextCursor };
},
};
}