Add public API for methods and values
This commit is contained in:
@@ -1,24 +1,63 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { isDatabaseConfigured } from "../../../../lib/server/env";
|
||||
import {
|
||||
listCatalogCoreValues,
|
||||
listCatalogMethods,
|
||||
type CatalogCoreValueDto,
|
||||
type CatalogMethodDto,
|
||||
} from "../../../../lib/server/governanceCatalog";
|
||||
import { listMethodRecommendations } from "../../../../lib/server/methodRecommendations";
|
||||
import { apiRoute } from "../../../../lib/server/apiRoute";
|
||||
import { dbUnavailable, errorJson } from "../../../../lib/server/responses";
|
||||
import { CATALOG_SECTION_IDS } from "../../../../lib/create/customRuleFacets";
|
||||
import {
|
||||
SECTION_IDS,
|
||||
type CatalogSectionId,
|
||||
type SectionId,
|
||||
flattenRequestedFacets,
|
||||
parseRequestedFacetsFromSearchParams,
|
||||
} from "../../../../lib/server/validation/methodFacetsSchemas";
|
||||
|
||||
const SECTION_SET = new Set<string>(SECTION_IDS);
|
||||
const CATALOG_SECTION_SET = new Set<string>(CATALOG_SECTION_IDS);
|
||||
const CATALOG_CACHE_CONTROL = "public, max-age=3600";
|
||||
|
||||
/** Route query alias → canonical `coreValues`. */
|
||||
function normalizeSectionParam(raw: string): string {
|
||||
return raw === "values" ? "coreValues" : raw;
|
||||
}
|
||||
|
||||
type MethodMatch = { score: number; matchedFacets: string[] };
|
||||
|
||||
function rankCatalogMethods(
|
||||
catalog: CatalogMethodDto[],
|
||||
matchesBySlug: Record<string, MethodMatch> | null,
|
||||
includeMatches: boolean,
|
||||
): Array<CatalogMethodDto & { matches?: MethodMatch }> {
|
||||
if (!matchesBySlug || !includeMatches) {
|
||||
return catalog;
|
||||
}
|
||||
|
||||
const indexBySlug = new Map(catalog.map((m, i) => [m.slug, i]));
|
||||
const sorted = [...catalog].sort((a, b) => {
|
||||
const sa = matchesBySlug[a.slug]?.score ?? 0;
|
||||
const sb = matchesBySlug[b.slug]?.score ?? 0;
|
||||
if (sa !== sb) return sb - sa;
|
||||
return (indexBySlug.get(a.slug) ?? 0) - (indexBySlug.get(b.slug) ?? 0);
|
||||
});
|
||||
|
||||
return sorted.map((m) => ({
|
||||
...m,
|
||||
matches: matchesBySlug[m.slug] ?? { score: 0, matchedFacets: [] },
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/create-flow/methods?section=<section>[&facet.*=...]
|
||||
*
|
||||
* Returns slugs + per-method match scores for one of the four card-deck
|
||||
* sections; the wizard renders by looking up the slug in the section's
|
||||
* messages file (`useMessages().create.customRule.<section>.methods`).
|
||||
* Returns the full built-in catalog for one facet: four method decks
|
||||
* (with optional facet ranking) or all preset core values. Copy is
|
||||
* included in v1 (English only).
|
||||
*
|
||||
* See `docs/guides/template-recommendation-matrix.md` §9.2 / §10.
|
||||
* See `docs/guides/template-recommendation-matrix.md` §9.2 / §9.5 (CR-115).
|
||||
*/
|
||||
export const GET = apiRoute(
|
||||
"createFlow.methods.get",
|
||||
@@ -28,29 +67,59 @@ export const GET = apiRoute(
|
||||
}
|
||||
|
||||
const sectionParam = request.nextUrl.searchParams.get("section");
|
||||
if (!sectionParam || !SECTION_SET.has(sectionParam)) {
|
||||
if (!sectionParam) {
|
||||
return errorJson(
|
||||
"validation_error",
|
||||
`Unknown section. Expected one of: ${SECTION_IDS.join(", ")}`,
|
||||
`Unknown section. Expected one of: ${CATALOG_SECTION_IDS.join(", ")} (alias: values → coreValues)`,
|
||||
400,
|
||||
);
|
||||
}
|
||||
const section = sectionParam as SectionId;
|
||||
|
||||
const normalized = normalizeSectionParam(sectionParam);
|
||||
if (!CATALOG_SECTION_SET.has(normalized)) {
|
||||
return errorJson(
|
||||
"validation_error",
|
||||
`Unknown section. Expected one of: ${CATALOG_SECTION_IDS.join(", ")} (alias: values → coreValues)`,
|
||||
400,
|
||||
);
|
||||
}
|
||||
const section = normalized as CatalogSectionId;
|
||||
|
||||
if (section === "coreValues") {
|
||||
const methods: CatalogCoreValueDto[] = listCatalogCoreValues();
|
||||
return NextResponse.json(
|
||||
{ section: "coreValues" as const, methods },
|
||||
{ headers: { "Cache-Control": CATALOG_CACHE_CONTROL } },
|
||||
);
|
||||
}
|
||||
|
||||
const facets = parseRequestedFacetsFromSearchParams(
|
||||
request.nextUrl.searchParams,
|
||||
);
|
||||
const result = await listMethodRecommendations({ section, facets });
|
||||
if (!result) {
|
||||
// DB query failed; return empty so the wizard falls back to its messages
|
||||
// deck in authoring order (§10).
|
||||
return NextResponse.json({ section, methods: [] });
|
||||
const catalog = listCatalogMethods(section as SectionId);
|
||||
const facetSection = section as SectionId;
|
||||
|
||||
const ranking = await listMethodRecommendations({
|
||||
section: facetSection,
|
||||
facets,
|
||||
});
|
||||
|
||||
const includeMatches = flattenRequestedFacets(facets).length > 0;
|
||||
|
||||
let matchesBySlug: Record<string, MethodMatch> | null = null;
|
||||
if (includeMatches && ranking) {
|
||||
matchesBySlug = ranking.matchesBySlug;
|
||||
}
|
||||
|
||||
const methods = result.rankedSlugs.map((slug) => ({
|
||||
slug,
|
||||
matches: result.matchesBySlug[slug] ?? { score: 0, matchedFacets: [] },
|
||||
}));
|
||||
return NextResponse.json({ section, methods });
|
||||
const methods = rankCatalogMethods(
|
||||
catalog,
|
||||
matchesBySlug,
|
||||
includeMatches,
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ section, methods },
|
||||
{ headers: { "Cache-Control": CATALOG_CACHE_CONTROL } },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { isDatabaseConfigured } from "../../../../lib/server/env";
|
||||
import { getRuleTemplateBySlug } from "../../../../lib/server/ruleTemplates";
|
||||
import { templateMethodsFromBody } from "../../../../lib/server/templateMethods";
|
||||
import { apiRoute } from "../../../../lib/server/apiRoute";
|
||||
import { dbUnavailable, notFound } from "../../../../lib/server/responses";
|
||||
|
||||
type RouteContext = { params: Promise<{ slug: string }> };
|
||||
|
||||
const CATALOG_CACHE_CONTROL = "public, max-age=3600";
|
||||
|
||||
/**
|
||||
* GET /api/templates/[slug]
|
||||
*
|
||||
* Single seeded template plus normalized `(section, slug)` composition
|
||||
* derived from `body`. Public read; 404 when unknown.
|
||||
*
|
||||
* See `docs/guides/template-recommendation-matrix.md` §9.4 (CR-115).
|
||||
*/
|
||||
export const GET = apiRoute<RouteContext>(
|
||||
"templates.bySlug",
|
||||
async (_request: NextRequest, context) => {
|
||||
if (!isDatabaseConfigured()) {
|
||||
return dbUnavailable();
|
||||
}
|
||||
|
||||
const { slug } = await context.params;
|
||||
const template = await getRuleTemplateBySlug(slug);
|
||||
if (!template) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const methods = templateMethodsFromBody(template.body);
|
||||
return NextResponse.json(
|
||||
{ template, methods },
|
||||
{ headers: { "Cache-Control": CATALOG_CACHE_CONTROL } },
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user