From a266fa54289a380f3915f1a8f063527c13363d5e Mon Sep 17 00:00:00 2001 From: Robert Bond Date: Thu, 25 Jun 2026 08:48:43 +0000 Subject: [PATCH] Merged PR 2429: Add public search API grouping facade slice Add public search API grouping facade slice Related work items: #23754 --- actions/services/searchDirectService.js | 10 +- context/api-grouping-adoption-roadmap.md | 41 ++++ memory-bank/change-log.md | 86 ++++++++ pages/api/search/advanced-paged.js | 5 + pages/api/search/advanced.js | 5 + pages/api/search/basic-details-paged.js | 5 + pages/api/search/basic-paged.js | 5 + .../api-grouping-facade-search.test.cjs | 199 ++++++++++++++++++ 8 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 pages/api/search/advanced-paged.js create mode 100644 pages/api/search/advanced.js create mode 100644 pages/api/search/basic-details-paged.js create mode 100644 pages/api/search/basic-paged.js create mode 100644 tests/phase22/api-grouping-facade-search.test.cjs diff --git a/actions/services/searchDirectService.js b/actions/services/searchDirectService.js index 5a3385de..9eee7403 100644 --- a/actions/services/searchDirectService.js +++ b/actions/services/searchDirectService.js @@ -51,7 +51,7 @@ export const getBasicSearchPaged = ( showNumberOfRecords ) => { return getJson( - "/api/endpoint/getbasicsearchpaged_api?searchString=" + + "/api/search/basic-paged?searchString=" + searchString + "&pageNumber=" + pageNumber + @@ -103,7 +103,7 @@ export const getBasicDNSSearchPaged = ( export const getAdvancedSearch = (searchString) => { return getJson( BASE_URL + - "/api/endpoint/getadvancedsearch_api?searchstring=" + + "/api/search/advanced?searchstring=" + JSON.stringify(searchString) ).catch(logAndReturnEmptyValueErrorResponse); }; @@ -116,7 +116,7 @@ export const getAdvancedSearchPaged = ( showNumberOfRecords ) => { return getJson( - "/api/endpoint/getadvancedsearchpaged_api?searchstring=" + + "/api/search/advanced-paged?searchstring=" + JSON.stringify(searchString) + "&pageNumber=" + pageNumber + @@ -186,12 +186,12 @@ export const getBasicSearchDetailsPaged = async ( }) .join(" or "); - const url = `/api/endpoint/getbasicsearchdetailspaged_api?appealTypeName=${appealTypeName}&primaryIdAttribute=${primaryIdAttribute}&incidentID=${encodeURIComponent( + const groupedUrl = `/api/search/basic-details-paged?appealTypeName=${appealTypeName}&primaryIdAttribute=${primaryIdAttribute}&incidentID=${encodeURIComponent( filter )}`; try { - const data = await getJson(url); + const data = await getJson(groupedUrl); return data.value || []; } catch (error) { consoleLogger(error); diff --git a/context/api-grouping-adoption-roadmap.md b/context/api-grouping-adoption-roadmap.md index 19925c20..3cd3243c 100644 --- a/context/api-grouping-adoption-roadmap.md +++ b/context/api-grouping-adoption-roadmap.md @@ -13,6 +13,9 @@ Pilot status update (2026-06-25): - active published-document metadata consumer (`actions/services/searchDirectService.js`) now targets grouped documents façade routes for details, paged details, and document-type reads - published document download remains on the existing grouped runtime path `pages/api/documents/download/[id].js` - legacy document metadata endpoint routes remain canonical and stable behind the additive façade wrappers +- third additive grouping façade vertical slice implemented for the active Public Search results journey under `pages/api/search/` +- active public search consumer (`actions/services/searchDirectService.js`) now targets grouped search façade routes for the proven in-scope public results path +- legacy public search endpoint routes remain canonical and stable behind the additive façade wrappers - legacy watched-case endpoint routes remain canonical and stable - no old route removal or migration has occurred - runtime behaviour is intended to remain unchanged because the grouped routes delegate to the existing handlers @@ -218,6 +221,44 @@ It means: - download behaviour remains unchanged - no route migration, deletion, or contract change has occurred +#### Additional vertical-slice proof point — Public Search + +- The active Public Search journey now has a third complete additive grouping slice for the bounded public results path: + - grouped façade routes exist under `pages/api/search/` for the proven active service calls: + - `basic-paged` + - `advanced` + - `advanced-paged` + - `basic-details-paged` + - `actions/services/searchDirectService.js` targets those grouped routes for: + - `getBasicSearchPaged` + - `getAdvancedSearch` + - `getAdvancedSearchPaged` + - `getBasicSearchDetailsPaged` + - legacy endpoint handlers remain present and canonical behind the façade: + - `pages/api/endpoint/getbasicsearchpaged_api.js` + - `pages/api/endpoint/getadvancedsearch_api.js` + - `pages/api/endpoint/getadvancedsearchpaged_api.js` + - `pages/api/endpoint/getbasicsearchdetailspaged_api.js` + - runtime behaviour remains unchanged through direct delegation and unchanged service-layer query contracts +- The bounded public-search audit also established that: + - the active public results UI is driven through `components/search/searchresults.js` + - the common service layer is `actions/services/searchDirectService.js` + - paged basic results are active in the public results path + - advanced search is in scope because `pages/advancedsearchresults.js` actively uses the advanced bootstrap route and the shared results UI actively uses the advanced paged route + - paged search-detail expansion is in scope because the active results UI uses `getSearchDetailsPaged(...)`, which calls `getBasicSearchDetailsPaged(...)` + - the following routes remain outside this grouped adoption scope because they belong to adjacent or excluded journeys rather than the active bounded public results path: + - `getbasicsearch_api.js` (used for adjacent case-detail bootstrap and other non-results contexts) + - `getbasicsearchdetails_api.js` (used for adjacent case-detail/bootstrap detail expansion) + - address-search variants + - DNS search variants + - myportal search pages +- For this Public Search slice, a complete vertical grouping slice means: + - active public results journey calls have grouped façade routes + - the active common service layer uses those grouped routes + - legacy handlers remain present and canonical + - excluded adjacent search journeys remain unchanged + - no route migration, deletion, or contract change has occurred + --- ### Stage 3 — Documentation Maturity diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 72d9c6fe..cb7f208f 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -18,6 +18,92 @@ Follow-ups: --- +### CL-2026-06-25-API-GROUPING-FACADE-SEARCH-VERTICAL-SLICE: active public search results grouping and service adoption + +date: 2026-06-25 +author: Cline +scope: `pages/api/search/basic-paged.js`, `pages/api/search/advanced.js`, `pages/api/search/advanced-paged.js`, `pages/api/search/basic-details-paged.js`, `actions/services/searchDirectService.js`, `tests/phase22/api-grouping-facade-search.test.cjs`, `context/api-grouping-adoption-roadmap.md` +type: change +rationale: Implement the next additive API grouping façade vertical slice by grouping the active Public Search results journey under `pages/api/search/` and adopting the grouped routes only in the existing common public-search service layer, while preserving legacy endpoint handlers unchanged and leaving adjacent/excluded search journeys alone. +impact: Runtime behaviour is intended to remain unchanged; grouped search façade routes now cover the proven active public-results path, `actions/services/searchDirectService.js` now targets those grouped routes for the bounded in-scope calls, legacy endpoint handlers remain canonical, and no migration, route deletion, query-parameter change, paging change, sorting change, payload change, or response-contract change has occurred. +status: completed + +Summary: + +- Confirmed the required context was read before implementation: + - `context/api-grouping-plan.md` + - `context/api-grouping-adoption-roadmap.md` + - `context/api-route-map.md` + - `context/journey-architecture-map.md` + - `context/portal-api-platform-assessment.md` + - `context/architecture.md` + - `memory-bank/change-log.md` +- Performed a bounded audit of the active Public Search journey across pages, components, services, and endpoint usage. +- Audit findings established that: + - the active public results UI is driven through `pages/searchresults.js` and `components/search/searchresults.js` + - the advanced results page `pages/advancedsearchresults.js` is part of the active public-search scope + - the common owning service layer is `actions/services/searchDirectService.js` + - `components/search/searchresults.js` actively uses: + - `getBasicSearchPaged` + - `getAdvancedSearchPaged` + - `getSearchDetailsPaged(...)`, which in turn uses `getBasicSearchDetailsPaged` + - `pages/advancedsearchresults.js` actively uses `getAdvancedSearch(...)` for bootstrap + - `getbasicsearch_api.js` and `getbasicsearchdetails_api.js` remain active in adjacent case-detail/bootstrap paths and therefore remained out of scope for this bounded public-results slice + - address-search routes, DNS routes, and myportal search pages were intentionally excluded as separate journeys +- Added grouped façade routes under `pages/api/search/` for the proven active public-results path only: + - `basic-paged.js` + - `advanced.js` + - `advanced-paged.js` + - `basic-details-paged.js` +- Implemented each façade as the smallest safe compatibility wrapper: + - import the existing legacy endpoint handler + - delegate `req` and `res` directly to that existing handler +- Updated only the suitable common service layer in `actions/services/searchDirectService.js` to target grouped façade routes for: + - `getBasicSearchPaged` + - `getAdvancedSearch` + - `getAdvancedSearchPaged` + - `getBasicSearchDetailsPaged` +- Left the following unchanged: + - existing endpoint handlers + - query parameter names + - CRM queries + - paging behaviour + - sorting behaviour + - search semantics + - case-detail behaviour + - document behaviour + - excluded search journeys +- Added focused characterization coverage to prove: + - grouped search façade routes delegate correctly + - active service methods now target grouped search routes + - legacy endpoint handlers remain present + - adopted service methods no longer target the legacy endpoint URLs directly + - request-contract strings/parameter names remain unchanged + - excluded routes remain outside grouped adoption scope +- Updated the adoption roadmap to record the active Public Search grouping slice and its exact scope boundary. + +Validation: + +- Focused validation only intended for this slice: + - `npx eslint actions/services/searchDirectService.js pages/api/search/*.js tests/phase22/api-grouping-facade-search.test.cjs` + - `node tests/phase22/api-grouping-facade-search.test.cjs` +- No repository-wide validation run. + +Follow-ups: + +- The active Public Search journey is now a complete vertical grouping slice for the bounded public-results scope: + - grouped façade routes + - active service-layer adoption + - legacy canonical handlers retained + - unchanged behaviour +- Adjacent or excluded search journeys should remain separate future slices if needed: + - case-detail bootstrap search + - address search + - DNS search + - myportal search + +--- + ### CL-2026-06-25-API-GROUPING-FACADE-DOCUMENTS-VERTICAL-SLICE: active published-document metadata grouping and service adoption date: 2026-06-25 diff --git a/pages/api/search/advanced-paged.js b/pages/api/search/advanced-paged.js new file mode 100644 index 00000000..ee040d8e --- /dev/null +++ b/pages/api/search/advanced-paged.js @@ -0,0 +1,5 @@ +import legacyAdvancedSearchPagedHandler from "../endpoint/getadvancedsearchpaged_api"; + +export default async function advancedSearchPagedFacade(req, res) { + return legacyAdvancedSearchPagedHandler(req, res); +} diff --git a/pages/api/search/advanced.js b/pages/api/search/advanced.js new file mode 100644 index 00000000..d662b0ac --- /dev/null +++ b/pages/api/search/advanced.js @@ -0,0 +1,5 @@ +import legacyAdvancedSearchHandler from "../endpoint/getadvancedsearch_api"; + +export default async function advancedSearchFacade(req, res) { + return legacyAdvancedSearchHandler(req, res); +} diff --git a/pages/api/search/basic-details-paged.js b/pages/api/search/basic-details-paged.js new file mode 100644 index 00000000..14b9fa27 --- /dev/null +++ b/pages/api/search/basic-details-paged.js @@ -0,0 +1,5 @@ +import legacyBasicSearchDetailsPagedHandler from "../endpoint/getbasicsearchdetailspaged_api"; + +export default async function basicSearchDetailsPagedFacade(req, res) { + return legacyBasicSearchDetailsPagedHandler(req, res); +} diff --git a/pages/api/search/basic-paged.js b/pages/api/search/basic-paged.js new file mode 100644 index 00000000..cd5742ad --- /dev/null +++ b/pages/api/search/basic-paged.js @@ -0,0 +1,5 @@ +import legacyBasicSearchPagedHandler from "../endpoint/getbasicsearchpaged_api"; + +export default async function basicSearchPagedFacade(req, res) { + return legacyBasicSearchPagedHandler(req, res); +} diff --git a/tests/phase22/api-grouping-facade-search.test.cjs b/tests/phase22/api-grouping-facade-search.test.cjs new file mode 100644 index 00000000..4b3c8d06 --- /dev/null +++ b/tests/phase22/api-grouping-facade-search.test.cjs @@ -0,0 +1,199 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const { loadModule, createRes } = require("../phase21/_shared.cjs"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +const read = (relativePath) => + fs.readFileSync(path.join(rootDir, relativePath), "utf8"); + +test("search basic-paged facade delegates to legacy handler", async () => { + const calls = []; + const mod = loadModule("pages/api/search/basic-paged.js", { + legacyBasicSearchPagedHandler: async (req, res) => { + calls.push({ req, res }); + return res.status(200).json({ ok: true, route: "basic-paged" }); + } + }); + + const req = { + method: "GET", + query: { + searchString: "cas", + pageNumber: "1", + orderby: "createdon", + fieldSort: "desc", + showNumberOfRecords: "10" + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].req, req); + assert.strictEqual(calls[0].res, res); +}); + +test("search advanced facade delegates to legacy handler", async () => { + const calls = []; + const mod = loadModule("pages/api/search/advanced.js", { + legacyAdvancedSearchHandler: async (req, res) => { + calls.push({ req, res }); + return res.status(200).json({ ok: true, route: "advanced" }); + } + }); + + const req = { method: "GET", query: { searchstring: '{"q":"cas"}' } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].req, req); + assert.strictEqual(calls[0].res, res); +}); + +test("search advanced-paged facade delegates to legacy handler", async () => { + const calls = []; + const mod = loadModule("pages/api/search/advanced-paged.js", { + legacyAdvancedSearchPagedHandler: async (req, res) => { + calls.push({ req, res }); + return res.status(200).json({ ok: true, route: "advanced-paged" }); + } + }); + + const req = { + method: "GET", + query: { + searchstring: '{"q":"cas"}', + pageNumber: "1", + orderby: "createdon", + fieldSort: "desc", + showNumberOfRecords: "10" + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].req, req); + assert.strictEqual(calls[0].res, res); +}); + +test("search basic-details-paged facade delegates to legacy handler", async () => { + const calls = []; + const mod = loadModule("pages/api/search/basic-details-paged.js", { + legacyBasicSearchDetailsPagedHandler: async (req, res) => { + calls.push({ req, res }); + return res.status(200).json({ value: [{ ticketnumber: "CAS-1" }] }); + } + }); + + const req = { + method: "GET", + query: { + appealTypeName: "pinswg_planningappeals78s", + primaryIdAttribute: "pinswg_planningappeals78id", + incidentID: "_pinswg_planningappeals78ids_value eq abc" + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].req, req); + assert.strictEqual(calls[0].res, res); +}); + +test("legacy public search endpoint handlers remain present", async () => { + const legacyFiles = [ + "pages/api/endpoint/getbasicsearchpaged_api.js", + "pages/api/endpoint/getadvancedsearch_api.js", + "pages/api/endpoint/getadvancedsearchpaged_api.js", + "pages/api/endpoint/getbasicsearchdetailspaged_api.js", + "pages/api/endpoint/getbasicsearch_api.js", + "pages/api/endpoint/getbasicsearchdetails_api.js" + ]; + + legacyFiles.forEach((filePath) => { + assert.strictEqual( + fs.existsSync(path.join(rootDir, filePath)), + true, + filePath + ); + }); +}); + +test("searchDirectService active public search methods now target grouped search routes", async () => { + const source = read("actions/services/searchDirectService.js"); + + assert.match(source, /\/api\/search\/basic-paged\?/); + assert.match(source, /\/api\/search\/advanced\?/); + assert.match(source, /\/api\/search\/advanced-paged\?/); + assert.match(source, /\/api\/search\/basic-details-paged\?/); +}); + +test("searchDirectService adopted public search methods no longer target legacy endpoint URLs", async () => { + const source = read("actions/services/searchDirectService.js"); + + assert.doesNotMatch(source, /\/api\/endpoint\/getbasicsearchpaged_api/); + assert.doesNotMatch(source, /\/api\/endpoint\/getadvancedsearch_api/); + assert.doesNotMatch(source, /\/api\/endpoint\/getadvancedsearchpaged_api/); + assert.doesNotMatch( + source, + /\/api\/endpoint\/getbasicsearchdetailspaged_api/ + ); +}); + +test("searchDirectService excluded routes remain outside grouped public search adoption scope", async () => { + const source = read("actions/services/searchDirectService.js"); + + assert.match(source, /\/api\/endpoint\/getbasicsearch_api/); + assert.match(source, /\/api\/endpoint\/getbasicsearchdetails_api/); + assert.match( + source, + /\/api\/endpoint\/getbasicsearch_by_address_paged_api/ + ); + assert.match(source, /\/api\/endpoint\/getbasicsearch_by_address_api/); + assert.match(source, /\/api\/endpoint\/getbasicdnssearch_api/); + assert.match(source, /\/api\/endpoint\/getbasicdnssearchpaged_api/); +}); + +test("searchDirectService public search request contract strings are preserved", async () => { + const source = read("actions/services/searchDirectService.js"); + + assert.match(source, /searchString=/); + assert.match(source, /searchstring=/); + assert.match(source, /pageNumber=/); + assert.match(source, /orderby=/); + assert.match(source, /fieldSort=/); + assert.match(source, /showNumberOfRecords=/); + assert.match(source, /appealTypeName=/); + assert.match(source, /primaryIdAttribute=/); + assert.match(source, /incidentID=/); +}); + +async function run() { + let failures = 0; + + for (const { name, fn } of tests) { + try { + await fn(); + console.log(`✓ ${name}`); + } catch (error) { + failures += 1; + console.error(`✗ ${name}`); + console.error(error); + } + } + + if (failures > 0) { + process.exit(1); + } +} + +run();