feat(012-admin-list-views): add GET /admin/products/:id/knowledge for governance
Follow-up to 012-admin-list-views, discovered while building supporthub- web's own knowledge-governance screen (001-agent-admin-ui User Story 7): GET /knowledge/retrieve only ever returns published entries (its own AI-consumption purpose), so a governance screen that needs to see and publish a draft entry had no endpoint to list it. Adds a small admin-list-views-style read query scoped to the knowledge module itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3bd068b031
commit
7948182988
@@ -20,6 +20,15 @@ export class KnowledgeController {
|
||||
return reply.status(201).send({ success: true, data: entry, meta: null });
|
||||
}
|
||||
|
||||
/** 012-admin-list-views follow-up: the governance screen's own data source (every status,
|
||||
* unlike GET /knowledge/retrieve which is published-only). */
|
||||
async listForGovernance(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { externalProductId } = request.params as { externalProductId: string };
|
||||
const productId = await resolveProductId(externalProductId);
|
||||
const entries = await this.service.listForGovernance(productId);
|
||||
return reply.status(200).send({ success: true, data: entries, meta: null });
|
||||
}
|
||||
|
||||
async publish(request: FastifyRequest, reply: FastifyReply) {
|
||||
const { code } = request.params as { code: string };
|
||||
const { effectiveDate } = publishKnowledgeEntrySchema.parse(request.body ?? {});
|
||||
|
||||
@@ -121,6 +121,16 @@ export class KnowledgeRepository {
|
||||
});
|
||||
}
|
||||
|
||||
/** 012-admin-list-views follow-up: every current-version entry for a product, any status —
|
||||
* `retrieve` below only ever returns `published` entries (AI-consumption path), so the
|
||||
* governance screen (which must see drafts to publish them) needs its own query. */
|
||||
async findAllForProduct(productId: string): Promise<KnowledgeEntry[]> {
|
||||
return this.prisma.knowledgeEntry.findMany({
|
||||
where: { productId, isCurrentVersion: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/** research.md "Retrieval — structured filtering": filters apply before any ranking; ranking
|
||||
* is validated-first, then most-recently-effective. */
|
||||
async retrieve(filters: RetrieveFilters): Promise<KnowledgeEntry[]> {
|
||||
|
||||
@@ -14,6 +14,12 @@ export async function knowledgeRoutes(fastify: FastifyInstance): Promise<void> {
|
||||
{ preHandler: [fastify.authenticate, requireRole('ADMIN')] },
|
||||
(req, reply) => knowledgeController.create(req, reply),
|
||||
);
|
||||
// 012-admin-list-views follow-up: the governance screen's own data source (every status).
|
||||
fastify.get(
|
||||
'/admin/products/:externalProductId/knowledge',
|
||||
{ preHandler: fastify.authenticate },
|
||||
(req, reply) => knowledgeController.listForGovernance(req, reply),
|
||||
);
|
||||
fastify.patch(
|
||||
'/admin/knowledge/:code/publish',
|
||||
{ preHandler: [fastify.authenticate, requireRole('ADMIN')] },
|
||||
|
||||
@@ -57,6 +57,12 @@ export class KnowledgeService {
|
||||
async retrieve(filters: RetrieveFilters): Promise<KnowledgeEntry[]> {
|
||||
return this.repo.retrieve(filters);
|
||||
}
|
||||
|
||||
/** 012-admin-list-views follow-up: every entry for a product, any status — the governance
|
||||
* screen's own data source (unlike `retrieve`, which is published-only). */
|
||||
async listForGovernance(productId: string): Promise<KnowledgeEntry[]> {
|
||||
return this.repo.findAllForProduct(productId);
|
||||
}
|
||||
}
|
||||
|
||||
export const knowledgeService = new KnowledgeService();
|
||||
|
||||
@@ -58,6 +58,38 @@ describe('Knowledge entry authoring, publishing, and versioning', () => {
|
||||
expect(afterPublish.json().data.find((e: { code: string }) => e.code === code)).toBeDefined();
|
||||
});
|
||||
|
||||
it('012-admin-list-views follow-up: the governance list shows a draft entry, unlike retrieve', async () => {
|
||||
const draftCode = `KB-TEST-DRAFT-${Date.now()}`;
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/admin/products/${externalProductId}/knowledge`,
|
||||
headers: authHeader(token),
|
||||
payload: { code: draftCode, type: 'faq', problem: 'Still a draft' },
|
||||
});
|
||||
|
||||
const governanceList = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/admin/products/${externalProductId}/knowledge`,
|
||||
headers: authHeader(token),
|
||||
});
|
||||
expect(governanceList.statusCode).toBe(200);
|
||||
const draftEntry = governanceList
|
||||
.json()
|
||||
.data.find((e: { code: string }) => e.code === draftCode);
|
||||
expect(draftEntry).toBeDefined();
|
||||
expect(draftEntry.status).toBe('draft');
|
||||
|
||||
const retrieveResult = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/knowledge/retrieve?productId=${externalProductId}`,
|
||||
});
|
||||
expect(
|
||||
retrieveResult.json().data.find((e: { code: string }) => e.code === draftCode),
|
||||
).toBeUndefined();
|
||||
|
||||
await prismaClient.knowledgeEntry.deleteMany({ where: { code: draftCode } });
|
||||
});
|
||||
|
||||
it('Scenario 2: editing creates a new version and preserves the prior one', async () => {
|
||||
const editResponse = await app.inject({
|
||||
method: 'PUT',
|
||||
|
||||
Reference in New Issue
Block a user