diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 77cc6362..50b7b973 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1071,3 +1071,39 @@ Validation: Follow-ups: - Continue P2-S2 with remaining legacy relay GET candidates (`createwatchedcases_api`, `deletewatchedcasesproxy_api`, `getadvancedsearch_api`, `getbasicsearch_by_address_api`, `getbasicsearch_by_lparref_api`, `getdnscoords_api`). + +--- + +### CL-029: TASK22229 P2-S2 Batch 12 (remaining relay GET candidates) + +date: 2026-03-24 +author: Cline +scope: `pages/api/middleware/relayForwarding.js`, `pages/api/endpoint/{createwatchedcases_api,deletewatchedcasesproxy_api,getadvancedsearch_api,getbasicsearch_by_address_api,getbasicsearch_by_lparref_api,getdnscoords_api}.js`, `tests/phase21/endpoint-handler-contract.test.cjs` +type: change +rationale: Complete Batch 12 by migrating the remaining listed legacy relay GET candidates to shared forwarding helpers while preserving endpoint-specific validation, transforms, and error contracts. +impact: Consolidated relay GET behavior and reduced endpoint-level relay boilerplate; no intended API contract changes. +status: completed + +Summary: + +- Extended relay middleware with reusable helpers: + - `relayGetData` now supports token reuse via `transformData` path + - Added `forwardGetData` to support custom base URLs (used by proxy-to-local endpoint) while preserving hash behavior +- Migrated remaining Batch 12 candidates: + - `getadvancedsearch_api.js` -> `relayGet` + `relayGetData` for project-type enrichment loop + - `getdnscoords_api.js` -> `relayGetData` for both DNS + SIPS source pulls + - `getbasicsearch_by_lparref_api.js` -> `relayGetData` + - `getbasicsearch_by_address_api.js` -> `relayGetData` for appeal-type and incident fetches + - `deletewatchedcasesproxy_api.js` -> `forwardGetData` with `BASE_URL` + - `createwatchedcases_api.js` -> `relayGetData` for record-exists pre-check (POST/PATCH upsert flow preserved) +- Preserved endpoint-specific guard and error contracts (including status codes such as 500 for `BASIC_SEARCH_BY_ADDRESS_FETCH_FAILED`). +- Updated phase21 endpoint contract tests for migrated handlers to mock `relayGetData` / `forwardGetData` where appropriate. + +Validation: + +- `node tests/phase21/endpoint-handler-contract.test.cjs` -> pass (152/152) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Batch 12 completes the previously listed remaining P2-S2 relay GET candidates. diff --git a/pages/api/endpoint/createwatchedcases_api.js b/pages/api/endpoint/createwatchedcases_api.js index da199830..0c67b6a2 100644 --- a/pages/api/endpoint/createwatchedcases_api.js +++ b/pages/api/endpoint/createwatchedcases_api.js @@ -15,26 +15,30 @@ import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; +import { relayGetData } from "../middleware/relayForwarding"; const WEBAPI_URL = process.env.RELAY_ROOT || "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; -const recordExists = async (incidentId, contactId, token) => { +const recordExists = async (incidentId, contactId, accessToken) => { const filter = `$filter=pinswg_WatchedCase/incidentid eq ${incidentId} and pinswg_Contact/contactid eq ${contactId}`; const queryUrl = `pinswg_watchlists?${filter}`; - const url = WEBAPI_URL + queryUrl + hashAPIPath(queryUrl); try { - const res = await axios.get(url, { - headers: { - Authorization: "Bearer " + token.access_token, - Accept: "application/json" - } + const response = await relayGetData({ + queryUrl, + accessToken, + requestOptionsBuilder: (token) => ({ + headers: { + Authorization: "Bearer " + token, + Accept: "application/json" + } + }) }); - if (res.data.value && res.data.value.length > 0) { - return res.data.value[0]; // return the existing record + if (response.data.value && response.data.value.length > 0) { + return response.data.value[0]; // return the existing record } return null; @@ -79,7 +83,11 @@ export default async function ApiProxy(req, res) { const incidentId = watchedCaseMatch[1]; const contactId = contactMatch[1]; - const existingRecord = await recordExists(incidentId, contactId, token); + const existingRecord = await recordExists( + incidentId, + contactId, + token.access_token + ); let method, queryUrl; diff --git a/pages/api/endpoint/deletewatchedcasesproxy_api.js b/pages/api/endpoint/deletewatchedcasesproxy_api.js index 4aa4466a..0a1b8246 100644 --- a/pages/api/endpoint/deletewatchedcasesproxy_api.js +++ b/pages/api/endpoint/deletewatchedcasesproxy_api.js @@ -17,16 +17,10 @@ * description: Success */ -import axios from "axios"; import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; -import { getToken } from "../../../actions/core/token"; -import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +import { forwardGetData } from "../middleware/relayForwarding"; const BASE_URL = process.env.API_ROOT || "http://localhost:3000"; @@ -45,15 +39,15 @@ export default async function ApiProxy(req, res) { } try { - const token = await getToken(); const queryUrl = "/api/endpoint/deletewatchedcases_api?watchedCaseID=" + watchedCaseID; - const { data } = await axios.get( - BASE_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) - ); + const { data } = await forwardGetData({ + baseUrl: BASE_URL, + queryUrl, + requestOptionsBuilder: (accessToken) => azureHeaders(accessToken) + }); return respondSuccess(res, data); } catch (error) { diff --git a/pages/api/endpoint/getadvancedsearch_api.js b/pages/api/endpoint/getadvancedsearch_api.js index 2f504689..e90592b5 100644 --- a/pages/api/endpoint/getadvancedsearch_api.js +++ b/pages/api/endpoint/getadvancedsearch_api.js @@ -21,16 +21,10 @@ * description: Success */ -import axios from "axios"; import { azureHeadersPaged } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; -import { getToken } from "../../../actions/core/token"; -import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +import { relayGet, relayGetData } from "../middleware/relayForwarding"; export default async function ApiProxy(req, res) { const encodedSearchString = req.query.searchstring; @@ -109,62 +103,63 @@ export default async function ApiProxy(req, res) { queryString + " pinswg_appealcasetype ne null and pinswg_publishtoweb eq true&$orderby=createdon desc&$count=true"; - try { - const token = await getToken(); - const { data } = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeadersPaged(token.access_token) - ); - - if (Object.prototype.hasOwnProperty.call(data, "@odata.nextLink")) { - const dataStr = JSON.stringify(data["@odata.nextLink"]); - data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]; - } - - if (searchString.projecttype != null) { - for (let i = 0; i < data.value.length; i++) { - const item = data.value[i]; - - try { - const projectTypeQuery = - "pinswg_sipses?$filter=_pinswg_sipscase_value eq " + - item.incidentid + - " and _pinswg_projecttype_value eq " + - searchString.projecttype + - "&$select=_pinswg_projecttype_value"; - - const response = await axios.get( - WEBAPI_URL + - projectTypeQuery + - hashAPIPath(projectTypeQuery), - azureHeadersPaged(token.access_token) - ); - - if ( - response.data.value.length > 0 && - response.data.value[0]._pinswg_projecttype_value != null - ) { - Object.assign(item, response.data.value[0]); - } - } catch (error) { - consoleLogger(error); - } + return relayGet({ + queryUrl, + res, + requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken), + transformData: async (data, accessToken) => { + if (Object.prototype.hasOwnProperty.call(data, "@odata.nextLink")) { + const dataStr = JSON.stringify(data["@odata.nextLink"]); + data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]; } - data.value = data.value.filter( - (item) => item._pinswg_projecttype_value != null - ); + if (searchString.projecttype != null) { + for (let i = 0; i < data.value.length; i++) { + const item = data.value[i]; - data["@odata.count"] = data.value.length; - } + try { + const projectTypeQuery = + "pinswg_sipses?$filter=_pinswg_sipscase_value eq " + + item.incidentid + + " and _pinswg_projecttype_value eq " + + searchString.projecttype + + "&$select=_pinswg_projecttype_value"; - return respondSuccess(res, data); - } catch (error) { - consoleLogger(error); - return respondError(res, { + const projectTypeResponse = await relayGetData({ + queryUrl: projectTypeQuery, + accessToken, + requestOptionsBuilder: (token) => + azureHeadersPaged(token) + }); + + if ( + projectTypeResponse.data.value.length > 0 && + projectTypeResponse.data.value[0] + ._pinswg_projecttype_value != null + ) { + Object.assign( + item, + projectTypeResponse.data.value[0] + ); + } + } catch (error) { + consoleLogger(error); + } + } + + data.value = data.value.filter( + (item) => item._pinswg_projecttype_value != null + ); + + data["@odata.count"] = data.value.length; + } + + return data; + }, + errorResponse: { status: 400, code: "ADVANCED_SEARCH_FETCH_FAILED", message: "Failed to fetch advanced search results" - }); - } + } + }); } diff --git a/pages/api/endpoint/getbasicsearch_by_address_api.js b/pages/api/endpoint/getbasicsearch_by_address_api.js index 02ab6644..477fdbed 100644 --- a/pages/api/endpoint/getbasicsearch_by_address_api.js +++ b/pages/api/endpoint/getbasicsearch_by_address_api.js @@ -1,14 +1,8 @@ -import axios from "axios"; import { JSONPath as jsonpath } from "jsonpath-plus"; -import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; -import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +import { relayGetData } from "../middleware/relayForwarding"; const escapeODataString = (value = "") => String(value).replace(/'/g, "''").trim(); @@ -366,15 +360,15 @@ const fetchSearchResultsForAppealType = async (item, token, params) => { "\n\n", item.LogicalCollectionName, "\n\n", - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + queryUrl, "\n==========================================\n" ); try { - const { data } = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) - ); + const { data } = await relayGetData({ + queryUrl, + accessToken: token.access_token + }); return (data.value || []).map((record) => ({ ...record, @@ -391,10 +385,10 @@ const fetchIncident = async (incidentId, token) => { const queryUrl = buildIncidentQueryUrl(incidentId); try { - const { data } = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) - ); + const { data } = await relayGetData({ + queryUrl, + accessToken: token.access_token + }); return data?.value?.[0] || null; } catch (error) { diff --git a/pages/api/endpoint/getbasicsearch_by_lparref_api.js b/pages/api/endpoint/getbasicsearch_by_lparref_api.js index 228bf990..ada7ae26 100644 --- a/pages/api/endpoint/getbasicsearch_by_lparref_api.js +++ b/pages/api/endpoint/getbasicsearch_by_lparref_api.js @@ -1,13 +1,6 @@ -import axios from "axios"; -import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; -import { getToken } from "../../../actions/core/token"; -import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +import { relayGetData } from "../middleware/relayForwarding"; // ✅ IMPORTANT: Only include the 23 types that actually contain pinswg_lpaapplicationreference // You can copy from your existing appealTypeArray but only keep those entities. @@ -281,7 +274,6 @@ var appealTypeArrayWithLpaRef = [ export default async function ApiProxy(req, res) { try { - const token = await getToken(); let lpaRef = (req.query.lpaRef || "").trim(); if (!lpaRef) { @@ -321,10 +313,7 @@ export default async function ApiProxy(req, res) { `&$count=true` + `&$orderby=modifiedon desc`; - const { data } = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) - ); + const { data } = await relayGetData({ queryUrl }); // Normalize response rows const normalized = (data?.value || []).map((r) => ({ diff --git a/pages/api/endpoint/getdnscoords_api.js b/pages/api/endpoint/getdnscoords_api.js index 4ecbdc19..1d253873 100644 --- a/pages/api/endpoint/getdnscoords_api.js +++ b/pages/api/endpoint/getdnscoords_api.js @@ -1,14 +1,7 @@ -import axios from "axios"; -import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; -import { getToken } from "../../../actions/core/token"; import OSPoint from "ospoint"; -import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +import { relayGetData } from "../middleware/relayForwarding"; const renameKeys = (obj) => { const keyMappings = { @@ -75,8 +68,6 @@ const keysToRemove = [ ]; export default async function ApiProxy(req, res) { - const token = await getToken(); - const queryUrl = "pinswg_dnses?$select=pinswg_projectlocation,pinswg_mapzoomlevel,pinswg_name,pinswg_anticipatedgridreferenceeastingtext,pinswg_anticipatedgridreferencenorthingtext,pinswg_projectname,pinswg_dnsid,_pinswg_associatedlpa_value,_pinswg_appellant_value&$filter=pinswg_anticipatedgridreferencenorthingtext ne null and pinswg_anticipatedgridreferenceeastingtext ne null&$count=true"; @@ -87,11 +78,8 @@ export default async function ApiProxy(req, res) { const coordsObj = { value: [] }; try { - // First Axios request - const dnsCoordsObj = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) - ); + // First relay request + const dnsCoordsObj = await relayGetData({ queryUrl }); // Merge the first response's 'value' array into the coordsObj's value array if (Array.isArray(dnsCoordsObj.data.value)) { coordsObj.value = coordsObj.value.concat(dnsCoordsObj.data.value); // Concatenate arrays @@ -99,11 +87,8 @@ export default async function ApiProxy(req, res) { coordsObj.value.push(dnsCoordsObj.data.value); // Push single value if it's not an array } - // Second Axios request - const sipsCoordsObj = await axios.get( - WEBAPI_URL + queryUrlSips + hashAPIPath(queryUrlSips), - azureHeaders(token.access_token) - ); + // Second relay request + const sipsCoordsObj = await relayGetData({ queryUrl: queryUrlSips }); // Merge the second response's 'value' array into the coordsObj's value array if (Array.isArray(sipsCoordsObj.data.value)) { coordsObj.value = coordsObj.value.concat(sipsCoordsObj.data.value); // Concatenate arrays diff --git a/pages/api/middleware/relayForwarding.js b/pages/api/middleware/relayForwarding.js index 45f07dd8..f5b4c16c 100644 --- a/pages/api/middleware/relayForwarding.js +++ b/pages/api/middleware/relayForwarding.js @@ -17,20 +17,60 @@ export const relayGet = async ({ requestOptionsBuilder }) => { try { - const token = await getToken(); - const { data } = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - typeof requestOptionsBuilder === "function" - ? requestOptionsBuilder(token.access_token) - : azureHeaders(token.access_token) - ); + const { data, accessToken } = await relayGetData({ + queryUrl, + requestOptionsBuilder + }); return respondSuccess( res, - typeof transformData === "function" ? transformData(data) : data + typeof transformData === "function" + ? await transformData(data, accessToken) + : data ); } catch (error) { consoleLogger(error); return respondError(res, errorResponse); } }; + +export const relayGetData = async ({ + queryUrl, + requestOptionsBuilder, + accessToken +}) => { + return forwardGetData({ + queryUrl, + requestOptionsBuilder, + accessToken + }); +}; + +export const forwardGetData = async ({ + baseUrl, + queryUrl, + requestOptionsBuilder, + accessToken, + appendHash = true +}) => { + const tokenAccessToken = + typeof accessToken === "string" && accessToken.length > 0 + ? accessToken + : (await getToken()).access_token; + + const resolvedBaseUrl = baseUrl || WEBAPI_URL; + const finalUrl = + resolvedBaseUrl + queryUrl + (appendHash ? hashAPIPath(queryUrl) : ""); + + const { data } = await axios.get( + finalUrl, + typeof requestOptionsBuilder === "function" + ? requestOptionsBuilder(tokenAccessToken) + : azureHeaders(tokenAccessToken) + ); + + return { + data, + accessToken: tokenAccessToken + }; +}; diff --git a/tests/phase21/endpoint-handler-contract.test.cjs b/tests/phase21/endpoint-handler-contract.test.cjs index ae92776b..23dd939d 100644 --- a/tests/phase21/endpoint-handler-contract.test.cjs +++ b/tests/phase21/endpoint-handler-contract.test.cjs @@ -1069,14 +1069,13 @@ test("getadvancedsearch catch path returns ADVANCED_SEARCH_FETCH_FAILED", async const mod = loadModule("pages/api/endpoint/getadvancedsearch_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, - getToken: async () => ({ access_token: "token" }), - hashAPIPath: () => "&hash=expected", + relayGet: async ({ res, errorResponse }) => + respondErrorMock(res, errorResponse), + relayGetData: async () => ({ + data: { value: [] }, + accessToken: "token" + }), azureHeadersPaged: () => ({}), - axios: { - get: async () => { - throw new Error("relay failed"); - } - }, consoleLogger: () => {} }); @@ -2005,12 +2004,9 @@ test("getbasicsearch_by_lparref catch path returns BASIC_SEARCH_LPA_REF_FETCH_FA { respondError: respondErrorMock, respondSuccess: respondSuccessMock, - getToken: async () => { - throw new Error("token failed"); + relayGetData: async () => { + throw new Error("relay failed"); }, - hashAPIPath: () => "&hash=expected", - azureHeaders: () => ({}), - axios: { get: async () => ({ data: { value: [] } }) }, consoleLogger: () => {} } ); @@ -2033,11 +2029,7 @@ test("getbasicsearch_by_address success returns existing payload contract", asyn respondError: respondErrorMock, respondSuccess: respondSuccessMock, getToken: async () => ({ access_token: "token" }), - hashAPIPath: () => "&hash=expected", - azureHeaders: () => ({}), - axios: { - get: async () => ({ data: { value: [] } }) - }, + relayGetData: async () => ({ data: { value: [] } }), consoleLogger: () => {}, jsonpath: () => [] } @@ -2063,10 +2055,8 @@ test("getbasicsearch_by_address catch path returns BASIC_SEARCH_BY_ADDRESS_FETCH getToken: async () => { throw new Error("token failed"); }, - hashAPIPath: () => "&hash=expected", - azureHeaders: () => ({}), - axios: { - get: async () => ({ data: { value: [] } }) + relayGetData: async () => { + throw new Error("relay failed"); }, consoleLogger: () => {}, jsonpath: () => [] @@ -2744,14 +2734,10 @@ test("deletewatchedcasesproxy catch path returns WATCHED_CASE_PROXY_DELETE_FAILE { respondError: respondErrorMock, respondSuccess: respondSuccessMock, - getToken: async () => ({ access_token: "token" }), - hashAPIPath: () => "&hash=expected", - azureHeaders: () => ({}), - axios: { - get: async () => { - throw new Error("proxy delete failed"); - } + forwardGetData: async () => { + throw new Error("proxy delete failed"); }, + azureHeaders: () => ({}), consoleLogger: () => {}, process: { env: {} } } @@ -3412,13 +3398,8 @@ test("getdnscoords catch path returns DNS_COORDS_FETCH_FAILED", async () => { const mod = loadModule("pages/api/endpoint/getdnscoords_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, - getToken: async () => ({ access_token: "token" }), - hashAPIPath: () => "&hash=expected", - azureHeaders: () => ({}), - axios: { - get: async () => { - throw new Error("relay failed"); - } + relayGetData: async () => { + throw new Error("relay failed"); }, consoleLogger: () => {}, OSPoint: function () {}