fix: coerce corpus route filters (#1776)

* fix: coerce corpus route filters

* test: cover unsupported corpus type filters
This commit is contained in:
suyua9
2026-04-15 15:58:01 +08:00
committed by GitHub
parent 2a2008bac2
commit eeb6841033
2 changed files with 232 additions and 4 deletions
@@ -12,6 +12,8 @@ import { CorpusBuilder } from '../../knowledge/CorpusBuilder.js';
import { KnowledgeAgent } from '../../knowledge/KnowledgeAgent.js';
import type { CorpusFilter } from '../../knowledge/types.js';
const ALLOWED_CORPUS_TYPES = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
export class CorpusRoutes extends BaseRouteHandler {
constructor(
private corpusStore: CorpusStore,
@@ -49,15 +51,31 @@ export class CorpusRoutes extends BaseRouteHandler {
const { name, description, project, types, concepts, files, query, date_start, date_end, limit } = req.body;
const coercedTypes = this.coerceStringArray(types, 'types', res);
if (coercedTypes === null) return;
if (coercedTypes && !coercedTypes.every(type => ALLOWED_CORPUS_TYPES.has(type))) {
this.badRequest(res, 'types must contain valid observation types');
return;
}
const coercedConcepts = this.coerceStringArray(concepts, 'concepts', res);
if (coercedConcepts === null) return;
const coercedFiles = this.coerceStringArray(files, 'files', res);
if (coercedFiles === null) return;
const coercedLimit = this.coercePositiveInteger(limit, 'limit', res);
if (coercedLimit === null) return;
const filter: CorpusFilter = {};
if (project) filter.project = project;
if (types) filter.types = types;
if (concepts) filter.concepts = concepts;
if (files) filter.files = files;
if (coercedTypes && coercedTypes.length > 0) filter.types = coercedTypes as CorpusFilter['types'];
if (coercedConcepts && coercedConcepts.length > 0) filter.concepts = coercedConcepts;
if (coercedFiles && coercedFiles.length > 0) filter.files = coercedFiles;
if (query) filter.query = query;
if (date_start) filter.date_start = date_start;
if (date_end) filter.date_end = date_end;
if (limit) filter.limit = limit;
if (coercedLimit !== undefined) filter.limit = coercedLimit;
const corpus = await this.corpusBuilder.build(name, description || '', filter);
@@ -66,6 +84,42 @@ export class CorpusRoutes extends BaseRouteHandler {
res.json(metadata);
});
private coerceStringArray(value: unknown, fieldName: string, res: Response): string[] | null | undefined {
if (value === undefined || value === null || value === '') {
return undefined;
}
let parsed = value;
if (typeof value === 'string') {
try {
parsed = JSON.parse(value);
} catch {
parsed = value.split(',').map(part => part.trim()).filter(Boolean);
}
}
if (!Array.isArray(parsed) || !parsed.every(item => typeof item === 'string')) {
this.badRequest(res, `${fieldName} must be an array of strings`);
return null;
}
return parsed.map(item => item.trim()).filter(Boolean);
}
private coercePositiveInteger(value: unknown, fieldName: string, res: Response): number | null | undefined {
if (value === undefined || value === null || value === '') {
return undefined;
}
const parsed = typeof value === 'string' ? Number(value) : value;
if (typeof parsed !== 'number' || !Number.isInteger(parsed) || parsed <= 0) {
this.badRequest(res, `${fieldName} must be a positive integer`);
return null;
}
return parsed;
}
/**
* List all corpora with stats
* GET /api/corpus