Reduce company skill list payloads

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-03-16 17:45:28 -05:00
parent cca086b863
commit 8460fee380
4 changed files with 58 additions and 13 deletions

View file

@ -29,7 +29,20 @@ export interface CompanySkill {
updatedAt: Date; updatedAt: Date;
} }
export interface CompanySkillListItem extends CompanySkill { export interface CompanySkillListItem {
id: string;
companyId: string;
slug: string;
name: string;
description: string | null;
sourceType: CompanySkillSourceType;
sourceLocator: string | null;
sourceRef: string | null;
trustLevel: CompanySkillTrustLevel;
compatibility: CompanySkillCompatibility;
fileInventory: CompanySkillFileInventoryEntry[];
createdAt: Date;
updatedAt: Date;
attachedAgentCount: number; attachedAgentCount: number;
editable: boolean; editable: boolean;
editableReason: string | null; editableReason: string | null;

View file

@ -31,6 +31,7 @@ const issueSvc = {
const companySkillSvc = { const companySkillSvc = {
list: vi.fn(), list: vi.fn(),
listFull: vi.fn(),
readFile: vi.fn(), readFile: vi.fn(),
importPackageFiles: vi.fn(), importPackageFiles: vi.fn(),
}; };
@ -148,7 +149,7 @@ describe("company portability", () => {
issueSvc.list.mockResolvedValue([]); issueSvc.list.mockResolvedValue([]);
issueSvc.getById.mockResolvedValue(null); issueSvc.getById.mockResolvedValue(null);
issueSvc.getByIdentifier.mockResolvedValue(null); issueSvc.getByIdentifier.mockResolvedValue(null);
companySkillSvc.list.mockResolvedValue([ const companySkills = [
{ {
id: "skill-1", id: "skill-1",
companyId: "company-1", companyId: "company-1",
@ -194,7 +195,9 @@ describe("company portability", () => {
sourceKind: "local_path", sourceKind: "local_path",
}, },
}, },
]); ];
companySkillSvc.list.mockResolvedValue(companySkills);
companySkillSvc.listFull.mockResolvedValue(companySkills);
companySkillSvc.readFile.mockImplementation(async (_companyId: string, skillId: string, relativePath: string) => { companySkillSvc.readFile.mockImplementation(async (_companyId: string, skillId: string, relativePath: string) => {
if (skillId === "skill-2") { if (skillId === "skill-2") {
return { return {

View file

@ -1551,7 +1551,7 @@ export function companyPortabilityService(db: Db) {
const allAgentRows = include.agents ? await agents.list(companyId, { includeTerminated: true }) : []; const allAgentRows = include.agents ? await agents.list(companyId, { includeTerminated: true }) : [];
const agentRows = allAgentRows.filter((agent) => agent.status !== "terminated"); const agentRows = allAgentRows.filter((agent) => agent.status !== "terminated");
const companySkillRows = await companySkills.list(companyId); const companySkillRows = await companySkills.listFull(companyId);
if (include.agents) { if (include.agents) {
const skipped = allAgentRows.length - agentRows.length; const skipped = allAgentRows.length - agentRows.length;
if (skipped > 0) { if (skipped > 0) {

View file

@ -881,6 +881,30 @@ function enrichSkill(skill: CompanySkill, attachedAgentCount: number, usedByAgen
}; };
} }
function toCompanySkillListItem(skill: CompanySkill, attachedAgentCount: number): CompanySkillListItem {
const source = deriveSkillSourceInfo(skill);
return {
id: skill.id,
companyId: skill.companyId,
slug: skill.slug,
name: skill.name,
description: skill.description,
sourceType: skill.sourceType,
sourceLocator: skill.sourceLocator,
sourceRef: skill.sourceRef,
trustLevel: skill.trustLevel,
compatibility: skill.compatibility,
fileInventory: skill.fileInventory,
createdAt: skill.createdAt,
updatedAt: skill.updatedAt,
attachedAgentCount,
editable: source.editable,
editableReason: source.editableReason,
sourceLabel: source.sourceLabel,
sourceBadge: source.sourceBadge,
};
}
export function companySkillService(db: Db) { export function companySkillService(db: Db) {
const agents = agentService(db); const agents = agentService(db);
const secretsSvc = secretService(db); const secretsSvc = secretService(db);
@ -905,21 +929,25 @@ export function companySkillService(db: Db) {
} }
async function list(companyId: string): Promise<CompanySkillListItem[]> { async function list(companyId: string): Promise<CompanySkillListItem[]> {
const rows = await listFull(companyId);
const agentRows = await agents.list(companyId);
return rows.map((skill) => {
const attachedAgentCount = agentRows.filter((agent) => {
const preference = readPaperclipSkillSyncPreference(agent.adapterConfig as Record<string, unknown>);
return preference.desiredSkills.includes(skill.slug);
}).length;
return toCompanySkillListItem(skill, attachedAgentCount);
});
}
async function listFull(companyId: string): Promise<CompanySkill[]> {
await ensureBundledSkills(companyId); await ensureBundledSkills(companyId);
const rows = await db const rows = await db
.select() .select()
.from(companySkills) .from(companySkills)
.where(eq(companySkills.companyId, companyId)) .where(eq(companySkills.companyId, companyId))
.orderBy(asc(companySkills.name), asc(companySkills.slug)); .orderBy(asc(companySkills.name), asc(companySkills.slug));
const agentRows = await agents.list(companyId); return rows.map((row) => toCompanySkill(row));
return rows.map((row) => {
const skill = toCompanySkill(row);
const attachedAgentCount = agentRows.filter((agent) => {
const preference = readPaperclipSkillSyncPreference(agent.adapterConfig as Record<string, unknown>);
return preference.desiredSkills.includes(skill.slug);
}).length;
return enrichSkill(skill, attachedAgentCount);
});
} }
async function getById(id: string) { async function getById(id: string) {
@ -1375,6 +1403,7 @@ export function companySkillService(db: Db) {
return { return {
list, list,
listFull,
getById, getById,
getBySlug, getBySlug,
detail, detail,